From c20ad8d6cdcf3c8cefaea7e9bdc333512312b67b Mon Sep 17 00:00:00 2001 From: "adam.gloyne" Date: Tue, 23 Jul 2024 13:05:55 +0100 Subject: [PATCH] feat: implement principal parse logic --- src/LEGO.AsyncAPI.Bindings/Sns/Principal.cs | 66 +++++++++++++++++++ .../Sns/SnsChannelBinding.cs | 2 +- src/LEGO.AsyncAPI.Bindings/Sns/Statement.cs | 6 +- src/LEGO.AsyncAPI.Bindings/Sqs/Principal.cs | 66 +++++++++++++++++++ .../Sqs/SqsChannelBinding.cs | 2 +- .../Sqs/SqsOperationBinding.cs | 2 +- src/LEGO.AsyncAPI.Bindings/Sqs/Statement.cs | 4 +- .../StringOrStringList.cs | 4 +- .../Bindings/Sns/SnsBindings_Should.cs | 21 ++++-- .../Bindings/Sqs/SqsBindings_should.cs | 48 +++++++++----- 10 files changed, 186 insertions(+), 35 deletions(-) create mode 100644 src/LEGO.AsyncAPI.Bindings/Sns/Principal.cs create mode 100644 src/LEGO.AsyncAPI.Bindings/Sqs/Principal.cs diff --git a/src/LEGO.AsyncAPI.Bindings/Sns/Principal.cs b/src/LEGO.AsyncAPI.Bindings/Sns/Principal.cs new file mode 100644 index 00000000..0316068e --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/Sns/Principal.cs @@ -0,0 +1,66 @@ +namespace LEGO.AsyncAPI.Bindings.Sns; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Nodes; +using LEGO.AsyncAPI.Models; +using LEGO.AsyncAPI.Models.Interfaces; +using LEGO.AsyncAPI.Readers.ParseNodes; + +public class Principal : IAsyncApiElement +{ + public Principal(AsyncApiAny value) + { + this.Value = value; + } + + public AsyncApiAny Value { get; } + + public static Principal Parse(ParseNode node) + { + switch (node) + { + case ValueNode: + var nodeValue = node.GetScalarValue(); + if (!IsStarString(nodeValue)) + { + throw new ArgumentException($"An error occured while parsing a {nameof(Principal)} node. " + + $"Principal value without a property name can only be a string value of '*'."); + } + + return new Principal(new AsyncApiAny(nodeValue)); + case MapNode mapNode: + { + var propertyNode = mapNode.First(); + if (!IsValidPrincipalProperty(propertyNode.Name)) + { + throw new ArgumentException($"An error occured while parsing a {nameof(Principal)} node. " + + $"Node should contain a valid AWS principal property name."); + } + + var parsedObject = new Dictionary() + { { propertyNode.Name, StringOrStringList.Parse(propertyNode.Value).Value } }; + + return new Principal(new AsyncApiAny(parsedObject)); + } + + default: + throw new ArgumentException($"An error occured while parsing a {nameof(Principal)} node. " + + $"Node should contain a string value of '*' or a valid AWS principal property."); + } + } + + private static bool IsStarString(JsonNode value) + { + var element = JsonDocument.Parse(value.ToJsonString()).RootElement; + + return element.ValueKind == JsonValueKind.String && element.ValueEquals("*"); + } + + private static bool IsValidPrincipalProperty(string property) + { + return new[] { "AWS", "Service" }.Contains(property); + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI.Bindings/Sns/SnsChannelBinding.cs b/src/LEGO.AsyncAPI.Bindings/Sns/SnsChannelBinding.cs index 0328068b..4d8668c9 100644 --- a/src/LEGO.AsyncAPI.Bindings/Sns/SnsChannelBinding.cs +++ b/src/LEGO.AsyncAPI.Bindings/Sns/SnsChannelBinding.cs @@ -57,7 +57,7 @@ public class SnsChannelBinding : ChannelBinding private static FixedFieldMap statementFixedFields = new() { { "effect", (a, n) => { a.Effect = n.GetScalarValue().GetEnumFromDisplayName(); } }, - { "principal", (a, n) => { a.Principal = n.CreateAny(); } }, + { "principal", (a, n) => { a.Principal = Principal.Parse(n); } }, { "action", (a, n) => { a.Action = StringOrStringList.Parse(n); } }, { "resource", (a, n) => { a.Resource = StringOrStringList.Parse(n); } }, { "condition", (a, n) => { a.Condition = n.CreateAny(); } }, diff --git a/src/LEGO.AsyncAPI.Bindings/Sns/Statement.cs b/src/LEGO.AsyncAPI.Bindings/Sns/Statement.cs index 4a15f0fb..2bccbb97 100644 --- a/src/LEGO.AsyncAPI.Bindings/Sns/Statement.cs +++ b/src/LEGO.AsyncAPI.Bindings/Sns/Statement.cs @@ -18,10 +18,10 @@ public class Statement : IAsyncApiExtensible /// /// The AWS account(s) or resource ARN(s) that this statement applies to. /// - public AsyncApiAny Principal { get; set; } + public Principal Principal { get; set; } /// - /// The SNS permission being allowed or denied e.g. sns:Publish + /// The SNS permission being allowed or denied e.g. sns:Publish. /// public StringOrStringList Action { get; set; } @@ -46,7 +46,7 @@ public void Serialize(IAsyncApiWriter writer) writer.WriteStartObject(); writer.WriteRequiredProperty("effect", this.Effect.GetDisplayName()); - writer.WriteRequiredObject("principal", this.Principal, (w, t) => t.Write(w)); + writer.WriteRequiredObject("principal", this.Principal, (w, t) => t.Value.Write(w)); writer.WriteRequiredObject("action", this.Action, (w, t) => t.Value.Write(w)); writer.WriteOptionalObject("resource", this.Resource, (w, t) => t?.Value.Write(w)); writer.WriteOptionalObject("condition", this.Condition, (w, t) => t?.Write(w)); diff --git a/src/LEGO.AsyncAPI.Bindings/Sqs/Principal.cs b/src/LEGO.AsyncAPI.Bindings/Sqs/Principal.cs new file mode 100644 index 00000000..5a67b588 --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/Sqs/Principal.cs @@ -0,0 +1,66 @@ +namespace LEGO.AsyncAPI.Bindings.Sqs; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Nodes; +using LEGO.AsyncAPI.Models; +using LEGO.AsyncAPI.Models.Interfaces; +using LEGO.AsyncAPI.Readers.ParseNodes; + +public class Principal : IAsyncApiElement +{ + public Principal(AsyncApiAny value) + { + this.Value = value; + } + + public AsyncApiAny Value { get; } + + public static Principal Parse(ParseNode node) + { + switch (node) + { + case ValueNode: + var nodeValue = node.GetScalarValue(); + if (!IsStarString(nodeValue)) + { + throw new ArgumentException($"An error occured while parsing a {nameof(Principal)} node. " + + $"Principal value without a property name can only be a string value of '*'."); + } + + return new Principal(new AsyncApiAny(nodeValue)); + case MapNode mapNode: + { + var propertyNode = mapNode.First(); + if (!IsValidPrincipalProperty(propertyNode.Name)) + { + throw new ArgumentException($"An error occured while parsing a {nameof(Principal)} node. " + + $"Node should contain a valid AWS principal property name."); + } + + var parsedObject = new Dictionary() + { { propertyNode.Name, StringOrStringList.Parse(propertyNode.Value).Value } }; + + return new Principal(new AsyncApiAny(parsedObject)); + } + + default: + throw new ArgumentException($"An error occured while parsing a {nameof(Principal)} node. " + + $"Node should contain a string value of '*' or a valid AWS principal property."); + } + } + + private static bool IsStarString(JsonNode value) + { + var element = JsonDocument.Parse(value.ToJsonString()).RootElement; + + return element.ValueKind == JsonValueKind.String && element.ValueEquals("*"); + } + + private static bool IsValidPrincipalProperty(string property) + { + return new[] { "AWS", "Service" }.Contains(property); + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI.Bindings/Sqs/SqsChannelBinding.cs b/src/LEGO.AsyncAPI.Bindings/Sqs/SqsChannelBinding.cs index 3670c0c5..f0b24be7 100644 --- a/src/LEGO.AsyncAPI.Bindings/Sqs/SqsChannelBinding.cs +++ b/src/LEGO.AsyncAPI.Bindings/Sqs/SqsChannelBinding.cs @@ -64,7 +64,7 @@ public class SqsChannelBinding : ChannelBinding private static FixedFieldMap statementFixedFields = new() { { "effect", (a, n) => { a.Effect = n.GetScalarValue().GetEnumFromDisplayName(); } }, - { "principal", (a, n) => { a.Principal = n.CreateAny(); } }, + { "principal", (a, n) => { a.Principal = Principal.Parse(n); } }, { "action", (a, n) => { a.Action = StringOrStringList.Parse(n); } }, { "resource", (a, n) => { a.Resource = StringOrStringList.Parse(n); } }, { "condition", (a, n) => { a.Condition = n.CreateAny(); } }, diff --git a/src/LEGO.AsyncAPI.Bindings/Sqs/SqsOperationBinding.cs b/src/LEGO.AsyncAPI.Bindings/Sqs/SqsOperationBinding.cs index 9aaf391a..ed278013 100644 --- a/src/LEGO.AsyncAPI.Bindings/Sqs/SqsOperationBinding.cs +++ b/src/LEGO.AsyncAPI.Bindings/Sqs/SqsOperationBinding.cs @@ -56,7 +56,7 @@ public class SqsOperationBinding : OperationBinding private static FixedFieldMap statementFixedFields = new() { { "effect", (a, n) => { a.Effect = n.GetScalarValue().GetEnumFromDisplayName(); } }, - { "principal", (a, n) => { a.Principal = n.CreateAny(); } }, + { "principal", (a, n) => { a.Principal = Principal.Parse(n); } }, { "action", (a, n) => { a.Action = StringOrStringList.Parse(n); } }, { "resource", (a, n) => { a.Resource = StringOrStringList.Parse(n); } }, { "condition", (a, n) => { a.Condition = n.CreateAny(); } }, diff --git a/src/LEGO.AsyncAPI.Bindings/Sqs/Statement.cs b/src/LEGO.AsyncAPI.Bindings/Sqs/Statement.cs index 1c015b32..938f69c3 100644 --- a/src/LEGO.AsyncAPI.Bindings/Sqs/Statement.cs +++ b/src/LEGO.AsyncAPI.Bindings/Sqs/Statement.cs @@ -19,7 +19,7 @@ public class Statement : IAsyncApiExtensible /// /// The AWS account(s) or resource ARN(s) that this statement applies to. /// - public AsyncApiAny Principal { get; set; } + public Principal Principal { get; set; } /// /// The SNS permission being allowed or denied e.g. sns:Publish. @@ -47,7 +47,7 @@ public void Serialize(IAsyncApiWriter writer) writer.WriteStartObject(); writer.WriteRequiredProperty("effect", this.Effect.GetDisplayName()); - writer.WriteRequiredObject("principal", this.Principal, (w, t) => t.Write(w)); + writer.WriteRequiredObject("principal", this.Principal, (w, t) => t.Value.Write(w)); writer.WriteRequiredObject("action", this.Action, (w, t) => t.Value.Write(w)); writer.WriteOptionalObject("resource", this.Resource, (w, t) => t?.Value.Write(w)); writer.WriteOptionalObject("condition", this.Condition, (w, t) => t?.Write(w)); diff --git a/src/LEGO.AsyncAPI.Bindings/StringOrStringList.cs b/src/LEGO.AsyncAPI.Bindings/StringOrStringList.cs index 6be69094..b9946f08 100644 --- a/src/LEGO.AsyncAPI.Bindings/StringOrStringList.cs +++ b/src/LEGO.AsyncAPI.Bindings/StringOrStringList.cs @@ -30,10 +30,10 @@ public static StringOrStringList Parse(ParseNode node) { case ValueNode: return new StringOrStringList(new AsyncApiAny(node.GetScalarValue())); - case ListNode: + case ListNode listNode: { var jsonArray = new JsonArray(); - foreach (var item in node as ListNode) + foreach (var item in listNode) { jsonArray.Add(item.GetScalarValue()); } diff --git a/test/LEGO.AsyncAPI.Tests/Bindings/Sns/SnsBindings_Should.cs b/test/LEGO.AsyncAPI.Tests/Bindings/Sns/SnsBindings_Should.cs index db03847e..a81b573f 100644 --- a/test/LEGO.AsyncAPI.Tests/Bindings/Sns/SnsBindings_Should.cs +++ b/test/LEGO.AsyncAPI.Tests/Bindings/Sns/SnsBindings_Should.cs @@ -2,6 +2,7 @@ namespace LEGO.AsyncAPI.Tests.Bindings.Sns { + using System; using System.Collections.Generic; using System.Linq; using FluentAssertions; @@ -86,7 +87,7 @@ public void SnsChannelBinding_WithFilledObject_SerializesAndDeserializes() new Statement() { Effect = Effect.Deny, - Principal = new AsyncApiAny("*"), + Principal = new Principal(new AsyncApiAny("*")), Action = new StringOrStringList(new AsyncApiAny(new List() { "sns:Publish", @@ -105,10 +106,10 @@ public void SnsChannelBinding_WithFilledObject_SerializesAndDeserializes() new Statement() { Effect = Effect.Allow, - Principal = new AsyncApiAny(new Dictionary>() + Principal = new Principal(new AsyncApiAny(new Dictionary>() { { "AWS", new List() { "arn:aws:iam::123456789012:user/alex.wichmann", "arn:aws:iam::123456789012:user/dec.kolakowski" } }, - }), + })), Action = new StringOrStringList(new AsyncApiAny("sns:Create")), Condition = new AsyncApiAny(new Dictionary() { @@ -163,8 +164,11 @@ public void SnsChannelBinding_WithFilledObject_SerializesAndDeserializes() var actual = channel.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); // Assert - var settings = new AsyncApiReaderSettings(); - settings.Bindings = BindingsCollection.Sns; + var settings = new AsyncApiReaderSettings + { + Bindings = BindingsCollection.Sns, + }; + var binding = new AsyncApiStringReader(settings).ReadFragment(actual, AsyncApiVersion.AsyncApi2_0, out _); // Assert @@ -407,8 +411,11 @@ public void SnsOperationBinding_WithFilledObject_SerializesAndDeserializes() var actual = operation.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); // Assert - var settings = new AsyncApiReaderSettings(); - settings.Bindings = BindingsCollection.Sns; + var settings = new AsyncApiReaderSettings + { + Bindings = BindingsCollection.Sns, + }; + var binding = new AsyncApiStringReader(settings).ReadFragment(actual, AsyncApiVersion.AsyncApi2_0, out _); var binding2 = new AsyncApiStringReader(settings).ReadFragment(expected, AsyncApiVersion.AsyncApi2_0, out _); binding2.Bindings.First().Value.Extensions.TryGetValue("x-bindingExtension", out IAsyncApiExtension any); diff --git a/test/LEGO.AsyncAPI.Tests/Bindings/Sqs/SqsBindings_should.cs b/test/LEGO.AsyncAPI.Tests/Bindings/Sqs/SqsBindings_should.cs index cfc52ac8..0fc0154e 100644 --- a/test/LEGO.AsyncAPI.Tests/Bindings/Sqs/SqsBindings_should.cs +++ b/test/LEGO.AsyncAPI.Tests/Bindings/Sqs/SqsBindings_should.cs @@ -3,6 +3,7 @@ namespace LEGO.AsyncAPI.Tests.Bindings.Sqs { using System.Collections.Generic; + using System.Linq; using FluentAssertions; using LEGO.AsyncAPI.Bindings; using LEGO.AsyncAPI.Bindings.Sqs; @@ -80,7 +81,7 @@ public void SqsChannelBinding_WithFilledObject_SerializesAndDeserializes() statements: - effect: allow principal: - AWS: arn:aws:iam::123456789012:user/alex.wichmann + Service: s3.amazonaws.com action: - sqs:* x-internalObject: @@ -135,10 +136,10 @@ public void SqsChannelBinding_WithFilledObject_SerializesAndDeserializes() new Statement() { Effect = Effect.Deny, - Principal = new AsyncApiAny(new Dictionary() + Principal = new Principal(new AsyncApiAny(new Dictionary() { { "AWS", "arn:aws:iam::123456789012:user/alex.wichmann" }, - }), + })), Action = new StringOrStringList(new AsyncApiAny(new List { "sqs:SendMessage", @@ -167,10 +168,10 @@ public void SqsChannelBinding_WithFilledObject_SerializesAndDeserializes() new Statement() { Effect = Effect.Allow, - Principal = new AsyncApiAny(new Dictionary>() + Principal = new Principal(new AsyncApiAny(new Dictionary>() { { "AWS", new List() { "arn:aws:iam::123456789012:user/alex.wichmann", "arn:aws:iam::123456789012:user/dec.kolakowski" } }, - }), + })), Action = new StringOrStringList(new AsyncApiAny("sqs:CreateQueue")), Condition = new AsyncApiAny(new Dictionary() { @@ -225,10 +226,10 @@ public void SqsChannelBinding_WithFilledObject_SerializesAndDeserializes() new Statement() { Effect = Effect.Allow, - Principal = new AsyncApiAny(new Dictionary() + Principal = new Principal(new AsyncApiAny(new Dictionary() { - { "AWS", "arn:aws:iam::123456789012:user/alex.wichmann" }, - }), + { "Service", "s3.amazonaws.com" }, + })), Action = new StringOrStringList(new AsyncApiAny(new List { "sqs:*", @@ -252,8 +253,10 @@ public void SqsChannelBinding_WithFilledObject_SerializesAndDeserializes() var actual = channel.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); // Assert - var settings = new AsyncApiReaderSettings(); - settings.Bindings = BindingsCollection.Sqs; + var settings = new AsyncApiReaderSettings + { + Bindings = BindingsCollection.Sqs, + }; var binding = new AsyncApiStringReader(settings).ReadFragment(actual, AsyncApiVersion.AsyncApi2_0, out _); @@ -261,6 +264,9 @@ public void SqsChannelBinding_WithFilledObject_SerializesAndDeserializes() actual.Should() .BePlatformAgnosticEquivalentTo(expected); binding.Should().BeEquivalentTo(channel); + + var expectedSqsBinding = (SqsChannelBinding)channel.Bindings.Values.First(); + expectedSqsBinding.Should().BeEquivalentTo((SqsChannelBinding)binding.Bindings.Values.First(), options => options.IgnoringCyclicReferences()); } [Test] @@ -376,10 +382,10 @@ public void SqsOperationBinding_WithFilledObject_SerializesAndDeserializes() new Statement() { Effect = Effect.Deny, - Principal = new AsyncApiAny(new Dictionary() + Principal = new Principal(new AsyncApiAny(new Dictionary() { { "AWS", "arn:aws:iam::123456789012:user/alex.wichmann" }, - }), + })), Action = new StringOrStringList(new AsyncApiAny(new List() { "sqs:SendMessage", @@ -399,10 +405,10 @@ public void SqsOperationBinding_WithFilledObject_SerializesAndDeserializes() new Statement() { Effect = Effect.Allow, - Principal = new AsyncApiAny(new Dictionary>() + Principal = new Principal(new AsyncApiAny(new Dictionary>() { { "AWS", new List() { "arn:aws:iam::123456789012:user/alex.wichmann", "arn:aws:iam::123456789012:user/dec.kolakowski" } }, - }), + })), Action = new StringOrStringList(new AsyncApiAny("sqs:CreateQueue")), }, }, @@ -448,10 +454,10 @@ public void SqsOperationBinding_WithFilledObject_SerializesAndDeserializes() new Statement() { Effect = Effect.Allow, - Principal = new AsyncApiAny(new Dictionary() + Principal = new Principal(new AsyncApiAny(new Dictionary() { { "AWS", "arn:aws:iam::123456789012:user/alex.wichmann" }, - }), + })), Action = new StringOrStringList(new AsyncApiAny(new List { "sqs:*", @@ -486,14 +492,20 @@ public void SqsOperationBinding_WithFilledObject_SerializesAndDeserializes() var actual = operation.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); // Assert - var settings = new AsyncApiReaderSettings(); - settings.Bindings = BindingsCollection.Sqs; + var settings = new AsyncApiReaderSettings + { + Bindings = BindingsCollection.Sqs, + }; + var binding = new AsyncApiStringReader(settings).ReadFragment(actual, AsyncApiVersion.AsyncApi2_0, out _); // Assert actual.Should() .BePlatformAgnosticEquivalentTo(expected); binding.Should().BeEquivalentTo(operation); + + var expectedSqsBinding = (SqsOperationBinding)operation.Bindings.Values.First(); + expectedSqsBinding.Should().BeEquivalentTo((SqsOperationBinding)binding.Bindings.Values.First(), options => options.IgnoringCyclicReferences()); } } } \ No newline at end of file