Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: extend AWS policy #187

Merged
merged 9 commits into from
Aug 15, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
66 changes: 66 additions & 0 deletions src/LEGO.AsyncAPI.Bindings/Sns/Principal.cs
Original file line number Diff line number Diff line change
@@ -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; }
Gadam8 marked this conversation as resolved.
Show resolved Hide resolved

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<string, AsyncApiAny>()
{ { 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);
}
}
4 changes: 3 additions & 1 deletion src/LEGO.AsyncAPI.Bindings/Sns/SnsChannelBinding.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,10 @@ public class SnsChannelBinding : ChannelBinding<SnsChannelBinding>
private static FixedFieldMap<Statement> statementFixedFields = new()
{
{ "effect", (a, n) => { a.Effect = n.GetScalarValue().GetEnumFromDisplayName<Effect>(); } },
{ "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(); } },
};

/// <inheritdoc/>
Expand Down
24 changes: 19 additions & 5 deletions src/LEGO.AsyncAPI.Bindings/Sns/Statement.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Indicates whether the policy allows or denies access.
/// </summary>
public Effect Effect { get; set; }

/// <summary>
/// The AWS account or resource ARN that this statement applies to.
/// The AWS account(s) or resource ARN(s) that this statement applies to.
/// </summary>
// public StringOrStringList Principal { get; set; }
public StringOrStringList Principal { get; set; }
public Principal Principal { get; set; }

/// <summary>
/// The SNS permission being allowed or denied e.g. sns:Publish
/// The SNS permission being allowed or denied e.g. sns:Publish.
/// </summary>
public StringOrStringList Action { get; set; }

/// <summary>
/// The resource(s) that this policy applies to.
/// </summary>
public StringOrStringList? Resource { get; set; }

/// <summary>
/// Specific circumstances under which the policy grants permission.
/// </summary>
public AsyncApiAny? Condition { get; set; }

public IDictionary<string, IAsyncApiExtension> Extensions { get; set; } = new Dictionary<string, IAsyncApiExtension>();

public void Serialize(IAsyncApiWriter writer)
Expand All @@ -36,6 +48,8 @@ public void Serialize(IAsyncApiWriter writer)
writer.WriteRequiredProperty("effect", this.Effect.GetDisplayName());
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));
writer.WriteExtensions(this.Extensions);
writer.WriteEndObject();
}
Expand Down
66 changes: 66 additions & 0 deletions src/LEGO.AsyncAPI.Bindings/Sqs/Principal.cs
Original file line number Diff line number Diff line change
@@ -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<string, AsyncApiAny>()
{ { 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);
}
}
4 changes: 3 additions & 1 deletion src/LEGO.AsyncAPI.Bindings/Sqs/SqsChannelBinding.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,10 @@ public class SqsChannelBinding : ChannelBinding<SqsChannelBinding>
private static FixedFieldMap<Statement> statementFixedFields = new()
{
{ "effect", (a, n) => { a.Effect = n.GetScalarValue().GetEnumFromDisplayName<Effect>(); } },
{ "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)
Expand Down
4 changes: 3 additions & 1 deletion src/LEGO.AsyncAPI.Bindings/Sqs/SqsOperationBinding.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,10 @@ public class SqsOperationBinding : OperationBinding<SqsOperationBinding>
private static FixedFieldMap<Statement> statementFixedFields = new()
{
{ "effect", (a, n) => { a.Effect = n.GetScalarValue().GetEnumFromDisplayName<Effect>(); } },
{ "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)
Expand Down
23 changes: 19 additions & 4 deletions src/LEGO.AsyncAPI.Bindings/Sqs/Statement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
/// <summary>
/// Indicates whether the policy allows or denies access.
/// </summary>
public Effect Effect { get; set; }

/// <summary>
/// The AWS account or resource ARN that this statement applies to.
/// The AWS account(s) or resource ARN(s) that this statement applies to.
/// </summary>
// public StringOrStringList Principal { get; set; }
public StringOrStringList Principal { get; set; }
public Principal Principal { get; set; }

/// <summary>
/// The SNS permission being allowed or denied e.g. sns:Publish
/// The SNS permission being allowed or denied e.g. sns:Publish.
/// </summary>
public StringOrStringList Action { get; set; }

/// <summary>
/// The resource(s) that this policy applies to.
/// </summary>
public StringOrStringList? Resource { get; set; }

/// <summary>
/// Specific circumstances under which the policy grants permission.
/// </summary>
public AsyncApiAny? Condition { get; set; }

public IDictionary<string, IAsyncApiExtension> Extensions { get; set; } = new Dictionary<string, IAsyncApiExtension>();

public void Serialize(IAsyncApiWriter writer)
Expand All @@ -36,6 +49,8 @@ public void Serialize(IAsyncApiWriter writer)
writer.WriteRequiredProperty("effect", this.Effect.GetDisplayName());
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));
writer.WriteExtensions(this.Extensions);
writer.WriteEndObject();
}
Expand Down
4 changes: 2 additions & 2 deletions src/LEGO.AsyncAPI.Bindings/StringOrStringList.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
Expand Down
55 changes: 44 additions & 11 deletions test/LEGO.AsyncAPI.Tests/Bindings/Sns/SnsBindings_Should.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace LEGO.AsyncAPI.Tests.Bindings.Sns
{
using System;
using System.Collections.Generic;
using System.Linq;
using FluentAssertions;
Expand Down Expand Up @@ -31,15 +32,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:
Expand Down Expand Up @@ -77,22 +87,39 @@ public void SnsChannelBinding_WithFilledObject_SerializesAndDeserializes()
new Statement()
{
Effect = Effect.Deny,
Principal = new StringOrStringList(new AsyncApiAny("arn:aws:iam::123456789012:user/alex.wichmann")),
Principal = new Principal(new AsyncApiAny("*")),
Action = new StringOrStringList(new AsyncApiAny(new List<string>()
{
"sns:Publish",
"sns:Delete",
})),
Condition = new AsyncApiAny(new Dictionary<string, object>()
{
{
"StringEquals", new Dictionary<string, List<string>>()
{
{ "aws:username", new List<string>() { "johndoe", "mrsmith" } },
}
},
}),
},
new Statement()
{
Effect = Effect.Allow,
Principal = new StringOrStringList(new AsyncApiAny(new List<string>()
Principal = new Principal(new AsyncApiAny(new Dictionary<string, List<string>>()
{
"arn:aws:iam::123456789012:user/alex.wichmann",
"arn:aws:iam::123456789012:user/dec.kolakowski",
{ "AWS", new List<string>() { "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<string, object>()
{
{
"NumericLessThanEquals", new Dictionary<string, string>()
{
{ "aws:MultiFactorAuthAge", "3600" },
}
},
}),
Extensions = new Dictionary<string, IAsyncApiExtension>()
{
{
Expand Down Expand Up @@ -137,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<AsyncApiChannel>(actual, AsyncApiVersion.AsyncApi2_0, out _);

// Assert
Expand Down Expand Up @@ -381,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<AsyncApiOperation>(actual, AsyncApiVersion.AsyncApi2_0, out _);
var binding2 = new AsyncApiStringReader(settings).ReadFragment<AsyncApiOperation>(expected, AsyncApiVersion.AsyncApi2_0, out _);
binding2.Bindings.First().Value.Extensions.TryGetValue("x-bindingExtension", out IAsyncApiExtension any);
Expand Down
Loading
Loading