Merge remote-tracking branch 'origin/master' into pdimit/dry

This commit is contained in:
Philip Dimitratos 2017-10-11 15:07:20 -07:00
Родитель 64b2e3d7d3 a2aa75ea87
Коммит c357195249
17 изменённых файлов: 284 добавлений и 63 удалений

Просмотреть файл

@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
<PackageId>Microsoft.Sia.Connectors.Tickets</PackageId>
<Version>1.0.5-alpha</Version>
<Version>1.0.6-alpha</Version>
<Authors>pdimit, magpint, jache, chtownes</Authors>
<Company>Microsoft</Company>
<Product>SRE Incident Assistant</Product>
@ -15,13 +15,13 @@
<ItemGroup>
<PackageReference Include="AutoMapper" Version="6.1.1" />
<PackageReference Include="Microsoft.Sia.Shared" Version="1.0.7-alpha" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Sia.Data.Incident\Sia.Data.Incidents.csproj" />
<ProjectReference Include="..\Sia.Domain.ApiModels\Sia.Domain.ApiModels.csproj" />
<ProjectReference Include="..\Sia.Domain\Sia.Domain.csproj" />
<ProjectReference Include="..\Sia.Shared\Sia.Shared.csproj" />
</ItemGroup>
</Project>

Просмотреть файл

@ -3,6 +3,7 @@
public enum AuthenticationType
{
None,
Certificate
Certificate,
CertificateFromKeyVault
}
}

Просмотреть файл

@ -1,6 +1,8 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Sia.Connectors.Tickets;
using Sia.Connectors.Tickets.TicketProxy;
using Sia.Shared.Authentication;
namespace Sia.Gateway.Initialization
{
@ -12,7 +14,10 @@ namespace Sia.Gateway.Initialization
public static IServiceCollection AddProxyWithCert(this IServiceCollection services, string endpoint, string certThumbprint)
=> services.AddProxy(new ProxyConnectionInfo(endpoint, certThumbprint));
public static IServiceCollection AddProxyWithCertFromKeyVault(this IServiceCollection services, string endpoint, KeyVaultConfiguration config, string certName)
=> services.AddProxy(new ProxyConnectionInfo(endpoint, config, certName));
private static IServiceCollection AddProxy(this IServiceCollection services, ProxyConnectionInfo proxyConnection)
{

Просмотреть файл

@ -1,4 +1,5 @@
using Sia.Shared.Authentication;
using Microsoft.Extensions.Configuration;
using Sia.Shared.Authentication;
using Sia.Shared.Validation;
using System;
@ -6,14 +7,38 @@ namespace Sia.Connectors.Tickets.TicketProxy
{
public class ProxyConnectionInfo
{
private AzureSecretVault _keyVault;
/// <summary>
/// Instantiates ProxyConnectionInfo with no Authentication
/// </summary>
/// <param name="endpoint">Proxy Endpoint</param>
public ProxyConnectionInfo(string endpoint)
: this(endpoint, AuthenticationType.None)
{ }
/// <summary>
/// Instantiates ProxyConnectionInfo with certificate authentication from a local cert
/// </summary>
/// <param name="endpoint">Proxy Endpoint</param>
/// <param name="certThumbprint">Thumbprint for searching local certificate store</param>
public ProxyConnectionInfo(string endpoint, string certThumbprint)
: this(endpoint, AuthenticationType.Certificate)
{
CertThumbprint = ThrowIf.NullOrWhiteSpace(certThumbprint, nameof(certThumbprint));
CertIdentifier = ThrowIf.NullOrWhiteSpace(certThumbprint, nameof(certThumbprint));
}
/// <summary>
/// Instantiates ProxyConnectionInfo with certificate authentication using a certificate retrieved from keyvault
/// </summary>
/// <param name="endpoint">Proxy Endpoint</param>
/// <param name="config">Configuration root for initialization</param>
/// <param name="vaultName">Key vault name</param>
public ProxyConnectionInfo(string endpoint, KeyVaultConfiguration config, string vaultName)
: this(endpoint, AuthenticationType.CertificateFromKeyVault)
{
_keyVault = new AzureSecretVault(config);
CertIdentifier = ThrowIf.NullOrWhiteSpace(vaultName, nameof(vaultName));
}
protected ProxyConnectionInfo(string endpoint, AuthenticationType authType)
@ -23,9 +48,9 @@ namespace Sia.Connectors.Tickets.TicketProxy
}
public ProxyClient GetClient() => new ProxyClient(ClientFactory.GetClient(), Endpoint);
public AuthenticationType AuthenticationType { get; set; }
public string Endpoint { get; set; }
public string CertThumbprint { get; set; }
public AuthenticationType AuthenticationType { get; protected set; }
public string Endpoint { get; protected set; }
public string CertIdentifier { get; protected set; }
protected IHttpClientFactory ClientFactory
{
@ -34,7 +59,9 @@ namespace Sia.Connectors.Tickets.TicketProxy
switch (AuthenticationType)
{
case AuthenticationType.Certificate:
return new LocalCertificateRetriever(CertThumbprint);
return new LocalCertificateRetriever(CertIdentifier);
case AuthenticationType.CertificateFromKeyVault:
return new KeyVaultCertificateRetriever(_keyVault, CertIdentifier);
case AuthenticationType.None:
return new UnauthenticatedClientFactory();
default:

Просмотреть файл

@ -15,11 +15,11 @@
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.DataAnnotations" Version="2.0.0" />
<PackageReference Include="Microsoft.Sia.Shared" Version="1.0.7-alpha" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Sia.Domain\Sia.Domain.csproj" />
<ProjectReference Include="..\Sia.Shared\Sia.Shared.csproj" />
</ItemGroup>
</Project>

Просмотреть файл

@ -1,8 +1,10 @@
using MediatR;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR.Client;
using Sia.Domain.ApiModels;
using Sia.Gateway.Authentication;
using Sia.Gateway.Protocol;
using Sia.Gateway.Hubs;
using Sia.Gateway.Requests;
using Sia.Gateway.Requests.Events;
using System.Threading.Tasks;
@ -13,10 +15,15 @@ namespace Sia.Gateway.Controllers
public class EventsController : BaseController
{
private const string notFoundMessage = "Incident or event not found";
private readonly HubConnectionBuilder _hubConnectionBuilder;
public EventsController(IMediator mediator, AzureActiveDirectoryAuthenticationInfo authConfig, IUrlHelper urlHelper)
public EventsController(IMediator mediator,
AzureActiveDirectoryAuthenticationInfo authConfig,
HubConnectionBuilder hubConnectionBuilder,
IUrlHelper urlHelper)
: base(mediator, authConfig, urlHelper)
{
_hubConnectionBuilder = hubConnectionBuilder;
}
[HttpGet(Name = nameof(GetEvents))]
@ -47,7 +54,18 @@ namespace Sia.Gateway.Controllers
{
return NotFound(notFoundMessage);
}
await SendEventToSubscribers(result);
return Created($"api/incidents/{result.IncidentId}/events/{result.Id}", result);
}
private async Task SendEventToSubscribers(Domain.Event result)
{
var eventHubConnection = _hubConnectionBuilder
.WithUrl($"{Request.Scheme}://{Request.Host}/{EventsHub.HubPath}")
.Build();
await eventHubConnection.StartAsync();
await eventHubConnection.SendAsync("Send", result);
eventHubConnection.DisposeAsync();
}
}
}

Просмотреть файл

@ -0,0 +1,30 @@
using Microsoft.AspNetCore.SignalR;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using Sia.Domain;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Sia.Gateway.Hubs
{
public class EventsHub : Hub
{
public const string HubPath = "events/live";
public EventsHub() : base()
{
}
public Task Send(Event ev)
{
return Clients.All.InvokeAsync("Send", Json(ev));
}
private string Json<T>(T toSerialize) => JsonConvert.SerializeObject(toSerialize, new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
});
}
}

Просмотреть файл

@ -4,13 +4,19 @@ using Sia.Shared.Authentication;
namespace Sia.Gateway.Initialization
{
public static class SecretVaultStartup
public static class ApplicationInsightsStartup
{
public static AzureSecretVault Initialize(IHostingEnvironment env, IConfigurationRoot configuration)
public static AzureSecretVault InitializeApplicationInsights(this IHostingEnvironment env, IConfigurationRoot configuration)
{
//Needs to be done in the initial Startup.Startup() method because Application Insights registers itself prior
//to ConfigureServices being run
var secrets = new AzureSecretVault(configuration);
var secrets = new AzureSecretVault(
new KeyVaultConfiguration(
configuration["ClientId"],
configuration["ClientSecret"],
configuration["KeyVault:VaultName"]
)
);
var vaultTask = secrets.Get(configuration.GetSection("KeyVault")["InstrumentationKeyName"]);
vaultTask.Wait();

Просмотреть файл

@ -2,6 +2,7 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using Sia.Gateway.Hubs;
using Sia.Gateway.Middleware;
using System;
using System.Collections.Generic;
@ -14,6 +15,9 @@ namespace Sia.Gateway.Initialization
public static void AddMiddleware(this IApplicationBuilder app, IHostingEnvironment env, IConfigurationRoot configuration)
{
app.UseAuthentication();
app.UseSession();
app.UseCors(builder =>
builder
.WithOrigins(LoadAcceptableOriginsFromConfig(configuration))
@ -21,8 +25,11 @@ namespace Sia.Gateway.Initialization
.AllowAnyMethod()
.AllowCredentials()
);
app.UseAuthentication();
app.UseSession();
app.UseSignalR(routes =>
{
routes.MapHub<EventsHub>(EventsHub.HubPath);
});
if (env.IsDevelopment() || env.IsStaging()) app.UseDeveloperExceptionPage();
app.UseMiddleware<ExceptionHandler>();

Просмотреть файл

@ -1,6 +1,7 @@
using MediatR;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Routing;
@ -13,7 +14,12 @@ using Sia.Data.Incidents;
using Sia.Gateway.Authentication;
using Sia.Gateway.Requests;
using Sia.Gateway.ServiceRepositories;
using Sia.Shared.Authentication;
using Sia.Shared.Validation;
using StackExchange.Redis;
using System;
using System.Collections.Generic;
using System.Net;
using System.Reflection;
using System.Runtime.Loader;
@ -30,34 +36,7 @@ namespace Sia.Gateway.Initialization
if (env.IsDevelopment()) services.AddDbContext<IncidentContext>(options => options.UseInMemoryDatabase("Live"));
if (env.IsStaging()) services.AddDbContext<IncidentContext>(options => options.UseSqlServer(config.GetConnectionString("incidentStaging")));
var ticketConnectorAssemblyPath = config["Connector:Ticket:Path"];
if (!string.IsNullOrEmpty(ticketConnectorAssemblyPath))
{
LoadConnectorFromAssembly(services, env, config, ticketConnectorAssemblyPath);
}
else
{
var proxyEndpoint = config["Connector:Ticket:ProxyEndpoint"];
if (!string.IsNullOrEmpty(proxyEndpoint))
{
services.AddIncidentClient(typeof(Ticket));
var proxyAuthType = config["Connector:Ticket:ProxyAuthType"];
if (proxyAuthType == "Certificate")
{
services.AddProxyWithCert(proxyEndpoint, config["Connector:Ticket:ProxyCertThumbprint"]);
}
else
{
services.AddProxyWithoutAuth(proxyEndpoint);
}
}
else
{
services.AddIncidentClient(typeof(EmptyTicket));
services.AddNoTicketingSystem();
}
}
AddTicketingConnector(services, env, config);
services.AddScoped<IEventRepository, EventRepository>();
services.AddScoped<IEngagementRepository, EngagementRepository>();
@ -66,6 +45,57 @@ namespace Sia.Gateway.Initialization
services.AddSingleton<AzureActiveDirectoryAuthenticationInfo>(i => incidentAuthConfig);
}
private static void AddTicketingConnector(IServiceCollection services, IHostingEnvironment env, IConfigurationRoot config)
{
if (TryGetConfigValue(config, "Connector:Ticket:Path", out var ticketConnectorAssemblyPath))
{
LoadConnectorFromAssembly(services, env, config, ticketConnectorAssemblyPath);
return;
}
if (TryGetConfigValue(config, "Connector:Ticket:ProxyEndpoint", out var proxyEndpoint))
{
AddProxyConnector(services, config, proxyEndpoint);
return;
}
services.AddIncidentClient(typeof(EmptyTicket));
services.AddNoTicketingSystem();
}
private static bool TryGetConfigValue(IConfigurationRoot config, string configName, out string configValue)
{
ThrowIf.NullOrWhiteSpace(configName, nameof(configName));
configValue = config[configName];
return !string.IsNullOrEmpty(configValue);
}
private static void AddProxyConnector(IServiceCollection services, IConfigurationRoot config, string proxyEndpoint)
{
services.AddIncidentClient(typeof(Ticket));
var proxyAuthType = config["Connector:Ticket:ProxyAuthType"];
switch(proxyAuthType)
{
case "Certificate":
services.AddProxyWithCert(proxyEndpoint, config["Connector:Ticket:ProxyCertThumbprint"]);
return;
case "VaultCertificate":
services.AddProxyWithCertFromKeyVault(
proxyEndpoint,
new KeyVaultConfiguration(
config["ClientId"],
config["ClientSecret"],
config["Connector:Ticket:VaultName"]
),
config["Connector:Ticket:CertName"]
);
return;
default:
services.AddProxyWithoutAuth(proxyEndpoint);
return;
}
}
private static void LoadConnectorFromAssembly(IServiceCollection services, IHostingEnvironment env, IConfigurationRoot config, string ticketConnectorAssemblyPath)
{
var connectorAssembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(ticketConnectorAssemblyPath);
@ -110,6 +140,14 @@ namespace Sia.Gateway.Initialization
services.AddDistributedMemoryCache();
services.AddSession();
services.AddCors();
services.AddSockets();
services.AddSignalR().AddRedis(redisOptions =>
{
redisOptions.Options.EndPoints.Add(config["Redis:CacheEndpoint"]);
redisOptions.Options.Ssl = true;
redisOptions.Options.Password = config["Redis:Password"];
});
services.AddScoped<HubConnectionBuilder>();
}
}
}

Просмотреть файл

@ -25,13 +25,12 @@ namespace Sia.Gateway
}
_configuration = builder.Build();
_secrets = SecretVaultStartup.Initialize(env, _configuration);
env.InitializeApplicationInsights(_configuration);
_env = env;
}
private IHostingEnvironment _env { get; }
private IConfigurationRoot _configuration { get; }
private ISecretVault _secrets { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)

Просмотреть файл

@ -14,6 +14,9 @@
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Cors" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Session" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.0.0-alpha1-final" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="1.0.0-alpha1-final" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Redis" Version="1.0.0-alpha1-final" />
<PackageReference Include="Microsoft.Azure.KeyVault" Version="2.3.2" />
<PackageReference Include="Microsoft.Azure.KeyVault.Core" Version="2.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="2.0.0" />
@ -21,13 +24,13 @@
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="2.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="2.0.0" />
<PackageReference Include="Microsoft.IdentityModel.Clients.ActiveDirectory" Version="3.16.1" />
<PackageReference Include="Microsoft.Sia.Connectors.Tickets" Version="1.0.5-alpha" />
<PackageReference Include="System.Globalization.Extensions" Version="4.3.0" />
</ItemGroup>
<ItemGroup>
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="1.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Sia.Connectors.Tickets\Sia.Connectors.Tickets.csproj" />
<ProjectReference Include="..\Sia.Data.Incident\Sia.Data.Incidents.csproj" />
<ProjectReference Include="..\Sia.Domain.ApiModels\Sia.Domain.ApiModels.csproj" />
<ProjectReference Include="..\Sia.Domain\Sia.Domain.csproj" />

Просмотреть файл

@ -15,5 +15,26 @@
"Your uri here" //Replace with your frontend uri
]
},
"Redis": {
"CacheEndpoint": "yourCache.redis.cache.windows.net:6380",
"Password": "YOURPASSWORD"
},
"Connector": {
"Ticket": {
//If loading from assembly
"Path": "Path to connector DLL",
//If using proxy
"ProxyEndpoint": "endpoint URL",
"ProxyAuthType": "Proxy authentication type (Certificate, VaultCertificate, or None)",
//If using 'Certificate' proxy auth type
"ProxyCertThumbprint": "Certificate thumbprint",
//If using VaultCertificate auth with either assembly or proxy
"CertName": "certificate name as stored in KeyVault",
"VaultName": "name of KeyVault instance to use when retrieving certificate"
}
},
"ClientSecret": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaa" //Replace with your client secret
}

Просмотреть файл

@ -2,7 +2,9 @@
using Microsoft.Azure.KeyVault.Models;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using Sia.Shared.Validation;
using System;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
namespace Sia.Shared.Authentication
@ -13,35 +15,48 @@ namespace Sia.Shared.Authentication
}
public class AzureSecretVault : ISecretVault
{
private readonly string _vault;
private readonly string _clientId;
private readonly string _secret;
private readonly KeyVaultConfiguration _config;
private const string _secretsEndpoint = "/secrets/";
private const string _keysEndpoint = "/keys/";
private const string _certificatesEndpoint = "/certificates/";
public AzureSecretVault(IConfigurationRoot configuration)
public AzureSecretVault(KeyVaultConfiguration configuration)
{
_clientId = configuration["ClientId"];
_secret = configuration["ClientSecret"];
_vault = String.Format(secretUriBase, configuration.GetSection("KeyVault")["VaultName"]);
_config = ThrowIf.Null(configuration, nameof(configuration));
}
public async Task<string> Get(string secretName)
{
try
{
var secret = await GetKeyVaultClient().GetSecretAsync(_vault + secretName).ConfigureAwait(false);
var secret = await GetKeyVaultClient().GetSecretAsync(_config.Vault + _secretsEndpoint + secretName).ConfigureAwait(false);
return secret.Value;
}
catch (KeyVaultErrorException)
catch (KeyVaultErrorException ex)
{
return string.Empty;
}
}
public async Task<X509Certificate2> GetCertificate(string certificateName)
{
try
{
var cert = await GetKeyVaultClient()
.GetCertificateAsync(_config.Vault, certificateName)
.ConfigureAwait(false);
return new X509Certificate2(cert.Cer);
}
catch (KeyVaultErrorException ex)
{
return null;
}
}
private async Task<string> GetToken(string authority, string resource, string scope)
{
var authContext = new AuthenticationContext(authority);
ClientCredential clientCred = new ClientCredential(_clientId,
_secret);
ClientCredential clientCred = new ClientCredential(_config.ClientId, _config.ClientSecret);
AuthenticationResult result = await authContext.AcquireTokenAsync(resource, clientCred);
if (result == null)
@ -50,8 +65,6 @@ namespace Sia.Shared.Authentication
return result.AccessToken;
}
private const string secretUriBase = "https://{0}.vault.azure.net/secrets/";
private KeyVaultClient GetKeyVaultClient() => new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(GetToken));
}

Просмотреть файл

@ -0,0 +1,29 @@
using Sia.Shared.Validation;
using System;
using System.Collections.Generic;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;
namespace Sia.Shared.Authentication
{
public class KeyVaultCertificateRetriever
: CertificateRetriever
{
private readonly X509Certificate2 _certificate;
public KeyVaultCertificateRetriever(AzureSecretVault certificateVault, string certificateName)
{
ThrowIf.NullOrWhiteSpace(certificateName, nameof(certificateName));
var certTask = certificateVault.GetCertificate(certificateName);
Task.WaitAll(new Task[] { certTask });
if (certTask.IsCompleted)
{
_certificate = certTask.Result;
}
}
public override X509Certificate2 Certificate => _certificate;
}
}

Просмотреть файл

@ -0,0 +1,24 @@
using Microsoft.Extensions.Configuration;
using Sia.Shared.Validation;
using System;
using System.Collections.Generic;
using System.Text;
namespace Sia.Shared.Authentication
{
public class KeyVaultConfiguration
{
public KeyVaultConfiguration(string clientId, string clientSecret, string vault)
{
ClientId = ThrowIf.NullOrWhiteSpace(clientId, nameof(clientId));
ClientSecret = ThrowIf.NullOrWhiteSpace(clientSecret, nameof(clientSecret));
Vault = String.Format(secretUriBase, ThrowIf.NullOrWhiteSpace(vault, nameof(vault)));
}
private const string secretUriBase = "https://{0}.vault.azure.net";
public readonly string Vault;
public readonly string ClientId;
public readonly string ClientSecret;
}
}

Просмотреть файл

@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<Version>1.0.7-alpha</Version>
<Version>1.0.8-alpha</Version>
<PackageId>Microsoft.Sia.Shared</PackageId>
<Authors>pdimit, magpint, jache, chtownes</Authors>
<Company>Microsoft</Company>