Skip to content
Draft
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
42 changes: 34 additions & 8 deletions src/Core/Services/OpenAPI/OpenApiDocumentor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ public class OpenApiDocumentor : IOpenApiDocumentor
public const string SP_RESPONSE_SUFFIX = "_sp_response";
public const string SCHEMA_OBJECT_TYPE = "object";
public const string RESPONSE_ARRAY_PROPERTY = "array";
public const string BEARER_AUTH_SECURITY_SCHEME = "BearerAuth";

// Routing constant
public const string OPENAPI_ROUTE = "openapi";
Expand Down Expand Up @@ -224,7 +225,7 @@ private OpenApiDocument BuildOpenApiDocument(RuntimeConfig runtimeConfig, string
});
}

return new OpenApiDocument()
OpenApiDocument document = new()
{
Info = new OpenApiInfo
{
Expand All @@ -239,6 +240,9 @@ private OpenApiDocument BuildOpenApiDocument(RuntimeConfig runtimeConfig, string
Components = components,
Tags = globalTagsDict.Values.ToList()
};

AddAuthenticationSecurity(document, runtimeConfig.Runtime?.Host?.Authentication);
return document;
}

/// <summary>
Expand Down Expand Up @@ -650,7 +654,7 @@ private OpenApiOperation CreateBaseOperation(string description, List<OpenApiTag
/// Helper method to populate operation parameters with all the custom headers like X-MS-API-ROLE, Authorization etc. headers.
/// </summary>
/// <param name="operation">OpenApi operation.</param>
private static void AddCustomHeadersToOperation(OpenApiOperation operation)
internal static void AddCustomHeadersToOperation(OpenApiOperation operation)
{
OpenApiSchema stringParamSchema = new()
{
Expand All @@ -666,16 +670,38 @@ private static void AddCustomHeadersToOperation(OpenApiOperation operation)
Schema = stringParamSchema
};
operation.Parameters.Add(paramForClientHeader);
}

// Add parameter for Authorization header.
OpenApiParameter paramForAuthHeader = new()
internal static void AddAuthenticationSecurity(OpenApiDocument document, AuthenticationOptions? authenticationOptions)
{
if (authenticationOptions is null || authenticationOptions.IsUnauthenticatedAuthenticationProvider())
{
Required = false,
return;
}

document.Components ??= new OpenApiComponents();
document.Components.SecuritySchemes ??= new Dictionary<string, OpenApiSecurityScheme>();
document.Components.SecuritySchemes[BEARER_AUTH_SECURITY_SCHEME] = new OpenApiSecurityScheme
{
Type = SecuritySchemeType.Http,
Scheme = "bearer",
BearerFormat = "JWT",
In = ParameterLocation.Header,
Name = "Authorization",
Schema = stringParamSchema
Name = AuthorizationResolver.AUTHORIZATION_HEADER
};
operation.Parameters.Add(paramForAuthHeader);

document.SecurityRequirements ??= new List<OpenApiSecurityRequirement>();
document.SecurityRequirements.Add(new OpenApiSecurityRequirement
{
[new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = BEARER_AUTH_SECURITY_SCHEME
}
}] = []
});
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -259,8 +259,7 @@ private async static Task<OpenApiDocument> GenerateOpenApiDocumentForGivenEntity
}

/// <summary>
/// Test to validate that the custom header parameters are present for each of the operation irrespective of the operation and the type
/// of the entity.
/// Test to validate that supported custom header parameters are present for each operation regardless of operation and entity type.
/// </summary>
/// <param name="entityName">Name of the entity.</param>
/// <param name="objectName">Name of the database object backing the entity.</param>
Expand All @@ -281,12 +280,10 @@ public async Task ValidateHeaderParametersForEntity(string entityName, string ob
{
foreach ((OperationType operationType, OpenApiOperation operation) in pathItem.Operations)
{
// Assert presence of Authorization header and the expected parameter properties for the header parameter.
Assert.IsTrue(operation.Parameters.Any(
// Assert absence of Authorization header because OpenAPI security schemes handle bearer auth.
Assert.IsFalse(operation.Parameters.Any(
param => param.In is ParameterLocation.Header &&
AuthorizationResolver.AUTHORIZATION_HEADER.Equals(param.Name) &&
JsonDataType.String.ToString().ToLower().Equals(param.Schema.Type) &&
param.Required is false));
AuthorizationResolver.AUTHORIZATION_HEADER.Equals(param.Name)));

// Assert presence of X-MS-API-ROLE header and the expected parameter properties for the header parameter.
Assert.IsTrue(operation.Parameters.Any(
Expand Down
96 changes: 96 additions & 0 deletions src/Service.Tests/OpenApiDocumentor/SecuritySchemeTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Collections.Generic;
using System.Linq;
using Azure.DataApiBuilder.Config.ObjectModel;
using Azure.DataApiBuilder.Core.Authorization;
using Azure.DataApiBuilder.Core.Services;
using Microsoft.OpenApi.Models;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Azure.DataApiBuilder.Service.Tests.OpenApiIntegration;

[TestClass]
public class SecuritySchemeTests
{
[TestMethod]
public void AddCustomHeadersToOperation_AddsRoleHeaderButNotAuthorizationHeader()
{
OpenApiOperation operation = new()
{
Parameters = new List<OpenApiParameter>()
};

OpenApiDocumentor.AddCustomHeadersToOperation(operation);

Assert.IsFalse(operation.Parameters.Any(param =>
param.In is ParameterLocation.Header &&
AuthorizationResolver.AUTHORIZATION_HEADER.Equals(param.Name)));

Assert.IsTrue(operation.Parameters.Any(param =>
param.In is ParameterLocation.Header &&
AuthorizationResolver.CLIENT_ROLE_HEADER.Equals(param.Name) &&
param.Schema?.Type.Equals("string") is true &&
param.Required is false));
}

[DataTestMethod]
[DataRow("AppService")]
[DataRow("StaticWebApps")]
[DataRow("Simulator")]
[DataRow("CustomJwt")]
public void AddAuthenticationSecurity_AddsBearerSecuritySchemeForAuthenticatedProviders(string provider)
{
OpenApiDocument document = new()
{
Components = new OpenApiComponents()
};

OpenApiDocumentor.AddAuthenticationSecurity(document, new AuthenticationOptions(Provider: provider));

Assert.IsNotNull(document.Components.SecuritySchemes);
Assert.AreEqual(1, document.Components.SecuritySchemes.Count);

OpenApiSecurityScheme securityScheme = document.Components.SecuritySchemes[OpenApiDocumentor.BEARER_AUTH_SECURITY_SCHEME];
Assert.AreEqual(SecuritySchemeType.Http, securityScheme.Type);
Assert.AreEqual("bearer", securityScheme.Scheme);
Assert.AreEqual("JWT", securityScheme.BearerFormat);
Assert.AreEqual(ParameterLocation.Header, securityScheme.In);
Assert.AreEqual(AuthorizationResolver.AUTHORIZATION_HEADER, securityScheme.Name);

Assert.IsNotNull(document.SecurityRequirements);
Assert.AreEqual(1, document.SecurityRequirements.Count);
Assert.IsTrue(document.SecurityRequirements.Single().Keys.Any(scheme =>
scheme.Reference?.Type is ReferenceType.SecurityScheme &&
OpenApiDocumentor.BEARER_AUTH_SECURITY_SCHEME.Equals(scheme.Reference.Id)));
}

[TestMethod]
public void AddAuthenticationSecurity_DoesNotAddBearerSecuritySchemeForUnauthenticatedProvider()
{
OpenApiDocument document = new()
{
Components = new OpenApiComponents()
};

OpenApiDocumentor.AddAuthenticationSecurity(document, new AuthenticationOptions(Provider: AuthenticationOptions.UNAUTHENTICATED_AUTHENTICATION));

Assert.IsTrue(document.Components.SecuritySchemes is null || document.Components.SecuritySchemes.Count is 0);
Assert.IsTrue(document.SecurityRequirements is null || document.SecurityRequirements.Count is 0);
}

[TestMethod]
public void AddAuthenticationSecurity_DoesNotAddBearerSecuritySchemeWhenAuthenticationOptionsAreNull()
{
OpenApiDocument document = new()
{
Components = new OpenApiComponents()
};

OpenApiDocumentor.AddAuthenticationSecurity(document, authenticationOptions: null);

Assert.IsTrue(document.Components.SecuritySchemes is null || document.Components.SecuritySchemes.Count is 0);
Assert.IsTrue(document.SecurityRequirements is null || document.SecurityRequirements.Count is 0);
}
}