Skip to content

Commit b364a59

Browse files
committed
Add support for multiple authorization behaviors
1 parent 3bae7e1 commit b364a59

14 files changed

Lines changed: 185 additions & 41 deletions

File tree

src/Dibix.Http.Server/Model/HttpActionDefinition.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public HttpControllerDefinition Controller
2727
public HttpRequestBody Body { get; set; }
2828
public HttpFileResponseDefinition FileResponse { get; set; }
2929
public string Description { get; set; }
30-
public HttpAuthorizationDefinition Authorization { get; set; }
30+
public ICollection<HttpAuthorizationDefinition> Authorization { get; } = new List<HttpAuthorizationDefinition>();
3131
public ICollection<string> SecuritySchemes { get; } = new Collection<string>();
3232
public IList<string> RequiredClaims { get; } = new Collection<string>();
3333
public IDictionary<int, HttpErrorResponse> StatusCodeDetectionResponses { get; }

src/Dibix.Http.Server/Model/HttpApiDescriptor.cs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ public HttpControllerDefinition Build()
7272
private sealed class HttpActionDefinitionBuilder : HttpActionBuilderBase, IHttpActionDefinitionBuilder, IHttpActionBuilderBase, IHttpParameterSourceSelector, IHttpActionDescriptor, IHttpActionMetadata
7373
{
7474
private readonly string _controllerName;
75-
private HttpAuthorizationBuilder _authorization;
75+
private readonly ICollection<HttpAuthorizationBuilder> _authorization;
7676
private Uri _uri;
7777

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

9393
public HttpActionDefinitionBuilder(EndpointMetadata endpointMetadata, string controllerName, IHttpActionTarget target)
9494
{
95-
Metadata = endpointMetadata;
9695
_controllerName = controllerName;
96+
_authorization = new List<HttpAuthorizationBuilder>();
97+
Metadata = endpointMetadata;
9798
Target = target.Build();
9899
StatusCodeDetectionResponses = new Dictionary<int, HttpErrorResponse>(HttpStatusCodeDetectionMap.Defaults);
99100
}
@@ -102,12 +103,12 @@ public HttpActionDefinitionBuilder(EndpointMetadata endpointMetadata, string con
102103

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

105-
public void WithAuthorization(IHttpActionTarget target, Action<IHttpAuthorizationBuilder> setupAction)
106+
public void AddAuthorizationBehavior(IHttpActionTarget target, Action<IHttpAuthorizationBuilder> setupAction)
106107
{
107108
HttpAuthorizationBuilder builder = new HttpAuthorizationBuilder(this, target);
108109
Guard.IsNotNull(setupAction, nameof(setupAction));
109110
setupAction(builder);
110-
_authorization = builder;
111+
_authorization.Add(builder);
111112
}
112113

113114
public void RegisterDelegate(Delegate @delegate) => Delegate = @delegate;
@@ -130,9 +131,9 @@ public HttpActionDefinition Build()
130131
Body = BodyContract != null ? new HttpRequestBody(BodyContract, BodyBinder) : null,
131132
FileResponse = FileResponse,
132133
Description = Description,
133-
Authorization = _authorization?.Build(),
134134
Delegate = Delegate
135135
};
136+
action.Authorization.AddRange(_authorization.Select(x => x.Build()));
136137
action.SecuritySchemes.AddRange(SecuritySchemes);
137138
action.RequiredClaims.AddRange(RequiredClaims);
138139
action.StatusCodeDetectionResponses.AddRange(StatusCodeDetectionResponses);

src/Dibix.Http.Server/Model/IHttpActionDefinitionBuilder.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public interface IHttpActionDefinitionBuilder : IHttpActionBuilderBase
1717

1818
void DisableStatusCodeDetection(int statusCode);
1919
void SetStatusCodeDetectionResponse(int statusCode, int errorCode, string errorMessage);
20-
void WithAuthorization(IHttpActionTarget target, Action<IHttpAuthorizationBuilder> setupAction);
20+
void AddAuthorizationBehavior(IHttpActionTarget target, Action<IHttpAuthorizationBuilder> setupAction);
2121
void RegisterDelegate(Delegate @delegate);
2222
}
2323
}

src/Dibix.Http.Server/Runtime/HttpActionInvoker.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,15 @@ public static async Task<object> Invoke<TRequest>(HttpActionDefinition action, T
1717
{
1818
try
1919
{
20-
if (action.Authorization != null)
20+
if (action.Authorization.Any())
2121
{
2222
// Clone the arguments, so they don't overwrite the endpoint arguments.
2323
// For example having a 'productid' parameter in both authorization behavior and endpoint with different meanings and different types can cause collisions.
2424
IDictionary<string, object> authorizationArguments = arguments.ToDictionary(x => x.Key, x => x.Value);
25-
_ = await Execute(action.Authorization, request, authorizationArguments, controllerActivator, parameterDependencyResolver, cancellationToken).ConfigureAwait(false);
25+
foreach (HttpAuthorizationDefinition authorizationDefinition in action.Authorization)
26+
{
27+
_ = await Execute(authorizationDefinition, request, authorizationArguments, controllerActivator, parameterDependencyResolver, cancellationToken).ConfigureAwait(false);
28+
}
2629
}
2730

2831
object result = await Execute(action, request, arguments, controllerActivator, parameterDependencyResolver, cancellationToken).ConfigureAwait(false);

src/Dibix.Sdk.CodeGeneration/Model/ActionDefinition.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
namespace Dibix.Sdk.CodeGeneration
1+
using System.Collections.Generic;
2+
using System.Collections.ObjectModel;
3+
4+
namespace Dibix.Sdk.CodeGeneration
25
{
36
public sealed class ActionDefinition : ActionTargetDefinition
47
{
@@ -9,7 +12,7 @@ public sealed class ActionDefinition : ActionTargetDefinition
912
public Token<string> ChildRoute { get; set; }
1013
public ActionRequestBody RequestBody { get; set; }
1114
public SecuritySchemeRequirements SecuritySchemes { get; } = new SecuritySchemeRequirements(SecuritySchemeOperator.Or);
12-
public AuthorizationBehavior Authorization { get; set; }
15+
public ICollection<AuthorizationBehavior> Authorization { get; set; } = new Collection<AuthorizationBehavior>();
1316
public ActionCompatibilityLevel CompatibilityLevel { get; set; } = ActionCompatibilityLevel.Native;
1417
}
1518
}

src/Dibix.Sdk.CodeGeneration/Output/ApiDescriptionWriter.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -199,10 +199,10 @@ private void WriteActionConfiguration(CodeGenerationContext context, StringWrite
199199
writer.WriteLine($"{variableName}.SetStatusCodeDetectionResponse({httpStatusCode}, {errorCode}, {(errorMessage != null ? $"\"{errorMessage}\"" : "errorMessage: null")});");
200200
}
201201

202-
if (action.Authorization != null)
202+
foreach (AuthorizationBehavior authorizationBehavior in action.Authorization)
203203
{
204-
writer.Write($"{variableName}.WithAuthorization(");
205-
WriteActionTarget(context, writer, action.Authorization, "authorization", WriteAuthorizationBehavior);
204+
writer.Write($"{variableName}.AddAuthorizationBehavior(");
205+
WriteActionTarget(context, writer, authorizationBehavior, "authorization", WriteAuthorizationBehavior);
206206
}
207207

208208
if (_compatibilityLevel == ActionCompatibilityLevel.Native)

src/Dibix.Sdk.CodeGeneration/Registration/ControllerDefinitionProvider.cs

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -523,54 +523,64 @@ private void CollectAuthorization(JObject actionJson, ActionDefinition actionDef
523523
return;
524524
}
525525

526-
CollectAuthorization(property, property.Value.Type, actionDefinition, pathParameters);
526+
CollectAuthorization(property.Value.Type, property.Value, actionDefinition, pathParameters);
527527
}
528-
private void CollectAuthorization(JProperty property, JTokenType type, ActionDefinition actionDefinition, IReadOnlyDictionary<string, PathParameter> pathParameters)
528+
private void CollectAuthorization(JTokenType type, JToken value, ActionDefinition actionDefinition, IReadOnlyDictionary<string, PathParameter> pathParameters)
529529
{
530530
switch (type)
531531
{
532532
case JTokenType.Object:
533-
JObject authorizationValue = (JObject)property.Value;
534-
JProperty templateProperty = authorizationValue.Property("name");
535-
CollectAuthorization(templateProperty, authorizationValue, actionDefinition, pathParameters);
533+
JObject authorizationValue = (JObject)value;
534+
JToken templateNameValue = authorizationValue.Property("name")?.Value;
535+
CollectAuthorization(templateNameValue, authorizationValue, actionDefinition, pathParameters);
536536
break;
537537

538-
case JTokenType.String when (string)property.Value == "none":
538+
case JTokenType.String when (string)value == "none":
539539
break;
540540

541541
case JTokenType.String:
542-
CollectAuthorization(templateProperty: property, authorizationValue: new JObject(), actionDefinition, pathParameters);
542+
CollectAuthorization(templateNameValue: value, authorizationValue: new JObject(), actionDefinition, pathParameters);
543+
break;
544+
545+
case JTokenType.Array:
546+
JArray array = (JArray)value;
547+
foreach (JToken item in array)
548+
CollectAuthorization(item.Type, item, actionDefinition, pathParameters);
549+
543550
break;
544551

545552
default:
546-
throw new ArgumentOutOfRangeException(nameof(type), type, property.Value.Path);
553+
throw new ArgumentOutOfRangeException(nameof(type), type, value.Path);
547554
}
548555
}
549-
private void CollectAuthorization(JProperty templateProperty, JObject authorizationValue, ActionDefinition actionDefinition, IReadOnlyDictionary<string, PathParameter> pathParameters)
556+
private void CollectAuthorization(JToken templateNameValue, JObject authorizationValue, ActionDefinition actionDefinition, IReadOnlyDictionary<string, PathParameter> pathParameters)
550557
{
551558
JObject authorization = authorizationValue;
552559

553-
if (templateProperty != null
554-
&& (authorization = ApplyAuthorizationTemplate(templateProperty, authorizationValue)) == null) // In case of error that has been previously logged
555-
return;
560+
// "name" property is optional, when the endpoint defines an authorization behavior manually
561+
if (templateNameValue != null)
562+
{
563+
authorization = ApplyAuthorizationTemplate(templateNameValue, authorizationValue);
564+
if (authorization == null) // If the template is not found, it will have been logged already at this point
565+
return;
566+
}
556567

557568
IReadOnlyDictionary<string, ExplicitParameter> explicitParameters = CollectExplicitParameters(authorization, requestBody: null, pathParameters);
558569
ICollection<string> bodyParameters = new Collection<string>();
559-
actionDefinition.Authorization = CreateActionDefinition<AuthorizationBehavior>(authorization, explicitParameters, pathParameters, bodyParameters, requestBody: null);
570+
AuthorizationBehavior authorizationBehavior = CreateActionDefinition<AuthorizationBehavior>(authorization, explicitParameters, pathParameters, bodyParameters, requestBody: null);
571+
actionDefinition.Authorization.Add(authorizationBehavior);
560572
}
561573

562-
private JObject ApplyAuthorizationTemplate(JProperty templateNameProperty, JObject authorizationTemplateReference)
574+
private JObject ApplyAuthorizationTemplate(JToken templateNameValue, JObject authorizationTemplateReference)
563575
{
564-
string templateName = (string)templateNameProperty.Value;
576+
string templateName = (string)templateNameValue;
565577
if (!_templates.Authorization.TryGetTemplate(templateName, out ConfigurationAuthorizationTemplate template))
566578
{
567-
SourceLocation templateNameLineInfo = templateNameProperty.Value.GetSourceInfo();
579+
SourceLocation templateNameLineInfo = templateNameValue.GetSourceInfo();
568580
Logger.LogError($"Unknown authorization template '{templateName}'", templateNameLineInfo.Source, templateNameLineInfo.Line, templateNameLineInfo.Column);
569581
return null;
570582
}
571583

572-
templateNameProperty.Remove();
573-
574584
JObject resolvedAuthorization = new JObject();
575585

576586
if (authorizationTemplateReference.HasValues)

src/Dibix.Testing/TestBase.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ protected void AssertEqual(string expected, string actual, string outputName, st
108108
actualNormalized = actual.NormalizeLineEndings();
109109
}
110110

111-
if (Equals(expectedNormalized, actualNormalized))
111+
if (Equals(expectedNormalized, actualNormalized))
112112
return;
113113

114114
TestResultComposer.AddFileComparison(expectedNormalized, actualNormalized, outputName, extension);

tests/Dibix.Http.Server.Tests/HttpActionInvokerTest.Base.cs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ private static async Task<object> Execute<TRequest>(HttpActionDefinition action,
5151
IDictionary<string, object> arguments = new Dictionary<string, object> { ["databaseAccessorFactory"] = null };
5252
foreach (KeyValuePair<string, object> parameter in parameters)
5353
arguments.Add(parameter);
54-
54+
5555
object result = await HttpActionInvoker.Invoke(action, request, responseFormatter, arguments, ControllerActivator.NotImplemented, parameterDependencyResolver.Object, default).ConfigureAwait(false);
5656
return result;
5757
}
@@ -99,8 +99,11 @@ public HttpApiRegistration(string testName, Action<IHttpActionDefinitionBuilder>
9999
configureActions?.Invoke(builder);
100100

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

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

114117
public void Add(int intValue, string stringValue) => base.AddValues(intValue, stringValue);
115118
}
119+
120+
private sealed class HttpAuthorizationBehaviorContext
121+
{
122+
public string Result { get; set; }
123+
}
116124
}
117125
}

tests/Dibix.Http.Server.Tests/HttpActionInvokerTest.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,13 +109,15 @@ public async Task Invoke_DDL_WithHttpClientError_ProducedByAuthorizationBehavior
109109
Assert.AreEqual(1, action.RequiredClaims.Count, "action.RequiredClaims.Count");
110110
Assert.AreEqual(ClaimTypes.NameIdentifier, action.RequiredClaims[0], "action.RequiredClaims[0]");
111111

112+
HttpAuthorizationBehaviorContext httpAuthorizationBehaviorContext = new HttpAuthorizationBehaviorContext();
112113
try
113114
{
114-
await Execute(action, request.Object, responseFormatter.Object).ConfigureAwait(false);
115+
await Execute(action, request.Object, responseFormatter.Object, [new KeyValuePair<string, object>("context", httpAuthorizationBehaviorContext)]).ConfigureAwait(false);
115116
Assert.Fail($"{nameof(HttpRequestExecutionException)} was expected but not thrown");
116117
}
117118
catch (HttpRequestExecutionException requestException)
118119
{
120+
Assert.AreEqual("FirstAuthorizationTargetCalled", httpAuthorizationBehaviorContext.Result);
119121
requestException.AppendToResponse(response.Object);
120122
Assert.AreEqual(@"403 Forbidden: Sorry
121123
CommandType: 0
@@ -128,7 +130,8 @@ public async Task Invoke_DDL_WithHttpClientError_ProducedByAuthorizationBehavior
128130
}
129131
}
130132
private static void Invoke_DDL_WithHttpClientError_ProducedByAuthorizationBehavior_IsMappedToHttpStatusCode_Target(IDatabaseAccessorFactory databaseAccessorFactory) { }
131-
private static void Invoke_DDL_WithHttpClientError_ProducedByAuthorizationBehavior_IsMappedToHttpStatusCode_Authorization_Target(IDatabaseAccessorFactory databaseAccessorFactory, string userid) => throw CreateException(errorInfoNumber: 403001, errorMessage: "Sorry");
133+
private static void Invoke_DDL_WithHttpClientError_ProducedByAuthorizationBehavior_IsMappedToHttpStatusCode_Authorization_Target1(IDatabaseAccessorFactory databaseAccessorFactory, HttpAuthorizationBehaviorContext context) => context.Result = "FirstAuthorizationTargetCalled";
134+
private static void Invoke_DDL_WithHttpClientError_ProducedByAuthorizationBehavior_IsMappedToHttpStatusCode_Authorization_Target2(IDatabaseAccessorFactory databaseAccessorFactory, string userid) => throw CreateException(errorInfoNumber: 403001, errorMessage: "Sorry");
132135

133136
[TestMethod]
134137
public async Task Invoke_DDL_WithHttpClientError_AutoDetectedByDatabaseErrorCode_IsMappedToHttpStatusCode()

0 commit comments

Comments
 (0)