Skip to content

Commit

Permalink
Feature work/aws account id parsing (#2920)
Browse files Browse the repository at this point in the history
  • Loading branch information
tippmar-nr authored Dec 6, 2024
1 parent c36d25c commit 55cc8a4
Show file tree
Hide file tree
Showing 15 changed files with 281 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

using System.Linq;
using System.Text.RegularExpressions;
using NewRelic.Agent.Extensions.Collections;

namespace NewRelic.Agent.Extensions.AwsSdk
{
Expand All @@ -15,8 +14,8 @@ public class ArnBuilder

public ArnBuilder(string partition, string region, string accountId)
{
Partition = partition ?? "";
Region = region ?? "";
Partition = string.IsNullOrEmpty(partition) ? "aws" : partition;
Region = string.IsNullOrEmpty(region) ? "(unknown)" : region;
AccountId = accountId ?? "";
}

Expand Down Expand Up @@ -135,11 +134,9 @@ public string BuildFromPartialLambdaArn(string invocationName)

public override string ToString()
{
string partition = string.IsNullOrEmpty(Partition) ? "[Missing]" : Partition;
string region = string.IsNullOrEmpty(Region) ? "[Missing]" : Region;
string idPresent = string.IsNullOrEmpty(AccountId) ? "[Missing]" : "[Present]";
return $"Partition: {partition}, Region: {region}, AccountId: {idPresent}";

return $"Partition: {Partition}, Region: {Region}, AccountId: {idPresent}";
}

private static Regex RegionRegex = new Regex(@"^[a-z]{2}((-gov)|(-iso([a-z]?)))?-[a-z]+-\d{1}$", RegexOptions.Compiled);
Expand All @@ -155,6 +152,5 @@ private string ConstructArn(string partition, string service, string region, str
}
return "arn:" + partition + ":" + service + ":" + region + ":" + accountId + ":" + resource;
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Copyright 2020 New Relic, Inc. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

using System;

namespace NewRelic.Agent.Extensions.AwsSdk
{
public static class AwsAccountIdDecoder
{
// magic number
private const long Mask = 0x7FFFFFFFFF80;

public static string GetAccountId(string awsAccessKeyId)
{
if (string.IsNullOrEmpty(awsAccessKeyId))
{
throw new ArgumentNullException(nameof(awsAccessKeyId), "AWS Access Key ID cannot be null or empty.");
}

if (awsAccessKeyId.Length < 14)
{
throw new ArgumentOutOfRangeException(nameof(awsAccessKeyId), "AWS Access Key ID must be at least 14 characters long.");
}

string accessKeyWithoutPrefix = awsAccessKeyId.Substring(4).ToLowerInvariant();
long encodedAccount = Base32Decode(accessKeyWithoutPrefix);

return ((encodedAccount & Mask) >> 7).ToString();
}

/// <summary>
/// Performs a Base-32 decode of the specified input string.
/// Allowed character range is a-z and 2-7. 'a' being 0 and '7' is 31.
///
/// public to allow for unit testing
/// </summary>
/// <param name="src">The string to be decoded. Must be at least 10 characters.</param>
/// <returns>A long containing first 6 bytes of the base 32 decoded data.</returns>
/// <exception cref="ArgumentException">If src has less than 10 characters.</exception>
/// <exception cref="ArgumentOutOfRangeException">If src contains invalid characters for Base-32</exception>
public static long Base32Decode(string src)
{
if (string.IsNullOrEmpty(src))
{
throw new ArgumentNullException(nameof(src), "The input string cannot be null or empty.");
}

if (src.Length < 10)
{
throw new ArgumentException("The input string must be at least 10 characters long.", nameof(src));
}

long baseValue = 0;
for (int i = 0; i < 10; i++)
{
baseValue <<= 5;
char c = src[i];
baseValue += c switch
{
>= 'a' and <= 'z' => c - 'a',
>= '2' and <= '7' => c - '2' + 26,
_ => throw new ArgumentOutOfRangeException(nameof(src),
"The input string must contain only characters in the range a-z and 2-7.")
};
}

return baseValue >> 2;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright 2020 New Relic, Inc. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

using System;
using NewRelic.Agent.Api;
using NewRelic.Agent.Extensions.AwsSdk;
using NewRelic.Agent.Extensions.Providers.Wrapper;

namespace NewRelic.Providers.Wrapper.AwsSdk
{
public class AmazonServiceClientWrapper : IWrapper
{
/// <summary>
/// The AWS account id.
/// Parsed from the access key in the credentials of the client - or fall back to the configuration value if parsing fails.
/// Assumes only a single account id is used in the application.
/// </summary>
public static string AwsAccountId { get; private set; }

public bool IsTransactionRequired => false;

public CanWrapResponse CanWrap(InstrumentedMethodInfo instrumentedMethodInfo)
{
return new CanWrapResponse(instrumentedMethodInfo.RequestedWrapperName == nameof(AmazonServiceClientWrapper));
}

public AfterWrappedMethodDelegate BeforeWrappedMethod(InstrumentedMethodCall instrumentedMethodCall, IAgent agent, ITransaction transaction)
{
if (AwsAccountId != null)
return Delegates.NoOp;

try
{
// get the AWSCredentials parameter
dynamic awsCredentials = instrumentedMethodCall.MethodCall.MethodArguments[0];

dynamic immutableCredentials = awsCredentials.GetCredentials();
string accessKey = immutableCredentials.AccessKey;

// convert the access key to an account id
AwsAccountId = AwsAccountIdDecoder.GetAccountId(accessKey);
}
catch (Exception e)
{
agent.Logger.Info($"Unable to parse AWS Account ID from AccessKey. Using AccountId from configuration instead. Exception: {e.Message}");
AwsAccountId = agent.Configuration.AwsAccountId;
}

return Delegates.NoOp;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@
// SPDX-License-Identifier: Apache-2.0

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NewRelic.Agent.Api;
using NewRelic.Agent.Extensions.AwsSdk;
using NewRelic.Agent.Extensions.Collections;
using NewRelic.Agent.Extensions.Providers.Wrapper;
using NewRelic.Providers.Wrapper.AwsSdk.RequestHandlers;

namespace NewRelic.Providers.Wrapper.AwsSdk
{
Expand All @@ -34,10 +33,13 @@ private ArnBuilder CreateArnBuilder(IAgent agent, dynamic requestContext)
try
{
accountId = GetAccountId(agent);
var clientconfig = requestContext.ClientConfig;
var regionEndpoint = clientconfig.RegionEndpoint;
systemName = regionEndpoint.SystemName;
partition = regionEndpoint.PartitionName;
var clientConfig = requestContext.ClientConfig;
if (clientConfig.RegionEndpoint != null)
{
var regionEndpoint = clientConfig.RegionEndpoint;
systemName = regionEndpoint.SystemName;
partition = regionEndpoint.PartitionName;
}
}
catch (Exception e)
{
Expand All @@ -48,12 +50,13 @@ private ArnBuilder CreateArnBuilder(IAgent agent, dynamic requestContext)
}
}

return new ArnBuilder(partition, systemName, accountId); ;
return new ArnBuilder(partition, systemName, accountId);
}

private string GetAccountId(IAgent agent)
{
string accountId = agent.Configuration.AwsAccountId;
string accountId = AmazonServiceClientWrapper.AwsAccountId;

if (accountId != null)
{
if ((accountId.Length != 12) || accountId.Any(c => (c < '0') || (c > '9')))
Expand All @@ -65,6 +68,7 @@ private string GetAccountId(IAgent agent)
}
}
}

return accountId;
}

Expand Down Expand Up @@ -103,17 +107,19 @@ public AfterWrappedMethodDelegate BeforeWrappedMethod(InstrumentedMethodCall ins
{
return SQSRequestHandler.HandleSQSRequest(instrumentedMethodCall, agent, transaction, request, isAsync, executionContext);
}
else if (requestType == "Amazon.Lambda.Model.InvokeRequest")

if (requestType == "Amazon.Lambda.Model.InvokeRequest")
{
return LambdaInvokeRequestHandler.HandleInvokeRequest(instrumentedMethodCall, agent, transaction, request, isAsync, builder);
}
else if (requestType.StartsWith("Amazon.DynamoDBv2"))
}

if (requestType.StartsWith("Amazon.DynamoDBv2"))
{
return DynamoDbRequestHandler.HandleDynamoDbRequest(instrumentedMethodCall, agent, transaction, request, isAsync, executionContext);
return DynamoDbRequestHandler.HandleDynamoDbRequest(instrumentedMethodCall, agent, transaction, request, isAsync, builder);
}

if (!_unsupportedRequestTypes.Contains(requestType)) // log once per unsupported request type
{
{
agent.Logger.Debug($"AwsSdkPipelineWrapper: Unsupported request type: {requestType}. Returning NoOp delegate.");
_unsupportedRequestTypes.Add(requestType);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,14 @@ SPDX-License-Identifier: Apache-2.0
<exactMethodMatcher methodName="InvokeAsync" parameters="Amazon.Runtime.IExecutionContext" />
</match>
</tracerFactory>
<tracerFactory name="AmazonServiceClientWrapper">
<!--
https://github.com/aws/aws-sdk-net/blob/main/sdk/src/Core/Amazon.Runtime/AmazonServiceClient.cs#L155
protected AmazonServiceClient(AWSCredentials credentials, ClientConfig config)
-->
<match assemblyName="AWSSDK.Core" className="Amazon.Runtime.AmazonServiceClient">
<exactMethodMatcher methodName=".ctor" parameters="Amazon.Runtime.AWSCredentials,Amazon.Runtime.ClientConfig" />
</match>
</tracerFactory>
</instrumentation>
</extension>
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,16 @@
using System.Collections.Concurrent;
using System.Threading.Tasks;
using NewRelic.Agent.Api;
using NewRelic.Agent.Extensions.AwsSdk;
using NewRelic.Agent.Extensions.Parsing;
using NewRelic.Agent.Extensions.Providers.Wrapper;

namespace NewRelic.Providers.Wrapper.AwsSdk
namespace NewRelic.Providers.Wrapper.AwsSdk.RequestHandlers
{
internal static class DynamoDbRequestHandler
{

private static ConcurrentDictionary<string,string> _operationNameCache = new ConcurrentDictionary<string,string>();
private static ConcurrentDictionary<string, string> _operationNameCache = new ConcurrentDictionary<string,string>();

public static AfterWrappedMethodDelegate HandleDynamoDbRequest(InstrumentedMethodCall instrumentedMethodCall, IAgent agent, ITransaction transaction, dynamic request, bool isAsync, dynamic executionContext)
{
Expand All @@ -30,6 +31,7 @@ public static AfterWrappedMethodDelegate HandleDynamoDbRequest(InstrumentedMetho
model = request.TableName;

var segment = transaction.StartDatastoreSegment(instrumentedMethodCall.MethodCall, new ParsedSqlStatement(DatastoreVendor.DynamoDB, model, operation), isLeaf: true);

return isAsync ?
Delegates.GetAsyncDelegateFor<Task>(agent, segment)
:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,16 @@
using System;
using System.Threading.Tasks;
using NewRelic.Agent.Api;
using NewRelic.Agent.Api.Experimental;
using NewRelic.Agent.Extensions.Providers.Wrapper;
using NewRelic.Reflection;
using NewRelic.Agent.Extensions.Helpers;
using NewRelic.Agent.Extensions.AwsSdk;

namespace NewRelic.Providers.Wrapper.AwsSdk
namespace NewRelic.Providers.Wrapper.AwsSdk.RequestHandlers
{
internal static class LambdaInvokeRequestHandler
{
private static Func<object, object> _getResultFromGenericTask;
private static ConcurrentDictionary<string, string> _arnCache = new ConcurrentDictionary<string, string>();
private static readonly ConcurrentDictionary<string, string> _arnCache = new();
private static bool _reportMissingRequestId = true;
private static bool _reportBadInvocationName = true;
private const int MAX_CACHE_SIZE = 25; // Shouldn't ever get this big, but just in case
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
using NewRelic.Agent.Extensions.Providers.Wrapper;
using NewRelic.Reflection;

namespace NewRelic.Providers.Wrapper.AwsSdk
namespace NewRelic.Providers.Wrapper.AwsSdk.RequestHandlers
{
internal static class SQSRequestHandler
{
Expand Down Expand Up @@ -48,7 +48,7 @@ public static AfterWrappedMethodDelegate HandleSQSRequest(InstrumentedMethodCall
var dtHeaders = agent.GetConfiguredDTHeaders();

string requestQueueUrl = request.QueueUrl;
ISegment segment = SqsHelper.GenerateSegment(transaction, instrumentedMethodCall.MethodCall, requestQueueUrl, action);
var segment = SqsHelper.GenerateSegment(transaction, instrumentedMethodCall.MethodCall, requestQueueUrl, action);
switch (action)
{
case MessageBrokerAction.Produce when requestType == "SendMessageRequest":
Expand Down Expand Up @@ -117,7 +117,7 @@ public static AfterWrappedMethodDelegate HandleSQSRequest(InstrumentedMethodCall

void ProcessResponse(Task responseTask)
{
if (!ValidTaskResponse(responseTask) || (segment == null) || action != MessageBrokerAction.Consume)
if (!ValidTaskResponse(responseTask) || segment == null || action != MessageBrokerAction.Consume)
return;

// taskResult is a ReceiveMessageResponse
Expand Down
4 changes: 2 additions & 2 deletions src/Agent/NewRelic/Home/Home.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
</PropertyGroup>

<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Exec Condition="'$(NR_DEV_BUILD_HOME)' != 'false'" Command="del /q &quot;$(TargetDir)*.*&quot;"/>
<Exec Condition="'$(NR_DEV_BUILD_HOME)' != 'false'" WorkingDirectory="$(SolutionDir)Build" Command="powershell.exe -ExecutionPolicy Bypass -NoProfile -NonInteractive -File .\build_home.ps1 -Configuration $(ConfigurationName)"/>
<Exec Condition="'$(NR_DEV_BUILD_HOME)' != 'false'" Command="del /q &quot;$(TargetDir)*.*&quot;" />
<Exec Condition="'$(NR_DEV_BUILD_HOME)' != 'false'" WorkingDirectory="$(SolutionDir)Build" Command="powershell.exe -ExecutionPolicy Bypass -NoProfile -NonInteractive -File .\build_home.ps1 -Configuration $(ConfigurationName)" />
</Target>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,7 @@ public virtual void Initialize()
catch (Exception ex)
{
TestLogger?.WriteLine("Exception occurred in Initialize: " + ex.ToString());
AgentLogExpected = false;
throw;
}
finally
Expand Down
Loading

0 comments on commit 55cc8a4

Please sign in to comment.