Skip to content

Commit f4d5993

Browse files
authored
Fix CA1873: Potentially expensive logging (#1695)
* Fix CA1873: Potentially expensive logging * Refactor UriExtensions.ToMaskedString into MaskedUri struct with implicit conversion * Mask request URLs in HTTP exchanges actuator
1 parent f0dfcf2 commit f4d5993

21 files changed

Lines changed: 244 additions & 149 deletions

File tree

shared-package.props

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,6 @@
3333
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
3434
</PropertyGroup>
3535

36-
<PropertyGroup>
37-
<!--
38-
Temporary workaround for CA1873: Evaluation of this argument may be expensive and unnecessary if logging is disabled.
39-
Should be revisited as part of https://github.com/SteeltoeOSS/Steeltoe/issues/969.
40-
-->
41-
<NoWarn>$(NoWarn);CA1873</NoWarn>
42-
</PropertyGroup>
43-
4436
<PropertyGroup Condition="'$(CI)' != ''">
4537
<!--
4638
While deterministic builds are enabled by default in .NET SDK projects, there is an extra property, ContinuousIntegrationBuild,
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information.
4+
5+
namespace Steeltoe.Common.Extensions;
6+
7+
/// <summary>
8+
/// Represents a <see cref="Uri" /> whose username and password are masked.
9+
/// </summary>
10+
internal readonly record struct MaskedUri
11+
{
12+
private readonly Uri? _value;
13+
14+
public MaskedUri(Uri? value)
15+
{
16+
_value = value;
17+
}
18+
19+
public override string ToString()
20+
{
21+
return _value == null ? string.Empty : ToMaskedString(_value);
22+
}
23+
24+
private static string ToMaskedString(Uri source)
25+
{
26+
string uris = source.ToString();
27+
28+
if (uris.Contains(','))
29+
{
30+
return string.Join(',',
31+
uris.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).Select(uri => Mask(new Uri(uri)).ToString()));
32+
}
33+
34+
return Mask(source).ToString();
35+
}
36+
37+
private static Uri Mask(Uri source)
38+
{
39+
if (string.IsNullOrEmpty(source.UserInfo))
40+
{
41+
return source;
42+
}
43+
44+
var builder = new UriBuilder(source)
45+
{
46+
UserName = "****",
47+
#pragma warning disable S2068 // Hard-coded credentials are security-sensitive
48+
Password = "****"
49+
#pragma warning restore S2068 // Hard-coded credentials are security-sensitive
50+
};
51+
52+
return builder.Uri;
53+
}
54+
55+
public static implicit operator MaskedUri(Uri? uri)
56+
{
57+
return new MaskedUri(uri);
58+
}
59+
}

src/Common/src/Common/Extensions/UriExtensions.cs

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -9,39 +9,6 @@ namespace Steeltoe.Common.Extensions;
99

1010
internal static class UriExtensions
1111
{
12-
public static string ToMaskedString(this Uri source)
13-
{
14-
ArgumentNullException.ThrowIfNull(source);
15-
16-
string uris = source.ToString();
17-
18-
if (uris.Contains(','))
19-
{
20-
return string.Join(',',
21-
uris.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).Select(uri => ToMaskedUri(new Uri(uri)).ToString()));
22-
}
23-
24-
return ToMaskedUri(source).ToString();
25-
}
26-
27-
private static Uri ToMaskedUri(Uri source)
28-
{
29-
if (string.IsNullOrEmpty(source.UserInfo))
30-
{
31-
return source;
32-
}
33-
34-
var builder = new UriBuilder(source)
35-
{
36-
UserName = "****",
37-
#pragma warning disable S2068 // Hard-coded credentials are security-sensitive
38-
Password = "****"
39-
#pragma warning restore S2068 // Hard-coded credentials are security-sensitive
40-
};
41-
42-
return builder.Uri;
43-
}
44-
4512
public static bool TryGetUsernamePassword(this Uri uri, [NotNullWhen(true)] out string? username, [NotNullWhen(true)] out string? password)
4613
{
4714
ArgumentNullException.ThrowIfNull(uri);

src/Common/src/Http/HttpClientExtensions.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,8 @@ public static async Task<string> GetAccessTokenAsync(this HttpClient httpClient,
7373

7474
if (string.IsNullOrEmpty(accessToken))
7575
{
76-
throw new HttpRequestException($"No access token was returned from '{accessTokenUri.ToMaskedString()}'.", null, response.StatusCode);
76+
MaskedUri masked = accessTokenUri;
77+
throw new HttpRequestException($"No access token was returned from '{masked}'.", null, response.StatusCode);
7778
}
7879

7980
return accessToken;
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using Steeltoe.Common.Extensions;
6+
7+
namespace Steeltoe.Common.Test.Extensions;
8+
9+
public sealed class MaskedUriTest
10+
{
11+
[Fact]
12+
public void MaskSingleBasicAuthentication()
13+
{
14+
const string source = "http://username:password@www.example.com/";
15+
const string expected = "http://****:****@www.example.com/";
16+
17+
MaskedUri uri = new Uri(source);
18+
19+
uri.ToString().Should().Be(expected);
20+
}
21+
22+
[Fact]
23+
public void MaskMultiBasicAuthentication()
24+
{
25+
const string source = "http://username:password@www.example.com/,http://user2:pass2@www.other.com/";
26+
const string expected = "http://****:****@www.example.com/,http://****:****@www.other.com/";
27+
28+
MaskedUri uri = new Uri(source);
29+
30+
uri.ToString().Should().Be(expected);
31+
}
32+
33+
[Fact]
34+
public void DoNotMaskIfNoBasicAuthentication()
35+
{
36+
const string source = "http://www.example.com/";
37+
const string expected = "http://www.example.com/";
38+
39+
MaskedUri uri = new Uri(source);
40+
41+
uri.ToString().Should().Be(expected);
42+
}
43+
}

src/Common/test/Common.Test/Extensions/UriExtensionsTest.cs

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -8,39 +8,6 @@ namespace Steeltoe.Common.Test.Extensions;
88

99
public sealed class UriExtensionsTest
1010
{
11-
[Fact]
12-
public void MaskSingleBasicAuthentication()
13-
{
14-
var uri = new Uri("http://username:password@www.example.com/");
15-
const string expected = "http://****:****@www.example.com/";
16-
17-
string masked = uri.ToMaskedString();
18-
19-
masked.Should().Be(expected);
20-
}
21-
22-
[Fact]
23-
public void MaskMultiBasicAuthentication()
24-
{
25-
var uri = new Uri("http://username:password@www.example.com/,http://user2:pass2@www.other.com/");
26-
const string expected = "http://****:****@www.example.com/,http://****:****@www.other.com/";
27-
28-
string masked = uri.ToMaskedString();
29-
30-
masked.Should().Be(expected);
31-
}
32-
33-
[Fact]
34-
public void DoNotMaskIfNoBasicAuthentication()
35-
{
36-
var uri = new Uri("http://www.example.com/");
37-
string expected = uri.ToString();
38-
39-
string masked = uri.ToMaskedString();
40-
41-
masked.Should().Be(expected);
42-
}
43-
4411
[Fact]
4512
public void TryGetUsernamePassword_AllowsColonInPassword()
4613
{

src/Configuration/src/Abstractions/CompositeConfigurationProvider.cs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,7 @@ public IEnumerable<string> GetChildKeys(IEnumerable<string> earlierKeys, string?
5656
ArgumentNullException.ThrowIfNull(earlierKeys);
5757

5858
string[] earlierKeysArray = earlierKeys as string[] ?? earlierKeys.ToArray();
59-
60-
if (_logger.IsEnabled(LogLevel.Trace))
61-
{
62-
string earlierKeyNames = string.Join(", ", earlierKeysArray.Select(key => $"'{key}'"));
63-
LogGetChildKeys(GetType().Name, earlierKeyNames, parentPath);
64-
}
59+
ExpensiveLogGetChildKeys(earlierKeysArray, parentPath);
6560

6661
IConfiguration? section = parentPath == null ? ConfigurationRoot : ConfigurationRoot?.GetSection(parentPath);
6762

@@ -77,6 +72,15 @@ public IEnumerable<string> GetChildKeys(IEnumerable<string> earlierKeys, string?
7772
return keys;
7873
}
7974

75+
private void ExpensiveLogGetChildKeys(string[] earlierKeysArray, string? parentPath)
76+
{
77+
if (_logger.IsEnabled(LogLevel.Trace))
78+
{
79+
string earlierKeyNames = string.Join(", ", earlierKeysArray.Select(key => $"'{key}'"));
80+
LogGetChildKeys(GetType().Name, earlierKeyNames, parentPath);
81+
}
82+
}
83+
8084
public virtual bool TryGet(string key, out string? value)
8185
{
8286
ArgumentNullException.ThrowIfNull(key);

src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -403,7 +403,7 @@ public override void Load()
403403
// Update configuration Data dictionary with any results
404404
if (env != null)
405405
{
406-
LogEnvironmentLocated(env.Name, string.Join(", ", env.Profiles.Select(p => $"'{p}'")), env.Label, env.Version, env.State);
406+
ExpensiveLogEnvironmentLocated(env);
407407

408408
if (updateDictionary)
409409
{
@@ -451,6 +451,15 @@ public override void Load()
451451
throw new ConfigServerException("Failed fetching remote configuration from server(s).", error);
452452
}
453453

454+
private void ExpensiveLogEnvironmentLocated(ConfigEnvironment environment)
455+
{
456+
if (_logger.IsEnabled(LogLevel.Debug))
457+
{
458+
string profiles = string.Join(", ", environment.Profiles.Select(profile => $"'{profile}'"));
459+
LogEnvironmentLocated(environment.Name, profiles, environment.Label, environment.Version, environment.State);
460+
}
461+
}
462+
454463
internal void ApplyLastDiscoveryLookupResultToClientOptions(ConfigServerClientOptions optionsSnapshot)
455464
{
456465
DiscoveryLookupResult? lastResult = _lastDiscoveryLookupResult;
@@ -601,7 +610,7 @@ internal async Task<HttpRequestMessage> GetConfigServerRequestMessageAsync(Confi
601610

602611
if (requestUri.TryGetUsernamePassword(out string? username, out string? password) && password.Length > 0)
603612
{
604-
LogAddingCredentials(requestUri.ToMaskedString());
613+
LogAddingCredentials(requestUri);
605614

606615
requestMessage.Headers.Authorization =
607616
new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes($"{username}:{password}")));
@@ -616,7 +625,7 @@ internal async Task<HttpRequestMessage> GetConfigServerRequestMessageAsync(Confi
616625
string accessToken =
617626
await httpClient.GetAccessTokenAsync(accessTokenUri, optionsSnapshot.ClientId, optionsSnapshot.ClientSecret, cancellationToken);
618627

619-
LogAccessTokenFetched(accessTokenUri.ToMaskedString());
628+
LogAccessTokenFetched(accessTokenUri);
620629
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
621630
}
622631
}
@@ -646,7 +655,7 @@ internal async Task<HttpRequestMessage> GetConfigServerRequestMessageAsync(Confi
646655
// Make Config Server URI from settings
647656
Uri uri = BuildConfigServerUri(optionsSnapshot, requestUri, label);
648657

649-
LogTryingToConnect(uri.ToMaskedString());
658+
LogTryingToConnect(uri);
650659
HttpRequestMessage request;
651660

652661
try
@@ -660,7 +669,7 @@ internal async Task<HttpRequestMessage> GetConfigServerRequestMessageAsync(Confi
660669
if (!string.IsNullOrEmpty(optionsSnapshot.AccessTokenUri))
661670
{
662671
var accessTokenUri = new Uri(optionsSnapshot.AccessTokenUri);
663-
LogFailedToFetchAccessToken(exception, accessTokenUri.ToMaskedString());
672+
LogFailedToFetchAccessToken(exception, accessTokenUri);
664673

665674
continue;
666675
}
@@ -672,7 +681,7 @@ internal async Task<HttpRequestMessage> GetConfigServerRequestMessageAsync(Confi
672681
LogSendingHttpRequest();
673682
using HttpResponseMessage response = await httpClient.SendAsync(request, cancellationToken);
674683

675-
LogConfigServerReturnedStatus(response.StatusCode, uri.ToMaskedString());
684+
LogConfigServerReturnedStatus(uri, response.StatusCode);
676685

677686
if (response.StatusCode != HttpStatusCode.OK)
678687
{
@@ -684,7 +693,8 @@ internal async Task<HttpRequestMessage> GetConfigServerRequestMessageAsync(Confi
684693
// Throw if status >= 400
685694
if (response.StatusCode >= HttpStatusCode.BadRequest)
686695
{
687-
throw new HttpRequestException($"Config Server returned status: {response.StatusCode} invoking path: {uri.ToMaskedString()}");
696+
MaskedUri masked = uri;
697+
throw new HttpRequestException($"Config Server returned status: {response.StatusCode} invoking path: {masked}");
688698
}
689699

690700
return null;
@@ -827,7 +837,7 @@ internal async Task RefreshVaultTokenAsync(ConfigServerClientOptions optionsSnap
827837
Uri uri = BuildVaultRenewUri(optionsSnapshot);
828838
HttpRequestMessage message = await GetVaultRenewRequestMessageAsync(optionsSnapshot, uri, cancellationToken);
829839

830-
LogRenewingVaultToken(obscuredToken, optionsSnapshot.TokenTtl, uri.ToMaskedString());
840+
LogRenewingVaultToken(obscuredToken, optionsSnapshot.TokenTtl, uri);
831841
using HttpResponseMessage response = await httpClient.SendAsync(message, cancellationToken);
832842

833843
if (response.StatusCode != HttpStatusCode.OK)
@@ -867,7 +877,7 @@ private async Task<HttpRequestMessage> GetVaultRenewRequestMessageAsync(ConfigSe
867877
string accessToken =
868878
await httpClient.GetAccessTokenAsync(accessTokenUri, optionsSnapshot.ClientId, optionsSnapshot.ClientSecret, cancellationToken);
869879

870-
LogAccessTokenFetched(accessTokenUri.ToMaskedString());
880+
LogAccessTokenFetched(accessTokenUri);
871881
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
872882
}
873883

@@ -1019,7 +1029,7 @@ private void ShutdownTimers()
10191029
[LoggerMessage(Level = LogLevel.Debug, Message = "Multiple Config Server uris listed.")]
10201030
private partial void LogMultipleConfigServerUris();
10211031

1022-
[LoggerMessage(Level = LogLevel.Debug,
1032+
[LoggerMessage(Level = LogLevel.Debug, SkipEnabledCheck = true,
10231033
Message = "Located environment with name {Name}, profiles {Profiles}, label {Label}, version {Version} and state {State}.")]
10241034
private partial void LogEnvironmentLocated(string? name, string profiles, string? label, string? version, string? state);
10251035

@@ -1030,22 +1040,22 @@ private void ShutdownTimers()
10301040
private partial void LogDataNotChanged();
10311041

10321042
[LoggerMessage(Level = LogLevel.Warning, Message = "Failed fetching remote configuration from server(s).")]
1033-
private partial void LogFetchingRemoteConfigurationFailed(Exception? error);
1043+
private partial void LogFetchingRemoteConfigurationFailed(Exception error);
10341044

10351045
[LoggerMessage(Level = LogLevel.Debug, Message = "Adding credentials from '{RequestUri}' to Authorization header.")]
1036-
private partial void LogAddingCredentials(string requestUri);
1046+
private partial void LogAddingCredentials(MaskedUri requestUri);
10371047

10381048
[LoggerMessage(Level = LogLevel.Debug, Message = "Fetched access token from {AccessTokenUri}.")]
1039-
private partial void LogAccessTokenFetched(string accessTokenUri);
1049+
private partial void LogAccessTokenFetched(MaskedUri accessTokenUri);
10401050

10411051
[LoggerMessage(Level = LogLevel.Warning, Message = "Failed to fetch access token from '{AccessTokenUri}'.")]
1042-
private partial void LogFailedToFetchAccessToken(Exception exception, string accessTokenUri);
1052+
private partial void LogFailedToFetchAccessToken(Exception exception, MaskedUri accessTokenUri);
10431053

10441054
[LoggerMessage(Level = LogLevel.Trace, Message = "Entered {Method}.")]
10451055
private partial void LogRemoteLoadEntered(string method);
10461056

10471057
[LoggerMessage(Level = LogLevel.Debug, Message = "Trying to connect to Config Server at {RequestUri}.")]
1048-
private partial void LogTryingToConnect(string requestUri);
1058+
private partial void LogTryingToConnect(MaskedUri requestUri);
10491059

10501060
[LoggerMessage(Level = LogLevel.Trace, Message = "Building HTTP request message.")]
10511061
private partial void LogBuildingHttpRequest();
@@ -1054,7 +1064,7 @@ private void ShutdownTimers()
10541064
private partial void LogSendingHttpRequest();
10551065

10561066
[LoggerMessage(Level = LogLevel.Debug, Message = "Config Server returned status {StatusCode} for path {RequestUri}.")]
1057-
private partial void LogConfigServerReturnedStatus(HttpStatusCode statusCode, string requestUri);
1067+
private partial void LogConfigServerReturnedStatus(MaskedUri requestUri, HttpStatusCode statusCode);
10581068

10591069
[LoggerMessage(Level = LogLevel.Trace, Message = "Parsing JSON response.")]
10601070
private partial void LogParsingJsonResponse();
@@ -1066,7 +1076,7 @@ private void ShutdownTimers()
10661076
private partial void LogConfigServerPropertyException(Exception exception, string key, Type type);
10671077

10681078
[LoggerMessage(Level = LogLevel.Debug, Message = "Renewing Vault token {Token} for {Ttl} milliseconds at Uri {Uri}.")]
1069-
private partial void LogRenewingVaultToken(string token, int ttl, string uri);
1079+
private partial void LogRenewingVaultToken(string token, int ttl, MaskedUri uri);
10701080

10711081
[LoggerMessage(Level = LogLevel.Warning, Message = "Renewing Vault token {Token} returned status {Status}.")]
10721082
private partial void LogVaultTokenRenewalStatus(string token, HttpStatusCode status);

0 commit comments

Comments
 (0)