Skip to content

Commit

Permalink
Add configurable cors support (#340)
Browse files Browse the repository at this point in the history
  • Loading branch information
brandonpollett authored Feb 28, 2019
1 parent 9bbd389 commit 9541e9e
Show file tree
Hide file tree
Showing 13 changed files with 309 additions and 15 deletions.
7 changes: 7 additions & 0 deletions build/package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@ steps:
pathToPublish: './testauthenvironment.json'
artifactName: 'deploy'
artifactType: 'container'

- task: PublishBuildArtifacts@1
displayName: 'publish corstestconfiguration.json'
inputs:
pathToPublish: './corstestconfiguration.json'
artifactName: 'deploy'
artifactType: 'container'

- task: PublishBuildArtifacts@1
displayName: 'publish release directory'
Expand Down
10 changes: 10 additions & 0 deletions corstestconfiguration.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"FhirServer": {
"Cors": {
"Origins": ["https://localhost:6001"],
"Methods": ["*"],
"Headers": ["*"],
"MaxAge": 1440
}
}
}
16 changes: 2 additions & 14 deletions samples/templates/default-azuredeploy.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,6 @@
],
"defaultValue": "S1"
},
"allowedOrigins": {
"type": "array",
"defaultValue": [],
"metadata": {
"description": "CORS Allowed Origins"
}
},
"securityAuthenticationAuthority": {
"type": "string",
"defaultValue": "",
Expand Down Expand Up @@ -200,12 +193,7 @@
},
"properties": {
"clientAffinityEnabled": false,
"serverFarmId": "[resourceId(variables('appServicePlanResourceGroup'), 'Microsoft.Web/serverfarms/', variables('appServicePlanName'))]",
"siteConfig": {
"cors": {
"allowedOrigins": "[parameters('allowedOrigins')]"
}
}
"serverFarmId": "[resourceId(variables('appServicePlanResourceGroup'), 'Microsoft.Web/serverfarms/', variables('appServicePlanName'))]"
},
"dependsOn": [
"nestedTemplate"
Expand All @@ -219,7 +207,7 @@
"[concat('Microsoft.Web/Sites/', variables('serviceName'))]",
"[if(variables('deployAppInsights'),concat('Microsoft.Insights/components/', variables('appInsightsName')),resourceId('Microsoft.KeyVault/vaults', variables('serviceName')))]"
],
"properties" : "[if(variables('deployAppInsights'), union(variables('combinedFhirServerConfigProperties'), json(concat('{\"APPINSIGHTS_INSTRUMENTATIONKEY\": \"', reference(concat('Microsoft.Insights/components/', variables('appInsightsName'))).InstrumentationKey, '\"}'))), variables('combinedFhirServerConfigProperties'))]"
"properties": "[if(variables('deployAppInsights'), union(variables('combinedFhirServerConfigProperties'), json(concat('{\"APPINSIGHTS_INSTRUMENTATIONKEY\": \"', reference(concat('Microsoft.Insights/components/', variables('appInsightsName'))).InstrumentationKey, '\"}'))), variables('combinedFhirServerConfigProperties'))]"
},
{
"apiVersion": "2015-08-01",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// -------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// -------------------------------------------------------------------------------------------------

using System;
using Microsoft.AspNetCore.Cors.Infrastructure;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Health.Fhir.Api.Configs;
using Microsoft.Health.Fhir.Api.Modules;
using Microsoft.Health.Fhir.Core.Configs;
using NSubstitute;
using Xunit;

namespace Microsoft.Health.Fhir.Api.UnitTests.Features.Cors
{
public class CorsModuleTests
{
private readonly CorsModule _corsModule;
private readonly CorsConfiguration _corsConfiguration = Substitute.For<CorsConfiguration>();
private readonly IServiceCollection _servicesCollection = Substitute.For<IServiceCollection>();

public CorsModuleTests()
{
var fhirServerConfiguration = Substitute.For<FhirServerConfiguration>();

fhirServerConfiguration.Cors.Returns(_corsConfiguration);

_corsModule = new CorsModule(fhirServerConfiguration);
}

[Fact]
public void GivenACorsConfiguration_WhenNoValuesSet_PolicyHasOnlyDefaults()
{
_corsModule.Load(_servicesCollection);

CorsPolicy corsPolicy = _corsModule.DefaultCorsPolicy;
Assert.Empty(corsPolicy.Origins);
Assert.Empty(corsPolicy.Headers);
Assert.Empty(corsPolicy.Methods);
Assert.False(corsPolicy.SupportsCredentials);
Assert.Null(corsPolicy.PreflightMaxAge);
}

[Fact]
public void GivenACorsConfiguration_WhenAllOriginsSet_PolicyHasAllowAnyOrigin()
{
_corsConfiguration.Origins.Add("*");
_corsModule.Load(_servicesCollection);

Assert.True(_corsModule.DefaultCorsPolicy.AllowAnyOrigin);
}

[Fact]
public void GivenACorsConfiguration_WhenAllMethodsSet_PolicyHasAllowAnyMethod()
{
_corsConfiguration.Methods.Add("*");
_corsModule.Load(_servicesCollection);

Assert.True(_corsModule.DefaultCorsPolicy.AllowAnyMethod);
}

[Fact]
public void GivenACorsConfiguration_WhenAllHeadersSet_PolicyHasAllowAnyHeader()
{
_corsConfiguration.Headers.Add("*");
_corsModule.Load(_servicesCollection);

Assert.True(_corsModule.DefaultCorsPolicy.AllowAnyHeader);
}

[Fact]
public void GivenACorsConfiguration_WhenAllowCredentials_PolicyHasSupportsCredentials()
{
_corsConfiguration.AllowCredentials = true;
_corsModule.Load(_servicesCollection);

Assert.True(_corsModule.DefaultCorsPolicy.SupportsCredentials);
}

[Fact]
public void GivenACorsConfiguration_WhenMaxAgeSet_PolicyHasMaxAge()
{
_corsConfiguration.MaxAge = 100;
_corsModule.Load(_servicesCollection);

Assert.Equal(TimeSpan.FromSeconds(100), _corsModule.DefaultCorsPolicy.PreflightMaxAge);
}

[Fact]
public void GivenACorsConfiguration_WhenMultipleValuesSet_PolicyHasSpecifiedValues()
{
_corsConfiguration.Origins.Add("https://example.com");
_corsConfiguration.Origins.Add("https://contoso");

_corsConfiguration.Methods.Add("PATCH");
_corsConfiguration.Methods.Add("DELETE");

_corsConfiguration.Headers.Add("authorization");
_corsConfiguration.Headers.Add("content-type");

_corsModule.Load(_servicesCollection);

Assert.Equal(2, _corsModule.DefaultCorsPolicy.Origins.Count);
Assert.Equal(2, _corsModule.DefaultCorsPolicy.Methods.Count);
Assert.Equal(2, _corsModule.DefaultCorsPolicy.Headers.Count);

Assert.Contains("https://example.com", _corsModule.DefaultCorsPolicy.Origins);
Assert.Contains("https://contoso", _corsModule.DefaultCorsPolicy.Origins);

Assert.Contains("PATCH", _corsModule.DefaultCorsPolicy.Methods);
Assert.Contains("DELETE", _corsModule.DefaultCorsPolicy.Methods);

Assert.Contains("authorization", _corsModule.DefaultCorsPolicy.Headers);
Assert.Contains("content-type", _corsModule.DefaultCorsPolicy.Headers);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,7 @@ public class FhirServerConfiguration
public ConformanceConfiguration Conformance { get; } = new ConformanceConfiguration();

public SecurityConfiguration Security { get; } = new SecurityConfiguration();

public virtual CorsConfiguration Cors { get; } = new CorsConfiguration();
}
}
59 changes: 59 additions & 0 deletions src/Microsoft.Health.Fhir.Api/Modules/CorsModule.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// -------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// -------------------------------------------------------------------------------------------------

using System;
using System.Linq;
using EnsureThat;
using Microsoft.AspNetCore.Cors.Infrastructure;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Health.Extensions.DependencyInjection;
using Microsoft.Health.Fhir.Api.Configs;
using Microsoft.Health.Fhir.Core.Configs;
using Microsoft.Health.Fhir.Core.Features.Cors;

namespace Microsoft.Health.Fhir.Api.Modules
{
public class CorsModule : IStartupModule
{
private readonly CorsConfiguration _corsConfiguration;

public CorsModule(FhirServerConfiguration fhirServerConfiguration)
{
EnsureArg.IsNotNull(fhirServerConfiguration, nameof(fhirServerConfiguration));
_corsConfiguration = fhirServerConfiguration.Cors;
}

internal CorsPolicy DefaultCorsPolicy { get; private set; }

public void Load(IServiceCollection services)
{
EnsureArg.IsNotNull(services, nameof(services));

var corsPolicyBuilder = new CorsPolicyBuilder()
.WithOrigins(_corsConfiguration.Origins.ToArray())
.WithHeaders(_corsConfiguration.Headers.ToArray())
.WithMethods(_corsConfiguration.Methods.ToArray());

if (_corsConfiguration.MaxAge != null)
{
corsPolicyBuilder.SetPreflightMaxAge(TimeSpan.FromSeconds(_corsConfiguration.MaxAge.Value));
}

if (_corsConfiguration.AllowCredentials)
{
corsPolicyBuilder.AllowCredentials();
}

DefaultCorsPolicy = corsPolicyBuilder.Build();

services.AddCors(options =>
{
options.AddPolicy(
Constants.DefaultCorsPolicy,
DefaultCorsPolicy);
});
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
using Microsoft.Health.Fhir.Api.Features.Context;
using Microsoft.Health.Fhir.Api.Features.Exceptions;
using Microsoft.Health.Fhir.Api.Features.Headers;
using Microsoft.Health.Fhir.Core.Features.Cors;
using Microsoft.Health.Fhir.Core.Registration;

namespace Microsoft.Extensions.DependencyInjection
Expand Down Expand Up @@ -52,6 +53,7 @@ public static IFhirServerBuilder AddFhirServer(
services.AddSingleton(Options.Options.Create(fhirServerConfiguration.Security));
services.AddSingleton(Options.Options.Create(fhirServerConfiguration.Conformance));
services.AddSingleton(Options.Options.Create(fhirServerConfiguration.Features));
services.AddSingleton(Options.Options.Create(fhirServerConfiguration.Cors));
services.AddTransient<IStartupFilter, FhirServerStartupFilter>();

services.RegisterAssemblyModules(Assembly.GetExecutingAssembly(), fhirServerConfiguration);
Expand Down Expand Up @@ -86,6 +88,8 @@ public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
// This middleware will add delegates to the OnStarting method of httpContext.Response for setting headers.
app.UseBaseHeaders();

app.UseCors(Constants.DefaultCorsPolicy);

// This middleware should be registered at the beginning since it generates correlation id among other things,
// which will be used in other middlewares.
app.UseFhirRequestContext();
Expand Down
22 changes: 22 additions & 0 deletions src/Microsoft.Health.Fhir.Core/Configs/CorsConfiguration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// -------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// -------------------------------------------------------------------------------------------------

using System.Collections.Generic;

namespace Microsoft.Health.Fhir.Core.Configs
{
public class CorsConfiguration
{
public IList<string> Origins { get; } = new List<string>();

public IList<string> Headers { get; } = new List<string>();

public IList<string> Methods { get; } = new List<string>();

public int? MaxAge { get; set; }

public bool AllowCredentials { get; set; }
}
}
12 changes: 12 additions & 0 deletions src/Microsoft.Health.Fhir.Core/Features/Cors/Constants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// -------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// -------------------------------------------------------------------------------------------------

namespace Microsoft.Health.Fhir.Core.Features.Cors
{
public static class Constants
{
public const string DefaultCorsPolicy = "DefaultCorsPolicy";
}
}
7 changes: 7 additions & 0 deletions src/Microsoft.Health.Fhir.Web/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@
"CosmosDb": {
"CollectionId": "fhir",
"InitialCollectionThroughput": null
},
"Cors": {
"Origins": [],
"Methods": [],
"Headers": [],
"MaxAge": null,
"AllowCredentials": false
}
},
"ControlPlane": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
<None Include="..\..\testauthenvironment.json" Link="testauthenvironment.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Include="..\..\corstestconfiguration.json" Link="corstestconfiguration.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>

<ItemGroup>
Expand Down
56 changes: 56 additions & 0 deletions test/Microsoft.Health.Fhir.Tests.E2E/Rest/CorsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// -------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// -------------------------------------------------------------------------------------------------

using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Health.Fhir.Tests.Common;
using Microsoft.Health.Fhir.Tests.E2E.Common;
using Microsoft.Health.Fhir.Web;
using Microsoft.Net.Http.Headers;
using Xunit;
using HttpMethod = System.Net.Http.HttpMethod;

namespace Microsoft.Health.Fhir.Tests.E2E.Rest
{
public class CorsTests : IClassFixture<HttpIntegrationTestFixture<Startup>>
{
private readonly FhirClient _client;

public CorsTests(HttpIntegrationTestFixture<Startup> fixture)
{
_client = fixture.FhirClient;
}

[Fact]
[Trait(Traits.Priority, Priority.One)]
public async Task WhenGettingOptions_GivenAppropriateHeaders_TheServerShouldReturnTheAppropriateCorsHeaders()
{
var message = new HttpRequestMessage
{
Method = HttpMethod.Options,
};

message.Headers.Add(HeaderNames.Origin, "https://localhost:6001");
message.Headers.Add(HeaderNames.AccessControlRequestMethod, "PUT");
message.Headers.Add(HeaderNames.AccessControlRequestHeaders, "authorization");
message.Headers.Add(HeaderNames.AccessControlRequestHeaders, "content-type");
message.RequestUri = new Uri(_client.HttpClient.BaseAddress, "/patient");

HttpResponseMessage response = await _client.HttpClient.SendAsync(message);

Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
Assert.Contains("https://localhost:6001", response.Headers.GetValues(HeaderNames.AccessControlAllowOrigin));

Assert.Contains("PUT", response.Headers.GetValues(HeaderNames.AccessControlAllowMethods));

Assert.Contains("authorization,content-type", response.Headers.GetValues(HeaderNames.AccessControlAllowHeaders));

Assert.Equal("1440", response.Headers.GetValues(HeaderNames.AccessControlMaxAge).First());
}
}
}
Loading

0 comments on commit 9541e9e

Please sign in to comment.