This commit is contained in:
Philip Dimitratos 2017-10-23 13:06:20 -07:00
Родитель 68bb6b8fd6 b945b6c149
Коммит d9ba93e9cc
63 изменённых файлов: 1094 добавлений и 590 удалений

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

@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26730.12
VisualStudioVersion = 15.0.26730.16
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sia.Shared", "src\Sia.Shared\Sia.Shared.csproj", "{C4AA9F6D-DFB1-4044-A7CD-FA3B6D5BA6AE}"
EndProject
@ -21,6 +21,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{931F26AB-5
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sia.Gateway.Tests", "test\Sia.Gateway.Tests\Sia.Gateway.Tests.csproj", "{7FED8930-1B06-4954-8337-C2DEC6697D6F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sia.Shared.Tests", "Sia.Shared.Tests\Sia.Shared.Tests.csproj", "{BF2EE536-9EB2-4E97-8F6A-BDF6571BAA67}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sia.Data.Playbooks", "Sia.Data.Playbooks\Sia.Data.Playbooks.csproj", "{8223434F-1377-4793-869A-ADF9EE2F08AF}"
EndProject
Global
@ -57,6 +59,10 @@ Global
{7FED8930-1B06-4954-8337-C2DEC6697D6F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7FED8930-1B06-4954-8337-C2DEC6697D6F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7FED8930-1B06-4954-8337-C2DEC6697D6F}.Release|Any CPU.Build.0 = Release|Any CPU
{BF2EE536-9EB2-4E97-8F6A-BDF6571BAA67}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BF2EE536-9EB2-4E97-8F6A-BDF6571BAA67}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BF2EE536-9EB2-4E97-8F6A-BDF6571BAA67}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BF2EE536-9EB2-4E97-8F6A-BDF6571BAA67}.Release|Any CPU.Build.0 = Release|Any CPU
{8223434F-1377-4793-869A-ADF9EE2F08AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8223434F-1377-4793-869A-ADF9EE2F08AF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8223434F-1377-4793-869A-ADF9EE2F08AF}.Release|Any CPU.ActiveCfg = Release|Any CPU
@ -73,6 +79,7 @@ Global
{B92790C5-C4F1-4052-ABA7-68448F28DDB2} = {09E441A6-06F8-457A-981A-394473FB8CD5}
{8D465104-1385-4EE4-A1ED-AEE7708A537F} = {09E441A6-06F8-457A-981A-394473FB8CD5}
{7FED8930-1B06-4954-8337-C2DEC6697D6F} = {931F26AB-58CD-4667-A281-EEC186F2FB59}
{BF2EE536-9EB2-4E97-8F6A-BDF6571BAA67} = {931F26AB-58CD-4667-A281-EEC186F2FB59}
{8223434F-1377-4793-869A-ADF9EE2F08AF} = {09E441A6-06F8-457A-981A-394473FB8CD5}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution

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

@ -0,0 +1,72 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json.Linq;
using Sia.Shared.Data;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Sia.Shared.Tests.Data
{
[TestClass]
public class PartialJsonResolverTests
{
[TestMethod]
public void ResolveJsonToString_Method_Resolve_SerializesSourceArgumentObjectToString()
{
var input = new TestHasJsonDataObject
{
Data = new JsonSerializationTestObject()
};
var expectedResultDataValue = JsonSerializationTestObject.ExpectedSerialization();
var objectUnderTest = new ResolveJsonToString<TestHasJsonDataObject, TestHasJsonDataString>();
var result = objectUnderTest.Resolve(input, null, null, null);
Assert.AreEqual(expectedResultDataValue, result, false);
}
[TestMethod]
public void ResolveStringToJson_Method_Resolve_SerializesSourceArgumentStringToObject()
{
var input = new TestHasJsonDataString()
{
Data = JsonSerializationTestObject.ExpectedSerialization()
};
var expectedResult = new JsonSerializationTestObject();
var objectUnderTest = new ResolveStringToJson<TestHasJsonDataString, TestHasJsonDataObject>();
var result = objectUnderTest.Resolve(input, null, null, null);
Assert.AreEqual(expectedResult.a, ExtractPropertyFromResult(result, "a"));
Assert.AreEqual(expectedResult.b, ExtractPropertyFromResult(result, "b"));
}
private static JToken ExtractPropertyFromResult(object result, string propName) => ((JObject)result).Property(propName).Value;
}
internal class JsonSerializationTestObject :IEquatable<JsonSerializationTestObject>
{
public static string ExpectedSerialization()
=> "{\"a\":\"ValueOfA\",\"b\":1}";
public bool Equals(JsonSerializationTestObject other)
=> a == other.a && b == other.b;
public string a { get; set; } = "ValueOfA";
public int b { get; set; } = 1;
}
internal class TestHasJsonDataString : IJsonDataString
{
public string Data { get; set; }
}
internal class TestHasJsonDataObject : IJsonDataObject
{
public object Data { get; set; }
}
}

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

@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.5.0-preview-20171012-09" />
<PackageReference Include="MSTest.TestAdapter" Version="1.2.0" />
<PackageReference Include="MSTest.TestFramework" Version="1.2.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\src\Sia.Shared\Sia.Shared.csproj" />
</ItemGroup>
</Project>

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

@ -0,0 +1,65 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Sia.Shared.Validation;
using System;
using System.Collections.Generic;
using System.Text;
namespace Sia.Shared.Tests.Validation
{
[TestClass]
public class ThrowIfTests
{
[TestMethod]
public void Null_StaticMethod_WhenObjectIsNotNull_ReturnsObject()
{
var input = new Object();
var result = ThrowIf.Null(input, nameof(input));
Assert.AreSame(input, result);
}
[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void Null_StaticMethod_WhenObjectIsNull_ThrowsArgumentNullException()
{
object input = null;
var result = ThrowIf.Null(input, nameof(input));
//expect exception
}
[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void NullOrWhiteSpace_StaticMethod_WhenInputIsNull_ThrowsArgumentException()
{
string input = null;
var result = ThrowIf.NullOrWhiteSpace(input, nameof(input));
//Expect exception
}
[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void NullOrWhiteSpace_StaticMethod_WhenInputIsOnlyWhitespace_ThrowsArgumentException()
{
string input = " ";
var result = ThrowIf.NullOrWhiteSpace(input, nameof(input));
//Expect exception
}
[TestMethod]
public void NullOrWhiteSpace_StaticMethod_WhenInputStringWithAnyNonWhitespace_ReturnsString()
{
string input = " . ";
var result = ThrowIf.NullOrWhiteSpace(input, nameof(input));
Assert.AreSame(input, result);
}
}
}

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

@ -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,14 +14,17 @@ 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)
{
return services
.AddScoped<Converter<Ticket>, ProxyConverter>()
.AddScoped<Client<Ticket>>(serv => proxyConnection.GetClient())
.AddScoped<Connector<Ticket>, ProxyConnector>();
.AddScoped<Converter<ProxyTicket>, ProxyConverter>()
.AddScoped<Client<ProxyTicket>>(serv => proxyConnection.GetClient())
.AddScoped<Connector<ProxyTicket>, ProxyConnector>();
}
}
}

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

@ -4,7 +4,7 @@ using System.Threading.Tasks;
namespace Sia.Connectors.Tickets.TicketProxy
{
public class ProxyClient : Client<Ticket>
public class ProxyClient : Client<ProxyTicket>
{
private readonly string _endpoint;
private readonly HttpClient _client;
@ -15,12 +15,12 @@ namespace Sia.Connectors.Tickets.TicketProxy
_client = singletonClient;
}
public override async Task<Ticket> GetAsync(string originId)
public override async Task<ProxyTicket> GetAsync(string originId)
{
string incidentUrl = $"{_endpoint}/{originId}";
var response = await _client.GetAsync(incidentUrl);
var content = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<Ticket>(content);
return JsonConvert.DeserializeObject<ProxyTicket>(content);
}
}
}

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

@ -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:

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

@ -1,8 +1,8 @@
namespace Sia.Connectors.Tickets.TicketProxy
{
public class ProxyConnector : Connector<Ticket>
public class ProxyConnector : Connector<ProxyTicket>
{
public ProxyConnector(Client<Ticket> client, Converter<Ticket> converter)
public ProxyConnector(Client<ProxyTicket> client, Converter<ProxyTicket> converter)
: base(client, converter)
{
}

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

@ -3,9 +3,9 @@ using System.Collections.Generic;
namespace Sia.Connectors.Tickets.TicketProxy
{
public class ProxyConverter : Converter<Ticket>
public class ProxyConverter : Converter<ProxyTicket>
{
public override ICollection<Event> ExtractEvents(Ticket ticket)
public override ICollection<Event> ExtractEvents(ProxyTicket ticket)
{
return new List<Event>();
}

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

@ -1,6 +1,6 @@
namespace Sia.Connectors.Tickets.TicketProxy
{
public class Ticket
public class ProxyTicket
{
public string OriginId { get; set; }
public long IncidentSystemId { get; set; }

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

@ -1,14 +1,18 @@
using Sia.Shared.Data;
using Microsoft.EntityFrameworkCore;
using Sia.Shared.Data;
using System;
using System.ComponentModel.DataAnnotations.Schema;
namespace Sia.Data.Incidents.Models
{
public class Event : IEntity
public class Event : IEntity, IJsonDataString
{
public long Id { get; set; }
public long? IncidentId { get; set; }
public long EventTypeId { get; set; }
public DateTime Occurred { get; set; }
public DateTime EventFired { get; set; }
[Column(TypeName = "varchar(max)")]
public string Data { get; set; }
}
}

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

@ -2,6 +2,7 @@
using Sia.Data.Incidents.Models;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Sia.Data.Incidents
{
@ -11,8 +12,10 @@ namespace Sia.Data.Incidents
const int differentEventTypes = 8;
const int eventCountForManyEvents = 1000;
//Some dev/test/demo data that was based on actual incidents has been [REDACTED]
public static void Add(IncidentContext incidentContext, SeedType seedtype)
public static void Add(IncidentContext incidentContext, SeedType seedtype = SeedType.Basic)
{
if (incidentContext.Incidents.Any()) return; //This context already has seed data loaded
var firstTestIncidentSystem = new TicketingSystem
{
Name = "Not Our Ticketing System Name"

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

@ -16,10 +16,13 @@
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="2.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="2.0.0" />
<PackageReference Include="Microsoft.Sia.Shared" Version="1.0.7-alpha" />
<PackageReference Include="System.Data.SqlClient" Version="4.4.0" />
<PackageReference Include="System.Globalization.Extensions" Version="4.3.0" />
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Sia.Shared\Sia.Shared.csproj" />
</ItemGroup>
</Project>

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

@ -1,9 +1,12 @@
using System;
using Sia.Shared.Data;
using System;
using System.ComponentModel.DataAnnotations;
using System.Dynamic;
namespace Sia.Domain.ApiModels
{
public class NewEvent
:IJsonDataObject
{
[Required]
public long? EventTypeId { get; set; }
@ -11,5 +14,6 @@ namespace Sia.Domain.ApiModels
public DateTime? Occurred { get; set; }
[Required]
public DateTime? EventFired { get; set; }
public object Data { get; set; }
}
}

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

@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
namespace Sia.Domain.ApiModels
{
@ -8,7 +9,26 @@ namespace Sia.Domain.ApiModels
[Required]
public string Title { get; set; }
[Required]
public Ticket PrimaryTicket { get; set; }
public Ticket PrimaryTicket
{
get
{
return Tickets.FirstOrDefault(ticket => ticket.IsPrimary);
}
set
{
if (Tickets == null) Tickets = new List<Ticket>();
foreach (var ticket in Tickets.Where(ticket => ticket.IsPrimary))
{
ticket.IsPrimary = false;
}
if (value == null) return;
if (!Tickets.Contains(value)) Tickets.Add(value);
value.IsPrimary = true;
}
}
public IList<Ticket> Tickets { get; set; }
= new List<Ticket>();
public IList<NewEvent> Events { get; set; }

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

@ -20,6 +20,7 @@
<ItemGroup>
<ProjectReference Include="..\Sia.Domain\Sia.Domain.csproj" />
<ProjectReference Include="..\Sia.Shared\Sia.Shared.csproj" />
</ItemGroup>
</Project>

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

@ -2,16 +2,18 @@
using Sia.Shared.Data;
using System;
using System.Collections.Generic;
using System.Dynamic;
using System.Text;
namespace Sia.Domain
{
public class Event : IEntity
public class Event : IEntity, IJsonDataObject
{
public long Id { get; set; }
public long? IncidentId { get; set; }
public long EventTypeId { get; set; }
public DateTime Occurred { get; set; }
public DateTime EventFired { get; set; }
public object Data { get; set; }
}
}

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

@ -8,7 +8,7 @@ using Sia.Gateway.Validation.Filters;
namespace Sia.Gateway.Controllers
{
[Return400BadRequestWhenModelStateInvalid]
//[Authorize()]
[Authorize()]
public abstract class BaseController : Controller
{
protected readonly IMediator _mediator;

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

@ -36,7 +36,7 @@ namespace Sia.Gateway.Controllers
{
return NotFound(notFoundMessage);
}
return Created($"api/incidents/{result.IncidentId}/engagements/{result.Id}", result);
return Created($"incidents/{result.IncidentId}/engagements/{result.Id}", result);
}
[HttpPut("{engagementId}")]

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

@ -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,17 +15,22 @@ 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))]
public async Task<IActionResult> GetEvents([FromRoute]long incidentId, [FromQuery]PaginationMetadata pagination)
{
var result = await _mediator.Send(new GetEventsRequest(incidentId, pagination, _authContext));
Response.Headers.AddPagination(new PaginationHeader(pagination, _urlHelper, nameof(GetEvents)));
Response.Headers.AddPagination(new LinksHeader(pagination, _urlHelper, nameof(GetEvents)));
return Ok(result);
}
@ -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);
await eventHubConnection.DisposeAsync();
}
}
}

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

@ -1,8 +1,10 @@
using MediatR;
using Microsoft.AspNetCore.Mvc;
using Sia.Domain;
using Sia.Domain.ApiModels;
using Sia.Gateway.Authentication;
using Sia.Gateway.Requests;
using System.Collections.Generic;
using System.Threading.Tasks;
@ -19,11 +21,7 @@ namespace Sia.Gateway.Controllers
[HttpGet("{id}")]
public async Task<IActionResult> Get(string id)
{
var result = await _mediator.Send(new GetIncidentsByTicketRequest(id, _authContext));
if (result == null)
{
return NotFound($"{nameof(Incident)} not found");
}
var result = await _mediator.Send(new GetIncidentsByTicketCreateIfNeededRequest(id, _authContext));
return Ok(result);
}
}

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

@ -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()
});
}
}

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

@ -0,0 +1,32 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Sia.Shared.Authentication;
namespace Sia.Gateway.Initialization
{
public static class ApplicationInsightsStartup
{
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(
new KeyVaultConfiguration(
configuration["ClientId"],
configuration["ClientSecret"],
configuration["KeyVault:VaultName"]
)
);
var instrumentationKey = configuration.GetSection("KeyVault")["InstrumentationKeyName"];
if (!string.IsNullOrWhiteSpace(instrumentationKey))
{
var vaultTask = secrets.Get(instrumentationKey);
vaultTask.Wait();
configuration.GetSection("ApplicationInsights")["InstrumentationKey"] = vaultTask.Result;
}
return secrets;
}
}
}

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

@ -2,6 +2,7 @@
using AutoMapper.EquivalencyExpression;
using Sia.Domain;
using Sia.Domain.ApiModels;
using Sia.Gateway.Protocol;
using Sia.Shared.Data;
namespace Sia.Gateway.Initialization
@ -32,25 +33,35 @@ namespace Sia.Gateway.Initialization
configuration.CreateMap<Participant, Data.Incidents.Models.Participant>();
configuration.CreateMap<Data.Incidents.Models.Participant, Participant>();
configuration.CreateMap<NewEvent, Data.Incidents.Models.Event>().EqualityInsertOnly();
configuration.CreateMap<Event, Data.Incidents.Models.Event>().EqualityById();
configuration.CreateMap<Data.Incidents.Models.Event, Event>().EqualityById();
configuration.CreateMap<NewEvent, Data.Incidents.Models.Event>().EqualityInsertOnly()
.UseResolveJsonToString();
configuration.CreateMap<Event, Data.Incidents.Models.Event>().EqualityById()
.UseResolveJsonToString();
configuration.CreateMap<Data.Incidents.Models.Event, Event>().EqualityById()
.UseResolveStringToJson();
});
}
private static IMappingExpression<TSource, TDestination> UseResolveJsonToString<TSource, TDestination>(this IMappingExpression<TSource, TDestination> mapping)
where TSource: IJsonDataObject
where TDestination: IJsonDataString
=> mapping.ForMember((ev) => ev.Data, (config) => config.ResolveUsing<ResolveJsonToString<TSource, TDestination>>());
private static IMappingExpression<TSource, TDestination> UseResolveStringToJson<TSource, TDestination>(this IMappingExpression<TSource, TDestination> mapping)
where TSource : IJsonDataString
where TDestination : IJsonDataObject
=> mapping.ForMember((ev) => ev.Data, (config) => config.ResolveUsing<ResolveStringToJson<TSource, TDestination>>());
public static IMappingExpression<T1, T2> EqualityInsertOnly<T1, T2>(this IMappingExpression<T1, T2> mappingExpression)
where T1 : class
where T2 : class
{
return mappingExpression.EqualityComparison((one, two) => false);
}
where T2 : class
=> mappingExpression.EqualityComparison((one, two) => false);
public static IMappingExpression<T1, T2> EqualityById<T1, T2>(this IMappingExpression<T1, T2> mappingExpression)
where T1 : class, IEntity
where T2 : class, IEntity
{
return mappingExpression.EqualityComparison((one, two) => one.Id == two.Id);
}
where T2 : class, IEntity
=> mappingExpression.EqualityComparison((one, two) => one.Id == two.Id);
}
}

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

@ -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,22 +0,0 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Sia.Shared.Authentication;
namespace Sia.Gateway.Initialization
{
public static class SecretVaultStartup
{
public static AzureSecretVault Initialize(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 vaultTask = secrets.Get(configuration.GetSection("KeyVault")["InstrumentationKeyName"]);
vaultTask.Wait();
configuration.GetSection("ApplicationInsights")["InstrumentationKey"] = vaultTask.Result;
return secrets;
}
}
}

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

@ -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;
@ -12,10 +13,14 @@ using Sia.Connectors.Tickets.TicketProxy;
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 System;
using System.Reflection;
using System.Runtime.Loader;
using Sia.Gateway.Protocol;
using System.Buffers;
using Sia.Domain;
namespace Sia.Gateway.Initialization
{
@ -24,45 +29,71 @@ namespace Sia.Gateway.Initialization
public static void AddFirstPartyServices(this IServiceCollection services, IHostingEnvironment env, IConfigurationRoot config)
{
ConfigureAuth(services, config);
var incidentAuthConfig = new AzureActiveDirectoryAuthenticationInfo(config["Incident:ClientId"], config["Incident:ClientSecret"], config["AzureAd:Tenant"]);
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();
}
}
services.AddScoped<IEventRepository, EventRepository>();
services.AddScoped<IEngagementRepository, EngagementRepository>();
services.AddTicketingConnector(env, config);
services.AddSingleton<IConfigurationRoot>(i => config);
}
private static void AddTicketingConnector(this 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(this 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(ProxyTicket));
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 ConfigureAuth(IServiceCollection services, IConfigurationRoot config)
{
var incidentAuthConfig = new AzureActiveDirectoryAuthenticationInfo(config["Incident:ClientId"], config["Incident:ClientSecret"], config["AzureAd:Tenant"]);
services.AddSingleton<AzureActiveDirectoryAuthenticationInfo>(i => incidentAuthConfig);
}
@ -80,21 +111,24 @@ namespace Sia.Gateway.Initialization
private static void AddIncidentClient(this IServiceCollection services, Type ticketType)
{
var clientType = typeof(IncidentRepository<>).MakeGenericType(new Type[] { ticketType });
services.AddScoped(typeof(IIncidentRepository), clientType);
var handlerType = typeof(GetIncidentHandler<>).MakeGenericType(new Type[] { ticketType });
services.AddScoped(typeof(IGetIncidentHandler), handlerType);
services.AddScoped<IAsyncRequestHandler<GetIncidentRequest, Incident>, GetIncidentHandlerWrapper>();
}
public static void AddThirdPartyServices(this IServiceCollection services, IConfigurationRoot config)
{
//Adds every request type in the Sia.Gateway assembly
services.AddMediatR(typeof(GetIncidentRequest).GetTypeInfo().Assembly);
services.AddSingleton<IActionContextAccessor, ActionContextAccessor>();
services.AddScoped<IUrlHelper, UrlHelper>(iFactory
=> new UrlHelper(iFactory.GetService<IActionContextAccessor>().ActionContext)
);
services.AddMvc();
services.AddMvc(options =>
{
options.OutputFormatters.Insert(0, new PartialSerializedJsonOutputFormatter(
new MvcJsonOptions().SerializerSettings,
ArrayPool<char>.Shared));
});
services
.AddAuthentication(authOptions =>
{
@ -110,6 +144,31 @@ namespace Sia.Gateway.Initialization
services.AddDistributedMemoryCache();
services.AddSession();
services.AddCors();
services.AddSockets();
services.AddSignalR(config);
services.AddScoped<HubConnectionBuilder>();
//Adds every request type in the Sia.Gateway assembly
services.AddMediatR(typeof(GetIncidentRequest).GetTypeInfo().Assembly);
}
private static IServiceCollection AddSignalR(this IServiceCollection services, IConfigurationRoot config)
{
var signalRBuilder = services.AddSignalR();
if (config.TryGetConfigValue("Redis:CacheEndpoint", out string cacheEndpoint)
&& config.TryGetConfigValue("Redis:Password", out string cachePassword))
{
signalRBuilder.AddRedis(redisOptions =>
{
redisOptions.Options.EndPoints.Add(cacheEndpoint);
redisOptions.Options.Ssl = true;
redisOptions.Options.Password = cachePassword;
});
}
return services;
}
}
}

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

@ -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)

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

@ -4,20 +4,20 @@ using Newtonsoft.Json;
namespace Sia.Gateway.Protocol
{
public class PaginationHeader
public class LinksHeader
{
private PaginationMetadata _metadata;
private IUrlHelper _urlHelper;
private string _routeName;
public PaginationHeader(PaginationMetadata metadata, IUrlHelper urlHelper, string routeName)
public LinksHeader(PaginationMetadata metadata, IUrlHelper urlHelper, string routeName)
{
_metadata = metadata;
_urlHelper = urlHelper;
_routeName = routeName;
}
public string HeaderName => "X-Pagination";
public const string HeaderName = "links";
public StringValues HeaderValues => JsonConvert.SerializeObject(new
{
PageNumber = _metadata.PageNumber,
@ -37,10 +37,10 @@ namespace Microsoft.AspNetCore.Mvc
public static class PaginationExtensions
{
public static void AddPagination(this IHeaderDictionary headers, PaginationHeader header)
public static void AddPagination(this IHeaderDictionary headers, LinksHeader header)
{
headers.Add("Access-Control-Expose-Headers", "X-Pagination");
headers.Add(header.HeaderName, header.HeaderValues);
headers.Add("Access-Control-Expose-Headers", LinksHeader.HeaderName);
headers.Add(LinksHeader.HeaderName, header.HeaderValues);
}
}
}

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

@ -0,0 +1,51 @@
using Microsoft.AspNetCore.Mvc.Formatters;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Newtonsoft.Json;
using System.Buffers;
using System.Text;
using Sia.Shared.Data;
namespace Sia.Gateway.Protocol
{
public class PartialSerializedJsonOutputFormatter : JsonOutputFormatter
{
public PartialSerializedJsonOutputFormatter(JsonSerializerSettings serializerSettings, ArrayPool<char> charPool)
: base(serializerSettings, charPool)
{
}
public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
{
var dataStream = (IEnumerable<IJsonDataObject>)context.Object;
foreach (var objectToWrite in dataStream)
{
var jsonData = objectToWrite.Data;
if (jsonData is string) objectToWrite.Data = Deserialize((string)jsonData);
}
return base.WriteResponseBodyAsync(context, selectedEncoding);
}
private const int NumberOfCharactersInGenericTypeNotUsedByGetInterfaceMethod = 3;
protected override bool CanWriteType(Type type)
{
if (!type.IsGenericType) return false;
if (type.GetGenericArguments().Count() != 1) return false;
var enumIntName = typeof(IEnumerable<>).ToString();
var enumerableInterface = type.GetInterface(enumIntName
.Substring(0, enumIntName.Length - NumberOfCharactersInGenericTypeNotUsedByGetInterfaceMethod));
if (enumerableInterface is null) return false;
return !(type.GetGenericArguments()[0].GetInterface(nameof(IJsonDataObject)) is null);
}
private object Deserialize(string serializedData) => JsonConvert.DeserializeObject(serializedData);
}
}

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

@ -1,9 +1,13 @@
using MediatR;
using AutoMapper;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Sia.Data.Incidents;
using Sia.Domain;
using Sia.Gateway.Authentication;
using Sia.Gateway.ServiceRepositories;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Sia.Gateway.Requests
{
public class GetEngagementRequest : AuthenticatedRequest, IRequest<Engagement>
@ -17,17 +21,24 @@ namespace Sia.Gateway.Requests
public long Id { get; }
public long IncidentId { get; }
}
public class GetEngagementHandler : IAsyncRequestHandler<GetEngagementRequest, Engagement>
{
private IEngagementRepository _incidentRepository;
public GetEngagementHandler(IEngagementRepository incidentRepository)
public class GetEngagementHandler
: IAsyncRequestHandler<GetEngagementRequest, Engagement>
{
private readonly IncidentContext _context;
public GetEngagementHandler(IncidentContext context)
{
_incidentRepository = incidentRepository;
_context = context;
}
public async Task<Engagement> Handle(GetEngagementRequest request)
{
return await _incidentRepository.GetEngagementAsync(request.IncidentId, request.Id, request.UserContext);
var EngagementRecord = await _context.Engagements
.Include(en => en.Participant)
.FirstOrDefaultAsync(ev => ev.IncidentId == request.IncidentId && ev.Id == request.Id);
if (EngagementRecord == null) throw new KeyNotFoundException();
return Mapper.Map<Engagement>(EngagementRecord);
}
}
}

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

@ -1,9 +1,14 @@
using MediatR;
using AutoMapper;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Sia.Data.Incidents;
using Sia.Domain;
using Sia.Domain.ApiModels;
using Sia.Gateway.Authentication;
using Sia.Gateway.ServiceRepositories;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Sia.Gateway.Requests
{
public class PostEngagementRequest : AuthenticatedRequest, IRequest<Engagement>
@ -18,18 +23,33 @@ namespace Sia.Gateway.Requests
public NewEngagement NewEngagement { get; }
public long IncidentId { get; }
}
public class PostEngagementHandler : IAsyncRequestHandler<PostEngagementRequest, Engagement>
public class PostEngagementHandler
: IAsyncRequestHandler<PostEngagementRequest, Engagement>
{
private IEngagementRepository _engagementRepository;
private readonly IncidentContext _context;
public PostEngagementHandler(IEngagementRepository engagementRepository)
public PostEngagementHandler(IncidentContext context)
{
_engagementRepository = engagementRepository;
_context = context;
}
public async Task<Engagement> Handle(PostEngagementRequest request)
{
return await _engagementRepository.PostEngagementAsync(request.IncidentId, request.NewEngagement, request.UserContext);
if (request.NewEngagement == null) throw new ArgumentNullException(nameof(request.NewEngagement));
var dataIncident = await _context.Incidents
.Include(cr => cr.Engagements)
.ThenInclude(en => en.Participant)
.FirstOrDefaultAsync(x => x.Id == request.IncidentId);
if (dataIncident == null) throw new KeyNotFoundException();
var dataEngagement = Mapper.Map<Data.Incidents.Models.Engagement>(request.NewEngagement);
dataEngagement.TimeEngaged = DateTime.UtcNow;
dataIncident.Engagements.Add(dataEngagement);
await _context.SaveChangesAsync();
return Mapper.Map<Engagement>(dataEngagement);
}
}
};
}

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

@ -1,9 +1,13 @@
using MediatR;
using AutoMapper;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Sia.Data.Incidents;
using Sia.Domain.ApiModels;
using Sia.Gateway.Authentication;
using Sia.Gateway.ServiceRepositories;
using System;
using System.Threading.Tasks;
namespace Sia.Gateway.Requests
{
public class PutEngagementRequest : AuthenticatedRequest, IRequest
@ -19,18 +23,26 @@ namespace Sia.Gateway.Requests
public long EngagementId { get; }
public long IncidentId { get; }
}
public class PutEngagementHandler : IAsyncRequestHandler<PutEngagementRequest>
public class PutEngagementHandler
: IAsyncRequestHandler<PutEngagementRequest>
{
private IEngagementRepository _engagementClient;
private readonly IncidentContext _context;
public PutEngagementHandler(IEngagementRepository incidentClient)
public PutEngagementHandler(IncidentContext context)
{
_engagementClient = incidentClient;
_context = context;
}
public async Task Handle(PutEngagementRequest request)
{
await _engagementClient.PutEngagementAsync(request.IncidentId, request.EngagementId, request.UpdateEngagement, request.UserContext);
if (request.UpdateEngagement == null) throw new ArgumentNullException(nameof(UpdateEngagement));
var existingRecord = await _context.Engagements
.Include(en => en.Participant)
.FirstOrDefaultAsync(engagement => engagement.IncidentId == request.IncidentId && engagement.Id == request.EngagementId);
var updatedModel = Mapper.Map(request.UpdateEngagement, existingRecord);
await _context.SaveChangesAsync();
}
}
}

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

@ -1,21 +0,0 @@
using MediatR;
using Sia.Gateway.ServiceRepositories;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Sia.Gateway.Requests.Events
{
public abstract class EventHandler<TRequest, TReturn> : IAsyncRequestHandler<TRequest, TReturn>
where TRequest : IRequest<TReturn>
{
protected EventHandler(IEventRepository eventRepository)
{
_eventRepository = eventRepository;
}
protected IEventRepository _eventRepository;
public abstract Task<TReturn> Handle(TRequest request);
}
}

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

@ -1,8 +1,10 @@
using MediatR;
using AutoMapper;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Sia.Data.Incidents;
using Sia.Domain;
using Sia.Gateway.Authentication;
using Sia.Gateway.Requests.Events;
using Sia.Gateway.ServiceRepositories;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Sia.Gateway.Requests
@ -19,17 +21,25 @@ namespace Sia.Gateway.Requests
public long Id { get; }
public long IncidentId { get; }
}
public class GetEventHandler : EventHandler<GetEventRequest, Event>
{
protected GetEventHandler(IEventRepository eventRepository)
: base(eventRepository)
{
}
public override async Task<Event> Handle(GetEventRequest request)
public class GetEventHandler : IAsyncRequestHandler<GetEventRequest, Event>
{
private readonly IncidentContext _context;
public GetEventHandler(IncidentContext context)
{
return await _eventRepository.GetEvent(request.IncidentId, request.Id, request.UserContext);
_context = context;
}
public async Task<Event> Handle(GetEventRequest request)
{
var eventRecord = await _context
.Events
.FirstOrDefaultAsync( ev
=> ev.IncidentId == request.IncidentId
&& ev.Id == request.Id);
if (eventRecord == null) throw new KeyNotFoundException();
return Mapper.Map<Event>(eventRecord);
}
}
}

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

@ -1,12 +1,13 @@
using MediatR;
using Sia.Domain;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Sia.Gateway.Authentication;
using Sia.Gateway.Protocol;
using Sia.Gateway.ServiceRepositories;
using Sia.Data.Incidents;
using AutoMapper.QueryableExtensions;
using Microsoft.EntityFrameworkCore;
namespace Sia.Gateway.Requests.Events
{
@ -23,16 +24,20 @@ namespace Sia.Gateway.Requests.Events
public PaginationMetadata Pagination { get; }
}
public class GetEventsHandler : EventHandler<GetEventsRequest, IEnumerable<Event>>
public class GetEventsHandler
: IAsyncRequestHandler<GetEventsRequest, IEnumerable<Event>>
{
public GetEventsHandler(IEventRepository eventRepository)
: base(eventRepository)
{
}
private readonly IncidentContext _context;
public override async Task<IEnumerable<Event>> Handle(GetEventsRequest request)
public GetEventsHandler(IncidentContext context)
{
return await _eventRepository.GetEventsAsync(request.IncidentId, request.Pagination, request.UserContext);
_context = context;
}
public async Task<IEnumerable<Event>> Handle(GetEventsRequest request)
=> await _context.Events
.Where(ev => ev.IncidentId == request.IncidentId)
.WithPagination(request.Pagination)
.ProjectTo<Event>()
.ToListAsync();
}
}

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

@ -1,8 +1,12 @@
using MediatR;
using AutoMapper;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Sia.Data.Incidents;
using Sia.Domain;
using Sia.Domain.ApiModels;
using Sia.Gateway.Authentication;
using Sia.Gateway.ServiceRepositories;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Sia.Gateway.Requests
@ -19,18 +23,31 @@ namespace Sia.Gateway.Requests
public NewEvent NewEvent { get; }
public long IncidentId { get; }
}
public class PostEventHandler : IAsyncRequestHandler<PostEventRequest, Event>
{
private IEventRepository _incidentRepository;
private readonly IncidentContext _context;
public PostEventHandler(IEventRepository incidentRepository)
public PostEventHandler(IncidentContext context)
{
_incidentRepository = incidentRepository;
_context = context;
}
public async Task<Event> Handle(PostEventRequest request)
{
return await _incidentRepository.PostEvent(request.IncidentId, request.NewEvent, request.UserContext);
if (request.NewEvent == null) throw new ArgumentNullException(nameof(request.NewEvent));
var dataCrisis = await _context
.Incidents
.Include(cr => cr.Events)
.FirstOrDefaultAsync(x => x.Id == request.IncidentId);
if (dataCrisis == null) throw new KeyNotFoundException();
var dataEvent = Mapper.Map<Data.Incidents.Models.Event>(request.NewEvent);
dataCrisis.Events.Add(dataEvent);
await _context.SaveChangesAsync();
return Mapper.Map<Event>(dataEvent);
}
}
}

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

@ -1,7 +1,11 @@
using MediatR;
using Microsoft.EntityFrameworkCore;
using Sia.Connectors.Tickets;
using Sia.Data.Incidents;
using Sia.Domain;
using Sia.Gateway.Authentication;
using Sia.Gateway.ServiceRepositories;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Sia.Gateway.Requests
@ -15,18 +19,53 @@ namespace Sia.Gateway.Requests
}
public long Id { get; }
}
public class GetIncidentHandler : IAsyncRequestHandler<GetIncidentRequest, Incident>
public class GetIncidentHandler<TTicket> : IGetIncidentHandler
{
private IIncidentRepository _incidentRepository;
public GetIncidentHandler(IIncidentRepository incidentRepository)
private readonly IncidentContext _context;
private readonly Connector<TTicket> _connector;
public GetIncidentHandler(IncidentContext context, Connector<TTicket> connector)
{
_incidentRepository = incidentRepository;
_context = context;
_connector = connector;
}
public async Task<Incident> Handle(GetIncidentRequest request)
public async Task<Incident> Handle(GetIncidentRequest getIncident)
{
return await _incidentRepository.GetIncidentAsync(request.Id, request.UserContext);
var incidentRecord = await _context.Incidents
.WithEagerLoading()
.FirstOrDefaultAsync(cr => cr.Id == getIncident.Id);
if (incidentRecord == null) throw new KeyNotFoundException();
var remoteId = incidentRecord
.Tickets
.FirstOrDefault(t => t.IsPrimary)
.OriginId;
var ticket = await _connector.Client.GetAsync(remoteId);
return _connector
.Converter
.AssembleIncident(incidentRecord, ticket);
}
}
public interface IGetIncidentHandler
{
Task<Incident> Handle(GetIncidentRequest getIncident);
}
//Why does this exist?
//Purely because I haven't been able to get Mediatr to work with generics
public class GetIncidentHandlerWrapper : IAsyncRequestHandler<GetIncidentRequest, Incident>
{
private readonly IGetIncidentHandler _actualHandler;
public GetIncidentHandlerWrapper(IGetIncidentHandler actualHandler)
{
_actualHandler = actualHandler;
}
public Task<Incident> Handle(GetIncidentRequest message)
=> _actualHandler.Handle(message);
}
}

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

@ -1,7 +1,9 @@
using MediatR;
using AutoMapper.QueryableExtensions;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Sia.Data.Incidents;
using Sia.Domain;
using Sia.Gateway.Authentication;
using Sia.Gateway.ServiceRepositories;
using System.Collections.Generic;
using System.Threading.Tasks;
@ -9,23 +11,28 @@ namespace Sia.Gateway.Requests
{
public class GetIncidentsRequest : AuthenticatedRequest, IRequest<IEnumerable<Incident>>
{
public GetIncidentsRequest(AuthenticatedUserContext userContext) : base(userContext)
public GetIncidentsRequest(AuthenticatedUserContext userContext)
: base(userContext)
{
}
}
public class GetIncidentsHandler : IAsyncRequestHandler<GetIncidentsRequest, IEnumerable<Incident>>
public class GetIncidentsHandler
: IAsyncRequestHandler<GetIncidentsRequest, IEnumerable<Incident>>
{
private IIncidentRepository _incidentRepository;
public GetIncidentsHandler(IIncidentRepository incidentClient)
private readonly IncidentContext _context;
public GetIncidentsHandler(IncidentContext context)
{
_incidentRepository = incidentClient;
_context = context;
}
public async Task<IEnumerable<Incident>> Handle(GetIncidentsRequest message)
public async Task<IEnumerable<Incident>> Handle(GetIncidentsRequest request)
{
var incidentResponse = await _incidentRepository.GetIncidentsAsync(message.UserContext);
return incidentResponse;
var incidentRecords = await _context.Incidents
.WithEagerLoading()
.ProjectTo<Incident>()
.ToListAsync();
return incidentRecords;
}
}
}

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

@ -1,8 +1,11 @@
using MediatR;
using AutoMapper.QueryableExtensions;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Sia.Data.Incidents;
using Sia.Domain;
using Sia.Gateway.Authentication;
using Sia.Gateway.ServiceRepositories;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Sia.Gateway.Requests
@ -17,18 +20,22 @@ namespace Sia.Gateway.Requests
}
}
public class GetIncidentsByTicketHandler : IAsyncRequestHandler<GetIncidentsByTicketRequest, IEnumerable<Incident>>
public class GetIncidentsByTicketHandler
: IAsyncRequestHandler<GetIncidentsByTicketRequest, IEnumerable<Incident>>
{
private IIncidentRepository _incidentRepository;
public GetIncidentsByTicketHandler(IIncidentRepository incidentClient)
private readonly IncidentContext _context;
public GetIncidentsByTicketHandler(IncidentContext context)
{
_incidentRepository = incidentClient;
_context = context;
}
public async Task<IEnumerable<Incident>> Handle(GetIncidentsByTicketRequest message)
public async Task<IEnumerable<Incident>> Handle(GetIncidentsByTicketRequest request)
{
var incidentResponse = await _incidentRepository.GetIncidentsByTicketAsync(message.TicketId, message.UserContext);
return incidentResponse;
var incidentRecords = await _context.Incidents
.WithEagerLoading()
.Where(incident => incident.Tickets.Any(inc => inc.OriginId == request.TicketId))
.ProjectTo<Incident>().ToListAsync();
return incidentRecords;
}
}
}

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

@ -0,0 +1,68 @@
using AutoMapper;
using AutoMapper.QueryableExtensions;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Sia.Data.Incidents;
using Sia.Domain;
using Sia.Domain.ApiModels;
using Sia.Gateway.Authentication;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Sia.Gateway.Requests
{
public class GetIncidentsByTicketCreateIfNeededRequest : AuthenticatedRequest, IRequest<IEnumerable<Incident>>
{
public GetIncidentsByTicketCreateIfNeededRequest(string ticketId, AuthenticatedUserContext userContext)
: base(userContext)
{
TicketId = ticketId;
}
public string TicketId { get; private set; }
}
public class GetIncidentsByTicketCreateIfNeededRequestHandler : IAsyncRequestHandler<GetIncidentsByTicketCreateIfNeededRequest, IEnumerable<Incident>>
{
private IncidentContext _context;
public GetIncidentsByTicketCreateIfNeededRequestHandler(IncidentContext context)
{
_context = context;
}
public IncidentContext Context { get; }
public async Task<IEnumerable<Incident>> Handle(GetIncidentsByTicketCreateIfNeededRequest message)
{
var incidents = await _context.Incidents
.WithEagerLoading()
.Where(incident => incident.Tickets.Any(inc => inc.OriginId == message.TicketId))
.ProjectTo<Incident>().ToListAsync();
if (incidents.Any())
{
return incidents;
}
var newIncident = new NewIncident
{
PrimaryTicket = new Ticket
{
TicketingSystemId = 1,
OriginId = message.TicketId
}
};
var dataIncident = Mapper.Map<Data.Incidents.Models.Incident>(newIncident);
var result = _context.Incidents.Add(dataIncident);
await _context.SaveChangesAsync();
return new List<Incident> { Mapper.Map<Incident>(result.Entity) };
}
}
}

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

@ -1,8 +1,11 @@
using MediatR;
using AutoMapper;
using MediatR;
using Sia.Data.Incidents;
using Sia.Domain;
using Sia.Domain.ApiModels;
using Sia.Gateway.Authentication;
using Sia.Gateway.ServiceRepositories;
using Sia.Shared.Exceptions;
using System;
using System.Threading.Tasks;
namespace Sia.Gateway.Requests
@ -16,20 +19,27 @@ namespace Sia.Gateway.Requests
}
public NewIncident Incident { get; private set; }
}
public class PostIncidentHandler : IAsyncRequestHandler<PostIncidentRequest, Incident>
public class PostIncidentHandler
: IAsyncRequestHandler<PostIncidentRequest, Incident>
{
private IIncidentRepository _incidentRepository;
public PostIncidentHandler(IIncidentRepository incidentClient)
private readonly IncidentContext _context;
public PostIncidentHandler(IncidentContext context)
{
_incidentRepository = incidentClient;
_context = context;
}
public async Task<Incident> Handle(PostIncidentRequest message)
public async Task<Incident> Handle(PostIncidentRequest request)
{
return await _incidentRepository.PostIncidentAsync(message.Incident, message.UserContext);
if (request.Incident == null) throw new ArgumentNullException(nameof(request.Incident));
if (request.Incident?.PrimaryTicket?.OriginId == null) throw new ConflictException("Please provide a primary incident with a valid originId");
var dataIncident = Mapper.Map<Data.Incidents.Models.Incident>(request.Incident);
var result = _context.Incidents.Add(dataIncident);
await _context.SaveChangesAsync();
return Mapper.Map<Incident>(dataIncident);
}
}
}

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

@ -1,70 +0,0 @@
using AutoMapper;
using Microsoft.EntityFrameworkCore;
using Sia.Data.Incidents;
using Sia.Domain;
using Sia.Domain.ApiModels;
using Sia.Gateway.Authentication;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Sia.Gateway.ServiceRepositories
{
public interface IEngagementRepository
{
Task<Engagement> GetEngagementAsync(long incidentId, long id, AuthenticatedUserContext userContext);
Task<Engagement> PostEngagementAsync(long incidentId, NewEngagement newEngagement, AuthenticatedUserContext userContext);
Task PutEngagementAsync(long incidentId, long engagementId, UpdateEngagement updatedEngagement, AuthenticatedUserContext userContext);
}
public class EngagementRepository : IEngagementRepository
{
private readonly IncidentContext _context;
public EngagementRepository(IncidentContext context)
{
_context = context;
}
public async Task<Engagement> GetEngagementAsync(long incidentId, long id, AuthenticatedUserContext userContext)
{
var EngagementRecord = await _context.Engagements
.Include(en => en.Participant)
.FirstOrDefaultAsync(ev => ev.IncidentId == incidentId && ev.Id == id);
if (EngagementRecord == null) throw new KeyNotFoundException();
return Mapper.Map<Engagement>(EngagementRecord);
}
public async Task<Engagement> PostEngagementAsync(long incidentId, NewEngagement newEngagement, AuthenticatedUserContext userContext)
{
if (newEngagement == null) throw new ArgumentNullException(nameof(newEngagement));
var dataIncident = await _context.Incidents
.Include(cr => cr.Engagements)
.ThenInclude(en => en.Participant)
.FirstOrDefaultAsync(x => x.Id == incidentId);
if (dataIncident == null) throw new KeyNotFoundException();
var dataEngagement = Mapper.Map<Data.Incidents.Models.Engagement>(newEngagement);
dataEngagement.TimeEngaged = DateTime.UtcNow;
dataIncident.Engagements.Add(dataEngagement);
await _context.SaveChangesAsync();
return Mapper.Map<Engagement>(dataEngagement);
}
public async Task PutEngagementAsync(long incidentId, long engagementId, UpdateEngagement updatedEngagement, AuthenticatedUserContext userContext)
{
if (updatedEngagement == null) throw new ArgumentNullException(nameof(updatedEngagement));
var existingRecord = await _context.Engagements
.Include(en => en.Participant)
.FirstOrDefaultAsync(engagement => engagement.IncidentId == incidentId && engagement.Id == engagementId);
var updatedModel = Mapper.Map(updatedEngagement, existingRecord);
await _context.SaveChangesAsync();
}
}
}

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

@ -1,66 +0,0 @@
using AutoMapper;
using AutoMapper.QueryableExtensions;
using Microsoft.EntityFrameworkCore;
using Sia.Data.Incidents;
using Sia.Domain;
using Sia.Domain.ApiModels;
using Sia.Gateway.Authentication;
using Sia.Gateway.Protocol;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Sia.Gateway.ServiceRepositories
{
public interface IEventRepository
{
Task<Event> GetEvent(long incidentId, long id, AuthenticatedUserContext userContext);
Task<Event> PostEvent(long incidentId, NewEvent newEvent, AuthenticatedUserContext userContext);
Task<IEnumerable<Event>> GetEventsAsync(long incidentId, PaginationMetadata pagination, AuthenticatedUserContext userContext);
}
public class EventRepository : IEventRepository
{
private readonly IncidentContext _context;
public EventRepository(IncidentContext context)
{
_context = context;
}
public async Task<Event> GetEvent(long incidentId, long id, AuthenticatedUserContext userContext)
{
var eventRecord = await _context.Events.FirstOrDefaultAsync(ev => ev.IncidentId == incidentId && ev.Id == id);
if (eventRecord == null) throw new KeyNotFoundException();
return Mapper.Map<Event>(eventRecord);
}
public async Task<IEnumerable<Event>> GetEventsAsync(long incidentId, PaginationMetadata pagination, AuthenticatedUserContext userContext)
{
return await _context.Events
.WithPagination(pagination)
.ProjectTo<Event>()
.ToListAsync();
}
public async Task<Event> PostEvent(long incidentId, NewEvent newEvent, AuthenticatedUserContext userContext)
{
if (newEvent == null) throw new ArgumentNullException(nameof(newEvent));
var dataCrisis = await _context.Incidents
.Include(cr => cr.Events)
.FirstOrDefaultAsync(x => x.Id == incidentId);
if (dataCrisis == null) throw new KeyNotFoundException();
var dataEvent = Mapper.Map<Data.Incidents.Models.Event>(newEvent);
dataCrisis.Events.Add(dataEvent);
await _context.SaveChangesAsync();
return Mapper.Map<Event>(dataEvent);
}
}
}

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

@ -1,75 +0,0 @@
using AutoMapper;
using AutoMapper.QueryableExtensions;
using Microsoft.EntityFrameworkCore;
using Sia.Connectors.Tickets;
using Sia.Data.Incidents;
using Sia.Domain;
using Sia.Domain.ApiModels;
using Sia.Gateway.Authentication;
using Sia.Shared.Exceptions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Sia.Gateway.ServiceRepositories
{
public interface IIncidentRepository
{
Task<Incident> GetIncidentAsync(long id, AuthenticatedUserContext userContext);
Task<IEnumerable<Incident>> GetIncidentsAsync(AuthenticatedUserContext userContext);
Task<Incident> PostIncidentAsync(NewIncident incident, AuthenticatedUserContext userContext);
Task<IEnumerable<Incident>> GetIncidentsByTicketAsync(string ticketId, AuthenticatedUserContext userContext);
}
public class IncidentRepository<TTicket> : IIncidentRepository
{
private readonly IncidentContext _context;
private readonly Connector<TTicket> _connector;
public IncidentRepository(IncidentContext context, Connector<TTicket> connector)
{
_context = context;
_connector = connector;
}
public async Task<Incident> GetIncidentAsync(long id, AuthenticatedUserContext userContext)
{
var incidentRecord = await _context.Incidents.WithEagerLoading().FirstOrDefaultAsync(cr => cr.Id == id);
if (incidentRecord == null) throw new KeyNotFoundException();
var ticket = await _connector.Client.GetAsync(incidentRecord.Tickets.FirstOrDefault(t => t.IsPrimary).OriginId);
return _connector.Converter.AssembleIncident(incidentRecord, ticket);
}
public async Task<IEnumerable<Incident>> GetIncidentsAsync(AuthenticatedUserContext userContext)
{
var incidentRecords = await _context.Incidents.WithEagerLoading().ProjectTo<Incident>().ToListAsync();
return incidentRecords;
}
public async Task<IEnumerable<Incident>> GetIncidentsByTicketAsync(string ticketId, AuthenticatedUserContext userContext)
{
var incidentRecords = await _context.Incidents
.WithEagerLoading()
.Where(incident => incident.Tickets.Any(inc => inc.OriginId == ticketId))
.ProjectTo<Incident>().ToListAsync();
return incidentRecords;
}
public async Task<Incident> PostIncidentAsync(NewIncident incident, AuthenticatedUserContext userContext)
{
if (incident == null) throw new ArgumentNullException(nameof(incident));
if (incident?.PrimaryTicket?.OriginId == null) throw new ConflictException("Please provide a primary incident with a valid originId");
var dataIncident = Mapper.Map<Data.Incidents.Models.Incident>(incident);
var result = _context.Incidents.Add(dataIncident);
await _context.SaveChangesAsync();
return Mapper.Map<Incident>(dataIncident);
}
}
}

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

@ -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;
}
}

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

@ -0,0 +1,39 @@
using AutoMapper;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Dynamic;
using System.Linq;
using System.Threading.Tasks;
namespace Sia.Shared.Data
{
public interface IJsonDataString
{
string Data { get; set; }
}
public interface IJsonDataObject
{
object Data { get; set; }
}
public class ResolveJsonToString<TSource, TDestination>
: IValueResolver<TSource, TDestination, string>
where TSource: IJsonDataObject
where TDestination: IJsonDataString
{
public string Resolve(TSource source, TDestination destination, string destMember, ResolutionContext context)
=> source.Data is null ? null : JsonConvert.SerializeObject(source.Data);
}
public class ResolveStringToJson<TSource, TDestination>
: IValueResolver<TSource, TDestination, object>
where TSource : IJsonDataString
where TDestination : IJsonDataObject
{
public object Resolve(TSource source, TDestination destination, object destMember, ResolutionContext context)
=> source.Data is null ? null : JsonConvert.DeserializeObject(source.Data);
}
}

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

@ -14,6 +14,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AutoMapper" Version="6.1.1" />
<PackageReference Include="Microsoft.Azure.KeyVault" Version="2.3.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="2.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="2.0.0" />

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

@ -1,7 +1,7 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Sia.Domain;
using Sia.Gateway.Initialization;
using Sia.Gateway.Requests;
using Sia.Gateway.ServiceRepositories;
using Sia.Gateway.Tests.TestDoubles;
using System.Threading.Tasks;
@ -10,20 +10,23 @@ namespace Sia.Gateway.Tests.Requests
[TestClass]
public class GetEventTests
{
[TestInitialize]
public void ConfigureAutomapper()
=> AutoMapperStartup.InitializeAutomapper();
[TestMethod]
public async Task Handle_WhenEventClientReturnsSuccessful_ReturnCorrectEvent()
{
long expectedEventId = 200;
long expectedEventTypeId = 50;
long expectedIncidentId = 2;
long expectedEventId = 1;
long expectedEventTypeId = 1;
long expectedIncidentId = 1;
var expectedEvent = new Event
{
Id = expectedEventId,
EventTypeId = expectedEventTypeId,
IncidentId = expectedIncidentId
};
IEventRepository mockRepository = new StubEventRepository(expectedEvent);
var serviceUnderTest = new GetEventHandler(mockRepository);
var serviceUnderTest = new GetEventHandler(MockFactory.IncidentContext("Get"));
var request = new GetEventRequest(expectedIncidentId, expectedEventId, new DummyAuthenticatedUserContext());

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

@ -1,7 +1,8 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Sia.Connectors.Tickets.None;
using Sia.Domain;
using Sia.Gateway.Initialization;
using Sia.Gateway.Requests;
using Sia.Gateway.ServiceRepositories;
using Sia.Gateway.Tests.TestDoubles;
using System.Threading.Tasks;
@ -10,18 +11,20 @@ namespace Sia.Gateway.Tests.Requests
[TestClass]
public class GetIncidentTests
{
[TestInitialize]
public void ConfigureAutomapper()
=> AutoMapperStartup.InitializeAutomapper();
[TestMethod]
public async Task Handle_WhenIncidentClientReturnsSuccessful_ReturnCorrectIncident()
{
long expectedIncidentId = 200;
string expectedIncidentTitle = "The thing we were looking for";
long expectedIncidentId = 1;
string expectedIncidentTitle = "Customers are unable to access [REDACTED] from [REDACTED]";
var expectedIncident = new Incident
{
Id = expectedIncidentId,
Title = expectedIncidentTitle
};
IIncidentRepository mockClient = new StubIncidentRepository(expectedIncident, null);
var serviceUnderTest = new GetIncidentHandler(mockClient);
var serviceUnderTest = new GetIncidentHandler<EmptyTicket>(MockFactory.IncidentContext("Get"), new NoConnector(new NoClient(), new NoConverter()));
var request = new GetIncidentRequest(expectedIncidentId, new DummyAuthenticatedUserContext());

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

@ -0,0 +1,49 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Sia.Domain;
using Sia.Gateway.Initialization;
using Sia.Gateway.Requests;
using Sia.Gateway.Tests.TestDoubles;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Sia.Gateway.Tests.Requests
{
[TestClass]
public class GetIncidentsByTicketCreateIfNeededRequestTest
{
[TestInitialize]
public void ConfigureAutomapper()
=> AutoMapperStartup.InitializeAutomapper();
[TestMethod]
public async Task Handle_WhenIncidentNotExist_ReturnNewIncident()
{
var serviceUnderTest = new GetIncidentsByTicketCreateIfNeededRequestHandler(MockFactory.IncidentContext("Get"));
var request = new GetIncidentsByTicketCreateIfNeededRequest("100", new DummyAuthenticatedUserContext());
var result = (await serviceUnderTest.Handle(request)).ToList();
Assert.AreEqual(1, result.Count());
Assert.AreEqual( "100", result[0].PrimaryTicket.OriginId);
}
[TestMethod]
public async Task Handle_WhenIncidentExists_ReturnCorrectIncidents()
{
var serviceUnderTest = new GetIncidentsByTicketCreateIfNeededRequestHandler(MockFactory.IncidentContext("Get"));
var request = new GetIncidentsByTicketCreateIfNeededRequest("44444444", new DummyAuthenticatedUserContext());
var result = (await serviceUnderTest.Handle(request)).ToList();
Assert.AreEqual(1, result.Count());
Assert.AreEqual(1, result[0].Id);
}
}
}

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

@ -1,7 +1,7 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Sia.Domain;
using Sia.Gateway.Initialization;
using Sia.Gateway.Requests;
using Sia.Gateway.ServiceRepositories;
using Sia.Gateway.Tests.TestDoubles;
using System.Linq;
using System.Threading.Tasks;
@ -12,11 +12,19 @@ namespace Sia.Gateway.Tests.Requests
[TestClass]
public class GetIncidentsTests
{
[TestInitialize]
public void ConfigureAutomapper()
=> AutoMapperStartup.InitializeAutomapper();
[TestMethod]
public async Task Handle_WhenIncidentClientReturnsSuccessful_ReturnCorrectIncidents()
{
long[] expectedIncidentIds = { 200, 300, 400 };
string[] expectedIncidentTitles = { "First", "Second", "Third" };
long[] expectedIncidentIds = { 1, 2, 3 };
string[] expectedIncidentTitles = {
"Customers are unable to access [REDACTED] from [REDACTED]",
"Loss of [REDACTED] Connectivity in [REDACTED]",
"[REDACTED] and [REDACTED] service management operations for a subset of users in [REDACTED] are failing"
};
Incident[] expectedIncidents = new Incident[expectedIncidentIds.Length];
for (int i = 0; i < expectedIncidents.Length; i++)
{
@ -26,8 +34,7 @@ namespace Sia.Gateway.Tests.Requests
Title = expectedIncidentTitles[i]
};
}
IIncidentRepository mockRepository = new StubIncidentRepository(expectedIncidents, null);
var serviceUnderTest = new GetIncidentsHandler(mockRepository);
var serviceUnderTest = new GetIncidentsHandler(MockFactory.IncidentContext("Get"));
var request = new GetIncidentsRequest(new DummyAuthenticatedUserContext());

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

@ -2,8 +2,8 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Sia.Domain;
using Sia.Domain.ApiModels;
using Sia.Gateway.Initialization;
using Sia.Gateway.Requests;
using Sia.Gateway.ServiceRepositories;
using Sia.Gateway.Tests.TestDoubles;
using System.Collections.Generic;
using System.Threading.Tasks;
@ -17,13 +17,7 @@ namespace Sia.Gateway.Tests.Requests
[TestInitialize]
public void ConfigureAutomapper()
{
Mapper.Initialize(configuration =>
{
configuration.CreateMap<NewIncident, Incident>();
});
_mapper = Mapper.Instance;
}
=> AutoMapperStartup.InitializeAutomapper();
[TestMethod]
public async Task Handle_WhenIncidentClientReturnsSuccessful_ReturnCorrectIncidents()
@ -32,9 +26,13 @@ namespace Sia.Gateway.Tests.Requests
var expectedIncident = new NewIncident
{
Title = expectedIncidentTitle,
PrimaryTicket = new Ticket()
{
OriginId = "testOnlyPleaseIgnore"
}
};
IIncidentRepository mockRepository = new StubIncidentRepository(new List<Incident>(), _mapper);
var serviceUnderTest = new PostIncidentHandler(mockRepository);
var serviceUnderTest = new PostIncidentHandler(MockFactory.IncidentContext(nameof(Handle_WhenIncidentClientReturnsSuccessful_ReturnCorrectIncidents)));
var request = new PostIncidentRequest(expectedIncident, new DummyAuthenticatedUserContext());

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

@ -7,8 +7,8 @@
<ItemGroup>
<PackageReference Include="AutoMapper" Version="6.1.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.3.0" />
<PackageReference Include="MSTest.TestAdapter" Version="1.1.18" />
<PackageReference Include="MSTest.TestFramework" Version="1.1.18" />
<PackageReference Include="MSTest.TestAdapter" Version="1.2.0" />
<PackageReference Include="MSTest.TestFramework" Version="1.2.0" />
<PackageReference Include="Newtonsoft.Json" Version="10.0.3" />
</ItemGroup>

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

@ -0,0 +1,27 @@
using Microsoft.EntityFrameworkCore;
using Sia.Data.Incidents;
using System;
using System.Collections.Generic;
using System.Text;
namespace Sia.Gateway.Tests.TestDoubles
{
public static class MockFactory
{
/// <summary>
/// Returns an in-memory Incident Context with seed data
/// </summary>
/// <param name="instance">Name of the particular in-memory store to use. Re-use is not suggested when modifying data during test (nameof() the test method is preferred)</param>
/// <returns></returns>
public static IncidentContext IncidentContext(string instance)
{
var options = new DbContextOptionsBuilder<IncidentContext>()
.UseInMemoryDatabase(instance)
.Options;
var context = new IncidentContext(options);
SeedData.Add(context);
return context;
}
}
}

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

@ -1,41 +0,0 @@
using AutoMapper;
using Sia.Domain;
using Sia.Domain.ApiModels;
using Sia.Gateway.Authentication;
using Sia.Gateway.ServiceRepositories;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
namespace Sia.Gateway.Tests.TestDoubles
{
public class StubEventRepository : IEventRepository
{
private List<Event> _events;
public StubEventRepository(Event ev)
: this(new List<Event>() { ev }) { }
public StubEventRepository(ICollection<Event> events)
{
_events = events.ToList();
StatusCodeToRespondWith = HttpStatusCode.OK;
IsSuccessStatusCodeToRespondWith = true;
ContentToRespondWith = "You weren't going to use this anyway";
}
public HttpStatusCode StatusCodeToRespondWith { get; set; }
public bool IsSuccessStatusCodeToRespondWith { get; set; }
public string ContentToRespondWith { get; set; }
public Task<Event> GetEvent(long incidentId, long id, AuthenticatedUserContext userContext)
{
return Task.FromResult(_events.First(ev => ev.Id == id && ev.IncidentId == incidentId));
}
public Task<Event> PostEvent(long incidentId, NewEvent newEvent, AuthenticatedUserContext userContext)
{
return Task.FromResult(Mapper.Map(newEvent, new Event()));
}
}
}

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

@ -1,60 +0,0 @@
using AutoMapper;
using Sia.Domain;
using Sia.Domain.ApiModels;
using Sia.Gateway.Authentication;
using Sia.Gateway.ServiceRepositories;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
namespace Sia.Gateway.Tests.TestDoubles
{
public class StubIncidentRepository
: IIncidentRepository
{
private IMapper _mapper;
public StubIncidentRepository(Incident incident, IMapper mapper)
: this(new List<Incident>() { incident }, mapper) { }
public StubIncidentRepository(ICollection<Incident> incidents, IMapper mapper)
{
_mapper = mapper;
_incidents = incidents.ToList();
StatusCodeToRespondWith = HttpStatusCode.OK;
IsSuccessStatusCodeToRespondWith = true;
ContentToRespondWith = "You weren't going to use this anyway";
}
public HttpStatusCode StatusCodeToRespondWith { get; set; }
public bool IsSuccessStatusCodeToRespondWith { get; set; }
public string ContentToRespondWith { get; set; }
List<Incident> _incidents { get; set; }
public Task<Incident> GetIncidentAsync(long id, AuthenticatedUserContext userContext)
{
return Task.FromResult(_incidents.First(cr => cr.Id == id));
}
public Task<IEnumerable<Incident>> GetIncidentsAsync(AuthenticatedUserContext userContext)
{
return Task.FromResult(_incidents.AsEnumerable());
}
public Task<IEnumerable<Incident>> GetIncidentsByTicketAsync(string ticketId, AuthenticatedUserContext userContext)
{
throw new NotImplementedException();
}
public Task<Incident> PostIncidentAsync(NewIncident incident, AuthenticatedUserContext userContext)
{
return Task.FromResult(_mapper.Map<NewIncident, Incident>(incident));
}
}
}