diff --git a/src/LEGO.AsyncAPI.Bindings/Sns/Principal.cs b/src/LEGO.AsyncAPI.Bindings/Sns/Principal.cs new file mode 100644 index 00000000..a803f6c2 --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/Sns/Principal.cs @@ -0,0 +1,63 @@ +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.Interfaces; +using LEGO.AsyncAPI.Readers.ParseNodes; +using LEGO.AsyncAPI.Writers; + +public abstract class Principal : IAsyncApiElement +{ + public abstract void Serialize(IAsyncApiWriter writer); + + 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 PrincipalStar(); + + 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 principalValue = new KeyValuePair( + propertyNode.Name, + StringOrStringList.Parse(propertyNode.Value)); + + return new PrincipalObject(principalValue); + } + + 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/PrincipalObject.cs b/src/LEGO.AsyncAPI.Bindings/Sns/PrincipalObject.cs new file mode 100644 index 00000000..a25c198f --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/Sns/PrincipalObject.cs @@ -0,0 +1,27 @@ +namespace LEGO.AsyncAPI.Bindings.Sns; + +using System; +using System.Collections.Generic; +using LEGO.AsyncAPI.Writers; + +public class PrincipalObject : Principal +{ + private KeyValuePair PrincipalValue; + + public PrincipalObject(KeyValuePair principalValue) + { + this.PrincipalValue = principalValue; + } + + public override void Serialize(IAsyncApiWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + writer.WriteStartObject(); + writer.WriteRequiredObject(this.PrincipalValue.Key, this.PrincipalValue.Value, (w, t) => t.Value.Write(w)); + writer.WriteEndObject(); + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI.Bindings/Sns/PrincipalStar.cs b/src/LEGO.AsyncAPI.Bindings/Sns/PrincipalStar.cs new file mode 100644 index 00000000..533e9fb7 --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/Sns/PrincipalStar.cs @@ -0,0 +1,24 @@ +namespace LEGO.AsyncAPI.Bindings.Sns; + +using System; +using LEGO.AsyncAPI.Writers; + +public class PrincipalStar : Principal +{ + private string PrincipalValue; + + public PrincipalStar() + { + this.PrincipalValue = "*"; + } + + public override void Serialize(IAsyncApiWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + writer.WriteValue(this.PrincipalValue); + } +} \ 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 13676168..4d8668c9 100644 --- a/src/LEGO.AsyncAPI.Bindings/Sns/SnsChannelBinding.cs +++ b/src/LEGO.AsyncAPI.Bindings/Sns/SnsChannelBinding.cs @@ -57,8 +57,10 @@ public class SnsChannelBinding : ChannelBinding private static FixedFieldMap statementFixedFields = new() { { "effect", (a, n) => { a.Effect = n.GetScalarValue().GetEnumFromDisplayName(); } }, - { "principal", (a, n) => { a.Principal = StringOrStringList.Parse(n); } }, + { "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 c92eaf94..170fe371 100644 --- a/src/LEGO.AsyncAPI.Bindings/Sns/Statement.cs +++ b/src/LEGO.AsyncAPI.Bindings/Sns/Statement.cs @@ -1,28 +1,40 @@ // Copyright (c) The LEGO Group. All rights reserved. - namespace LEGO.AsyncAPI.Bindings.Sns { using System; using System.Collections.Generic; using LEGO.AsyncAPI.Attributes; + using LEGO.AsyncAPI.Models; using LEGO.AsyncAPI.Models.Interfaces; using LEGO.AsyncAPI.Writers; public class Statement : IAsyncApiExtensible { + /// + /// Indicates whether the policy allows or denies access. + /// public Effect Effect { get; set; } /// - /// The AWS account or resource ARN that this statement applies to. + /// The AWS account(s) or resource ARN(s) that this statement applies to. /// - // public StringOrStringList Principal { get; set; } - public StringOrStringList Principal { get; set; } + public Principal Principal { get; set; } /// /// The SNS permission being allowed or denied e.g. sns:Publish. /// public StringOrStringList Action { get; set; } + /// + /// The resource(s) that this policy applies to. + /// + public StringOrStringList? Resource { get; set; } + + /// + /// Specific circumstances under which the policy grants permission. + /// + public AsyncApiAny? Condition { get; set; } + public IDictionary Extensions { get; set; } = new Dictionary(); public void Serialize(IAsyncApiWriter writer) @@ -34,8 +46,10 @@ public void Serialize(IAsyncApiWriter writer) writer.WriteStartObject(); writer.WriteRequiredProperty("effect", this.Effect.GetDisplayName()); - writer.WriteRequiredObject("principal", this.Principal, (w, t) => t.Value.Write(w)); + writer.WriteRequiredObject("principal", this.Principal, (w, t) => t.Serialize(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)); writer.WriteExtensions(this.Extensions); writer.WriteEndObject(); } diff --git a/src/LEGO.AsyncAPI.Bindings/Sqs/Principal.cs b/src/LEGO.AsyncAPI.Bindings/Sqs/Principal.cs new file mode 100644 index 00000000..2821f952 --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/Sqs/Principal.cs @@ -0,0 +1,65 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +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.Interfaces; +using LEGO.AsyncAPI.Readers.ParseNodes; +using LEGO.AsyncAPI.Writers; + +public abstract class Principal : IAsyncApiElement +{ + public abstract void Serialize(IAsyncApiWriter writer); + + 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 PrincipalStar(); + + 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 principalValue = new KeyValuePair( + propertyNode.Name, + StringOrStringList.Parse(propertyNode.Value)); + + return new PrincipalObject(principalValue); + } + + 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/PrincipalObject.cs b/src/LEGO.AsyncAPI.Bindings/Sqs/PrincipalObject.cs new file mode 100644 index 00000000..2652060d --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/Sqs/PrincipalObject.cs @@ -0,0 +1,27 @@ +namespace LEGO.AsyncAPI.Bindings.Sqs; + +using System; +using System.Collections.Generic; +using LEGO.AsyncAPI.Writers; + +public class PrincipalObject : Principal +{ + private KeyValuePair PrincipalValue; + + public PrincipalObject(KeyValuePair principalValue) + { + this.PrincipalValue = principalValue; + } + + public override void Serialize(IAsyncApiWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + writer.WriteStartObject(); + writer.WriteRequiredObject(this.PrincipalValue.Key, this.PrincipalValue.Value, (w, t) => t.Value.Write(w)); + writer.WriteEndObject(); + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI.Bindings/Sqs/PrincipalStar.cs b/src/LEGO.AsyncAPI.Bindings/Sqs/PrincipalStar.cs new file mode 100644 index 00000000..9e54bc5a --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/Sqs/PrincipalStar.cs @@ -0,0 +1,24 @@ +namespace LEGO.AsyncAPI.Bindings.Sqs; + +using System; +using LEGO.AsyncAPI.Writers; + +public class PrincipalStar : Principal +{ + private string PrincipalValue; + + public PrincipalStar() + { + this.PrincipalValue = "*"; + } + + public override void Serialize(IAsyncApiWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + writer.WriteValue(this.PrincipalValue); + } +} \ 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 6f98da99..f0b24be7 100644 --- a/src/LEGO.AsyncAPI.Bindings/Sqs/SqsChannelBinding.cs +++ b/src/LEGO.AsyncAPI.Bindings/Sqs/SqsChannelBinding.cs @@ -64,8 +64,10 @@ public class SqsChannelBinding : ChannelBinding private static FixedFieldMap statementFixedFields = new() { { "effect", (a, n) => { a.Effect = n.GetScalarValue().GetEnumFromDisplayName(); } }, - { "principal", (a, n) => { a.Principal = StringOrStringList.Parse(n); } }, + { "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(); } }, }; public override void SerializeProperties(IAsyncApiWriter writer) diff --git a/src/LEGO.AsyncAPI.Bindings/Sqs/SqsOperationBinding.cs b/src/LEGO.AsyncAPI.Bindings/Sqs/SqsOperationBinding.cs index 98110209..c8e0993c 100644 --- a/src/LEGO.AsyncAPI.Bindings/Sqs/SqsOperationBinding.cs +++ b/src/LEGO.AsyncAPI.Bindings/Sqs/SqsOperationBinding.cs @@ -56,8 +56,10 @@ public class SqsOperationBinding : OperationBinding private static FixedFieldMap statementFixedFields = new() { { "effect", (a, n) => { a.Effect = n.GetScalarValue().GetEnumFromDisplayName(); } }, - { "principal", (a, n) => { a.Principal = StringOrStringList.Parse(n); } }, + { "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(); } }, }; public override void SerializeProperties(IAsyncApiWriter writer) diff --git a/src/LEGO.AsyncAPI.Bindings/Sqs/Statement.cs b/src/LEGO.AsyncAPI.Bindings/Sqs/Statement.cs index 0bed331f..4abc05a6 100644 --- a/src/LEGO.AsyncAPI.Bindings/Sqs/Statement.cs +++ b/src/LEGO.AsyncAPI.Bindings/Sqs/Statement.cs @@ -5,24 +5,37 @@ namespace LEGO.AsyncAPI.Bindings.Sqs using System; using System.Collections.Generic; using LEGO.AsyncAPI.Attributes; + using LEGO.AsyncAPI.Models; using LEGO.AsyncAPI.Models.Interfaces; using LEGO.AsyncAPI.Writers; public class Statement : IAsyncApiExtensible { + /// + /// Indicates whether the policy allows or denies access. + /// public Effect Effect { get; set; } /// - /// The AWS account or resource ARN that this statement applies to. + /// The AWS account(s) or resource ARN(s) that this statement applies to. /// - // public StringOrStringList Principal { get; set; } - public StringOrStringList Principal { get; set; } + public Principal Principal { get; set; } /// /// The SNS permission being allowed or denied e.g. sns:Publish. /// public StringOrStringList Action { get; set; } + /// + /// The resource(s) that this policy applies to. + /// + public StringOrStringList? Resource { get; set; } + + /// + /// Specific circumstances under which the policy grants permission. + /// + public AsyncApiAny? Condition { get; set; } + public IDictionary Extensions { get; set; } = new Dictionary(); public void Serialize(IAsyncApiWriter writer) @@ -34,8 +47,10 @@ public void Serialize(IAsyncApiWriter writer) writer.WriteStartObject(); writer.WriteRequiredProperty("effect", this.Effect.GetDisplayName()); - writer.WriteRequiredObject("principal", this.Principal, (w, t) => t.Value.Write(w)); + writer.WriteRequiredObject("principal", this.Principal, (w, t) => t.Serialize(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)); writer.WriteExtensions(this.Extensions); writer.WriteEndObject(); } 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/src/LEGO.AsyncAPI/LEGO.AsyncAPI.csproj b/src/LEGO.AsyncAPI/LEGO.AsyncAPI.csproj index 560c0f98..99e69016 100644 --- a/src/LEGO.AsyncAPI/LEGO.AsyncAPI.csproj +++ b/src/LEGO.AsyncAPI/LEGO.AsyncAPI.csproj @@ -19,7 +19,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + <_Parameter1>$(MSBuildProjectName).Tests diff --git a/test/LEGO.AsyncAPI.Tests/Bindings/Sns/SnsBindings_Should.cs b/test/LEGO.AsyncAPI.Tests/Bindings/Sns/SnsBindings_Should.cs index 6d4f5779..fbb3622e 100644 --- a/test/LEGO.AsyncAPI.Tests/Bindings/Sns/SnsBindings_Should.cs +++ b/test/LEGO.AsyncAPI.Tests/Bindings/Sns/SnsBindings_Should.cs @@ -31,15 +31,24 @@ public void SnsChannelBinding_WithFilledObject_SerializesAndDeserializes() policy: statements: - effect: Deny - principal: arn:aws:iam::123456789012:user/alex.wichmann + principal: '*' action: - sns:Publish - sns:Delete + condition: + StringEquals: + aws:username: + - johndoe + - mrsmith - effect: Allow principal: - - arn:aws:iam::123456789012:user/alex.wichmann - - arn:aws:iam::123456789012:user/dec.kolakowski + AWS: + - arn:aws:iam::123456789012:user/alex.wichmann + - arn:aws:iam::123456789012:user/dec.kolakowski action: sns:Create + condition: + NumericLessThanEquals: + aws:MultiFactorAuthAge: '3600' x-statementExtension: statementXPropertyName: statementXPropertyValue x-policyExtension: @@ -77,22 +86,38 @@ public void SnsChannelBinding_WithFilledObject_SerializesAndDeserializes() new Statement() { Effect = Effect.Deny, - Principal = new StringOrStringList(new AsyncApiAny("arn:aws:iam::123456789012:user/alex.wichmann")), + Principal = new PrincipalStar(), Action = new StringOrStringList(new AsyncApiAny(new List() { "sns:Publish", "sns:Delete", })), + Condition = new AsyncApiAny(new Dictionary() + { + { + "StringEquals", new Dictionary>() + { + { "aws:username", new List() { "johndoe", "mrsmith" } }, + } + }, + }), }, new Statement() { Effect = Effect.Allow, - Principal = new StringOrStringList(new AsyncApiAny(new List() - { - "arn:aws:iam::123456789012:user/alex.wichmann", - "arn:aws:iam::123456789012:user/dec.kolakowski", - })), + Principal = new PrincipalObject(new KeyValuePair( + "AWS", new StringOrStringList(new AsyncApiAny(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() + { + { + "NumericLessThanEquals", new Dictionary() + { + { "aws:MultiFactorAuthAge", "3600" }, + } + }, + }), Extensions = new Dictionary() { { @@ -137,8 +162,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 @@ -381,8 +409,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 c031621c..3a0337a3 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; @@ -42,17 +43,27 @@ public void SqsChannelBinding_WithFilledObject_SerializesAndDeserializes() policy: statements: - effect: deny - principal: arn:aws:iam::123456789012:user/alex.wichmann + principal: + AWS: arn:aws:iam::123456789012:user/alex.wichmann action: - sqs:SendMessage - sqs:ReceiveMessage + condition: + StringEquals: + aws:username: + - johndoe + - mrsmith x-statementExtension: statementXPropertyName: statementXPropertyValue - effect: allow principal: - - arn:aws:iam::123456789012:user/alex.wichmann - - arn:aws:iam::123456789012:user/dec.kolakowski + AWS: + - arn:aws:iam::123456789012:user/alex.wichmann + - arn:aws:iam::123456789012:user/dec.kolakowski action: sqs:CreateQueue + condition: + NumericLessThanEquals: + aws:MultiFactorAuthAge: '3600' x-policyExtension: policyXPropertyName: policyXPropertyValue tags: @@ -69,7 +80,8 @@ public void SqsChannelBinding_WithFilledObject_SerializesAndDeserializes() policy: statements: - effect: allow - principal: arn:aws:iam::123456789012:user/alex.wichmann + principal: + Service: s3.amazonaws.com action: - sqs:* x-internalObject: @@ -124,12 +136,22 @@ public void SqsChannelBinding_WithFilledObject_SerializesAndDeserializes() new Statement() { Effect = Effect.Deny, - Principal = new StringOrStringList(new AsyncApiAny("arn:aws:iam::123456789012:user/alex.wichmann")), + Principal = new PrincipalObject(new KeyValuePair( + "AWS", new StringOrStringList(new AsyncApiAny("arn:aws:iam::123456789012:user/alex.wichmann")))), Action = new StringOrStringList(new AsyncApiAny(new List { "sqs:SendMessage", "sqs:ReceiveMessage", })), + Condition = new AsyncApiAny(new Dictionary() + { + { + "StringEquals", new Dictionary>() + { + { "aws:username", new List() { "johndoe", "mrsmith" } }, + } + }, + }), Extensions = new Dictionary() { { @@ -144,12 +166,19 @@ public void SqsChannelBinding_WithFilledObject_SerializesAndDeserializes() new Statement() { Effect = Effect.Allow, - Principal = new StringOrStringList(new AsyncApiAny(new List - { - "arn:aws:iam::123456789012:user/alex.wichmann", - "arn:aws:iam::123456789012:user/dec.kolakowski", - })), + Principal = new PrincipalObject(new KeyValuePair( + "AWS", new StringOrStringList(new AsyncApiAny(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() + { + { + "NumericLessThanEquals", new Dictionary() + { + { "aws:MultiFactorAuthAge", "3600" }, + } + }, + }), }, }, Extensions = new Dictionary() @@ -194,7 +223,8 @@ public void SqsChannelBinding_WithFilledObject_SerializesAndDeserializes() new Statement() { Effect = Effect.Allow, - Principal = new StringOrStringList(new AsyncApiAny("arn:aws:iam::123456789012:user/alex.wichmann")), + Principal = new PrincipalObject(new KeyValuePair( + "Service", new StringOrStringList(new AsyncApiAny("s3.amazonaws.com")))), Action = new StringOrStringList(new AsyncApiAny(new List { "sqs:*", @@ -218,8 +248,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 _); @@ -227,6 +259,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] @@ -254,7 +289,8 @@ public void SqsOperationBinding_WithFilledObject_SerializesAndDeserializes() policy: statements: - effect: deny - principal: arn:aws:iam::123456789012:user/alex.wichmann + principal: + AWS: arn:aws:iam::123456789012:user/alex.wichmann action: - sqs:SendMessage - sqs:ReceiveMessage @@ -262,8 +298,9 @@ public void SqsOperationBinding_WithFilledObject_SerializesAndDeserializes() statementXPropertyName: statementXPropertyValue - effect: allow principal: - - arn:aws:iam::123456789012:user/alex.wichmann - - arn:aws:iam::123456789012:user/dec.kolakowski + AWS: + - arn:aws:iam::123456789012:user/alex.wichmann + - arn:aws:iam::123456789012:user/dec.kolakowski action: sqs:CreateQueue x-policyExtension: policyXPropertyName: policyXPropertyValue @@ -280,7 +317,8 @@ public void SqsOperationBinding_WithFilledObject_SerializesAndDeserializes() policy: statements: - effect: allow - principal: arn:aws:iam::123456789012:user/alex.wichmann + principal: + AWS: arn:aws:iam::123456789012:user/alex.wichmann action: - sqs:* x-queueExtension: @@ -339,7 +377,8 @@ public void SqsOperationBinding_WithFilledObject_SerializesAndDeserializes() new Statement() { Effect = Effect.Deny, - Principal = new StringOrStringList(new AsyncApiAny("arn:aws:iam::123456789012:user/alex.wichmann")), + Principal = new PrincipalObject(new KeyValuePair( + "AWS", new StringOrStringList(new AsyncApiAny("arn:aws:iam::123456789012:user/alex.wichmann")))), Action = new StringOrStringList(new AsyncApiAny(new List() { "sqs:SendMessage", @@ -359,11 +398,9 @@ public void SqsOperationBinding_WithFilledObject_SerializesAndDeserializes() new Statement() { Effect = Effect.Allow, - Principal = new StringOrStringList(new AsyncApiAny(new List - { - "arn:aws:iam::123456789012:user/alex.wichmann", - "arn:aws:iam::123456789012:user/dec.kolakowski", - })), + Principal = new PrincipalObject(new KeyValuePair( + "AWS", new StringOrStringList(new AsyncApiAny(new List + { "arn:aws:iam::123456789012:user/alex.wichmann", "arn:aws:iam::123456789012:user/dec.kolakowski" })))), Action = new StringOrStringList(new AsyncApiAny("sqs:CreateQueue")), }, }, @@ -409,7 +446,8 @@ public void SqsOperationBinding_WithFilledObject_SerializesAndDeserializes() new Statement() { Effect = Effect.Allow, - Principal = new StringOrStringList(new AsyncApiAny("arn:aws:iam::123456789012:user/alex.wichmann")), + Principal = new PrincipalObject(new KeyValuePair( + "AWS", new StringOrStringList(new AsyncApiAny("arn:aws:iam::123456789012:user/alex.wichmann")))), Action = new StringOrStringList(new AsyncApiAny(new List { "sqs:*", @@ -444,14 +482,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