937 lines
32 KiB
C#
937 lines
32 KiB
C#
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.ComponentModel.DataAnnotations;
|
|
using System.Diagnostics.Metrics;
|
|
using System.IdentityModel.Tokens.Jwt;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Net.Mime;
|
|
using System.Reflection;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Serialization;
|
|
using System.Threading.Tasks;
|
|
using Amazon;
|
|
using EpicGames.AspNet;
|
|
using Jupiter.Common;
|
|
using Microsoft.AspNetCore.Authentication;
|
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Builder;
|
|
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
|
|
using Microsoft.AspNetCore.Hosting;
|
|
using Microsoft.AspNetCore.HttpOverrides;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
|
using Microsoft.Extensions.Hosting;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Options;
|
|
using Microsoft.Extensions.Primitives;
|
|
using Microsoft.Net.Http.Headers;
|
|
using Microsoft.OpenApi.Models;
|
|
using Okta.AspNet.Abstractions;
|
|
using Okta.AspNetCore;
|
|
using OpenTelemetry.Logs;
|
|
using OpenTelemetry.Metrics;
|
|
using OpenTelemetry.Resources;
|
|
using OpenTelemetry.Trace;
|
|
using Serilog;
|
|
using OktaWebApiOptions = Okta.AspNetCore.OktaWebApiOptions;
|
|
|
|
namespace Jupiter
|
|
{
|
|
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
|
|
|
public abstract class BaseStartup
|
|
{
|
|
protected ILogger Logger { get; }
|
|
|
|
protected BaseStartup(IConfiguration configuration, ILogger logger)
|
|
{
|
|
Configuration = configuration;
|
|
Auth = new AuthSettings();
|
|
Logger = logger;
|
|
}
|
|
|
|
protected static ILogger CreateLogger<T>()
|
|
{
|
|
using ILoggerFactory loggerFactory = LoggerFactory.Create(builder =>
|
|
{
|
|
builder.SetMinimumLevel(LogLevel.Information);
|
|
builder.AddSerilog();
|
|
});
|
|
return loggerFactory.CreateLogger<T>();
|
|
}
|
|
|
|
protected IConfiguration Configuration { get; }
|
|
|
|
private AuthSettings Auth { get; }
|
|
|
|
// This method gets called by the runtime. Use this method to add services to the container.
|
|
public void ConfigureServices(IServiceCollection services)
|
|
{
|
|
CbConvertersAspNet.AddAspnetConverters();
|
|
|
|
services.AddServerTiming();
|
|
services.AddLogging(builder => builder.AddSerilog());
|
|
|
|
// aws specific settings
|
|
services.AddOptions<AWSCredentialsSettings>().Bind(Configuration.GetSection("AWSCredentials")).ValidateDataAnnotations();
|
|
services.AddDefaultAWSOptions(Configuration.GetAWSOptions());
|
|
// send log4net logs to serilog and configure aws to log to log4net (they lack a serilog implementation)
|
|
AWSConfigs.LoggingConfig.LogTo = LoggingOptions.Log4Net;
|
|
|
|
services.AddOptions<AuthSettings>().Bind(Configuration.GetSection("Auth")).ValidateDataAnnotations().ValidateOnStart();
|
|
Configuration.GetSection("Auth").Bind(Auth);
|
|
|
|
services.AddOptions<ServiceAccountAuthOptions>().Bind(Configuration.GetSection("ServiceAccounts")).ValidateDataAnnotations();
|
|
|
|
services.AddOptions<JupiterSettings>().Bind(Configuration.GetSection("Jupiter")).ValidateDataAnnotations();
|
|
services.AddOptions<NamespaceSettings>().Bind(Configuration.GetSection("Namespaces")).ValidateDataAnnotations();
|
|
|
|
services.AddSingleton(typeof(INamespacePolicyResolver), typeof(NamespacePolicyResolver));
|
|
|
|
// allow CORS from any domain to call this api as you will need a valid token anyway
|
|
services.AddCors(options =>
|
|
{
|
|
options.AddDefaultPolicy(policy =>
|
|
{
|
|
policy.AllowAnyOrigin();
|
|
policy.AllowAnyHeader();
|
|
policy.AllowAnyMethod();
|
|
});
|
|
});
|
|
|
|
services.AddControllers()
|
|
.AddMvcOptions(options =>
|
|
{
|
|
options.InputFormatters.Add(new CbInputFormatter());
|
|
options.OutputFormatters.Add(new CbOutputFormatter());
|
|
options.OutputFormatters.Add(new RawOutputFormatter(CreateLogger<RawOutputFormatter>()));
|
|
options.FormatterMappings.SetMediaTypeMappingForFormat("raw", MediaTypeNames.Application.Octet);
|
|
options.FormatterMappings.SetMediaTypeMappingForFormat("uecb", CustomMediaTypeNames.UnrealCompactBinary);
|
|
options.FormatterMappings.SetMediaTypeMappingForFormat("uecbpkg", CustomMediaTypeNames.UnrealCompactBinaryPackage);
|
|
|
|
OnAddControllers(options);
|
|
}).ConfigureApiBehaviorOptions(options =>
|
|
{
|
|
options.InvalidModelStateResponseFactory = context =>
|
|
{
|
|
BadRequestObjectResult result = new BadRequestObjectResult(context.ModelState);
|
|
// always return errors as json objects
|
|
// we could allow more types here, but we do not want raw for instance
|
|
result.ContentTypes.Add(MediaTypeNames.Application.Json);
|
|
|
|
return result;
|
|
};
|
|
}).AddJsonOptions(jsonOptions => ConfigureJsonOptions(jsonOptions.JsonSerializerOptions));
|
|
|
|
services.AddHttpContextAccessor();
|
|
|
|
const string ForwardingScheme = "ForwardingScheme";
|
|
List<string> availableSchemes = new List<string>();
|
|
|
|
AuthenticationBuilder authenticationBuilder = services.AddAuthentication(options =>
|
|
{
|
|
if (Auth.Enabled)
|
|
{
|
|
if (Auth.Schemes.Count > 1)
|
|
{
|
|
// we have multiple schemes, so we set the default to the forwarding scheme which will use the jwtAuthority to pick the correct scheme for the token
|
|
options.DefaultAuthenticateScheme = ForwardingScheme;
|
|
options.DefaultChallengeScheme = ForwardingScheme;
|
|
}
|
|
else
|
|
{
|
|
// if we only have one scheme we set it to default
|
|
options.DefaultAuthenticateScheme = Auth.DefaultScheme;
|
|
options.DefaultChallengeScheme = Auth.DefaultScheme;
|
|
|
|
}
|
|
}
|
|
else
|
|
{
|
|
options.DefaultAuthenticateScheme = DisabledAuthenticationHandler.AuthenticateScheme;
|
|
options.DefaultChallengeScheme = DisabledAuthenticationHandler.AuthenticateScheme;
|
|
}
|
|
}
|
|
);
|
|
|
|
if (Auth.Enabled)
|
|
{
|
|
foreach (KeyValuePair<string, AuthSchemeEntry> schemeEntry in Auth.Schemes)
|
|
{
|
|
string name = schemeEntry.Key;
|
|
AuthSchemeEntry scheme = schemeEntry.Value;
|
|
|
|
switch (scheme.Implementation)
|
|
{
|
|
case SchemeImplementations.ServiceAccount:
|
|
availableSchemes.Add(name);
|
|
authenticationBuilder.AddScheme<ServiceAccountAuthOptions, ServiceAccountAuthHandler>(name, options => { });
|
|
break;
|
|
case SchemeImplementations.JWTBearer:
|
|
availableSchemes.Add(name);
|
|
authenticationBuilder.AddJwtBearer(name, options =>
|
|
{
|
|
options.Authority = scheme.JwtAuthority;
|
|
options.Audience = scheme.JwtAudience;
|
|
});
|
|
break;
|
|
case SchemeImplementations.Okta:
|
|
JwtBearerEvents bearerEvents = new JwtBearerEvents();
|
|
if (Auth.RemapNameClaim)
|
|
{
|
|
bool firstTime = true;
|
|
bearerEvents.OnMessageReceived += context =>
|
|
{
|
|
if (!firstTime)
|
|
{
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
firstTime = false;
|
|
context.Options.TokenValidationParameters.NameClaimType = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier";
|
|
return Task.CompletedTask;
|
|
};
|
|
}
|
|
|
|
availableSchemes.Add(name);
|
|
authenticationBuilder.AddOktaWebApi(name, new OktaWebApiOptions
|
|
{
|
|
OktaDomain = scheme.OktaDomain,
|
|
AuthorizationServerId = scheme.OktaAuthorizationServerId,
|
|
Audience = scheme.JwtAudience,
|
|
JwtBearerEvents = bearerEvents
|
|
});
|
|
break;
|
|
default:
|
|
throw new NotSupportedException($"Unknown implementation type {scheme.Implementation}");
|
|
}
|
|
}
|
|
|
|
authenticationBuilder.AddPolicyScheme(ForwardingScheme, ForwardingScheme, options =>
|
|
{
|
|
options.ForwardDefaultSelector = context =>
|
|
{
|
|
string? authorization = context.Request.Headers[HeaderNames.Authorization];
|
|
string name = "Bearer";
|
|
string tokenName = $"{name} ";
|
|
if (string.IsNullOrEmpty(authorization) || !authorization.StartsWith(tokenName, StringComparison.InvariantCulture))
|
|
{
|
|
return Auth.DefaultScheme;
|
|
}
|
|
|
|
string token = authorization.Substring(tokenName.Length).Trim();
|
|
JwtSecurityTokenHandler jwtHandler = new JwtSecurityTokenHandler();
|
|
|
|
if (!jwtHandler.CanReadToken(token))
|
|
{
|
|
return Auth.DefaultScheme;
|
|
}
|
|
|
|
JwtSecurityToken jwtToken = jwtHandler.ReadJwtToken(token);
|
|
|
|
foreach (KeyValuePair<string, AuthSchemeEntry> entry in Auth.Schemes)
|
|
{
|
|
if (entry.Value.JwtAuthority == jwtToken.Issuer)
|
|
{
|
|
return entry.Key;
|
|
}
|
|
}
|
|
|
|
return Auth.DefaultScheme;
|
|
|
|
};
|
|
});
|
|
}
|
|
else
|
|
{
|
|
availableSchemes.Add(DisabledAuthenticationHandler.AuthenticateScheme);
|
|
authenticationBuilder.AddTestAuth(options => { });
|
|
}
|
|
|
|
services.AddAuthorization(options =>
|
|
{
|
|
options.AddPolicy(NamespaceAccessRequirement.Name, policy =>
|
|
{
|
|
policy.AuthenticationSchemes = availableSchemes;
|
|
policy.Requirements.Add(new NamespaceAccessRequirement());
|
|
});
|
|
|
|
options.AddPolicy(GlobalAccessRequirement.Name, policy =>
|
|
{
|
|
policy.AuthenticationSchemes = availableSchemes;
|
|
policy.Requirements.Add(new GlobalAccessRequirement());
|
|
});
|
|
|
|
|
|
options.AddPolicy(ScopeAccessRequirement.Name, policy =>
|
|
{
|
|
policy.AuthenticationSchemes = availableSchemes;
|
|
policy.Requirements.Add(new ScopeAccessRequirement());
|
|
});
|
|
|
|
// A policy that grants any authenticated user access
|
|
options.AddPolicy("Any", policy =>
|
|
{
|
|
policy.AuthenticationSchemes = availableSchemes;
|
|
policy.RequireAuthenticatedUser();
|
|
});
|
|
|
|
OnAddAuthorization(options, availableSchemes);
|
|
});
|
|
services.AddSingleton<IAuthorizationHandler, NamespaceAuthorizationHandler>();
|
|
services.AddSingleton<IAuthorizationHandler, GlobalAuthorizationHandler>();
|
|
services.AddSingleton<IAuthorizationHandler, ScopeAuthorizationHandler>();
|
|
|
|
string otelServiceName = Configuration["OTEL_SERVICE_NAME"] ?? "unreal-cloud-ddc";
|
|
string? otelServiceVersion = Configuration["OTEL_SERVICE_VERSION"];
|
|
|
|
string? useConsoleExporterString = Configuration["OTEL_USE_CONSOLE_EXPORTER"];
|
|
_ = bool.TryParse(useConsoleExporterString, out bool useConsoleExporter);
|
|
|
|
services.AddOpenTelemetry().ConfigureResource(builder =>
|
|
{
|
|
builder.AddService("UnrealCloudDDC", serviceNamespace: "Jupiter", serviceVersion: otelServiceVersion)
|
|
.AddEnvironmentVariableDetector();
|
|
}).WithTracing(builder =>
|
|
{
|
|
builder.AddHttpClientInstrumentation(options =>
|
|
{
|
|
options.EnrichWithHttpRequestMessage = (activity, message) =>
|
|
{
|
|
activity.AddTag("service.name", otelServiceName + "-http-client");
|
|
activity.AddTag("operation.name", "http-request");
|
|
|
|
string url = $"{message.Method} {message.Headers.Host}{message.RequestUri?.LocalPath}";
|
|
activity.DisplayName = url;
|
|
|
|
activity.AddTag("resource.name", url);
|
|
};
|
|
});
|
|
builder.AddAspNetCoreInstrumentation(options =>
|
|
{
|
|
options.EnrichWithHttpRequest = (activity, request) =>
|
|
{
|
|
if (request.Headers.TryGetValue("x-ue-session", out StringValues ueSessionValues))
|
|
{
|
|
if (ueSessionValues.Count != 0)
|
|
{
|
|
activity.AddTag("ue-session", ueSessionValues.First());
|
|
}
|
|
}
|
|
|
|
if (request.Headers.TryGetValue("ue-session", out StringValues ueSessionValuesOld))
|
|
{
|
|
if (ueSessionValuesOld.Count != 0)
|
|
{
|
|
activity.AddTag("ue-session", ueSessionValuesOld.First());
|
|
}
|
|
}
|
|
|
|
if (request.Headers.TryGetValue("x-ue-request", out StringValues ueRequestValues))
|
|
{
|
|
if (ueRequestValues.Count != 0)
|
|
{
|
|
activity.AddTag("ue-request", ueRequestValues.First());
|
|
}
|
|
}
|
|
|
|
if (request.Headers.TryGetValue("ue-request", out StringValues ueRequestValuesOld))
|
|
{
|
|
if (ueRequestValuesOld.Count != 0)
|
|
{
|
|
activity.AddTag("ue-request", ueRequestValuesOld.First());
|
|
}
|
|
}
|
|
|
|
if (request.Headers.TryGetValue("ue-IsBuildMachine", out StringValues ueIsBuildMachine))
|
|
{
|
|
if (ueIsBuildMachine.Count != 0)
|
|
{
|
|
activity.AddTag("ue-IsBuildMachine", ueIsBuildMachine.First());
|
|
}
|
|
}
|
|
};
|
|
});
|
|
builder.SetSampler(new AlwaysOnSampler());
|
|
builder.AddOtlpExporter();
|
|
if (useConsoleExporter)
|
|
{
|
|
builder.AddConsoleExporter();
|
|
}
|
|
builder.AddSource("UnrealCloudDDC", "ScyllaDB");
|
|
}).WithMetrics(builder =>
|
|
{
|
|
builder
|
|
.AddMeter("UnrealCloudDDC", "ScyllaDB")
|
|
.AddOtlpExporter()
|
|
.AddAspNetCoreInstrumentation()
|
|
.AddHttpClientInstrumentation()
|
|
.AddRuntimeInstrumentation();
|
|
|
|
if (useConsoleExporter)
|
|
{
|
|
builder.AddConsoleExporter();
|
|
}
|
|
});
|
|
|
|
services.Configure<OpenTelemetryLoggerOptions>(opt =>
|
|
{
|
|
opt.IncludeScopes = true;
|
|
opt.ParseStateValues = true;
|
|
opt.IncludeFormattedMessage = true;
|
|
});
|
|
|
|
services.Configure<ForwardedHeadersOptions>(options =>
|
|
{
|
|
// include enough ips to allow for multiple proxies in front
|
|
options.ForwardLimit = 3;
|
|
|
|
// we do not know the address of load balancers in front of us but want to accept the forwarding headers from them so we reset these lists
|
|
// we generally assume that traffic happens over https and that the network we are on is very limited in scope (only ingress via load balancers)
|
|
// TODO: If we wanted to we could introduce a setting to specify the ip range of the load balancers here, but would be kind of annoying to have to specify
|
|
// , but it could be useful if this is run in less secure networks
|
|
options.KnownNetworks.Clear();
|
|
options.KnownProxies.Clear();
|
|
});
|
|
|
|
services.AddSingleton<Tracer>(CreateTracer);
|
|
|
|
services.AddSingleton<Meter>(CreateMeter);
|
|
|
|
services.Configure<ForwardedHeadersOptions>(options =>
|
|
{
|
|
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
|
|
});
|
|
|
|
services.AddSwaggerGen(settings =>
|
|
{
|
|
string? assemblyName = Assembly.GetEntryAssembly()?.GetName().Name;
|
|
settings.SwaggerDoc("v1", info: new OpenApiInfo
|
|
{
|
|
Title = "Unreal Cloud DDC",
|
|
Contact = new OpenApiContact
|
|
{
|
|
Name = "Joakim Lindqvist",
|
|
Email = "joakim.lindqvist@epicgames.com",
|
|
}
|
|
});
|
|
|
|
// Set the comments path for the Swagger JSON and UI.
|
|
string xmlFile = $"{assemblyName}.xml";
|
|
string xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
|
|
if (File.Exists(xmlPath))
|
|
{
|
|
settings.IncludeXmlComments(xmlPath);
|
|
}
|
|
});
|
|
|
|
OnAddService(services);
|
|
|
|
OnAddHealthChecks(services);
|
|
}
|
|
|
|
public static void ConfigureJsonOptions(JsonSerializerOptions options)
|
|
{
|
|
options.AllowTrailingCommas = true;
|
|
options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
|
|
options.PropertyNameCaseInsensitive = true;
|
|
|
|
options.Converters.Add(new JsonStringEnumConverter());
|
|
}
|
|
|
|
private Meter CreateMeter(IServiceProvider provider)
|
|
{
|
|
return new Meter("UnrealCloudDDC");
|
|
}
|
|
|
|
private Tracer CreateTracer(IServiceProvider provider)
|
|
{
|
|
Tracer tracer = TracerProvider.Default.GetTracer("UnrealCloudDDC");
|
|
|
|
return tracer;
|
|
}
|
|
|
|
private void OnAddHealthChecks(IServiceCollection services)
|
|
{
|
|
IHealthChecksBuilder healthChecks = services.AddHealthChecks()
|
|
.AddCheck("self", () => HealthCheckResult.Healthy(), tags: new[] { "self" });
|
|
OnAddHealthChecks(services, healthChecks);
|
|
|
|
string? ddAgentHost = System.Environment.GetEnvironmentVariable("DD_AGENT_HOST");
|
|
if (!string.IsNullOrEmpty(ddAgentHost))
|
|
{
|
|
healthChecks.AddDatadogPublisher("jupiter.healthchecks");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Register health checks for individual services
|
|
/// </summary>
|
|
/// <remarks>Use the self tag for checks if the service is running while the services tag can be used for any dependencies which needs to work</remarks>
|
|
/// <param name="services">DI service injector</param>
|
|
/// <param name="healthChecks">A already configured builder that you can add more checks to</param>
|
|
protected abstract void OnAddHealthChecks(IServiceCollection services, IHealthChecksBuilder healthChecks);
|
|
|
|
protected abstract void OnAddAuthorization(AuthorizationOptions authorizationOptions, List<string> defaultSchemes);
|
|
|
|
protected virtual void OnAddControllers(MvcOptions options)
|
|
{
|
|
}
|
|
|
|
protected abstract void OnAddService(IServiceCollection services);
|
|
|
|
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
|
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
|
{
|
|
JupiterSettings jupiterSettings = app.ApplicationServices.GetService<IOptionsMonitor<JupiterSettings>>()!.CurrentValue;
|
|
|
|
if (jupiterSettings.ShowPII)
|
|
{
|
|
Logger.LogError("Personally Identifiable information being shown. This should not be generally enabled in prod.");
|
|
|
|
// do not hide personal information during development
|
|
Microsoft.IdentityModel.Logging.IdentityModelEventSource.ShowPII = true;
|
|
}
|
|
|
|
ConfigureMiddlewares(jupiterSettings, app, env);
|
|
}
|
|
|
|
private void ConfigureMiddlewares(JupiterSettings jupiterSettings, IApplicationBuilder app, IWebHostEnvironment env)
|
|
{
|
|
// enable use of forwarding headers as we expect a reverse proxy to be running in front of us
|
|
app.UseForwardedHeaders();
|
|
|
|
if (jupiterSettings.UseRequestLogging)
|
|
{
|
|
app.UseSerilogRequestLogging();
|
|
}
|
|
|
|
OnConfigureAppEarly(app, env);
|
|
|
|
if (env.IsDevelopment() && UseDeveloperExceptionPage)
|
|
{
|
|
app.UseDeveloperExceptionPage();
|
|
}
|
|
else
|
|
{
|
|
app.UseExceptionHandler("/error");
|
|
}
|
|
|
|
app.UseRouting();
|
|
|
|
app.UseCors();
|
|
|
|
app.UseAuthentication();
|
|
app.UseAuthorization();
|
|
|
|
app.UseMiddleware<SuppressExceptionMiddleware>();
|
|
app.UseMiddleware<ServerTimingMiddleware>();
|
|
|
|
app.UseEndpoints(endpoints =>
|
|
{
|
|
static bool PassAllChecks(HealthCheckRegistration check) => true;
|
|
|
|
// Ready checks in Kubernetes is to verify that the service is working, if this returns false the app will not get any traffic (load balancer ignores it)
|
|
endpoints.MapHealthChecks("/health/readiness", options: new HealthCheckOptions()
|
|
{
|
|
Predicate = jupiterSettings.DisableHealthChecks ? PassAllChecks : (check) => check.Tags.Contains("self"),
|
|
});
|
|
|
|
// Live checks in Kubernetes to see if the pod is working as it should, if this returns false the entire pod is killed
|
|
endpoints.MapHealthChecks("/health/liveness", options: new HealthCheckOptions()
|
|
{
|
|
Predicate = jupiterSettings.DisableHealthChecks ? PassAllChecks : (check) => check.Tags.Contains("services"),
|
|
});
|
|
|
|
endpoints.MapGet("/health/ready", async context =>
|
|
{
|
|
context.Response.StatusCode = 200;
|
|
context.Response.Headers.ContentType = "text/plain";
|
|
await context.Response.Body.WriteAsync(Encoding.ASCII.GetBytes("Healthy"));
|
|
});
|
|
|
|
endpoints.MapGet("/health/live", async context =>
|
|
{
|
|
context.Response.StatusCode = 200;
|
|
context.Response.Headers.ContentType = "text/plain";
|
|
await context.Response.Body.WriteAsync(Encoding.ASCII.GetBytes("Healthy"));
|
|
});
|
|
|
|
endpoints.MapControllers();
|
|
});
|
|
|
|
if (jupiterSettings.HostSwaggerDocumentation)
|
|
{
|
|
app.UseSwagger();
|
|
app.UseReDoc(options => { options.SpecUrl = "/swagger/v1/swagger.json"; });
|
|
}
|
|
|
|
OnConfigureApp(app, env);
|
|
}
|
|
|
|
public virtual bool UseDeveloperExceptionPage { get; } = false;
|
|
|
|
protected virtual void OnConfigureAppEarly(IApplicationBuilder app, IWebHostEnvironment env)
|
|
{
|
|
}
|
|
|
|
protected virtual void OnConfigureApp(IApplicationBuilder app, IWebHostEnvironment env)
|
|
{
|
|
|
|
}
|
|
}
|
|
|
|
/*public class MvcJsonOptionsWrapper : IConfigureOptions<MvcNewtonsoftJsonOptions>
|
|
{
|
|
readonly IServiceProvider ServiceProvider;
|
|
|
|
public MvcJsonOptionsWrapper(IServiceProvider serviceProvider)
|
|
{
|
|
ServiceProvider = serviceProvider;
|
|
}
|
|
public void Configure(MvcNewtonsoftJsonOptions options)
|
|
{
|
|
options.SerializerSettings.ContractResolver = new FieldFilteringResolver(ServiceProvider);
|
|
}
|
|
}*/
|
|
|
|
/*public class FieldFilteringResolver : DefaultContractResolver
|
|
{
|
|
private readonly IHttpContextAccessor _httpContextAccessor;
|
|
|
|
public FieldFilteringResolver(IServiceProvider sp)
|
|
{
|
|
_httpContextAccessor = sp.GetRequiredService<IHttpContextAccessor>();
|
|
|
|
NamingStrategy = new CamelCaseNamingStrategy(false, true);
|
|
}
|
|
|
|
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
|
|
{
|
|
|
|
JsonProperty property = base.CreateProperty(member, memberSerialization);
|
|
|
|
property.ShouldSerialize = o =>
|
|
{
|
|
HttpContext? httpContext = _httpContextAccessor.HttpContext;
|
|
|
|
if (httpContext == null)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// if no fields are being filtered we should serialize the property
|
|
if (!httpContext.Request.Query.ContainsKey("fields"))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
StringValues fields = httpContext.Request.Query["fields"];
|
|
bool ignore = true;
|
|
foreach (string field in fields)
|
|
{
|
|
// a empty field to filter for is considered a match for everything as fields= should be the same as just omitting fields
|
|
if (string.IsNullOrEmpty(field))
|
|
{
|
|
return true;
|
|
}
|
|
if (string.Equals(field, property.PropertyName, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
ignore = false;
|
|
}
|
|
}
|
|
|
|
return !ignore;
|
|
};
|
|
|
|
return property;
|
|
}
|
|
}*/
|
|
|
|
public enum SchemeImplementations
|
|
{
|
|
JWTBearer,
|
|
Okta,
|
|
ServiceAccount
|
|
};
|
|
|
|
public class AuthSchemeEntry : IValidatableObject
|
|
{
|
|
/// <summary>
|
|
/// The implementation to use, this controls which other configuration values needs to be set. For most servers JWTBearer should work fine.
|
|
/// </summary>
|
|
[Required]
|
|
public SchemeImplementations Implementation { get; set; } = SchemeImplementations.JWTBearer;
|
|
|
|
/// <summary>
|
|
/// The Okta domain (url to your okta server - do not include the authorization server id)
|
|
/// </summary>
|
|
public string OktaDomain { get; set; } = "";
|
|
/// <summary>
|
|
/// The Okta AuthorizationServerId, this is used if you have more then one authorization server within your Okta server. We recommend using a separate authorization server for each major set of systems to reduce blast radius of security issues.
|
|
/// </summary>
|
|
public string OktaAuthorizationServerId { get; set; } = OktaWebOptions.DefaultAuthorizationServerId;
|
|
|
|
/// <summary>
|
|
/// The JWT Authority (url to your IdP)
|
|
/// </summary>
|
|
public string JwtAuthority { get; set; } = "";
|
|
|
|
/// <summary>
|
|
/// The audience for the token, this is usually defined by your IdP
|
|
/// </summary>
|
|
[Required]
|
|
public string JwtAudience { get; set; } = "";
|
|
|
|
/// <summary>
|
|
/// The namespaces which these scheme is allowed to grant access to, all if this is omitted or empty
|
|
/// </summary>
|
|
public string[] AllowedNamespaces { get; set; } = Array.Empty<string>();
|
|
|
|
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
|
{
|
|
List<ValidationResult> validationResults = new List<ValidationResult>();
|
|
if (Implementation == SchemeImplementations.JWTBearer)
|
|
{
|
|
if (string.IsNullOrEmpty(JwtAuthority))
|
|
{
|
|
validationResults.Add(new ValidationResult("JWT Authority must be specified when using JWTBearer implementation"));
|
|
}
|
|
if (string.IsNullOrEmpty(JwtAudience))
|
|
{
|
|
validationResults.Add(new ValidationResult("JWT Audience must be specified when using JWTBearer implementation"));
|
|
}
|
|
}
|
|
else if (Implementation == SchemeImplementations.Okta)
|
|
{
|
|
if (string.IsNullOrEmpty(OktaDomain))
|
|
{
|
|
validationResults.Add(new ValidationResult("Okta Domain must be specified when using Okta implementation"));
|
|
}
|
|
if (string.IsNullOrEmpty(JwtAudience))
|
|
{
|
|
validationResults.Add(new ValidationResult("JWT Audience must be specified when using Okta implementation"));
|
|
}
|
|
if (string.IsNullOrEmpty(JwtAuthority))
|
|
{
|
|
validationResults.Add(new ValidationResult("JWT Authority must be specified when using Okta implementation"));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
throw new NotSupportedException($"Unknown auth implementation {Implementation}");
|
|
}
|
|
|
|
return validationResults;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The definition of how a specific oidc client works, should be the same as ProviderInfo in EpicGames.Okta. This will be read by EpicGames.Okta so its definition is authoritative.
|
|
/// </summary>
|
|
public class ProviderInfo
|
|
{
|
|
/// <summary>
|
|
/// The server uri to the IdP
|
|
/// </summary>
|
|
[Required] public Uri ServerUri { get; set; } = null!;
|
|
|
|
/// <summary>
|
|
/// The client id as set by the IdP
|
|
/// </summary>
|
|
|
|
[Required] public string ClientId { get; set; } = null!;
|
|
|
|
/// <summary>
|
|
/// The display name to use for this provider, usually the same as the provider id
|
|
/// </summary>
|
|
|
|
[Required] public string DisplayName { get; set; } = null!;
|
|
|
|
/// <summary>
|
|
/// The redirect uri - deprecated - use PossibleRedirectUri instead as supporting multiple ports is very useful when running on localhost were ports maybe taken
|
|
/// </summary>
|
|
public Uri? RedirectUri { get; set; } = null;
|
|
|
|
/// <summary>
|
|
/// List of redirect uris that can be used (are allow listed in the IdP). In priority order.
|
|
/// </summary>
|
|
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "Used by the configuration system")]
|
|
public List<Uri>? PossibleRedirectUri { get; set; } = null!;
|
|
|
|
/// <summary>
|
|
/// Enable to load claims from the user profile after the token has been read. Converted it from a thin token to a think token.
|
|
/// </summary>
|
|
[Required] public bool LoadClaimsFromUserProfile { get; set; } = false;
|
|
|
|
/// <summary>
|
|
/// The scopes to request
|
|
/// </summary>
|
|
public string Scopes { get; set; } = "openid profile offline_access email";
|
|
|
|
/// <summary>
|
|
/// A string added to the local error page when failing to login, usually a good place to tell users who and how to get help.
|
|
/// </summary>
|
|
public string? GenericErrorInformation { get; set; }
|
|
|
|
/// <summary>
|
|
/// Can be disabled to not use the discovery documents that are part of the oidc standard to find endpoints.
|
|
/// </summary>
|
|
public bool UseDiscoveryDocument { get; set; } = true;
|
|
|
|
// We should absolutely not include client secrets here as this information is public on internet, thus you want public clients used
|
|
//public string? ClientSecret { get; set; } = null;
|
|
}
|
|
|
|
public class ClientOidcConfiguration
|
|
{
|
|
public string? DefaultProvider { get; set; } = null;
|
|
|
|
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "Used by the configuration system")]
|
|
public Dictionary<string, ProviderInfo> Providers { get; set; } = new Dictionary<string, ProviderInfo>();
|
|
}
|
|
|
|
public class AuthSettings : IValidatableObject
|
|
{
|
|
/// <summary>
|
|
/// The name of the scheme to use by default
|
|
/// </summary>
|
|
public string DefaultScheme { get; set; } = "Bearer";
|
|
|
|
/// <summary>
|
|
/// Used to disable authentication, not recommended to set for anything other then local use cases
|
|
/// </summary>
|
|
public bool Enabled { get; set; } = true;
|
|
|
|
/// <summary>
|
|
/// Can be set to require acls to be used even if auth is disabled, mostly used for testing
|
|
/// </summary>
|
|
public bool RequireAcls { get; set; } = false;
|
|
|
|
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "Used by the configuration system")]
|
|
public Dictionary<string, AuthSchemeEntry> Schemes { get; set; } = new Dictionary<string, AuthSchemeEntry>();
|
|
|
|
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "Used by the configuration system")]
|
|
public List<AclEntry> Acls { get; set; } = new List<AclEntry>();
|
|
|
|
/// <summary>
|
|
/// Remaps name claim from http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name to http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier which is required for Okta
|
|
/// </summary>
|
|
public bool RemapNameClaim { get; set; } = true;
|
|
|
|
/// <summary>
|
|
/// Setup policies for how access control is managed
|
|
/// </summary>
|
|
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "Used by the configuration system")]
|
|
public List<AclPolicy> Policies { get; set; } = new List<AclPolicy>();
|
|
|
|
/// <summary>
|
|
/// Set to true to also consider the legacy configuration in the acl fields of this object as well as within the namespace policy for endpoints that support the acl policies
|
|
/// </summary>
|
|
public bool UseLegacyConfiguration { get; set; } = false;
|
|
|
|
/// <summary>
|
|
/// Configuration read by clients on how to interactively login locally
|
|
/// </summary>
|
|
public ClientOidcConfiguration? ClientOidcConfiguration { get; set; } = null;
|
|
|
|
/// <summary>
|
|
/// The encryption key used to encrypt the configuration - usually not modified as you then need to also distribute this key. This needs to be exactly 16 bytes
|
|
/// </summary>
|
|
public string ClientOidcEncryptionKey { get; set; } = "892a27ef5cbf4894af2e6bd53a54aa48";
|
|
|
|
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
|
{
|
|
List<ValidationResult> validationResults = new List<ValidationResult>();
|
|
if (!Enabled)
|
|
{
|
|
return validationResults;
|
|
}
|
|
|
|
if (Schemes.Count == 0)
|
|
{
|
|
validationResults.Add(new ValidationResult("You must have at least one scheme when authentication is enabled"));
|
|
}
|
|
|
|
if (!Schemes.ContainsKey(DefaultScheme))
|
|
{
|
|
validationResults.Add(new ValidationResult($"Expected to find a scheme with the name {DefaultScheme} as its set as the default scheme"));
|
|
}
|
|
|
|
return validationResults;
|
|
}
|
|
}
|
|
|
|
public class JupiterSettings
|
|
{
|
|
// enable to unhide potentially personal information, see https://aka.ms/IdentityModel/PII
|
|
public bool ShowPII { get; set; } = false;
|
|
public bool DisableHealthChecks { get; set; } = false;
|
|
public bool HostSwaggerDocumentation { get; set; } = true;
|
|
|
|
/// <summary>
|
|
/// Port used to host the internally accessible api (as well as the public api).
|
|
/// This hosts both public and private namespaces
|
|
/// </summary>
|
|
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "Used by the configuration system")]
|
|
|
|
public List<int> InternalApiPorts { get; set; } = new List<int>() { 8080 };
|
|
|
|
/// <summary>
|
|
/// Port that hosts public and private namespaces
|
|
/// </summary>
|
|
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "Used by the configuration system")]
|
|
|
|
public List<int> CorpApiPorts { get; set; } = new List<int>() { 8008 };
|
|
|
|
/// <summary>
|
|
/// Port that only hosts the public namespaces
|
|
/// </summary>
|
|
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "Used by the configuration system")]
|
|
|
|
public List<int> PublicApiPorts { get; set; } = new List<int>() { 80 };
|
|
|
|
// Enable to echo every request to the log file, usually this is more efficiently done on the load balancer
|
|
public bool UseRequestLogging { get; set; } = false;
|
|
|
|
/// <summary>
|
|
/// Name of the current site, has to be globally unique across all deployments
|
|
/// </summary>
|
|
[Required]
|
|
[Key]
|
|
public string CurrentSite { get; set; } = "";
|
|
|
|
/// <summary>
|
|
/// Used to move where domain sockets are allocated
|
|
/// </summary>
|
|
public string DomainSocketsRoot { get; set; } = "/tmp/sockets";
|
|
|
|
/// <summary>
|
|
/// Enable to create unix domain sockets for inter process communication
|
|
/// </summary>
|
|
public bool UseDomainSockets { get; set; } = false;
|
|
|
|
/// <summary>
|
|
/// Enable to change access (chmod) the sockets to allow for anyone to access them
|
|
/// </summary>
|
|
public bool ChmodDomainSockets { get; set; } = false;
|
|
|
|
/// <summary>
|
|
/// Assumes that any local connection should have full access, this is used only for tests
|
|
/// </summary>
|
|
public bool AssumeLocalConnectionsHasFullAccess { get; set; }
|
|
|
|
/// <summary>
|
|
/// Set this to increase the max number of TCP connections that are allowed to be queued.
|
|
/// </summary>
|
|
public int? PendingConnectionMax { get; set; }
|
|
}
|
|
}
|