Skip to content

Commit

Permalink
Add support for multiple authorization behaviors
Browse files Browse the repository at this point in the history
  • Loading branch information
C0nquistadore committed Sep 18, 2024
1 parent 3bae7e1 commit b364a59
Show file tree
Hide file tree
Showing 14 changed files with 185 additions and 41 deletions.
2 changes: 1 addition & 1 deletion src/Dibix.Http.Server/Model/HttpActionDefinition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public HttpControllerDefinition Controller
public HttpRequestBody Body { get; set; }
public HttpFileResponseDefinition FileResponse { get; set; }
public string Description { get; set; }
public HttpAuthorizationDefinition Authorization { get; set; }
public ICollection<HttpAuthorizationDefinition> Authorization { get; } = new List<HttpAuthorizationDefinition>();
public ICollection<string> SecuritySchemes { get; } = new Collection<string>();
public IList<string> RequiredClaims { get; } = new Collection<string>();
public IDictionary<int, HttpErrorResponse> StatusCodeDetectionResponses { get; }
Expand Down
11 changes: 6 additions & 5 deletions src/Dibix.Http.Server/Model/HttpApiDescriptor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ public HttpControllerDefinition Build()
private sealed class HttpActionDefinitionBuilder : HttpActionBuilderBase, IHttpActionDefinitionBuilder, IHttpActionBuilderBase, IHttpParameterSourceSelector, IHttpActionDescriptor, IHttpActionMetadata
{
private readonly string _controllerName;
private HttpAuthorizationBuilder _authorization;
private readonly ICollection<HttpAuthorizationBuilder> _authorization;
private Uri _uri;

public EndpointMetadata Metadata { get; }
Expand All @@ -92,8 +92,9 @@ private sealed class HttpActionDefinitionBuilder : HttpActionBuilderBase, IHttpA

public HttpActionDefinitionBuilder(EndpointMetadata endpointMetadata, string controllerName, IHttpActionTarget target)
{
Metadata = endpointMetadata;
_controllerName = controllerName;
_authorization = new List<HttpAuthorizationBuilder>();
Metadata = endpointMetadata;
Target = target.Build();
StatusCodeDetectionResponses = new Dictionary<int, HttpErrorResponse>(HttpStatusCodeDetectionMap.Defaults);
}
Expand All @@ -102,12 +103,12 @@ public HttpActionDefinitionBuilder(EndpointMetadata endpointMetadata, string con

public void SetStatusCodeDetectionResponse(int statusCode, int errorCode, string errorMessage) => StatusCodeDetectionResponses[statusCode] = new HttpErrorResponse(statusCode, errorCode, errorMessage);

public void WithAuthorization(IHttpActionTarget target, Action<IHttpAuthorizationBuilder> setupAction)
public void AddAuthorizationBehavior(IHttpActionTarget target, Action<IHttpAuthorizationBuilder> setupAction)
{
HttpAuthorizationBuilder builder = new HttpAuthorizationBuilder(this, target);
Guard.IsNotNull(setupAction, nameof(setupAction));
setupAction(builder);
_authorization = builder;
_authorization.Add(builder);
}

public void RegisterDelegate(Delegate @delegate) => Delegate = @delegate;
Expand All @@ -130,9 +131,9 @@ public HttpActionDefinition Build()
Body = BodyContract != null ? new HttpRequestBody(BodyContract, BodyBinder) : null,
FileResponse = FileResponse,
Description = Description,
Authorization = _authorization?.Build(),
Delegate = Delegate
};
action.Authorization.AddRange(_authorization.Select(x => x.Build()));
action.SecuritySchemes.AddRange(SecuritySchemes);
action.RequiredClaims.AddRange(RequiredClaims);
action.StatusCodeDetectionResponses.AddRange(StatusCodeDetectionResponses);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public interface IHttpActionDefinitionBuilder : IHttpActionBuilderBase

void DisableStatusCodeDetection(int statusCode);
void SetStatusCodeDetectionResponse(int statusCode, int errorCode, string errorMessage);
void WithAuthorization(IHttpActionTarget target, Action<IHttpAuthorizationBuilder> setupAction);
void AddAuthorizationBehavior(IHttpActionTarget target, Action<IHttpAuthorizationBuilder> setupAction);
void RegisterDelegate(Delegate @delegate);
}
}
7 changes: 5 additions & 2 deletions src/Dibix.Http.Server/Runtime/HttpActionInvoker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,15 @@ public static async Task<object> Invoke<TRequest>(HttpActionDefinition action, T
{
try
{
if (action.Authorization != null)
if (action.Authorization.Any())
{
// Clone the arguments, so they don't overwrite the endpoint arguments.
// For example having a 'productid' parameter in both authorization behavior and endpoint with different meanings and different types can cause collisions.
IDictionary<string, object> authorizationArguments = arguments.ToDictionary(x => x.Key, x => x.Value);
_ = await Execute(action.Authorization, request, authorizationArguments, controllerActivator, parameterDependencyResolver, cancellationToken).ConfigureAwait(false);
foreach (HttpAuthorizationDefinition authorizationDefinition in action.Authorization)
{
_ = await Execute(authorizationDefinition, request, authorizationArguments, controllerActivator, parameterDependencyResolver, cancellationToken).ConfigureAwait(false);
}
}

object result = await Execute(action, request, arguments, controllerActivator, parameterDependencyResolver, cancellationToken).ConfigureAwait(false);
Expand Down
7 changes: 5 additions & 2 deletions src/Dibix.Sdk.CodeGeneration/Model/ActionDefinition.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
namespace Dibix.Sdk.CodeGeneration
using System.Collections.Generic;
using System.Collections.ObjectModel;

namespace Dibix.Sdk.CodeGeneration
{
public sealed class ActionDefinition : ActionTargetDefinition
{
Expand All @@ -9,7 +12,7 @@ public sealed class ActionDefinition : ActionTargetDefinition
public Token<string> ChildRoute { get; set; }
public ActionRequestBody RequestBody { get; set; }
public SecuritySchemeRequirements SecuritySchemes { get; } = new SecuritySchemeRequirements(SecuritySchemeOperator.Or);
public AuthorizationBehavior Authorization { get; set; }
public ICollection<AuthorizationBehavior> Authorization { get; set; } = new Collection<AuthorizationBehavior>();
public ActionCompatibilityLevel CompatibilityLevel { get; set; } = ActionCompatibilityLevel.Native;
}
}
6 changes: 3 additions & 3 deletions src/Dibix.Sdk.CodeGeneration/Output/ApiDescriptionWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -199,10 +199,10 @@ private void WriteActionConfiguration(CodeGenerationContext context, StringWrite
writer.WriteLine($"{variableName}.SetStatusCodeDetectionResponse({httpStatusCode}, {errorCode}, {(errorMessage != null ? $"\"{errorMessage}\"" : "errorMessage: null")});");
}

if (action.Authorization != null)
foreach (AuthorizationBehavior authorizationBehavior in action.Authorization)
{
writer.Write($"{variableName}.WithAuthorization(");
WriteActionTarget(context, writer, action.Authorization, "authorization", WriteAuthorizationBehavior);
writer.Write($"{variableName}.AddAuthorizationBehavior(");
WriteActionTarget(context, writer, authorizationBehavior, "authorization", WriteAuthorizationBehavior);
}

if (_compatibilityLevel == ActionCompatibilityLevel.Native)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -523,54 +523,64 @@ private void CollectAuthorization(JObject actionJson, ActionDefinition actionDef
return;
}

CollectAuthorization(property, property.Value.Type, actionDefinition, pathParameters);
CollectAuthorization(property.Value.Type, property.Value, actionDefinition, pathParameters);
}
private void CollectAuthorization(JProperty property, JTokenType type, ActionDefinition actionDefinition, IReadOnlyDictionary<string, PathParameter> pathParameters)
private void CollectAuthorization(JTokenType type, JToken value, ActionDefinition actionDefinition, IReadOnlyDictionary<string, PathParameter> pathParameters)
{
switch (type)
{
case JTokenType.Object:
JObject authorizationValue = (JObject)property.Value;
JProperty templateProperty = authorizationValue.Property("name");
CollectAuthorization(templateProperty, authorizationValue, actionDefinition, pathParameters);
JObject authorizationValue = (JObject)value;
JToken templateNameValue = authorizationValue.Property("name")?.Value;
CollectAuthorization(templateNameValue, authorizationValue, actionDefinition, pathParameters);
break;

case JTokenType.String when (string)property.Value == "none":
case JTokenType.String when (string)value == "none":
break;

case JTokenType.String:
CollectAuthorization(templateProperty: property, authorizationValue: new JObject(), actionDefinition, pathParameters);
CollectAuthorization(templateNameValue: value, authorizationValue: new JObject(), actionDefinition, pathParameters);
break;

case JTokenType.Array:
JArray array = (JArray)value;
foreach (JToken item in array)
CollectAuthorization(item.Type, item, actionDefinition, pathParameters);

break;

default:
throw new ArgumentOutOfRangeException(nameof(type), type, property.Value.Path);
throw new ArgumentOutOfRangeException(nameof(type), type, value.Path);
}
}
private void CollectAuthorization(JProperty templateProperty, JObject authorizationValue, ActionDefinition actionDefinition, IReadOnlyDictionary<string, PathParameter> pathParameters)
private void CollectAuthorization(JToken templateNameValue, JObject authorizationValue, ActionDefinition actionDefinition, IReadOnlyDictionary<string, PathParameter> pathParameters)
{
JObject authorization = authorizationValue;

if (templateProperty != null
&& (authorization = ApplyAuthorizationTemplate(templateProperty, authorizationValue)) == null) // In case of error that has been previously logged
return;
// "name" property is optional, when the endpoint defines an authorization behavior manually
if (templateNameValue != null)
{
authorization = ApplyAuthorizationTemplate(templateNameValue, authorizationValue);
if (authorization == null) // If the template is not found, it will have been logged already at this point
return;
}

IReadOnlyDictionary<string, ExplicitParameter> explicitParameters = CollectExplicitParameters(authorization, requestBody: null, pathParameters);
ICollection<string> bodyParameters = new Collection<string>();
actionDefinition.Authorization = CreateActionDefinition<AuthorizationBehavior>(authorization, explicitParameters, pathParameters, bodyParameters, requestBody: null);
AuthorizationBehavior authorizationBehavior = CreateActionDefinition<AuthorizationBehavior>(authorization, explicitParameters, pathParameters, bodyParameters, requestBody: null);
actionDefinition.Authorization.Add(authorizationBehavior);
}

private JObject ApplyAuthorizationTemplate(JProperty templateNameProperty, JObject authorizationTemplateReference)
private JObject ApplyAuthorizationTemplate(JToken templateNameValue, JObject authorizationTemplateReference)
{
string templateName = (string)templateNameProperty.Value;
string templateName = (string)templateNameValue;
if (!_templates.Authorization.TryGetTemplate(templateName, out ConfigurationAuthorizationTemplate template))
{
SourceLocation templateNameLineInfo = templateNameProperty.Value.GetSourceInfo();
SourceLocation templateNameLineInfo = templateNameValue.GetSourceInfo();
Logger.LogError($"Unknown authorization template '{templateName}'", templateNameLineInfo.Source, templateNameLineInfo.Line, templateNameLineInfo.Column);
return null;
}

templateNameProperty.Remove();

JObject resolvedAuthorization = new JObject();

if (authorizationTemplateReference.HasValues)
Expand Down
2 changes: 1 addition & 1 deletion src/Dibix.Testing/TestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ protected void AssertEqual(string expected, string actual, string outputName, st
actualNormalized = actual.NormalizeLineEndings();
}

if (Equals(expectedNormalized, actualNormalized))
if (Equals(expectedNormalized, actualNormalized))
return;

TestResultComposer.AddFileComparison(expectedNormalized, actualNormalized, outputName, extension);
Expand Down
14 changes: 11 additions & 3 deletions tests/Dibix.Http.Server.Tests/HttpActionInvokerTest.Base.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ private static async Task<object> Execute<TRequest>(HttpActionDefinition action,
IDictionary<string, object> arguments = new Dictionary<string, object> { ["databaseAccessorFactory"] = null };
foreach (KeyValuePair<string, object> parameter in parameters)
arguments.Add(parameter);

object result = await HttpActionInvoker.Invoke(action, request, responseFormatter, arguments, ControllerActivator.NotImplemented, parameterDependencyResolver.Object, default).ConfigureAwait(false);
return result;
}
Expand Down Expand Up @@ -99,8 +99,11 @@ public HttpApiRegistration(string testName, Action<IHttpActionDefinitionBuilder>
configureActions?.Invoke(builder);

string authorizationMethodName = $"{testName}_Authorization_Target";
if (typeof(HttpActionInvokerTest).GetMethod(authorizationMethodName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static) != null)
builder.WithAuthorization(ReflectionHttpActionTarget.Create(typeof(HttpActionInvokerTest), authorizationMethodName), configureAuthorization ?? (_ => { }));
foreach (MethodInfo method in typeof(HttpActionInvokerTest).GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static).OrderBy(x => x.Name))
{
if (method.Name.StartsWith(authorizationMethodName, StringComparison.Ordinal))
builder.AddAuthorizationBehavior(ReflectionHttpActionTarget.Create(typeof(HttpActionInvokerTest), method.Name), configureAuthorization ?? (_ => { }));
}
};
}

Expand All @@ -113,5 +116,10 @@ private sealed class X : StructuredType<X, int, string>

public void Add(int intValue, string stringValue) => base.AddValues(intValue, stringValue);
}

private sealed class HttpAuthorizationBehaviorContext
{
public string Result { get; set; }
}
}
}
7 changes: 5 additions & 2 deletions tests/Dibix.Http.Server.Tests/HttpActionInvokerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -109,13 +109,15 @@ public async Task Invoke_DDL_WithHttpClientError_ProducedByAuthorizationBehavior
Assert.AreEqual(1, action.RequiredClaims.Count, "action.RequiredClaims.Count");
Assert.AreEqual(ClaimTypes.NameIdentifier, action.RequiredClaims[0], "action.RequiredClaims[0]");

HttpAuthorizationBehaviorContext httpAuthorizationBehaviorContext = new HttpAuthorizationBehaviorContext();
try
{
await Execute(action, request.Object, responseFormatter.Object).ConfigureAwait(false);
await Execute(action, request.Object, responseFormatter.Object, [new KeyValuePair<string, object>("context", httpAuthorizationBehaviorContext)]).ConfigureAwait(false);
Assert.Fail($"{nameof(HttpRequestExecutionException)} was expected but not thrown");
}
catch (HttpRequestExecutionException requestException)
{
Assert.AreEqual("FirstAuthorizationTargetCalled", httpAuthorizationBehaviorContext.Result);
requestException.AppendToResponse(response.Object);
Assert.AreEqual(@"403 Forbidden: Sorry
CommandType: 0
Expand All @@ -128,7 +130,8 @@ public async Task Invoke_DDL_WithHttpClientError_ProducedByAuthorizationBehavior
}
}
private static void Invoke_DDL_WithHttpClientError_ProducedByAuthorizationBehavior_IsMappedToHttpStatusCode_Target(IDatabaseAccessorFactory databaseAccessorFactory) { }
private static void Invoke_DDL_WithHttpClientError_ProducedByAuthorizationBehavior_IsMappedToHttpStatusCode_Authorization_Target(IDatabaseAccessorFactory databaseAccessorFactory, string userid) => throw CreateException(errorInfoNumber: 403001, errorMessage: "Sorry");
private static void Invoke_DDL_WithHttpClientError_ProducedByAuthorizationBehavior_IsMappedToHttpStatusCode_Authorization_Target1(IDatabaseAccessorFactory databaseAccessorFactory, HttpAuthorizationBehaviorContext context) => context.Result = "FirstAuthorizationTargetCalled";
private static void Invoke_DDL_WithHttpClientError_ProducedByAuthorizationBehavior_IsMappedToHttpStatusCode_Authorization_Target2(IDatabaseAccessorFactory databaseAccessorFactory, string userid) => throw CreateException(errorInfoNumber: 403001, errorMessage: "Sorry");

[TestMethod]
public async Task Invoke_DDL_WithHttpClientError_AutoDetectedByDatabaseErrorCode_IsMappedToHttpStatusCode()
Expand Down
16 changes: 16 additions & 0 deletions tests/Dibix.Sdk.Tests.Database/Endpoints/GenericEndpoint.json
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,22 @@
"password": null
},
"authorization": "AssertAuthorizedOne"
},
{
"method": "DELETE",
"childRoute": "MultipleAuthorizationBehaviors",
"target": "EmptyWithParams",
"params": {
"c": null,
"password": null
},
"authorization": [
"AssertAuthorizedOne",
{
"name": "AssertAuthorized",
"right": 1
}
]
}
]
}
Loading

0 comments on commit b364a59

Please sign in to comment.