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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 4 additions & 11 deletions src/Distech.CloudRelay.API/Distech.CloudRelay.API.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
<TargetFramework>net6.0</TargetFramework>

<IsPackable>true</IsPackable>
<PackageId>Distech.CloudRelay.API</PackageId>
Expand All @@ -11,20 +11,13 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.7.1" />
<PackageReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.AzureAD.UI" Version="2.1.1" />
<PackageReference Include="Microsoft.AspNetCore.Razor.Design" Version="2.1.2" PrivateAssets="All" />
<PackageReference Include="Microsoft.Azure.Devices" Version="1.31.0" />
<PackageReference Include="Microsoft.Identity.Client" Version="4.5.1" />
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.21.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.AzureAD.UI" Version="6.0.9" />
<PackageReference Include="Microsoft.Azure.Devices" Version="1.38.1" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Distech.CloudRelay.Common\Distech.CloudRelay.Common.csproj" />
</ItemGroup>

<ItemGroup>
<Folder Include="Properties\PublishProfiles\" />
</ItemGroup>

</Project>
11 changes: 7 additions & 4 deletions src/Distech.CloudRelay.API/Model/DeviceRequestHeaders.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,19 @@ public class DeviceRequestHeaders
/// <summary>
/// Gets or sets the content-type header.
/// </summary>
[JsonProperty(PropertyName = HeaderNames.ContentType, NullValueHandling = NullValueHandling.Ignore)]
[JsonProperty(PropertyName = "Content-Type", NullValueHandling = NullValueHandling.Ignore)]
public string ContentType { get; set; }

/// <summary>
/// Gets or sets the content-disposition header.
/// </summary>
[JsonProperty(PropertyName = HeaderNames.ContentDisposition, NullValueHandling = NullValueHandling.Ignore)]
[JsonProperty(PropertyName = "Content-Disposition", NullValueHandling = NullValueHandling.Ignore)]
public string ContentDisposition { get; set; }

/// <summary>
/// Gets or sets the content-length header.
/// </summary>
[JsonProperty(PropertyName = HeaderNames.ContentLength, NullValueHandling = NullValueHandling.Ignore)]
[JsonProperty(PropertyName = "Content-Length", NullValueHandling = NullValueHandling.Ignore)]
public long? ContentLength { get; set; }

#endregion
Expand All @@ -52,7 +52,10 @@ public DeviceRequestHeaders()
/// <param name="request"></param>
public DeviceRequestHeaders(HttpRequest request)
{
Accept = request.GetTypedHeaders().Accept?.Select(a => a.ToString()).Aggregate((accept1, accept2) => $"{accept1}, {accept2}");
Accept = request.GetTypedHeaders().Accept
.Select(a => a.ToString())
.DefaultIfEmpty() //aggregate does not support empty sequence
.Aggregate((accept1, accept2) => $"{accept1}, {accept2}");
ContentType = request.GetTypedHeaders().ContentType?.ToString();
ContentDisposition = request.GetTypedHeaders().ContentDisposition?.ToString();
ContentLength = request.ContentLength;
Expand Down
4 changes: 2 additions & 2 deletions src/Distech.CloudRelay.API/Model/DeviceResponseHeaders.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ public class DeviceResponseHeaders
/// <summary>
/// Gets or sets the content-type header.
/// </summary>
[JsonProperty(PropertyName = HeaderNames.ContentType)]
[JsonProperty(PropertyName = "Content-Type")]
public string ContentType { get; set; }

/// <summary>
/// Gets or sets the content-disposition header.
/// </summary>
[JsonProperty(PropertyName = HeaderNames.ContentDisposition)]
[JsonProperty(PropertyName = "Content-Disposition")]
public string ContentDisposition { get; set; }

#endregion
Expand Down
143 changes: 135 additions & 8 deletions src/Distech.CloudRelay.API/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,148 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore;
using Distech.CloudRelay.API;
using Distech.CloudRelay.API.Middleware;
using Distech.CloudRelay.API.Model;
using Distech.CloudRelay.API.Options;
using Distech.CloudRelay.API.Services;
using Microsoft.ApplicationInsights.Extensibility;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.AzureAD.UI;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Authorization;
using Microsoft.Azure.Devices;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using Microsoft.Net.Http.Headers;

namespace Distech.CloudRelay.API
var builder = WebApplication.CreateBuilder(args);

//inject authentication related entities and services
#pragma warning disable 0618 //requires breaking changes to replace with Microsoft.Identity.Web
builder.Services.AddAuthentication(AzureADDefaults.BearerAuthenticationScheme)
#pragma warning restore 0618
.AddAzureADBearer(options => builder.Configuration.Bind(ApiEnvironment.AzureADOptions, options));

if (builder.Environment.IsDevelopment())
{
public class Program
//add diagnostics support for the JwtBearer middleware
#pragma warning disable 0618 //requires breaking changes to replace with Microsoft.Identity.Web
builder.Services.PostConfigure<JwtBearerOptions>(AzureADDefaults.JwtBearerAuthenticationScheme, static options =>
#pragma warning restore 0618
{
public static void Main(string[] args)
options.Events = JwtBearerMiddlewareDiagnostics.Subscribe(options.Events);
});
}

//inject common services
builder.Services.AddCloudRelayFileService(static (serviceProvider, options) =>
{
var config = serviceProvider.GetRequiredService<IConfiguration>();
config.GetSection(ApiEnvironment.FileStorageSectionKey).Bind(options);

var adapterOptions = serviceProvider.GetService<IOptionsSnapshot<AzureIotHubAdapterOptions>>();
options.ConnectionString = ApiEnvironment.GetConnectionString(ApiEnvironment.ResourceType.StorageAccount, adapterOptions.Value.EnvironmentId, config);

// files uploaded by the server are stored in a dedicated sub folder
options.ServerFileUploadSubFolder = adapterOptions.Value.MethodName;
});

//inject local services
builder.Services.AddScoped<IDeviceCommunicationAdapter, AzureIotHubAdapter>();
builder.Services.AddScoped<IDeviceService, DeviceService>();
builder.Services.AddScoped<ServiceClient>(static serviceProvider =>
{
var config = serviceProvider.GetRequiredService<IConfiguration>();
var options = serviceProvider.GetService<IOptionsSnapshot<AzureIotHubAdapterOptions>>();
var connectionString = ApiEnvironment.GetConnectionString(ApiEnvironment.ResourceType.IoTHubService, options.Value.EnvironmentId, config);
return ServiceClient.CreateFromConnectionString(connectionString);
});
builder.Services.ConfigureOptions<AzureIotHubAdapterPostConfigureOptions>();

//enables Application Insights telemetry (APPINSIGHTS_INSTRUMENTATIONKEY and ApplicationInsights:InstrumentationKey are supported)
builder.Services.AddApplicationInsightsTelemetry(static options =>
{
options.ApplicationVersion = Assembly.GetEntryAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>().InformationalVersion;
});
builder.Services.AddApplicationInsightsTelemetryProcessor<ExpectedExceptionTelemetryProcessor>();
builder.Services.AddSingleton<ITelemetryInitializer, RequestHeadersTelemetryInitializer>();

//enables CORS using the default policy
builder.Services.AddCors(static options =>
{
options.AddDefaultPolicy(static corsBuilder =>
{
corsBuilder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader().WithExposedHeaders(HeaderNames.ContentDisposition);
});
});

//allow accessing the HttpContext inside a service class
builder.Services.AddHttpContextAccessor();

builder.Services.AddControllers(configure =>
{
// 'AzureAD' is currently the only supported authentication provider name and anything else will allow anonymous access
if (IdentityProviders.AzureActiveDirectory.Equals(builder.Configuration.GetSection(ApiEnvironment.AuthenticationProvider).Value, StringComparison.OrdinalIgnoreCase))
{
AuthorizationPolicy policy;

string[] roles = builder.Configuration.GetSection(ApiEnvironment.AzureADRoles).GetChildren().Select(role => role.Value).ToArray();
string[] scopes = builder.Configuration.GetSection(ApiEnvironment.AzureADScopes).GetChildren().Select(role => role.Value).ToArray();

if (roles.Length > 0)
{
// authentication + authorization based on application permissions aka roles
policy = new AuthorizationPolicyBuilder().RequireRole(roles).Build();
}
else if (scopes.Length > 0)
{
// authentication + authorization based on delegated permissions aka scopes
policy = new AuthorizationPolicyBuilder().RequireAssertion((ctx =>
{
var scopeClaim = ctx.User.FindFirst(c => c.Type == "http://schemas.microsoft.com/identity/claims/scope")?.Value.Split(' ');
return scopeClaim != null && scopes.All(s => scopeClaim.Contains(s));
}))
.Build();
}
else
{
CreateWebHostBuilder(args).Build().Run();
// authentication only
policy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build();
}

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();
configure.Filters.Add(new AuthorizeFilter(policy));
}
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}

app.UseCors();
app.UseHttpsRedirection();

app.UseAuthentication();
app.UseAuthorization();

app.UseApplicationErrorHandler();
app.UseMultiPartRequestBuffering();

app.MapControllers();

app.Run();

/// <summary>
/// Intended for test projects using WebApplicationFactory.
/// </summary>
public partial class Program { }
14 changes: 7 additions & 7 deletions src/Distech.CloudRelay.API/Properties/launchSettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,19 @@
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"Distech.CloudRelay.API": {
"commandName": "Project",
"launchBrowser": false,
"launchUrl": "api",
"applicationUrl": "https://localhost:5001;http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Distech.CloudRelay.API": {
"commandName": "Project",
"launchBrowser": true,
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": false,
"launchUrl": "api",
"applicationUrl": "https://localhost:5001;http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
Expand Down
Loading