From 9a5f9f9332d75db3774d9fe42b907284255c402c Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Wed, 11 Aug 2021 12:39:46 +0000 Subject: [PATCH 01/23] Update dependencies from https://github.com/dotnet/diagnostics build 20210810.1 (#707) [main] Update dependencies from dotnet/diagnostics --- eng/Version.Details.xml | 8 ++++---- eng/Versions.props | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 96eadc3ccac..ad6fff7404a 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -4,13 +4,13 @@ https://github.com/dotnet/command-line-api 166610c56ff732093f0145a2911d4f6c40b786da - + https://github.com/dotnet/diagnostics - 7cb03dbd4b93564f1d45c9f04ac8bf8961e01cc7 + 5e4f7a31054550ed4dc66f2676035489706b090c - + https://github.com/dotnet/diagnostics - 7cb03dbd4b93564f1d45c9f04ac8bf8961e01cc7 + 5e4f7a31054550ed4dc66f2676035489706b090c diff --git a/eng/Versions.props b/eng/Versions.props index ee009bcc23a..aa4b220a204 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -31,8 +31,8 @@ 6.0.0-rc.1.21410.1 - 5.0.0-preview.21409.1 - 5.0.0-preview.21409.1 + 5.0.0-preview.21410.1 + 5.0.0-preview.21410.1 6.0.0-rc.1.21406.5 From 1ac28937baab41cbee97b42a21867d7219725200 Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Wed, 11 Aug 2021 12:48:03 +0000 Subject: [PATCH 02/23] Update dependencies from https://github.com/dotnet/runtime build 20210811.2 (#708) [main] Update dependencies from dotnet/runtime --- eng/Version.Details.xml | 4 ++-- eng/Versions.props | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index ad6fff7404a..a26177cecd3 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -30,9 +30,9 @@ https://github.com/dotnet/aspnetcore 3535cfb61dd955a7fa6c31bdf72a3eba5353f1ce - + https://github.com/dotnet/runtime - 58efa4b79751a2dad08d9bf7ca67930f8160afe3 + c0662e8129beaf93b8050d39a863cc6d16a0308c diff --git a/eng/Versions.props b/eng/Versions.props index aa4b220a204..9f6fad3e777 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -34,7 +34,7 @@ 5.0.0-preview.21410.1 5.0.0-preview.21410.1 - 6.0.0-rc.1.21406.5 + 6.0.0-rc.1.21411.2 1.0.240901 From cf94ef78c80aca5f19933add98150e14d5fd6d50 Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Wed, 11 Aug 2021 12:48:11 +0000 Subject: [PATCH 03/23] Update dependencies from https://github.com/dotnet/aspnetcore build 20210810.16 (#709) [main] Update dependencies from dotnet/aspnetcore --- eng/Version.Details.xml | 4 ++-- eng/Versions.props | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index a26177cecd3..e7b32b53341 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -26,9 +26,9 @@ https://github.com/dotnet/symstore d8e2990b89c53632653d7d67f3481cc72773f25c - + https://github.com/dotnet/aspnetcore - 3535cfb61dd955a7fa6c31bdf72a3eba5353f1ce + a671f9652808921d6bbe74994c16065372bec6f6 https://github.com/dotnet/runtime diff --git a/eng/Versions.props b/eng/Versions.props index 9f6fad3e777..78c159a8296 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -29,7 +29,7 @@ 6.0.0-beta.21406.6 - 6.0.0-rc.1.21410.1 + 6.0.0-rc.1.21410.16 5.0.0-preview.21410.1 5.0.0-preview.21410.1 From be7ded106c8c9e44c5a252e06911d0b2bfa73fe3 Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Thu, 12 Aug 2021 12:43:05 +0000 Subject: [PATCH 04/23] Update dependencies from https://github.com/dotnet/diagnostics build 20210811.1 (#711) [main] Update dependencies from dotnet/diagnostics --- eng/Version.Details.xml | 8 ++++---- eng/Versions.props | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index e7b32b53341..c64ea96f28a 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -4,13 +4,13 @@ https://github.com/dotnet/command-line-api 166610c56ff732093f0145a2911d4f6c40b786da - + https://github.com/dotnet/diagnostics - 5e4f7a31054550ed4dc66f2676035489706b090c + 626b7f8f23053672f989a8174336897fe1b57434 - + https://github.com/dotnet/diagnostics - 5e4f7a31054550ed4dc66f2676035489706b090c + 626b7f8f23053672f989a8174336897fe1b57434 diff --git a/eng/Versions.props b/eng/Versions.props index 78c159a8296..9e6c956fd96 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -31,8 +31,8 @@ 6.0.0-rc.1.21410.16 - 5.0.0-preview.21410.1 - 5.0.0-preview.21410.1 + 5.0.0-preview.21411.1 + 5.0.0-preview.21411.1 6.0.0-rc.1.21411.2 From 24b4b13852df670d0c7dd9574fd5645ccaebac22 Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Thu, 12 Aug 2021 12:55:07 +0000 Subject: [PATCH 05/23] Update dependencies from https://github.com/dotnet/runtime build 20210811.5 (#713) [main] Update dependencies from dotnet/runtime --- eng/Version.Details.xml | 4 ++-- eng/Versions.props | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index c64ea96f28a..73790218b23 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -30,9 +30,9 @@ https://github.com/dotnet/aspnetcore a671f9652808921d6bbe74994c16065372bec6f6 - + https://github.com/dotnet/runtime - c0662e8129beaf93b8050d39a863cc6d16a0308c + 0ea8653e1f0ada5c7a15515430c6f16585911af4 diff --git a/eng/Versions.props b/eng/Versions.props index 9e6c956fd96..ced245669ab 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -34,7 +34,7 @@ 5.0.0-preview.21411.1 5.0.0-preview.21411.1 - 6.0.0-rc.1.21411.2 + 6.0.0-rc.1.21411.5 1.0.240901 From 5bed1b40d232618bd49b64477c1a73a4edfeaede Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Thu, 12 Aug 2021 12:55:14 +0000 Subject: [PATCH 06/23] Update dependencies from https://github.com/dotnet/aspnetcore build 20210811.15 (#714) [main] Update dependencies from dotnet/aspnetcore --- eng/Version.Details.xml | 4 ++-- eng/Versions.props | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 73790218b23..f8df3060b3a 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -26,9 +26,9 @@ https://github.com/dotnet/symstore d8e2990b89c53632653d7d67f3481cc72773f25c - + https://github.com/dotnet/aspnetcore - a671f9652808921d6bbe74994c16065372bec6f6 + 9c2b65a8f9ac334db5575160b2e07a35c25d0585 https://github.com/dotnet/runtime diff --git a/eng/Versions.props b/eng/Versions.props index ced245669ab..ea95f09e8fd 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -29,7 +29,7 @@ 6.0.0-beta.21406.6 - 6.0.0-rc.1.21410.16 + 6.0.0-rc.1.21411.15 5.0.0-preview.21411.1 5.0.0-preview.21411.1 From d42c23473d0f73675b7cc28c7ff67fc7221e252f Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Thu, 12 Aug 2021 17:41:56 +0000 Subject: [PATCH 07/23] Update dependencies from https://github.com/dotnet/arcade build 20210810.8 (#712) [main] Update dependencies from dotnet/arcade --- eng/Version.Details.xml | 8 ++++---- eng/Versions.props | 2 +- global.json | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index f8df3060b3a..3e476aeda9a 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -14,13 +14,13 @@ - + https://github.com/dotnet/arcade - 382667fff0b58c362855a42c3529ba294fd0514c + e10772e3594e46a031574c20a4145441737ac56d - + https://github.com/dotnet/arcade - 382667fff0b58c362855a42c3529ba294fd0514c + e10772e3594e46a031574c20a4145441737ac56d https://github.com/dotnet/symstore diff --git a/eng/Versions.props b/eng/Versions.props index ea95f09e8fd..f454e89d669 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -27,7 +27,7 @@ --> - 6.0.0-beta.21406.6 + 6.0.0-beta.21410.8 6.0.0-rc.1.21411.15 diff --git a/global.json b/global.json index 38355eab28a..1d09eebba7e 100644 --- a/global.json +++ b/global.json @@ -16,6 +16,6 @@ }, "msbuild-sdks": { "Microsoft.Build.NoTargets": "2.0.1", - "Microsoft.DotNet.Arcade.Sdk": "6.0.0-beta.21406.6" + "Microsoft.DotNet.Arcade.Sdk": "6.0.0-beta.21410.8" } } From b914277dccdeb6d20b32e3060ddcc0741edb3569 Mon Sep 17 00:00:00 2001 From: Patrick Fenelon Date: Thu, 12 Aug 2021 23:04:22 -0700 Subject: [PATCH 08/23] Switch to Bearer auth (#463) * Replace MonitorApiKey key/keyhash auth type with signed JWT implementation * Add MonitorGenerateKeyRunner to test the generatekey command Closes #463 Closes #589 --- documentation/api-key-format.md | 73 +++ documentation/authentication.md | 85 ++-- documentation/schema.json | 45 +- eng/Versions.props | 3 + .../AuthenticationOptions.cs | 18 + ...uration.cs => CorsConfigurationOptions.cs} | 2 +- ...tionOptions.cs => MonitorApiKeyOptions.cs} | 13 +- .../OptionsDisplayStrings.Designer.cs | 33 +- .../OptionsDisplayStrings.resx | 20 +- .../Auth/AuthConstants.cs | 10 +- ...tics.Monitoring.ConfigurationSchema.csproj | 2 +- .../Options/RootOptions.Logging.cs | 2 +- .../Runners/DotNetRunner.cs | 12 +- .../Runners/LoggingRunnerAdapter.cs | 58 ++- .../TaskExtensions.cs | 23 + .../AuthenticationTests.cs | 418 +++++++++++++++--- .../GenerateKeyTests.cs | 98 ++++ .../JsonSerializerOptionsFactory.cs | 52 +++ .../MetricsTests.cs | 8 +- ...ics.Monitoring.Tool.FunctionalTests.csproj | 10 + .../Options/OptionsExtensions.cs | 243 +++++----- .../ProcessTests.cs | 2 +- .../Runners/MonitorCollectRunner.cs | 262 +++++++++++ .../Runners/MonitorGenerateKeyRunner.cs | 233 ++++++++++ .../Runners/MonitorRunner.cs | 225 ++-------- .../Runners/MonitorRunnerExtensions.cs | 21 +- .../Runners/ScenarioRunner.cs | 4 +- .../Auth/ApiKeyAuthenticationHandler.cs | 119 ----- ...piKeyAuthenticationPostConfigureOptions.cs | 117 ----- .../{AuthOptions.cs => AuthConfiguration.cs} | 8 +- .../Auth/AuthorizedUserRequirement.cs | 4 +- .../dotnet-monitor/Auth/GeneratedApiKey.cs | 65 --- .../dotnet-monitor/Auth/GeneratedJwtKey.cs | 59 +++ .../Auth/HashAlgorithmChecker.cs | 40 -- ...{IAuthOptions.cs => IAuthConfiguration.cs} | 4 +- .../Auth/JwtAlgorithmChecker.cs | 63 +++ .../Auth/JwtBearerChangeTokenSource.cs | 54 +++ .../Auth/JwtBearerPostConfigure.cs | 61 +++ .../Auth/LiveJwtBearerHandler.cs | 30 ++ ...e.cs => MonitorApiKeyChangeTokenSource.cs} | 14 +- ...tions.cs => MonitorApiKeyConfiguration.cs} | 17 +- ... => MonitorApiKeyConfigurationObserver.cs} | 28 +- .../Auth/MonitorApiKeyPostConfigure.cs | 113 +++++ .../Auth/RejectAllSecurityValidator.cs | 33 ++ .../Auth/UserAuthorizationHandler.cs | 17 +- .../dotnet-monitor/CommonOptionsExtensions.cs | 128 ++++++ .../dotnet-monitor/ConfigurationJsonWriter.cs | 21 +- src/Tools/dotnet-monitor/ConfigurationKeys.cs | 14 +- .../DiagnosticsMonitorCommandHandler.cs | 29 +- .../GenerateApiKeyCommandHandler.cs | 94 ++-- src/Tools/dotnet-monitor/LoggingExtensions.cs | 37 +- src/Tools/dotnet-monitor/OutputFormat.cs | 24 + src/Tools/dotnet-monitor/Program.cs | 21 +- .../dotnet-monitor}/RootOptions.cs | 4 +- .../ServiceCollectionExtensions.cs | 35 +- src/Tools/dotnet-monitor/Startup.cs | 12 +- src/Tools/dotnet-monitor/Strings.Designer.cs | 150 +++++-- src/Tools/dotnet-monitor/Strings.resx | 88 +++- .../dotnet-monitor/dotnet-monitor.csproj | 2 + 59 files changed, 2460 insertions(+), 1020 deletions(-) create mode 100644 documentation/api-key-format.md create mode 100644 src/Microsoft.Diagnostics.Monitoring.Options/AuthenticationOptions.cs rename src/Microsoft.Diagnostics.Monitoring.Options/{CorsConfiguration.cs => CorsConfigurationOptions.cs} (93%) rename src/Microsoft.Diagnostics.Monitoring.Options/{ApiAuthenticationOptions.cs => MonitorApiKeyOptions.cs} (66%) create mode 100644 src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/GenerateKeyTests.cs create mode 100644 src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/JsonSerializerOptionsFactory.cs create mode 100644 src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Runners/MonitorCollectRunner.cs create mode 100644 src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Runners/MonitorGenerateKeyRunner.cs delete mode 100644 src/Tools/dotnet-monitor/Auth/ApiKeyAuthenticationHandler.cs delete mode 100644 src/Tools/dotnet-monitor/Auth/ApiKeyAuthenticationPostConfigureOptions.cs rename src/Tools/dotnet-monitor/Auth/{AuthOptions.cs => AuthConfiguration.cs} (80%) delete mode 100644 src/Tools/dotnet-monitor/Auth/GeneratedApiKey.cs create mode 100644 src/Tools/dotnet-monitor/Auth/GeneratedJwtKey.cs delete mode 100644 src/Tools/dotnet-monitor/Auth/HashAlgorithmChecker.cs rename src/Tools/dotnet-monitor/Auth/{IAuthOptions.cs => IAuthConfiguration.cs} (84%) create mode 100644 src/Tools/dotnet-monitor/Auth/JwtAlgorithmChecker.cs create mode 100644 src/Tools/dotnet-monitor/Auth/JwtBearerChangeTokenSource.cs create mode 100644 src/Tools/dotnet-monitor/Auth/JwtBearerPostConfigure.cs create mode 100644 src/Tools/dotnet-monitor/Auth/LiveJwtBearerHandler.cs rename src/Tools/dotnet-monitor/Auth/{ApiKeyAuthenticationOptionsChangeTokenSource.cs => MonitorApiKeyChangeTokenSource.cs} (68%) rename src/Tools/dotnet-monitor/Auth/{ApiKeyAuthenticationOptions.cs => MonitorApiKeyConfiguration.cs} (51%) rename src/Tools/dotnet-monitor/Auth/{ApiKeyAuthenticationOptionsObserver.cs => MonitorApiKeyConfigurationObserver.cs} (58%) create mode 100644 src/Tools/dotnet-monitor/Auth/MonitorApiKeyPostConfigure.cs create mode 100644 src/Tools/dotnet-monitor/Auth/RejectAllSecurityValidator.cs create mode 100644 src/Tools/dotnet-monitor/CommonOptionsExtensions.cs create mode 100644 src/Tools/dotnet-monitor/OutputFormat.cs rename src/{Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Options => Tools/dotnet-monitor}/RootOptions.cs (83%) diff --git a/documentation/api-key-format.md b/documentation/api-key-format.md new file mode 100644 index 00000000000..7b5db262bb9 --- /dev/null +++ b/documentation/api-key-format.md @@ -0,0 +1,73 @@ +# API Key Format +API Keys or MonitorApiKeys used in `dotnet monitor` are JSON Web Tokens or JWTs as defined by [RFC 7519: JSON Web Token (JWT)](https://datatracker.ietf.org/doc/html/rfc7519). +> **Note:** Because the API Key is a `Bearer` token, it should be treated as a secret and always transmitted over `TLS` or anther protected protocol. + +It is possible to make your own API Keys for `dotnet monitor` by following the format as defined below. Although, it is recommended to use the `generatekey` command unless you have a specific reason to make your own key. + +## Token Format +To use a JWT for authentication with `dotnet monitor`, the token must follow certain constraints. These constraints will be validated by `dotnet monitor` at configuration load time, authentication time, and authorization time. + +For this example, let's consider the API Key given on the [Authentication page](authentication.md). This token consists of 3 parts: Header, Payload, and Signature; explained in detail later. This is the entire portion passed as a `Bearer` type in the `Authorization` HTTP header: +```yaml +eyJhbGciOiJFUffffffffffffCI6IkpXVCJ9.eyJhdWQiOiJodffffffffffffGh1Yi5jb20vZG90bmV0L2RvdG5ldC1tb25pdG9yIiwiaXNzIjoiaHR0cHM6Ly9naXRodWIuY29tL2RvdG5ldC9kb3RuZXQtbW9uaXRvci9nZW5lcmF0ZWtleStNb25pdG9yQXBpS2V5Iiwic3ViIjoiYWU1NDczYjYtOGRhZC00OThkLWI5MTUtNTNiOWM2ODQwMDBlIn0.RZffffffffffff_yIyApvFKcxFpDJ65HJZek1_dt7jCTCMEEEffffffffffffR08OyhZZHs46PopwAsf_6fdTLKB1UGvLr95volwEwIFnHjdvMfTJ9ffffffffffffAU +``` +>**Note:** While all values provided in this document are the correct length and format, the raw values have been edited to prevent this public example being used as a dotnet-monitor configuration. + +### Header +The header (decoded from the token above) must contain at least 2 elements: `alg` (or [Algorithm](https://www.rfc-editor.org/rfc/rfc7518.html#section-3.1)), and `typ` (or [Type](https://datatracker.ietf.org/doc/html/rfc7519#section-5.1)). `dotnet monitor` expects the `typ` to always be `JWT` for a JSON Web Token. `dotnet monitor` supports 6 `alg` values: `ES256`, `ES384`, `ES512`, `RS256`, `RS384`, and `RS512`. + +```json +{ + "alg": "ES384", + "typ": "JWT" +} +``` +>**Note:** The `alg` requirement is designed to enforce `dotnet monitor` to use public/private key signed tokens. This allows the key that is stored in configuration (as `Authentication__MonitorApiKey__PublicKey`) to only contain public key information and thus does not need to be kept secret. + +### Payload +The payload (also decoded from the token above) must contain at least 2 elements: `aud` (or [Audience](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3)), and `sub` (or [Subject](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.2)). `dotnet monitor` expects the `aud` to always be `https://github.com/dotnet/dotnet-monitor` which signals that the token is intended for dotnet-monitor. The `sub` field is any non-empty string defined in `Authentication__MonitorApiKey__Subject`, this is used to validate that the token provided is for the expected instance and is user-defined in configuration. + +When using the `generatekey` command, the `sub` field will be a randomly-generated `Guid` but the `sub` field may be any non-empty string that matches the configuration. The `iss` (or [Issuer](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1)) field will be set to `https://github.com/dotnet/dotnet-monitor/generatekey+MonitorApiKey` to specify the source of the token, however `dotnet monitor` will accept any `iss` field value, and does not need to be present. +```json +{ + "aud": "https://github.com/dotnet/dotnet-monitor", + "iss": "https://github.com/dotnet/dotnet-monitor/generatekey+MonitorApiKey", + "sub": "ae5473b6-8dad-498d-b915-ffffffffffff" +} +``` + +### Signature +JSON web tokens may be cryptographically signed; `dotnet monitor` requires all tokens to be signed and supports `RSA PKCS1` and `ECDSA` signed tokens, these are tokens with `RS*` and `ES*` as `alg` values. `dotnet monitor` needs the public key portion of this cryptographic material in order to validate the token. See the [Providing a Public Key](#providing-a-public-key) section for information on how to encode a key. + +## Providing a Public Key + +The public key is provided to `dotnet monitor` as a JSON Web Key or JWK, as defined by [RFC 7517: JSON Web Key (JWK)](https://www.rfc-editor.org/rfc/rfc7517.html). This key must be serialized as JSON and then Base64 URL encoded into a single string. + +`dotnet monitor` imposes the following constraints on JWKs: +- The key should **not** have private key data. The key is only used for signature verification, and thus private key parameters are not needed. A warning message will be shown if the private key data is included. +- The `kty` (or [Key Type](https://www.rfc-editor.org/rfc/rfc7517.html#section-4.1)) must be `RSA` for a RSA public key or `EC` for an elliptic-curve public key. + +The key used for the token in this example is: + +```json +{ + "AdditionalData":{}, + "Crv":"P-384", + "KeyOps":[], + "Kty":"EC", + "X":"HoffffffffffffuHyjH_57Yf4AkPLEhI5QOTnRugE192Xz_VqcffffffffffffOj", + "X5c":[], + "Y":"JyffffffffffffhzyV-VCMdUttelaY2a8WmileII4MzaYp9j6EffffffffffffFi", + "KeySize":384, + "HasPrivateKey":false, + "CryptoProviderFactory": { + "CryptoProviderCache":{}, + "CacheSignatureProviders":true, + "SignatureProviderObjectPoolCacheSize":32 + } +} +``` +The JWK above is then Base64 URL encoded into the following value which is passed to `dotnet monitor` as `Authentication__MonitorApiKey__PublicKey` +```yaml +eyffffffffffffFsRGF0YSI6e30sIkNydiI6IlAtMzg0IiwiS2V5T3BzIjpbXSwiS3R5IjoiRUMiLCJYIjoiTnhIRnhVZ19QM1dhVUZWVzk0U3dUY3FzVk5zNlFLYjZxc3AzNzVTRmJfQ3QyZHdpN0RWRl8tUTVheERtYlJuWSIsIlg1YyI6W10sIlkiOiJmMXBDdmNoUkVpTWEtc1h6SlZQaS02YmViMHdrZmxfdUZBN0Vka2dwcjF5N251Wmk2cy1NcHl5RzhKdVFSNWZOIiwiS2V5U2l6ZSI6Mzg0LCJIYXNQcml2YXRlS2V5IjpmYWxzZSwiQ3J5cHRvUHJvdmlkZXJGYWN0b3J5Ijp7IkNyeXB0b1Byb3ZpZGVyQ2FjaGUiOnt9LCJDYWNoZVNpZ25hdHVyZVByb3ZpZGVycyI6dHJ1ZSwiU2lnbmF0dXJlUHJvdmlkZXJPYmplY3RQb29sQ2FjaGffffffffffff19 +``` diff --git a/documentation/authentication.md b/documentation/authentication.md index 8c8f309e8bf..2a1c62f7889 100644 --- a/documentation/authentication.md +++ b/documentation/authentication.md @@ -14,29 +14,38 @@ Windows authentication doesn't require explicit configuration and is enabled aut > **NOTE:** Windows authentication will not be attempted if you are running `dotnet monitor` as an Administrator ## API Key Authentication -An API Key is the recommended authentication mechanism for `dotnet monitor`. To enable it, you will need to generate a secret key, update the configuration of `dotnet monitor`, and then specify the API Key in the Authorization header on all requests to `dotnet monitor`. +An API Key is the recommended authentication mechanism for `dotnet monitor`. API Keys are referred to as `MonitorApiKey` in configuration and source code but we will shorten the term to "API key" in this document. To enable it, you will need to generate a secret token, update the configuration of `dotnet monitor`, and then specify the secret token in the `Authorization` header on all requests to `dotnet monitor`. > **NOTE:** API Key Authentication should only be used when [TLS is enabled](#) to protect the key while in transit. `dotnet monitor` will emit a warning if authentication is enabled over an insecure transport medium. ### Generating an API Key -The API Key you use to secure `dotnet monitor` should be a 32-byte cryptographically random secret. You can generate a key either using `dotnet monitor` or via your shell. To generate an API Key with `dotnet monitor`, simply invoke the `generatekey` command: +The API Key you use to secure `dotnet monitor` is a secret JWT token, cryptographically signed by a public/private key algorithm. You can generate a key either using `dotnet monitor` or via your shell. To generate an API Key with `dotnet monitor`, simply invoke the `generatekey` command: ```powershell dotnet monitor generatekey ``` -The output from this command will display the API Key formatted as an authentication header along with its hash and associated hashing algorithm. You will need to store the `ApiKeyHash` and `ApiKeyHashType` in the configuration for `dotnet monitor` and use the authorization header value when making requests to the `dotnet monitor` HTTPS endpoint. +The output from this command will display the API key (a bearer JWT token) formatted as an `Authorization` header along with its corresponding configuration for `dotnet monitor`. You will need to store the `Subject` and `PublicKey` in the configuration for `dotnet monitor` and use the `Authorization` header value when making requests to the `dotnet monitor` HTTPS endpoint. ```yaml -Authorization: MonitorApiKey fffffffffffffffffffffffffffffffffffffffffff= -ApiKeyHash: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -ApiKeyHashType: SHA256 +Generated ApiKey for dotnet-monitor; use the following header for authorization: + +Authorization: Bearer eyJhbGciOiJFUffffffffffffCI6IkpXVCJ9.eyJhdWQiOiJodffffffffffffGh1Yi5jb20vZG90bmV0L2RvdG5ldC1tb25pdG9yIiwiaXNzIjoiaHR0cHM6Ly9naXRodWIuY29tL2RvdG5ldC9kb3RuZXQtbW9uaXRvci9nZW5lcmF0ZWtleStNb25pdG9yQXBpS2V5Iiwic3ViIjoiYWU1NDczYjYtOGRhZC00OThkLWI5MTUtNTNiOWM2ODQwMDBlIn0.RZffffffffffff_yIyApvFKcxFpDJ65HJZek1_dt7jCTCMEEEffffffffffffR08OyhZZHs46PopwAsf_6fdTLKB1UGvLr95volwEwIFnHjdvMfTJ9ffffffffffffAU + +Settings in Text format: +Subject: ae5473b6-8dad-498d-b915-ffffffffffff +Public Key: eyffffffffffffFsRGF0YSI6e30sIkNydiI6IlAtMzg0IiwiS2V5T3BzIjpbXSwiS3R5IjoiRUMiLCJYIjoiTnhIRnhVZ19QM1dhVUZWVzk0U3dUY3FzVk5zNlFLYjZxc3AzNzVTRmJfQ3QyZHdpN0RWRl8tUTVheERtYlJuWSIsIlg1YyI6W10sIlkiOiJmMXBDdmNoUkVpTWEtc1h6SlZQaS02YmViMHdrZmxfdUZBN0Vka2dwcjF5N251Wmk2cy1NcHl5RzhKdVFSNWZOIiwiS2V5U2l6ZSI6Mzg0LCJIYXNQcml2YXRlS2V5IjpmYWxzZSwiQ3J5cHRvUHJvdmlkZXJGYWN0b3J5Ijp7IkNyeXB0b1Byb3ZpZGVyQ2FjaGUiOnt9LCJDYWNoZVNpZ25hdHVyZVByb3ZpZGVycyI6dHJ1ZSwiU2lnbmF0dXJlUHJvdmlkZXJPYmplY3RQb29sQ2FjaGffffffffffff19 ``` +>**Note:** While all values provided in this document are the correct length and format, the raw values have been edited to provent this public example being used as a dotnet-monitor configuration. -The API Key is hashed using the SHA256 algorithm when using the `generatekey` command, but other [secure hash implementations](https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.hashalgorithm.create?view=net-5.0#System_Security_Cryptography_HashAlgorithm_Create_System_String) (such as SHA384 and SHA512) are also supported; due to collision problems the SHA1 and MD5 algorithms are not permitted. +The `generatekey` command supports 1 parameter `--output`/`-o` to specify the configuration format. By default, `dotnet monitor generatekey` will use the `--output json` format. Currently, the values in the list below are supported values for `--output`. -> NOTE: The generated API Key should be secured at rest. We recommend using a tool such as a password manager to save it. +- `Json` output format will provide a json blob in the correct format to merge with an `appsettings.json` file to specify configuration via json file. +- `Text` output format write the individual parameters in an easily human-readable format. +- `Cmd` output format in environment variables for a `cmd.exe` prompt. +- `PowerShell` output format in environment variables for a `powershell` or `pwsh` prompt. +- `Shell` output format in environment variables for a `bash` shell or another linux shell prompt. ### Configuring dotnet-monitor to use an API Key @@ -48,29 +57,31 @@ If you're running on Windows, you can save these settings to `%USERPROFILE%\.dot ```json { - "ApiAuthentication": { - "ApiKeyHash": "CB233C3BE9F650146CFCA81D7AA608E3A3865D7313016DFA02DAF82A2505C683", - "ApiKeyHashType": "SHA256" + "Authentication": { + "MonitorApiKey": { + "Subject": "ae5473b6-8dad-498d-b915-ffffffffffff", + "PublicKey": "eyffffffffffffFsRGF0YSI6e30sIkNydiI6IlAtMzg0IiwiS2V5T3BzIjpbXSwiS3R5IjoiRUMiLCJYIjoiTnhIRnhVZ19QM1dhVUZWVzk0U3dUY3FzVk5zNlFLYjZxc3AzNzVTRmJfQ3QyZHdpN0RWRl8tUTVheERtYlJuWSIsIlg1YyI6W10sIlkiOiJmMXBDdmNoUkVpTWEtc1h6SlZQaS02YmViMHdrZmxfdUZBN0Vka2dwcjF5N251Wmk2cy1NcHl5RzhKdVFSNWZOIiwiS2V5U2l6ZSI6Mzg0LCJIYXNQcml2YXRlS2V5IjpmYWxzZSwiQ3J5cHRvUHJvdmlkZXJGYWN0b3J5Ijp7IkNyeXB0b1Byb3ZpZGVyQ2FjaGUiOnt9LCJDYWNoZVNpZ25hdHVyZVByb3ZpZGVycyI6dHJ1ZSwiU2lnbmF0dXJlUHJvdmlkZXJPYmplY3RQb29sQ2FjaGffffffffffff19" + } } } ``` -Alternatively, you can use environment variables to specify the configuration. +Alternatively, you can use environment variables to specify the configuration. Use `dotnet monitor generatekey --output shell` to get the format below. ```sh -DotnetMonitor_ApiAuthentication__ApiKeyHash="CB233C3BE9F650146CFCA81D7AA608E3A3865D7313016DFA02DAF82A2505C683" -DotnetMonitor_ApiAuthentication__ApiKeyHashType="SHA256" +export Authentication__MonitorApiKey__Subject="ae5473b6-8dad-498d-b915-ffffffffffff" +export Authentication__MonitorApiKey__PublicKey="eyffffffffffffFsRGF0YSI6e30sIkNydiI6IlAtMzg0IiwiS2V5T3BzIjpbXSwiS3R5IjoiRUMiLCJYIjoiTnhIRnhVZ19QM1dhVUZWVzk0U3dUY3FzVk5zNlFLYjZxc3AzNzVTRmJfQ3QyZHdpN0RWRl8tUTVheERtYlJuWSIsIlg1YyI6W10sIlkiOiJmMXBDdmNoUkVpTWEtc1h6SlZQaS02YmViMHdrZmxfdUZBN0Vka2dwcjF5N251Wmk2cy1NcHl5RzhKdVFSNWZOIiwiS2V5U2l6ZSI6Mzg0LCJIYXNQcml2YXRlS2V5IjpmYWxzZSwiQ3J5cHRvUHJvdmlkZXJGYWN0b3J5Ijp7IkNyeXB0b1Byb3ZpZGVyQ2FjaGUiOnt9LCJDYWNoZVNpZ25hdHVyZVByb3ZpZGVycyI6dHJ1ZSwiU2lnbmF0dXJlUHJvdmlkZXJPYmplY3RQb29sQ2FjaGffffffffffff19" ``` -> **NOTE:** When you use environment variables to configure the API Key hash, you must restart `dotnet monitor` for the changes to take effect. +> **NOTE:** When you use environment variables to configure the API Key, you must restart `dotnet monitor` for the changes to take effect. ### Configuring an API Key in a Kubernetes Cluster If you're running in Kubernetes, we recommend creating secrets and mounting them into the `dotnet monitor` sidecar container via a deployment manifest. You can use this `kubectl` command to either create or rotate the API Key. ```sh kubectl create secret generic apikey \ - --from-literal=ApiAuthentication__ApiKeyHash=$hash \ - --from-literal=ApiAuthentication__ApiKeyHashType=SHA256 \ + --from-literal=Authentication__MonitorApiKey__Subject='ae5473b6-8dad-498d-b915-ffffffffffff' \ + --from-literal=Authentication__MonitorApiKey__PublicKey='eyffffffffffff...19' \ --dry-run=client -o yaml \ | kubectl apply -f - ``` @@ -94,52 +105,30 @@ spec: > **NOTE:** For a complete example of running dotnet-monitor in Kubernetes, see [Running in a Kubernetes Cluster](getting-started.md#running-in-a-kubernetes-cluster) in the Getting Started guide. -### Generate an API Key with PowerShell -```powershell -$rng = New-Object System.Security.Cryptography.RNGCryptoServiceProvider -$secret = [byte[]]::new(32) -$rng.GetBytes($secret) -$API_KEY = [Convert]::ToBase64String($secret) -"Authorization: MonitorApiKey $API_KEY" - -$secretStream = [System.IO.MemoryStream]::new($secret) -$HASHED_KEY = (Get-FileHash -Algorithm SHA256 -InputStream $secretStream).Hash -$rng.Dispose() - -Write-Output "ApiKeyHash: $HASHED_KEY" -Write-Output "ApiKeyHashType: SHA256" -``` - -### Generate an API Key with a Linux shell - -```sh -API_KEY=`openssl rand -base64 32` -echo "Authorization: MonitorApiKey $API_KEY" +### Generate API Keys manually -HASHED_KEY=`$API_KEY | base64 -d | sha256sum` -echo "ApiKeyHash: $HASHED_KEY" -echo "ApiKeyHashType: SHA256" -``` +API keys for `dotnet monitor` are standard JSON Web Tokens or JWTs and can be generated without the `generatekey` command on `dotnet monitor`. For more details see [Api Key Format](api-key-format.md). ## Authenticating requests When using Windows Authentication, your browser will automatically handle the Windows authentication challenge. If you are using an API Key, you must specify it via the `Authorization` header. ```sh -curl -H "Authorization: MonitorApiKey fffffffffffffffffffffffffffffffffffffffffff=" https://localhost:52323/processes +curl -H "Authorization: Bearer eyJhbGciOiJFUffffffffffffCI6IkpXVCJ9.eyJhdWQiOiJodffffffffffffGh1Yi5jb20vZG90bmV0L2RvdG5ldC1tb25pdG9yIiwiaXNzIjoiaHR0cHM6Ly9naXRodWIuY29tL2RvdG5ldC9kb3RuZXQtbW9uaXRvci9nZW5lcmF0ZWtleStNb25pdG9yQXBpS2V5Iiwic3ViIjoiYWU1NDczYjYtOGRhZC00OThkLWI5MTUtNTNiOWM2ODQwMDBlIn0.RZffffffffffff_yIyApvFKcxFpDJ65HJZek1_dt7jCTCMEEEffffffffffffR08OyhZZHs46PopwAsf_6fdTLKB1UGvLr95volwEwIFnHjdvMfTJ9ffffffffffffAU" https://localhost:52323/processes ``` -If using PowerShell, be sure to use `curl.exe`, as `curl` is an alias for `Invoke-WebRequest` that does not accept the same parameters. +If using PowerShell, you can use `Invoke-WebRequest` but it does not accept the same parameters. ```powershell -curl.exe -H "Authorization: MonitorApiKey fffffffffffffffffffffffffffffffffffffffffff=" https://localhost:52323/processes + (Invoke-WebRequest -Uri https://localhost:52323/processes -Headers @{ 'Authorization' = 'Bearer eyJhbGciOiJFUffffffffffffCI6IkpXVCJ9.eyJhdWQiOiJodffffffffffffGh1Yi5jb20vZG90bmV0L2RvdG5ldC1tb25pdG9yIiwiaXNzIjoiaHR0cHM6Ly9naXRodWIuY29tL2RvdG5ldC9kb3RuZXQtbW9uaXRvci9nZW5lcmF0ZWtleStNb25pdG9yQXBpS2V5Iiwic3ViIjoiYWU1NDczYjYtOGRhZC00OThkLWI5MTUtNTNiOWM2ODQwMDBlIn0.RZffffffffffff_yIyApvFKcxFpDJ65HJZek1_dt7jCTCMEEEffffffffffffR08OyhZZHs46PopwAsf_6fdTLKB1UGvLr95volwEwIFnHjdvMfTJ9ffffffffffffAU' }).Content | ConvertFrom-Json ``` -To use Windows authentication with PowerShell, you can specify the `--negotiate` flag +To use Windows authentication with PowerShell, you can specify the `-UseDefaultCredentials` flag for `Invoke-WebRequest` or `--negotiate` for `curl.exe` ```powershell curl.exe --negotiate https://localhost:52323/processes -u $(whoami) ``` - - +```powershell +(Invoke-WebRequest -Uri https://localhost:52323/processes -UseDefaultCredentials).Content | ConvertFrom-Json +``` ## Disabling Authentication diff --git a/documentation/schema.json b/documentation/schema.json index b7ce988a80a..2c1f961ea89 100644 --- a/documentation/schema.json +++ b/documentation/schema.json @@ -16,14 +16,14 @@ } ] }, - "ApiAuthentication": { + "Authentication": { "default": {}, "oneOf": [ { "type": "null" }, { - "$ref": "#/definitions/ApiAuthenticationOptions" + "$ref": "#/definitions/AuthenticationOptions" } ] }, @@ -34,7 +34,7 @@ "type": "null" }, { - "$ref": "#/definitions/CorsConfiguration" + "$ref": "#/definitions/CorsConfigurationOptions" } ] }, @@ -335,28 +335,45 @@ } } }, - "ApiAuthenticationOptions": { + "AuthenticationOptions": { "type": "object", "additionalProperties": false, "required": [ - "ApiKeyHash", - "ApiKeyHashType" + "MonitorApiKey" ], "properties": { - "ApiKeyHash": { + "MonitorApiKey": { + "description": "The parameters used to validate MonitorApiKey JWT tokens.", + "oneOf": [ + { + "$ref": "#/definitions/MonitorApiKeyOptions" + } + ] + } + } + }, + "MonitorApiKeyOptions": { + "type": "object", + "additionalProperties": false, + "required": [ + "Subject", + "PublicKey" + ], + "properties": { + "Subject": { "type": "string", - "description": "API key in hashed form. Each byte should be two hexadecimal-based digits.", - "minLength": 64, - "pattern": "[0-9a-fA-F]+" + "description": "The value of the 'sub' or Subject field in the JWT (JSON Web Token).", + "minLength": 1 }, - "ApiKeyHashType": { + "PublicKey": { "type": "string", - "description": "Hash algorithm used to compute ApiKeyHash, typically 'SHA256'. 'SHA1' and 'MD5' are not allowed.", - "minLength": 1 + "description": "The public key used to sign the JWT (JSON Web Token) used for authentication. This field is a JSON Web Key serialized as JSON encoded with base64Url encoding. The JWK must have a kty field of RSA or EC and should not have the private key information.", + "minLength": 1, + "pattern": "[0-9a-zA-Z_-]+" } } }, - "CorsConfiguration": { + "CorsConfigurationOptions": { "type": "object", "additionalProperties": false, "required": [ diff --git a/eng/Versions.props b/eng/Versions.props index f454e89d669..280ddcfadbc 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -48,6 +48,7 @@ 12.6.0 + 3.1.17 3.1.10 2.1.22 2.1.3 @@ -57,9 +58,11 @@ 5.0.0 5.0.0 5.0.1 + 6.11.1 1.2.3 2.0.0-beta1.20468.1 5.0.0 + 6.11.1 4.5.1 5.0.0 4.7.2 diff --git a/src/Microsoft.Diagnostics.Monitoring.Options/AuthenticationOptions.cs b/src/Microsoft.Diagnostics.Monitoring.Options/AuthenticationOptions.cs new file mode 100644 index 00000000000..205e88dab21 --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.Options/AuthenticationOptions.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Diagnostics.Monitoring.WebApi; +using System.ComponentModel.DataAnnotations; + +namespace Microsoft.Diagnostics.Tools.Monitor +{ + internal sealed class AuthenticationOptions + { + [Display( + ResourceType = typeof(OptionsDisplayStrings), + Description = nameof(OptionsDisplayStrings.DisplayAttributeDescription_AuthenticationOptions_MonitorApiKey))] + [Required] + public MonitorApiKeyOptions MonitorApiKey { get; set; } + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.Options/CorsConfiguration.cs b/src/Microsoft.Diagnostics.Monitoring.Options/CorsConfigurationOptions.cs similarity index 93% rename from src/Microsoft.Diagnostics.Monitoring.Options/CorsConfiguration.cs rename to src/Microsoft.Diagnostics.Monitoring.Options/CorsConfigurationOptions.cs index 2d551dac340..91bbfac1a11 100644 --- a/src/Microsoft.Diagnostics.Monitoring.Options/CorsConfiguration.cs +++ b/src/Microsoft.Diagnostics.Monitoring.Options/CorsConfigurationOptions.cs @@ -6,7 +6,7 @@ namespace Microsoft.Diagnostics.Monitoring.WebApi { - public class CorsConfiguration + public class CorsConfigurationOptions { [Display( ResourceType = typeof(OptionsDisplayStrings), diff --git a/src/Microsoft.Diagnostics.Monitoring.Options/ApiAuthenticationOptions.cs b/src/Microsoft.Diagnostics.Monitoring.Options/MonitorApiKeyOptions.cs similarity index 66% rename from src/Microsoft.Diagnostics.Monitoring.Options/ApiAuthenticationOptions.cs rename to src/Microsoft.Diagnostics.Monitoring.Options/MonitorApiKeyOptions.cs index a0bdc3cddfd..6e0149a0571 100644 --- a/src/Microsoft.Diagnostics.Monitoring.Options/ApiAuthenticationOptions.cs +++ b/src/Microsoft.Diagnostics.Monitoring.Options/MonitorApiKeyOptions.cs @@ -7,20 +7,19 @@ namespace Microsoft.Diagnostics.Tools.Monitor { - internal sealed class ApiAuthenticationOptions + internal sealed class MonitorApiKeyOptions { [Display( ResourceType = typeof(OptionsDisplayStrings), - Description = nameof(OptionsDisplayStrings.DisplayAttributeDescription_ApiAuthenticationOptions_ApiKeyHash))] - [RegularExpression("[0-9a-fA-F]+")] - [MinLength(64)] + Description = nameof(OptionsDisplayStrings.DisplayAttributeDescription_MonitorApiKeyOptions_Subject))] [Required] - public string ApiKeyHash { get; set; } + public string Subject { get; set; } [Display( ResourceType = typeof(OptionsDisplayStrings), - Description = nameof(OptionsDisplayStrings.DisplayAttributeDescription_ApiAuthenticationOptions_ApiKeyHashType))] + Description = nameof(OptionsDisplayStrings.DisplayAttributeDescription_MonitorApiKeyOptions_PublicKey))] + [RegularExpression("[0-9a-zA-Z_-]+")] [Required] - public string ApiKeyHashType { get; set; } + public string PublicKey { get; set; } } } diff --git a/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.Designer.cs b/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.Designer.cs index 45f2b3659fc..8c810f1d307 100644 --- a/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.Designer.cs +++ b/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.Designer.cs @@ -61,20 +61,11 @@ internal OptionsDisplayStrings() { } /// - /// Looks up a localized string similar to API key in hashed form. Each byte should be two hexadecimal-based digits.. + /// Looks up a localized string similar to The parameters used to validate MonitorApiKey JWT tokens.. /// - public static string DisplayAttributeDescription_ApiAuthenticationOptions_ApiKeyHash { + public static string DisplayAttributeDescription_AuthenticationOptions_MonitorApiKey { get { - return ResourceManager.GetString("DisplayAttributeDescription_ApiAuthenticationOptions_ApiKeyHash", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Hash algorithm used to compute ApiKeyHash, typically 'SHA256'. 'SHA1' and 'MD5' are not allowed.. - /// - public static string DisplayAttributeDescription_ApiAuthenticationOptions_ApiKeyHashType { - get { - return ResourceManager.GetString("DisplayAttributeDescription_ApiAuthenticationOptions_ApiKeyHashType", resourceCulture); + return ResourceManager.GetString("DisplayAttributeDescription_AuthenticationOptions_MonitorApiKey", resourceCulture); } } @@ -467,6 +458,24 @@ public static string DisplayAttributeDescription_MetricsOptions_UpdateIntervalSe } } + /// + /// Looks up a localized string similar to The public key used to sign the JWT (JSON Web Token) used for authentication. This field is a JSON Web Key serialized as JSON encoded with base64Url encoding. The JWK must have a kty field of RSA or EC and should not have the private key information.. + /// + public static string DisplayAttributeDescription_MonitorApiKeyOptions_PublicKey { + get { + return ResourceManager.GetString("DisplayAttributeDescription_MonitorApiKeyOptions_PublicKey", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The value of the 'sub' or Subject field in the JWT (JSON Web Token).. + /// + public static string DisplayAttributeDescription_MonitorApiKeyOptions_Subject { + get { + return ResourceManager.GetString("DisplayAttributeDescription_MonitorApiKeyOptions_Subject", resourceCulture); + } + } + /// /// Looks up a localized string similar to The criteria used to compare against the target process.. /// diff --git a/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.resx b/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.resx index 5684a54647e..acba0a92790 100644 --- a/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.resx +++ b/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.resx @@ -117,14 +117,6 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - API key in hashed form. Each byte should be two hexadecimal-based digits. - The description provided for the ApiKeyHash parameter on ApiAuthenticationOptions. - - - Hash algorithm used to compute ApiKeyHash, typically 'SHA256'. 'SHA1' and 'MD5' are not allowed. - The description provided for the ApiKeyHashType parameter on ApiAuthenticationOptions. - The account key used to access the Azure blob storage account. The description provided for the AccountKey parameter on AzureBlobEgressProviderOptions. @@ -197,6 +189,10 @@ The minimum level of messages that are written to Console.Error. The description provided for the LogToStandardErrorThreshold parameter on ConsoleLoggerOptions. + + The parameters used to validate MonitorApiKey JWT tokens. + The description provided for the MonitorApiKey parameter on AuthenticationOptions. + List of allowed CORS origins, separated by semicolons. The description provided for the AllowedOrigins parameter on CorsConfiguration. @@ -297,6 +293,14 @@ How often metrics are collected. The description provided for the UpdateIntervalSeconds parameter on MetricsOptions. + + The public key used to sign the JWT (JSON Web Token) used for authentication. This field is a JSON Web Key serialized as JSON encoded with base64Url encoding. The JWK must have a kty field of RSA or EC and should not have the private key information. + The description provided for the PublicKey parameter on MonitorApiKeyOptions. + + + The value of the 'sub' or Subject field in the JWT (JSON Web Token). + The description provided for the Subject parameter on MonitorApiKeyOptions. + The criteria used to compare against the target process. The description provided for the Key parameter on ProcessFilterDescriptor. diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Auth/AuthConstants.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Auth/AuthConstants.cs index fde16fec49b..8770c0000ee 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Auth/AuthConstants.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Auth/AuthConstants.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. + namespace Microsoft.Diagnostics.Monitoring.WebApi { internal static class AuthConstants @@ -10,6 +11,13 @@ internal static class AuthConstants public const string NegotiateSchema = "Negotiate"; public const string NtlmSchema = "NTLM"; public const string KerberosSchema = "Kerberos"; - public const string ApiKeySchema = "MonitorApiKey"; + public const string FederationAuthType = "AuthenticationTypes.Federation"; + public const string ApiKeySchema = "Bearer"; + public const string ApiKeyJwtType = "JWT"; + public const string ApiKeyJwtInternalIssuer = "https://github.com/dotnet/dotnet-monitor/generatekey+MonitorApiKey"; + public const string ApiKeyJwtAudience = "https://github.com/dotnet/dotnet-monitor"; + public const string ClaimAudienceStr = "aud"; + public const string ClaimIssuerStr = "iss"; + public const string ClaimSubjectStr = "sub"; } } diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.ConfigurationSchema/Microsoft.Diagnostics.Monitoring.ConfigurationSchema.csproj b/src/Tests/Microsoft.Diagnostics.Monitoring.ConfigurationSchema/Microsoft.Diagnostics.Monitoring.ConfigurationSchema.csproj index 45aa14474e9..25f1234f32f 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.ConfigurationSchema/Microsoft.Diagnostics.Monitoring.ConfigurationSchema.csproj +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.ConfigurationSchema/Microsoft.Diagnostics.Monitoring.ConfigurationSchema.csproj @@ -6,7 +6,7 @@ - + diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.ConfigurationSchema/Options/RootOptions.Logging.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.ConfigurationSchema/Options/RootOptions.Logging.cs index 2cd30f41b84..0856313ad1f 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.ConfigurationSchema/Options/RootOptions.Logging.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.ConfigurationSchema/Options/RootOptions.Logging.cs @@ -6,7 +6,7 @@ namespace Microsoft.Diagnostics.Tools.Monitor { - partial class RootOptions + internal partial class RootOptions { public LoggingOptions Logging { get; set; } } diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/Runners/DotNetRunner.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/Runners/DotNetRunner.cs index 3200d59a1f9..855806001d7 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/Runners/DotNetRunner.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/Runners/DotNetRunner.cs @@ -41,6 +41,11 @@ public sealed class DotNetRunner : IDisposable /// public IDictionary Environment => _process.StartInfo.Environment; + /// + /// Gets a indicating if has been called and the process has been started. + /// + public bool HasStarted { get; private set; } = false; + /// /// Retrieves the exit code of the process. /// @@ -59,7 +64,7 @@ public sealed class DotNetRunner : IDisposable /// /// Determines if the process has exited. /// - public bool HasExited => _process.HasExited; + public bool HasExited => HasStarted && _process.HasExited; /// /// Gets the process ID of the running process. @@ -133,6 +138,7 @@ public async Task StartAsync(CancellationToken token) { throw new InvalidOperationException($"Unable to start: {_process.StartInfo.FileName} {_process.StartInfo.Arguments}"); } + HasStarted = true; if (WaitForDiagnosticPipe) { @@ -161,7 +167,7 @@ public async Task StartAsync(CancellationToken token) /// public async Task WaitForExitAsync(CancellationToken token) { - if (!_process.HasExited) + if (HasStarted && !_process.HasExited) { TaskCompletionSource cancellationSource = new(TaskCreationOptions.RunContinuationsAsynchronously); using IDisposable _ = token.Register(() => cancellationSource.TrySetCanceled(token)); @@ -177,7 +183,7 @@ await Task.WhenAny( /// public void ForceClose() { - if (!_process.HasExited) + if (HasStarted && !_process.HasExited) { _process.Kill(); } diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/Runners/LoggingRunnerAdapter.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/Runners/LoggingRunnerAdapter.cs index facf729d0f9..bda63809b07 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/Runners/LoggingRunnerAdapter.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/Runners/LoggingRunnerAdapter.cs @@ -19,6 +19,7 @@ public sealed class LoggingRunnerAdapter : IAsyncDisposable private readonly List _standardErrorLines = new(); private readonly List _standardOutputLines = new(); + private bool _finishReads; private int? _exitCode; private bool _isDiposed; private int? _processId; @@ -26,7 +27,6 @@ public sealed class LoggingRunnerAdapter : IAsyncDisposable private Task _standardOutputTask; public Dictionary Environment { get; } = new(); - public int ExitCode => _exitCode.HasValue ? _exitCode.Value : throw new InvalidOperationException("Must call WaitForExitAsync before getting exit code."); @@ -108,42 +108,80 @@ public async Task StartAsync(CancellationToken token) public async Task WaitForExitAsync(CancellationToken token) { - if (_runner.HasExited) + int? exitCode; + if (!_runner.HasStarted) + { + _outputHelper.WriteLine("Runner Never Started."); + throw new InvalidOperationException("The has runner has never been started, call StartAsync first."); + } + else if (_runner.HasExited) { _outputHelper.WriteLine("Already exited."); + exitCode = _runner.ExitCode; } else { _outputHelper.WriteLine("Waiting for exit..."); await _runner.WaitForExitAsync(token).ConfigureAwait(false); + exitCode = _runner.ExitCode; } - _exitCode = _runner.ExitCode; - _outputHelper.WriteLine("Exit Code: {0}", _runner.ExitCode); - return _runner.ExitCode; + _outputHelper.WriteLine("Exit Code: {0}", _exitCode); + _exitCode = exitCode; + return exitCode.Value; } - private static async Task ReadLinesAsync(StreamReader reader, List lines, Action callback, CancellationToken token) + public async Task ReadToEnd(CancellationToken token) + { + // First we need to wait for the process to end + await WaitForExitAsync(token); + + // Then tell the readers to end by setting _finishReads and grab the waiter tasks + _finishReads = true; + Task errorWaiter = _standardErrorTask.SafeAwait(_outputHelper); + Task stdWaiter = _standardOutputTask.SafeAwait(_outputHelper); + + // Wait for everything to end with the cancellation token still allowed to abort the wait + await Task.WhenAll(stdWaiter, errorWaiter).WithCancellation(token).ConfigureAwait(false); + } + + private async Task ReadLinesAsync(StreamReader reader, List lines, Action callback, CancellationToken cancelToken) { // Closing the reader to cancel the async await will dispose the underlying stream. - // Technically, this means the reader/stream cannot be used after cancelling reading of lines + // Technically, this means the reader/stream cannot be used after canceling reading of lines // from the process, but this is probably okay since the adapter is already logging each line // and providing a callback to callers to read each line. It's unlikely the reader/stream will - // be accessed after this adapter is diposed. - using var _ = token.Register(() => reader.Close()); + // be accessed after this adapter is disposed. + using var cancelReg = cancelToken.Register(() => reader.Close()); try { - while (true) + bool readAborted = false; + while (!_finishReads) { // ReadLineAsync does not have cancellation string line = await reader.ReadLineAsync().ConfigureAwait(false); if (null == line) + { + readAborted = true; break; + } lines.Add(line); callback?.Invoke(line); } + + // If the loop ended because _finishReads was set, we should read to the end of the + // stream if readAborded is not set. This is so we can ensure that the entire stream is read. + if (!readAborted && _finishReads) + { + string remainder = await reader.ReadToEndAsync().ConfigureAwait(false); + foreach (string line in remainder.Split(new string[] { "\r\n", "\r", "\n" }, StringSplitOptions.None)) + { + lines.Add(line); + callback?.Invoke(line); + } + } } catch (OperationCanceledException) { diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/TaskExtensions.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/TaskExtensions.cs index 141c56c4d35..a928aef0a8c 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/TaskExtensions.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/TaskExtensions.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.Threading; using System.Threading.Tasks; using Xunit.Abstractions; @@ -40,5 +41,27 @@ public static async Task SafeAwait(this Task task, ITestOutputHelper outputHelpe } return fallbackValue; } + + public static async Task WithCancellation(this Task task, CancellationToken token) + { + CancellationTokenSource localTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token); + + try + { + // Creating a Task.Delay with Infinite timeout is not intended to work with a cancellation token that never fires + // So we need to make a local "combined" token that we can cancel if the provided CancellationToken token is never canceled + // This allows the framework to properly cleanup the Task and it's associated token registrations + Task waitOnCancellation = Task.Delay(Timeout.Infinite, localTokenSource.Token); + await Task.WhenAny(task, waitOnCancellation).Unwrap().ConfigureAwait(false); + } + finally + { + // If the token provided wasn't cancelled, cancel our own token + if (!token.IsCancellationRequested) + { + localTokenSource.Cancel(); + } + } + } } } diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/AuthenticationTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/AuthenticationTests.cs index 78270a660a7..d638f1f66ec 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/AuthenticationTests.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/AuthenticationTests.cs @@ -9,25 +9,31 @@ using Microsoft.Diagnostics.Monitoring.TestCommon.Options; using Microsoft.Diagnostics.Monitoring.TestCommon.Runners; using Microsoft.Extensions.DependencyInjection; +using Microsoft.IdentityModel.Tokens; using System; +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; using System.Net; using System.Net.Http; using System.Net.Http.Headers; +using System.Security.Claims; using System.Security.Cryptography; +using System.Text.Json; using System.Threading.Tasks; using Xunit; using Xunit.Abstractions; using Microsoft.Diagnostics.Tools.Monitor; +using System.Threading; +using Microsoft.Diagnostics.Monitoring.WebApi; namespace Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests { [Collection(DefaultCollectionFixture.Name)] public class AuthenticationTests { - internal const string ApiKeyScheme = "MonitorApiKey"; - private readonly IHttpClientFactory _httpClientFactory; private readonly ITestOutputHelper _outputHelper; + private readonly List<(string fieldName, DateTime time)> _warnPrivateKeyLog = new(); public AuthenticationTests(ITestOutputHelper outputHelper, ServiceProviderFixture serviceProviderFixture) { @@ -42,7 +48,7 @@ public AuthenticationTests(ITestOutputHelper outputHelper, ServiceProviderFixtur [Fact] public async Task DefaultAddressTest() { - await using MonitorRunner toolRunner = new(_outputHelper); + await using MonitorCollectRunner toolRunner = new(_outputHelper); await toolRunner.StartAsync(); @@ -64,13 +70,13 @@ public async Task DefaultAddressTest() } /// - /// Tests the behavior of the mertics URL address, namely that all routes will return + /// Tests the behavior of the metrics URL address, namely that all routes will return /// 404 Not Found except for the metrics route (200 OK). /// [Fact] public async Task MetricsAddressTest() { - await using MonitorRunner toolRunner = new(_outputHelper); + await using MonitorCollectRunner toolRunner = new(_outputHelper); await toolRunner.StartAsync(); @@ -95,7 +101,7 @@ public async Task MetricsAddressTest() [Fact] public async Task DisableAuthenticationTest() { - await using MonitorRunner toolRunner = new(_outputHelper); + await using MonitorCollectRunner toolRunner = new(_outputHelper); toolRunner.DisableAuthentication = true; await toolRunner.StartAsync(); @@ -113,23 +119,20 @@ public async Task DisableAuthenticationTest() /// /// Tests that API key authentication can be configured correctly and - /// that the key can be rotated wihtout shutting down dotnet-monitor. + /// that the key can be rotated without shutting down dotnet-monitor. /// [Fact] public async Task ApiKeyAuthenticationSchemeTest() { - const int keyLength = 32; - const string hashAlgorithm = "SHA256"; - await using MonitorRunner toolRunner = new(_outputHelper); + const string signingAlgo = "ES384"; + await using MonitorCollectRunner toolRunner = new(_outputHelper); + toolRunner.DisableMetricsViaCommandLine = true; _outputHelper.WriteLine("Generating API key."); - // Generate initial API key - byte[] apiKey = GenerateApiKey(keyLength); - // Set API key via key-per-file RootOptions options = new(); - options.UseApiKey(hashAlgorithm, apiKey); + options.UseApiKey(signingAlgo, Guid.NewGuid(), out string apiKey); toolRunner.WriteKeyPerValueConfiguration(options); // Start dotnet-monitor @@ -138,7 +141,7 @@ public async Task ApiKeyAuthenticationSchemeTest() // Create HttpClient with default request headers using HttpClient httpClient = await toolRunner.CreateHttpClientDefaultAddressAsync(_httpClientFactory); httpClient.DefaultRequestHeaders.Authorization = - new AuthenticationHeaderValue(ApiKeyScheme, Convert.ToBase64String(apiKey)); + new AuthenticationHeaderValue(AuthConstants.ApiKeySchema, apiKey); ApiClient apiClient = new(_outputHelper, httpClient); // Check that /processes does not challenge for authentication @@ -148,21 +151,22 @@ public async Task ApiKeyAuthenticationSchemeTest() _outputHelper.WriteLine("Rotating API key."); // Rotate the API key - byte[] apiKey2 = GenerateApiKey(keyLength); - - options.UseApiKey(hashAlgorithm, apiKey2); + options.UseApiKey(signingAlgo, Guid.NewGuid(), out string apiKey2); toolRunner.WriteKeyPerValueConfiguration(options); // Wait for the key rotation to be consumed by dotnet-monitor; detect this // by checking for when API returns a 401. Ideally, key rotation would write // log event and runner monitor for event and notify. int attempts = 0; + int currMsDelay = 300; + const int MaxDelayMs = 3000; while (true) { attempts++; _outputHelper.WriteLine("Waiting for key rotation (attempt #{0}).", attempts); - await Task.Delay(TimeSpan.FromSeconds(3)); + await Task.Delay(TimeSpan.FromMilliseconds(currMsDelay)); + currMsDelay = Math.Min(MaxDelayMs, currMsDelay * 2); try { @@ -172,15 +176,24 @@ public async Task ApiKeyAuthenticationSchemeTest() { break; } + catch (ApiStatusCodeException ex) when (ex.StatusCode == HttpStatusCode.Forbidden) + { + // Forbidden means that the user authorization handler picked up the change but not user authentication handler. + // We need to continue to wait. + } Assert.True(attempts < 10); } + // check that the old key is now invalid + ApiStatusCodeException thrownEx = await Assert.ThrowsAsync(async () => await apiClient.GetProcessesAsync()); + Assert.True(HttpStatusCode.Unauthorized == thrownEx.StatusCode); + _outputHelper.WriteLine("Verifying new API key."); // Use new API key httpClient.DefaultRequestHeaders.Authorization = - new AuthenticationHeaderValue(ApiKeyScheme, Convert.ToBase64String(apiKey2)); + new AuthenticationHeaderValue(AuthConstants.ApiKeySchema, apiKey2); // Check that /processes does not challenge for authentication processes = await apiClient.GetProcessesAsync(); @@ -192,31 +205,78 @@ public async Task ApiKeyAuthenticationSchemeTest() /// disallowed algorithms are enforced. /// [Theory] - [InlineData(32, "SHA256", HttpStatusCode.OK)] - [InlineData(33, "SHA256", HttpStatusCode.OK)] - [InlineData(34, "SHA256", HttpStatusCode.OK)] - [InlineData(2048, "SHA256", HttpStatusCode.OK)] - [InlineData(32, "SHA512", HttpStatusCode.OK)] - [InlineData(2048, "SHA512", HttpStatusCode.OK)] - [InlineData(0, "SHA256", HttpStatusCode.Unauthorized)] - [InlineData(31, "SHA256", HttpStatusCode.Unauthorized)] - [InlineData(2049, "SHA256", HttpStatusCode.Unauthorized)] - [InlineData(100000, "SHA256", HttpStatusCode.RequestHeaderFieldsTooLarge)] - [InlineData(32, "SHA1", HttpStatusCode.Unauthorized)] - [InlineData(32, "MD5", HttpStatusCode.Unauthorized)] - public async Task ApiKeyLimitsTest(int keyLength, string hashAlgorithm, HttpStatusCode expectedCode) + [InlineData(SecurityAlgorithms.EcdsaSha256, true)] + [InlineData(SecurityAlgorithms.EcdsaSha256Signature, true)] + [InlineData(SecurityAlgorithms.EcdsaSha384, true)] + [InlineData(SecurityAlgorithms.EcdsaSha384Signature, true)] + [InlineData(SecurityAlgorithms.EcdsaSha512, true)] + [InlineData(SecurityAlgorithms.EcdsaSha512Signature, true)] + [InlineData(SecurityAlgorithms.RsaSha256, true)] + [InlineData(SecurityAlgorithms.RsaSha256Signature, true)] + [InlineData(SecurityAlgorithms.RsaSha384, true)] + [InlineData(SecurityAlgorithms.RsaSha384Signature, true)] + [InlineData(SecurityAlgorithms.RsaSha512, true)] + [InlineData(SecurityAlgorithms.RsaSha512Signature, true)] + [InlineData(SecurityAlgorithms.HmacSha256, false)] + [InlineData(SecurityAlgorithms.HmacSha384, false)] + [InlineData(SecurityAlgorithms.HmacSha512, false)] + public async Task ApiKeyAlgorithmTest(string signingAlgo, bool valid) { - await using MonitorRunner toolRunner = new(_outputHelper); + MonitorCollectRunner toolRunner = new(_outputHelper); + toolRunner.WarnPrivateKey += ToolRunner_WarnPrivateKey; + await using (toolRunner) + { + toolRunner.DisableMetricsViaCommandLine = true; + + _outputHelper.WriteLine("Generating API key."); + + toolRunner.ConfigurationFromEnvironment.UseApiKey(signingAlgo, Guid.NewGuid(), out string apiKey); + + // Start dotnet-monitor + await toolRunner.StartAsync(); + + // Create HttpClient with default request headers + using HttpClient httpClient = await toolRunner.CreateHttpClientDefaultAddressAsync(_httpClientFactory); + httpClient.DefaultRequestHeaders.Authorization = + new AuthenticationHeaderValue(AuthConstants.ApiKeySchema, apiKey); + ApiClient apiClient = new(_outputHelper, httpClient); + + if (valid) + { + var processes = await apiClient.GetProcessesAsync(); + Assert.NotNull(processes); + } + else + { + ApiStatusCodeException ex = await Assert.ThrowsAsync(async () => { await apiClient.GetProcessesAsync(); }); + Assert.Equal(HttpStatusCode.Unauthorized, ex.StatusCode); + } + } + toolRunner.WarnPrivateKey -= ToolRunner_WarnPrivateKey; + + Assert.Empty(_warnPrivateKeyLog); + } + + /// + /// This tests that a valid JWT with the correct subject ID + /// that is signed with a key other than the specified one will get rejected. + /// + [Theory] + [InlineData(SecurityAlgorithms.EcdsaSha384)] + [InlineData(SecurityAlgorithms.RsaSha384)] + public async Task RejectsWrongSigningKey(string signingAlgo) + { + await using MonitorCollectRunner toolRunner = new(_outputHelper); + toolRunner.DisableMetricsViaCommandLine = true; _outputHelper.WriteLine("Generating API key."); - // Generate initial API key - byte[] apiKey = GenerateApiKey(keyLength); + Guid subject = Guid.NewGuid(); + toolRunner.ConfigurationFromEnvironment.UseApiKey(signingAlgo, subject, out string apiKeyReal); - // Set API key via key-per-file - RootOptions options = new(); - options.UseApiKey(hashAlgorithm, apiKey); - toolRunner.WriteKeyPerValueConfiguration(options); + _outputHelper.WriteLine("Getting fake API key."); + // Regenerate a new JWT with the same sub but different signing key, don't update the config + new RootOptions().UseApiKey(signingAlgo, subject, out string apiKeyFake); // Start dotnet-monitor await toolRunner.StartAsync(); @@ -224,31 +284,239 @@ public async Task ApiKeyLimitsTest(int keyLength, string hashAlgorithm, HttpStat // Create HttpClient with default request headers using HttpClient httpClient = await toolRunner.CreateHttpClientDefaultAddressAsync(_httpClientFactory); httpClient.DefaultRequestHeaders.Authorization = - new AuthenticationHeaderValue(ApiKeyScheme, Convert.ToBase64String(apiKey)); + new AuthenticationHeaderValue(AuthConstants.ApiKeySchema, apiKeyFake); ApiClient apiClient = new(_outputHelper, httpClient); - if (expectedCode == HttpStatusCode.OK) - { - // We expect the combo to work - var processes = await apiClient.GetProcessesAsync(); - Assert.NotNull(processes); - } - else + var statusCodeException = await Assert.ThrowsAsync( + () => apiClient.GetProcessesAsync()); + Assert.Equal(HttpStatusCode.Unauthorized, statusCodeException.StatusCode); + } + + /// + /// This tests that a valid JWT is not accepted when not configured + /// + [Fact] + public async Task RejectsApiKeyNotConfigured() + { + await using MonitorCollectRunner toolRunner = new(_outputHelper); + toolRunner.DisableMetricsViaCommandLine = true; + + _outputHelper.WriteLine("Generating API key."); + + // Get a apiKey, but throw away the config + (new RootOptions()).UseApiKey(SecurityAlgorithms.EcdsaSha384, Guid.NewGuid(), out string apiKey); + + // Start dotnet-monitor + await toolRunner.StartAsync(); + + // Create HttpClient with default request headers + using HttpClient httpClient = await toolRunner.CreateHttpClientDefaultAddressAsync(_httpClientFactory); + httpClient.DefaultRequestHeaders.Authorization = + new AuthenticationHeaderValue(AuthConstants.ApiKeySchema, apiKey); + ApiClient apiClient = new(_outputHelper, httpClient); + + var statusCodeException = await Assert.ThrowsAsync( + () => apiClient.GetProcessesAsync()); + Assert.Equal(HttpStatusCode.Unauthorized, statusCodeException.StatusCode); + } + + [Fact] + public async Task RejectsBadAudience() + { + await using MonitorCollectRunner toolRunner = new(_outputHelper); + toolRunner.DisableMetricsViaCommandLine = true; + + _outputHelper.WriteLine("Generating API key."); + + Guid subject = Guid.NewGuid(); + string subjectStr = subject.ToString("D"); + const string BadApiKeyJwtAudience = "SomeOtherAudience"; + JwtPayload newPayload = GetJwtPayload(BadApiKeyJwtAudience, subjectStr, AuthConstants.ApiKeyJwtInternalIssuer); + + toolRunner.ConfigurationFromEnvironment.UseApiKey(SecurityAlgorithms.EcdsaSha384, subjectStr, newPayload, out string apiKey); + + // Start dotnet-monitor + await toolRunner.StartAsync(); + + // Create HttpClient with default request headers + using HttpClient httpClient = await toolRunner.CreateHttpClientDefaultAddressAsync(_httpClientFactory); + httpClient.DefaultRequestHeaders.Authorization = + new AuthenticationHeaderValue(AuthConstants.ApiKeySchema, apiKey); + ApiClient apiClient = new(_outputHelper, httpClient); + + var statusCodeException = await Assert.ThrowsAsync( + () => apiClient.GetProcessesAsync()); + Assert.Equal(HttpStatusCode.Unauthorized, statusCodeException.StatusCode); + } + + [Fact] + public async Task RejectsMissingAudience() + { + await using MonitorCollectRunner toolRunner = new(_outputHelper); + toolRunner.DisableMetricsViaCommandLine = true; + + _outputHelper.WriteLine("Generating API key."); + + Guid subject = Guid.NewGuid(); + string subjectStr = subject.ToString("D"); + JwtPayload newPayload = GetJwtPayload(null, subjectStr, AuthConstants.ApiKeyJwtInternalIssuer); + + toolRunner.ConfigurationFromEnvironment.UseApiKey(SecurityAlgorithms.EcdsaSha384, subjectStr, newPayload, out string apiKey); + + // Start dotnet-monitor + await toolRunner.StartAsync(); + + // Create HttpClient with default request headers + using HttpClient httpClient = await toolRunner.CreateHttpClientDefaultAddressAsync(_httpClientFactory); + httpClient.DefaultRequestHeaders.Authorization = + new AuthenticationHeaderValue(AuthConstants.ApiKeySchema, apiKey); + ApiClient apiClient = new(_outputHelper, httpClient); + + var statusCodeException = await Assert.ThrowsAsync( + () => apiClient.GetProcessesAsync()); + Assert.Equal(HttpStatusCode.Unauthorized, statusCodeException.StatusCode); + } + + [Fact] + public async Task AllowDifferentIssuer() + { + await using MonitorCollectRunner toolRunner = new(_outputHelper); + toolRunner.DisableMetricsViaCommandLine = true; + + _outputHelper.WriteLine("Generating API key."); + + Guid subject = Guid.NewGuid(); + string subjectStr = subject.ToString("D"); + const string ApiKeyJwtIssuer = "MyOtherServiceMintingTokens"; + JwtPayload newPayload = GetJwtPayload(AuthConstants.ApiKeyJwtAudience, subjectStr, ApiKeyJwtIssuer); + + toolRunner.ConfigurationFromEnvironment.UseApiKey(SecurityAlgorithms.EcdsaSha384, subjectStr, newPayload, out string apiKey); + + // Start dotnet-monitor + await toolRunner.StartAsync(); + + // Create HttpClient with default request headers + using HttpClient httpClient = await toolRunner.CreateHttpClientDefaultAddressAsync(_httpClientFactory); + httpClient.DefaultRequestHeaders.Authorization = + new AuthenticationHeaderValue(AuthConstants.ApiKeySchema, apiKey); + ApiClient apiClient = new(_outputHelper, httpClient); + + var processes = await apiClient.GetProcessesAsync(); + Assert.NotNull(processes); + } + + /// + /// This tests that invalid subject vs configured subject gets rejected. Any string is valid + /// but it must match between the 'sub' field in the jwt and the Subject configuration parameter. + /// + [Theory] + // Guids that don't match should get rejected + [InlineData("980d2b17-71e1-4313-a084-c077e962680c", "10253b7a-454d-41bb-a3f5-5f2e6b26ed93", HttpStatusCode.Forbidden)] + // Empty string isn't valid even when signed and configured correctly + [InlineData("", "", HttpStatusCode.Unauthorized)] + [InlineData("10253b7a-454d-41bb-a3f5-5f2e6b26ed93", "", HttpStatusCode.Unauthorized)] + [InlineData("", "10253b7a-454d-41bb-a3f5-5f2e6b26ed93", HttpStatusCode.Forbidden)] + public async Task RejectsBadSubject(string jwtSubject, string configSubject, HttpStatusCode expectedError) + { + await using MonitorCollectRunner toolRunner = new(_outputHelper); + toolRunner.DisableMetricsViaCommandLine = true; + + _outputHelper.WriteLine("Generating API key."); + + JwtPayload newPayload = GetJwtPayload(AuthConstants.ApiKeyJwtAudience, jwtSubject, AuthConstants.ApiKeyJwtInternalIssuer); + + toolRunner.ConfigurationFromEnvironment.UseApiKey(SecurityAlgorithms.EcdsaSha384, configSubject, newPayload, out string apiKey); + + // Start dotnet-monitor + await toolRunner.StartAsync(); + + // Create HttpClient with default request headers + using HttpClient httpClient = await toolRunner.CreateHttpClientDefaultAddressAsync(_httpClientFactory); + httpClient.DefaultRequestHeaders.Authorization = + new AuthenticationHeaderValue(AuthConstants.ApiKeySchema, apiKey); + ApiClient apiClient = new(_outputHelper, httpClient); + + var statusCodeException = await Assert.ThrowsAsync( + () => apiClient.GetProcessesAsync()); + Assert.Equal(expectedError, statusCodeException.StatusCode); + } + + /// + /// Tests that we get a warning message when a user provides a private key in the public key configuration. + /// + [Theory] + [InlineData(SecurityAlgorithms.EcdsaSha384)] + [InlineData(SecurityAlgorithms.RsaSha384)] + public async Task WarnOnPrivateKey(string signingAlgo) + { + MonitorCollectRunner toolRunner = new(_outputHelper); + toolRunner.WarnPrivateKey += ToolRunner_WarnPrivateKey; + await using (toolRunner) { - // We expect the authentication handler to reject our request - var statusCodeException = await Assert.ThrowsAsync( - () => apiClient.GetProcessesAsync()); - Assert.Equal(expectedCode, statusCodeException.StatusCode); + toolRunner.DisableMetricsViaCommandLine = true; + + _outputHelper.WriteLine("Generating API key."); + + Guid subject = Guid.NewGuid(); + string subjectStr = subject.ToString("D"); + JwtPayload newPayload = GetJwtPayload(AuthConstants.ApiKeyJwtAudience, subjectStr, AuthConstants.ApiKeyJwtInternalIssuer); + RootOptions opts = new(); + opts.UseApiKey(signingAlgo, subjectStr, newPayload, out string apiKey, out SecurityKey creds); + + JsonWebKey exportableJwk = null; + if (signingAlgo.StartsWith("RS")) + { + exportableJwk = JsonWebKeyConverter.ConvertFromRSASecurityKey(creds as RsaSecurityKey); + } + else if (signingAlgo.StartsWith("ES")) + { + exportableJwk = JsonWebKeyConverter.ConvertFromECDsaSecurityKey(creds as ECDsaSecurityKey); + } + else + { + Assert.True(false, "Unknown algorithm"); + } + + JsonSerializerOptions serializerOptions = JsonSerializerOptionsFactory.Create(JsonSerializerOptionsFactory.JsonIgnoreCondition.WhenWritingNull); + serializerOptions.IgnoreReadOnlyProperties = true; + string privateKeyJson = JsonSerializer.Serialize(exportableJwk, serializerOptions); + string privateKeyEncoded = Base64UrlEncoder.Encode(privateKeyJson); + + AuthenticationOptions authOpts = new AuthenticationOptions() + { + MonitorApiKey = new MonitorApiKeyOptions() + { + Subject = opts.Authentication.MonitorApiKey.Subject, + PublicKey = privateKeyEncoded, + }, + }; + toolRunner.ConfigurationFromEnvironment.Authentication = authOpts; + + // Start dotnet-monitor + await toolRunner.StartAsync(); + + // Create HttpClient with default request headers + using HttpClient httpClient = await toolRunner.CreateHttpClientDefaultAddressAsync(_httpClientFactory); + httpClient.DefaultRequestHeaders.Authorization = + new AuthenticationHeaderValue(AuthConstants.ApiKeySchema, apiKey); + ApiClient apiClient = new(_outputHelper, httpClient); + + await apiClient.GetProcessesAsync(); } + toolRunner.WarnPrivateKey -= ToolRunner_WarnPrivateKey; + + Assert.Single(_warnPrivateKeyLog); } + /// /// Tests that --temp-apikey flag can be used to generate a key. /// [Fact] public async Task TempApiKeyTest() { - await using MonitorRunner toolRunner = new(_outputHelper); + await using MonitorCollectRunner toolRunner = new(_outputHelper); + toolRunner.DisableMetricsViaCommandLine = true; toolRunner.UseTempApiKey = true; await toolRunner.StartAsync(); @@ -276,17 +544,12 @@ public async Task TempApiKeyTest() [Fact] public async Task TempApiKeyOverridesApiAuthenticationTest() { - const string AlgorithmName = "SHA256"; - - await using MonitorRunner toolRunner = new(_outputHelper); + await using MonitorCollectRunner toolRunner = new(_outputHelper); toolRunner.UseTempApiKey = true; - // Generate initial API key - byte[] apiKey = GenerateApiKey(); - // Set API key via key-per-file RootOptions options = new(); - options.UseApiKey(AlgorithmName, apiKey); + options.UseApiKey("ES256", Guid.NewGuid(), out string apiKey); toolRunner.WriteKeyPerValueConfiguration(options); await toolRunner.StartAsync(); @@ -300,7 +563,7 @@ public async Task TempApiKeyOverridesApiAuthenticationTest() // Test that setting the Authorization header for the supplied config will result in a 401 httpClient.DefaultRequestHeaders.Authorization = - new AuthenticationHeaderValue(ApiKeyScheme, Convert.ToBase64String(apiKey)); + new AuthenticationHeaderValue(AuthConstants.ApiKeySchema, apiKey); var statusCodeException = await Assert.ThrowsAsync( () => apiClient.GetProcessesAsync()); Assert.Equal(HttpStatusCode.Unauthorized, statusCodeException.StatusCode); @@ -312,7 +575,7 @@ public async Task TempApiKeyOverridesApiAuthenticationTest() [ConditionalFact(typeof(TestConditions), nameof(TestConditions.IsWindows))] public async Task NegotiateAuthenticationSchemeTest() { - await using MonitorRunner toolRunner = new(_outputHelper); + await using MonitorCollectRunner toolRunner = new(_outputHelper); await toolRunner.StartAsync(); // Create HttpClient and HttpClientHandler that uses the current @@ -340,11 +603,34 @@ public async Task NegotiateAuthenticationSchemeTest() } } - private static byte[] GenerateApiKey(int keyLength = 32) + private void ToolRunner_WarnPrivateKey(string fieldName) { - byte[] apiKey = new byte[keyLength]; // 256 bits - RandomNumberGenerator.Fill(apiKey); - return apiKey; + _warnPrivateKeyLog.Add((fieldName, DateTime.Now)); + } + + private static JwtPayload GetJwtPayload(string audience, string subject, string issuer) + { + List claims = new(); + + if (audience != null) + { + Claim audClaim = new Claim(AuthConstants.ClaimAudienceStr, audience); + claims.Add(audClaim); + } + if (subject != null) + { + Claim audClaim = new Claim(AuthConstants.ClaimSubjectStr, subject); + claims.Add(audClaim); + } + if (issuer != null) + { + Claim audClaim = new Claim(AuthConstants.ClaimIssuerStr, issuer); + claims.Add(audClaim); + } + + JwtPayload newPayload = new JwtPayload(claims); + + return newPayload; } } } diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/GenerateKeyTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/GenerateKeyTests.cs new file mode 100644 index 00000000000..fb8caaac1c2 --- /dev/null +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/GenerateKeyTests.cs @@ -0,0 +1,98 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Diagnostics.Monitoring.TestCommon; +using Microsoft.Diagnostics.Monitoring.TestCommon.Options; +using Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests.Fixtures; +using Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests.Runners; +using Microsoft.Diagnostics.Monitoring.WebApi; +using Microsoft.IdentityModel.Tokens; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Threading; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests +{ + [Collection(DefaultCollectionFixture.Name)] + public class GenerateKeyTests + { + private readonly ITestOutputHelper _outputHelper; + + public GenerateKeyTests(ITestOutputHelper outputHelper) + { + _outputHelper = outputHelper; + } + + [Theory] + [InlineData(null)] + [InlineData(OutputFormat.Json)] + [InlineData(OutputFormat.Text)] + [InlineData(OutputFormat.Cmd)] + [InlineData(OutputFormat.PowerShell)] + [InlineData(OutputFormat.Shell)] + public async Task GenerateKey(OutputFormat? format) + { + using CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(TestTimeouts.OperationTimeout); + CancellationToken cancellationToken = cancellationTokenSource.Token; + + await using MonitorGenerateKeyRunner toolRunner = new(_outputHelper); + toolRunner.Format = format; + await toolRunner.StartAsync(cancellationToken); + await toolRunner.WaitForExitAsync(cancellationToken); + + string tokenStr = await toolRunner.GetBearerToken(cancellationToken); + Assert.NotNull(tokenStr); + + string formatStr = await toolRunner.GetFormat(cancellationToken); + Assert.NotNull(formatStr); + Assert.Equal(toolRunner.FormatUsed.ToString(), formatStr); + + string subject = await toolRunner.GetSubject(cancellationToken); + Assert.NotNull(subject); + + string publicKey = await toolRunner.GetPublicKey(cancellationToken); + Assert.NotNull(publicKey); + string pubKeyJson = Base64UrlEncoder.Decode(publicKey); + Assert.NotNull(pubKeyJson); + JsonWebKey validatingKey = JsonWebKey.Create(pubKeyJson); + Assert.NotNull(validatingKey); + + JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler(); + Assert.True(tokenHandler.CanReadToken(tokenStr)); + + TokenValidationParameters tokenValidationParams = new TokenValidationParameters() + { + // Signing Settings + RequireSignedTokens = true, + ValidAlgorithms = JwtAlgorithmChecker.GetAllowedJwsAlgorithmList(), + + // Issuer Settings + ValidateIssuer = true, // This setting differs from actual token validation in the product because we want to make sure we set our Issuer + ValidIssuer = AuthConstants.ApiKeyJwtInternalIssuer, + ValidateIssuerSigningKey = true, + IssuerSigningKeys = new SecurityKey[] { validatingKey }, + TryAllIssuerSigningKeys = true, + + // Audience Settings + ValidateAudience = true, + ValidAudiences = new string[] { AuthConstants.ApiKeyJwtAudience }, + + // Other Settings + ValidateActor = false, + ValidateLifetime = false, + }; + ClaimsPrincipal claimsPrinciple = tokenHandler.ValidateToken(tokenStr, tokenValidationParams, out SecurityToken validatedToken); + + ITestOutputHelper validationHelper = new PrefixedOutputHelper(_outputHelper, "[JWT Validation] "); + foreach (Claim c in claimsPrinciple.Claims) + { + validationHelper.WriteLine($"Token Claim: {c.Issuer}:{c.Type}=[{c.ValueType}]{c.Value}"); + } + Assert.True(claimsPrinciple.HasClaim(ClaimTypes.NameIdentifier, subject)); + } + } +} diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/JsonSerializerOptionsFactory.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/JsonSerializerOptionsFactory.cs new file mode 100644 index 00000000000..1e5d8b89b91 --- /dev/null +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/JsonSerializerOptionsFactory.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Text.Json; + +namespace Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests +{ + internal static class JsonSerializerOptionsFactory + { + public static JsonSerializerOptions Create(JsonIgnoreCondition ignoreCondition) + { + JsonSerializerOptions serializerOptions = new() + { +#if NET5_0_OR_GREATER + DefaultIgnoreCondition = (System.Text.Json.Serialization.JsonIgnoreCondition)ignoreCondition, +#else + IgnoreNullValues = ignoreCondition == JsonIgnoreCondition.WhenWritingNull, +#endif + }; + return serializerOptions; + } + + + /// + /// Controls how the System.Text.Json.Serialization.JsonIgnoreAttribute ignores properties + /// on serialization and deserialization. + /// + /// This is a copy of System.Text.Json.Serialization.JsonIgnoreCondition for use older versions of .net. + public enum JsonIgnoreCondition + { + // + // Summary: + // Property will always be serialized and deserialized, regardless of System.Text.Json.JsonSerializerOptions.IgnoreNullValues + // configuration. + Never = 0, + // + // Summary: + // Property will always be ignored. + Always = 1, + // + // Summary: + // Property will only be ignored if it is null. + WhenWritingDefault = 2, + // + // Summary: + // If the value is null, the property is ignored during serialization. This is applied + // only to reference-type properties and fields. + WhenWritingNull = 3 + } + } +} diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/MetricsTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/MetricsTests.cs index 7401cbcf364..53baa88b313 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/MetricsTests.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/MetricsTests.cs @@ -38,7 +38,7 @@ public MetricsTests(ITestOutputHelper outputHelper, ServiceProviderFixture servi [Fact] public async Task DisableMetricsViaCommandLineTest() { - await using MonitorRunner toolRunner = new(_outputHelper); + await using MonitorCollectRunner toolRunner = new(_outputHelper); toolRunner.DisableMetricsViaCommandLine = true; await toolRunner.StartAsync(); @@ -58,7 +58,7 @@ public async Task DisableMetricsViaCommandLineTest() [Fact] public async Task DisableMetricsViaEnvironmentTest() { - await using MonitorRunner toolRunner = new(_outputHelper); + await using MonitorCollectRunner toolRunner = new(_outputHelper); toolRunner.ConfigurationFromEnvironment.Metrics = new() { Enabled = false @@ -81,7 +81,7 @@ public async Task DisableMetricsViaEnvironmentTest() [Fact] public async Task DisableMetricsViaSettingsTest() { - await using MonitorRunner toolRunner = new(_outputHelper); + await using MonitorCollectRunner toolRunner = new(_outputHelper); await toolRunner.WriteUserSettingsAsync(new RootOptions() { @@ -109,7 +109,7 @@ await toolRunner.WriteUserSettingsAsync(new RootOptions() [Fact] public async Task DisableMetricsViaKeyPerFileTest() { - await using MonitorRunner toolRunner = new(_outputHelper); + await using MonitorCollectRunner toolRunner = new(_outputHelper); toolRunner.WriteKeyPerValueConfiguration(new RootOptions() { diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests.csproj b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests.csproj index afc7cb3c5ec..fa7e5a57714 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests.csproj +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests.csproj @@ -9,8 +9,18 @@ + + + + + + + + + + diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Options/OptionsExtensions.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Options/OptionsExtensions.cs index 4bd9d6ed406..7c9aa7cb512 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Options/OptionsExtensions.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Options/OptionsExtensions.cs @@ -5,63 +5,19 @@ using Microsoft.Diagnostics.Tools.Monitor; using Microsoft.Diagnostics.Tools.Monitor.Egress.FileSystem; using System; -using System.Collections; using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Reflection; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; using System.Security.Cryptography; -using System.Text; +using System.Text.Json; +using Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests; +using Microsoft.IdentityModel.Tokens; +using Microsoft.Diagnostics.Monitoring.WebApi; namespace Microsoft.Diagnostics.Monitoring.TestCommon.Options { internal static class OptionsExtensions { - /// - /// Generates an environment variable map of the options. - /// - /// - /// Each key is the variable name; each value is the variable value. - /// - public static IDictionary ToEnvironmentConfiguration(this RootOptions options) - { - Dictionary variables = new(StringComparer.OrdinalIgnoreCase); - MapObject(options, "DotNetMonitor_", variables); - return variables; - } - - /// - /// Generates a key-per-file map of the options. - /// - /// - /// Each key is the file name; each value is the file content. - /// - public static IDictionary ToKeyPerFileConfiguration(this RootOptions options) - { - Dictionary variables = new(StringComparer.OrdinalIgnoreCase); - MapObject(options, string.Empty, variables); - return variables; - } - - /// - /// Sets API Key authentication. - /// - public static RootOptions UseApiKey(this RootOptions options, string algorithmName, byte[] apiKey) - { - if (null == options.ApiAuthentication) - { - options.ApiAuthentication = new ApiAuthenticationOptions(); - } - - using var hashAlgorithm = HashAlgorithm.Create(algorithmName); - - byte[] hash = hashAlgorithm.ComputeHash(apiKey); - options.ApiAuthentication.ApiKeyHash = ToHexString(hash); - options.ApiAuthentication.ApiKeyHashType = algorithmName; - - return options; - } - public static RootOptions AddFileSystemEgress(this RootOptions options, string name, string outputPath) { var egressProvider = new FileSystemEgressProviderOptions() @@ -80,89 +36,152 @@ public static RootOptions AddFileSystemEgress(this RootOptions options, string n return options; } - private static void MapDictionary(IDictionary dictionary, string prefix, IDictionary map) + /// + /// Sets API Key authentication. Use this overload for most operations, unless specifically testing Authentication or Authorization. + /// + public static RootOptions UseApiKey(this RootOptions options, string algorithmName, Guid subject, out string token) { - foreach (var key in dictionary.Keys) - { - object value = dictionary[key]; - if (null != value) - { - string keyString = Convert.ToString(key, CultureInfo.InvariantCulture); - MapValue( - value, - FormattableString.Invariant($"{prefix}{keyString}"), - map); - } - } + string subjectStr = subject.ToString("D"); + Claim audClaim = new Claim(AuthConstants.ClaimAudienceStr, AuthConstants.ApiKeyJwtAudience); + Claim issClaim = new Claim(AuthConstants.ClaimIssuerStr, AuthConstants.ApiKeyJwtInternalIssuer); + Claim subClaim = new Claim(AuthConstants.ClaimSubjectStr, subjectStr); + JwtPayload newPayload = new JwtPayload(new Claim[] { audClaim, issClaim, subClaim }); + + return options.UseApiKey(algorithmName, subjectStr, newPayload, out token); } - private static void MapList(IList list, string prefix, IDictionary map) + public static RootOptions UseApiKey(this RootOptions options, string algorithmName, string subject, JwtPayload customPayload, out string token) { - for (int index = 0; index < list.Count; index++) + return options.UseApiKey(algorithmName, subject, customPayload, out token, out SecurityKey _); + } + + public static RootOptions UseApiKey(this RootOptions options, string algorithmName, string subject, JwtPayload customPayload, out string token, out SecurityKey privateKey) + { + if (null == options.Authentication) { - object value = list[index]; - if (null != value) - { - MapValue( - value, - FormattableString.Invariant($"{prefix}{index}"), - map); - } + options.Authentication = new AuthenticationOptions(); + } + + if (null == options.Authentication.MonitorApiKey) + { + options.Authentication.MonitorApiKey = new MonitorApiKeyOptions(); + } + + SigningCredentials signingCreds; + JsonWebKey exportableJwk; + switch (algorithmName) + { + case SecurityAlgorithms.EcdsaSha256: + case SecurityAlgorithms.EcdsaSha256Signature: + case SecurityAlgorithms.EcdsaSha384: + case SecurityAlgorithms.EcdsaSha384Signature: + case SecurityAlgorithms.EcdsaSha512: + case SecurityAlgorithms.EcdsaSha512Signature: + ECDsa ecDsa = ECDsa.Create(GetEcCurveFromName(algorithmName)); + ECDsaSecurityKey ecSecKey = new ECDsaSecurityKey(ecDsa); + signingCreds = new SigningCredentials(ecSecKey, algorithmName); + ECDsa pubEcDsa = ECDsa.Create(ecDsa.ExportParameters(false)); + ECDsaSecurityKey pubEcSecKey = new ECDsaSecurityKey(pubEcDsa); + exportableJwk = JsonWebKeyConverter.ConvertFromECDsaSecurityKey(pubEcSecKey); + privateKey = ecSecKey; + break; + + case SecurityAlgorithms.RsaSha256: + case SecurityAlgorithms.RsaSha256Signature: + case SecurityAlgorithms.RsaSha384: + case SecurityAlgorithms.RsaSha384Signature: + case SecurityAlgorithms.RsaSha512: + case SecurityAlgorithms.RsaSha512Signature: + RSA rsa = RSA.Create(GetRsaKeyLengthFromName(algorithmName)); + RsaSecurityKey rsaSecKey = new RsaSecurityKey(rsa); + signingCreds = new SigningCredentials(rsaSecKey, algorithmName); + RSA pubRsa = RSA.Create(rsa.ExportParameters(false)); + RsaSecurityKey pubRsaSecKey = new RsaSecurityKey(pubRsa); + exportableJwk = JsonWebKeyConverter.ConvertFromRSASecurityKey(pubRsaSecKey); + privateKey = rsaSecKey; + break; + + case SecurityAlgorithms.HmacSha256: + case SecurityAlgorithms.HmacSha384: + case SecurityAlgorithms.HmacSha512: + HMAC hmac = HMAC.Create(GetHmacAlgorithmFromName(algorithmName)); + SymmetricSecurityKey hmacSecKey = new SymmetricSecurityKey(hmac.Key); + signingCreds = new SigningCredentials(hmacSecKey, algorithmName); + exportableJwk = JsonWebKeyConverter.ConvertFromSymmetricSecurityKey(hmacSecKey); + privateKey = hmacSecKey; + break; + + default: + throw new ArgumentException($"Algorithm name '{algorithmName}' not supported", nameof(algorithmName)); } + + JwtHeader newHeader = new JwtHeader(signingCreds, null, JwtConstants.HeaderType); + JwtSecurityToken newToken = new JwtSecurityToken(newHeader, customPayload); + JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler(); + string resultToken = tokenHandler.WriteToken(newToken); + + JsonSerializerOptions serializerOptions = JsonSerializerOptionsFactory.Create(JsonSerializerOptionsFactory.JsonIgnoreCondition.WhenWritingNull); + string publicKeyJson = JsonSerializer.Serialize(exportableJwk, serializerOptions); + + string publicKeyEncoded = Base64UrlEncoder.Encode(publicKeyJson); + + options.Authentication.MonitorApiKey.Subject = subject; + options.Authentication.MonitorApiKey.PublicKey = publicKeyEncoded; + + token = resultToken; + + return options; } - private static void MapObject(object obj, string prefix, IDictionary map) + private static string GetHmacAlgorithmFromName(string algorithmName) { - foreach (PropertyInfo property in obj.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)) + switch (algorithmName) { - if (!property.GetIndexParameters().Any()) - { - MapValue( - property.GetValue(obj), - FormattableString.Invariant($"{prefix}{property.Name}"), - map); - } + case SecurityAlgorithms.HmacSha256: + return typeof(HMACSHA256).FullName; + case SecurityAlgorithms.HmacSha384: + return typeof(HMACSHA384).FullName; + case SecurityAlgorithms.HmacSha512: + return typeof(HMACSHA512).FullName; + default: + throw new ArgumentException($"Algorithm name '{algorithmName}' not supported", nameof(algorithmName)); } } - private static void MapValue(object value, string valueName, IDictionary map) + private static int GetRsaKeyLengthFromName(string algorithmName) { - if (null != value) + switch (algorithmName) { - Type valueType = value.GetType(); - if (valueType.IsPrimitive || typeof(string) == valueType) - { - map.Add( - valueName, - Convert.ToString(value, CultureInfo.InvariantCulture)); - } - else - { - string prefix = FormattableString.Invariant($"{valueName}__"); - if (value is IDictionary dictionary) - { - MapDictionary(dictionary, prefix, map); - } - else if (value is IList list) - { - MapList(list, prefix, map); - } - else - { - MapObject(value, prefix, map); - } - } + case SecurityAlgorithms.RsaSha256: + case SecurityAlgorithms.RsaSha256Signature: + return 2048; + case SecurityAlgorithms.RsaSha384: + case SecurityAlgorithms.RsaSha384Signature: + return 3072; + case SecurityAlgorithms.RsaSha512: + case SecurityAlgorithms.RsaSha512Signature: + return 4096; + default: + throw new ArgumentException($"Algorithm name '{algorithmName}' not supported", nameof(algorithmName)); } } - private static string ToHexString(byte[] data) + private static ECCurve GetEcCurveFromName(string algorithmName) { - StringBuilder builder = new(2 * data.Length); - foreach (byte b in data) + switch (algorithmName) { - builder.Append(b.ToString("X2", CultureInfo.InvariantCulture)); + case SecurityAlgorithms.EcdsaSha256: + case SecurityAlgorithms.EcdsaSha256Signature: + return ECCurve.NamedCurves.nistP256; + case SecurityAlgorithms.EcdsaSha384: + case SecurityAlgorithms.EcdsaSha384Signature: + return ECCurve.NamedCurves.nistP384; + case SecurityAlgorithms.EcdsaSha512: + case SecurityAlgorithms.EcdsaSha512Signature: + return ECCurve.NamedCurves.nistP521; + default: + throw new ArgumentException($"Algorithm name '{algorithmName}' not supported", nameof(algorithmName)); } - return builder.ToString(); } } } diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/ProcessTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/ProcessTests.cs index a8112d72982..5e6f1d76c25 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/ProcessTests.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/ProcessTests.cs @@ -100,7 +100,7 @@ public async Task MultiProcessIdentificationTest(DiagnosticPortConnectionMode mo out DiagnosticPortConnectionMode appConnectionMode, out string diagnosticPortPath); - await using MonitorRunner toolRunner = new(_outputHelper); + await using MonitorCollectRunner toolRunner = new(_outputHelper); toolRunner.ConnectionMode = mode; toolRunner.DiagnosticPortPath = diagnosticPortPath; toolRunner.DisableAuthentication = true; diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Runners/MonitorCollectRunner.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Runners/MonitorCollectRunner.cs new file mode 100644 index 00000000000..bb72fe587d8 --- /dev/null +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Runners/MonitorCollectRunner.cs @@ -0,0 +1,262 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Diagnostics.Monitoring.TestCommon; +using Microsoft.Diagnostics.Monitoring.TestCommon.Options; +using Microsoft.Diagnostics.Monitoring.TestCommon.Runners; +using Microsoft.Diagnostics.Monitoring.WebApi; +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests.Runners +{ + /// + /// Runner for the dotnet-monitor tool. + /// + internal sealed class MonitorCollectRunner : MonitorRunner + { + // Completion source containing the bound address of the default URL (e.g. provided by --urls argument) + private readonly TaskCompletionSource _defaultAddressSource = + new(TaskCreationOptions.RunContinuationsAsynchronously); + + // Completion source containing the bound address of the metrics URL (e.g. provided by --metricUrls argument) + private readonly TaskCompletionSource _metricsAddressSource = + new(TaskCreationOptions.RunContinuationsAsynchronously); + + // Completion source containing the string representing the base64 encoded MonitorApiKey for accessing the monitor (e.g. provided by --temp-apikey argument) + private readonly TaskCompletionSource _monitorApiKeySource = + new(TaskCreationOptions.RunContinuationsAsynchronously); + + // Completion source containing a string which is fired when the monitor enters a ready idle state + private readonly TaskCompletionSource _readySource = + new(TaskCreationOptions.RunContinuationsAsynchronously); + + private bool _isDiposed; + + /// + /// Event callback for when a Private Key warning message is seen. + /// + public event Action WarnPrivateKey; + + /// + /// The mode of the diagnostic port connection. Default is + /// (the tool is searching for apps that are in listen mode). + /// + /// + /// Set to if tool needs to establish the diagnostic port listener. + /// + public DiagnosticPortConnectionMode ConnectionMode { get; set; } = DiagnosticPortConnectionMode.Connect; + + /// + /// Path of the diagnostic port to establish when is . + /// + public string DiagnosticPortPath { get; set; } + + /// + /// Determines whether authentication is disabled when starting dotnet-monitor. + /// + public bool DisableAuthentication { get; set; } + + /// + /// Determines whether HTTP egress is disabled when starting dotnet-monitor. + /// + public bool DisableHttpEgress { get; set; } + + /// + /// Determines whether a temporary api key should be generated while starting dotnet-monitor. + /// + public bool UseTempApiKey { get; set; } + + /// + /// Determines whether metrics are disabled via the command line when starting dotnet-monitor. + /// + public bool DisableMetricsViaCommandLine { get; set; } + + + public MonitorCollectRunner(ITestOutputHelper outputHelper) + : base(outputHelper) + { + } + + public override async ValueTask DisposeAsync() + { + lock (_lock) + { + if (_isDiposed) + { + return; + } + _isDiposed = true; + } + + CancelCompletionSources(CancellationToken.None); + + await base.DisposeAsync(); + } + + public async Task StartAsync(CancellationToken token) + { + List argsList = new(); + + const string command = "collect"; + + argsList.Add("--urls"); + argsList.Add("http://127.0.0.1:0"); + + if (DisableMetricsViaCommandLine) + { + argsList.Add("--metrics:false"); + } + else + { + argsList.Add("--metricUrls"); + argsList.Add("http://127.0.0.1:0"); + } + + if (ConnectionMode == DiagnosticPortConnectionMode.Listen) + { + argsList.Add("--diagnostic-port"); + if (string.IsNullOrEmpty(DiagnosticPortPath)) + { + throw new ArgumentNullException(nameof(DiagnosticPortPath)); + } + argsList.Add(DiagnosticPortPath); + } + + if (DisableAuthentication) + { + argsList.Add("--no-auth"); + } + + if (DisableHttpEgress) + { + argsList.Add("--no-http-egress"); + } + + if (UseTempApiKey) + { + argsList.Add("--temp-apikey"); + } + + using IDisposable _ = token.Register(() => CancelCompletionSources(token)); + + await base.StartAsync(command, argsList.ToArray(), token); + + Task runnerExitTask = RunnerExitedTask; + Task endingTask = await Task.WhenAny(_readySource.Task, runnerExitTask); + // Await ready and exited tasks in case process exits before it is ready. + if (runnerExitTask == endingTask) + { + throw new InvalidOperationException("Process exited before it was ready."); + } + + await _readySource.Task; + } + + protected override void StandardOutputCallback(string line) + { + ConsoleLogEvent logEvent = JsonSerializer.Deserialize(line); + + switch (logEvent.Category) + { + case "Microsoft.Hosting.Lifetime": + HandleLifetimeEvent(logEvent); + break; + case "Microsoft.Diagnostics.Tools.Monitor.Startup": + HandleStartupEvent(logEvent); + break; + default: + HandleGenericLogEvent(logEvent); + break; + } + } + + private void CancelCompletionSources(CancellationToken token) + { + _defaultAddressSource.TrySetCanceled(token); + _metricsAddressSource.TrySetCanceled(token); + _readySource.TrySetCanceled(token); + _monitorApiKeySource.TrySetCanceled(token); + } + + public Task GetDefaultAddressAsync(CancellationToken token) + { + return _defaultAddressSource.GetAsync(token); + } + + public Task GetMetricsAddressAsync(CancellationToken token) + { + return _metricsAddressSource.GetAsync(token); + } + + public Task GetMonitorApiKey(CancellationToken token) + { + return _monitorApiKeySource.GetAsync(token); + } + + private void HandleLifetimeEvent(ConsoleLogEvent logEvent) + { + // Lifetime events do not have unique EventIds, thus use the format + // string to differentiate the individual events. + if (logEvent.State.TryGetValue("{OriginalFormat}", out string format)) + { + switch (format) + { + case "Application started. Press Ctrl+C to shut down.": + Assert.True(_readySource.TrySetResult(null)); + break; + } + } + } + + private void HandleStartupEvent(ConsoleLogEvent logEvent) + { + switch (logEvent.EventId) + { + case 16: // Bound default address: {address} + if (logEvent.State.TryGetValue("address", out string defaultAddress)) + { + _outputHelper.WriteLine("Default Address: {0}", defaultAddress); + Assert.True(_defaultAddressSource.TrySetResult(defaultAddress)); + } + break; + case 17: // Bound metrics address: {address} + if (logEvent.State.TryGetValue("address", out string metricsAddress)) + { + _outputHelper.WriteLine("Metrics Address: {0}", metricsAddress); + Assert.True(_metricsAddressSource.TrySetResult(metricsAddress)); + } + break; + case 23: + if (logEvent.State.TryGetValue("MonitorApiKey", out string monitorApiKey)) + { + _outputHelper.WriteLine("MonitorApiKey: {0}", monitorApiKey); + Assert.True(_monitorApiKeySource.TrySetResult(monitorApiKey)); + } + break; + } + } + + private void HandleGenericLogEvent(ConsoleLogEvent logEvent) + { + switch (logEvent.EventId) + { + // 26: NotifyPrivateKey + // The configuration field {fieldName} contains private key information. The private key information is not required for dotnet-monitor to verify a token signature and it is strongly recomended to only provide the public key. + case 26: + if (logEvent.State.TryGetValue("fieldName", out string fieldName)) + { + _outputHelper.WriteLine("Private Key data detected in field: {0}", fieldName); + WarnPrivateKey?.Invoke(fieldName); + } + break; + } + } + } +} diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Runners/MonitorGenerateKeyRunner.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Runners/MonitorGenerateKeyRunner.cs new file mode 100644 index 00000000000..e57d6050edc --- /dev/null +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Runners/MonitorGenerateKeyRunner.cs @@ -0,0 +1,233 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Diagnostics.Monitoring.TestCommon; +using Microsoft.Diagnostics.Monitoring.TestCommon.Options; +using Microsoft.Diagnostics.Tools.Monitor; +using System; +using System.Collections.Generic; +using System.Text; +using System.Text.Json; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests.Runners +{ + /// + /// Runner for the dotnet-monitor tool. This runner is for the "generatekey" command. + /// + internal sealed class MonitorGenerateKeyRunner : MonitorRunner + { + // Completion source containing the bearer token emitted by the generatekey command + private readonly TaskCompletionSource _bearerTokenTaskSource = + new(TaskCreationOptions.RunContinuationsAsynchronously); + private readonly Regex _bearerTokenRegex = + new Regex("^Authorization: Bearer (?[a-zA-Z0-9_-]+\\.[a-zA-Z0-9_-]+\\.[a-zA-Z0-9_-]+)$", RegexOptions.Compiled); + + // Completion source containing the format type emitted by the generatekey command + private readonly TaskCompletionSource _formatHeaderSource = + new(TaskCreationOptions.RunContinuationsAsynchronously); + private readonly Regex _formatHeaderRegex = + new Regex("^Settings in (?[a-zA-Z0-9-_]+) format:$", RegexOptions.Compiled); + + // Completion source containing the Subject field emitted by the generatekey command + private readonly TaskCompletionSource _subjectSource = + new(TaskCreationOptions.RunContinuationsAsynchronously); + private readonly Dictionary _subjectRegexMap = + new Dictionary() + { + { OutputFormat.Json, new Regex("\\\"Subject\\\":\\s*\\\"(?[0-9a-zA-Z-_!@#\\$%\\^&\\*\\(\\)\\{\\}\\[\\]|\\,\\.;:/]+)\\\"", RegexOptions.Compiled) }, + { OutputFormat.Text, new Regex("Subject:\\s*(?[0-9a-zA-Z-_!@#\\$%\\^&\\*\\(\\)\\{\\}\\[\\]|\\,\\.;:/]+)\\Z", RegexOptions.Compiled) }, + { OutputFormat.Cmd, new Regex("set\\s*Authentication__MonitorApiKey__Subject=(?[0-9a-zA-Z-_!@#\\$%\\^&\\*\\(\\)\\{\\}\\[\\]|\\,\\.;:/]+)\\Z", RegexOptions.Compiled) }, + { OutputFormat.PowerShell, new Regex("\\$env\\:Authentication__MonitorApiKey__Subject\\s*=\\s*\\\"(?[0-9a-zA-Z-_!@#\\$%\\^&\\*\\(\\)\\{\\}\\[\\]|\\,\\.;:/]+)\\\"", RegexOptions.Compiled) }, + { OutputFormat.Shell, new Regex("export\\s*Authentication__MonitorApiKey__Subject=\\\"(?[0-9a-zA-Z-_!@#\\$%\\^&\\*\\(\\)\\{\\}\\[\\]|\\,\\.;:/]+)\\\"", RegexOptions.Compiled) }, + }; + + // Completion source containing the PublicKey field emitted by the generatekey command + private readonly TaskCompletionSource _publicKeySource = + new(TaskCreationOptions.RunContinuationsAsynchronously); + private readonly Dictionary _publicKeyRegexMap = + new Dictionary() + { + { OutputFormat.Json, new Regex("\\\"PublicKey\\\":\\s*\\\"(?[a-zA-Z0-9_-]{2,}?)\\\"", RegexOptions.Compiled) }, + { OutputFormat.Text, new Regex("Public Key:\\s(?[a-zA-Z0-9_-]{2,}?)\\Z", RegexOptions.Compiled) }, + { OutputFormat.Cmd, new Regex("set\\s*Authentication__MonitorApiKey__PublicKey=(?[a-zA-Z0-9_-]{2,}?)\\Z", RegexOptions.Compiled) }, + { OutputFormat.PowerShell, new Regex("\\$env\\:Authentication__MonitorApiKey__PublicKey\\s*=\\s*\\\"(?[a-zA-Z0-9_-]{2,}?)\\\"", RegexOptions.Compiled) }, + { OutputFormat.Shell, new Regex("export\\s*Authentication__MonitorApiKey__PublicKey=\\\"(?[a-zA-Z0-9_-]{2,}?)\\\"", RegexOptions.Compiled) }, + }; + + // Completion source containing the full output in the specified format (this is everything after the _formatHeaderRegex line) + private readonly TaskCompletionSource _outputSource = + new(TaskCreationOptions.RunContinuationsAsynchronously); + + private StringBuilder _outputBuilder = new(); + + /// + /// Gets the expected default when no --output parameter is specified at the command line. + /// + private const OutputFormat DefaultOutputFormat = OutputFormat.Json; + /// + /// A bool indicating if has been called and the field should not be updated. + /// + private bool _executionStarted = false; + /// + /// A value indicating which format is being used. This should not be updated after is called. + /// + private OutputFormat? _selectedFormat = null; + + /// + /// Gets the that was used to execute dotnet-monitor. If is set to then + /// the default is returned. + /// + /// Will be thrown when has not yet been called. + public OutputFormat FormatUsed + { + get + { + if (!_executionStarted) + { + throw new InvalidOperationException($"Can't get {nameof(FormatUsed)} until {nameof(StartAsync)} is called."); + } + + return _selectedFormat ?? DefaultOutputFormat; + } + } + + /// + /// Gets or sets the to be used while executing dotnet-monitor. + /// This can not be set after is called. + /// + public OutputFormat? Format + { + get + { + return _selectedFormat; + } + set + { + if (_executionStarted) + { + throw new InvalidOperationException($"Can't set {nameof(Format)} after {nameof(StartAsync)} is called."); + } + + _selectedFormat = value; + } + } + + public MonitorGenerateKeyRunner(ITestOutputHelper outputHelper) + : base(outputHelper) + { + } + + public async Task StartAsync(CancellationToken token) + { + _executionStarted = true; + + List argsList = new(); + + const string command = "generatekey"; + + if (null != Format) + { + argsList.Add("--output"); + argsList.Add(Format.ToString()); + } + + await base.StartAsync(command, argsList.ToArray(), token); + } + + public override async Task WaitForExitAsync(CancellationToken token) + { + await base.WaitForExitAsync(token).ConfigureAwait(false); + + Assert.True(_outputSource.TrySetResult(_outputBuilder.ToString())); + + if (FormatUsed == OutputFormat.Json) + { + RootOptions parsedOpts = JsonSerializer.Deserialize(_outputBuilder.ToString()); + + Assert.True(_subjectSource.TrySetResult(parsedOpts?.Authentication?.MonitorApiKey?.Subject)); + Assert.True(_publicKeySource.TrySetResult(parsedOpts?.Authentication?.MonitorApiKey?.PublicKey)); + } + } + + protected override void StandardOutputCallback(string line) + { + if (_formatHeaderSource.Task.IsCompletedSuccessfully) + { + _outputBuilder.AppendLine(line); + } + + Match tokenMatch = _bearerTokenRegex.Match(line); + if (tokenMatch.Success) + { + string tokenValue = tokenMatch.Groups["token"].Value; + _outputHelper.WriteLine($"Found Bearer Token: {tokenValue}"); + Assert.True(_bearerTokenTaskSource.TrySetResult(tokenValue)); + } + + Match formatMatch = _formatHeaderRegex.Match(line); + if (formatMatch.Success) + { + string formatValue = formatMatch.Groups["format"].Value; + _outputHelper.WriteLine($"Output Format: {formatValue}"); + Assert.True(_formatHeaderSource.TrySetResult(formatValue)); + } + + Match subjectMatch = _subjectRegexMap[FormatUsed].Match(line); + if (subjectMatch.Success) + { + string subjectValue = subjectMatch.Groups["subject"].Value; + _outputHelper.WriteLine($"Subject: {subjectValue}"); + + // for Json we will parse the whole blob and set the value that way + if (FormatUsed != OutputFormat.Json) + { + Assert.True(_subjectSource.TrySetResult(subjectValue)); + } + } + + Match publicKeyMatch = _publicKeyRegexMap[FormatUsed].Match(line); + if (publicKeyMatch.Success) + { + string publicKeyValue = publicKeyMatch.Groups["publickey"].Value; + _outputHelper.WriteLine($"Public Key: {publicKeyValue}"); + + // for Json we will parse the whole blob and set the value that way + if (FormatUsed != OutputFormat.Json) + { + Assert.True(_publicKeySource.TrySetResult(publicKeyValue)); + } + } + } + + public Task GetBearerToken(CancellationToken token) + { + return _bearerTokenTaskSource.GetAsync(token); + } + + public Task GetFormat(CancellationToken token) + { + return _formatHeaderSource.GetAsync(token); + } + + public Task GetOutput(CancellationToken token) + { + return _outputSource.GetAsync(token); + } + + public Task GetSubject(CancellationToken token) + { + return _subjectSource.GetAsync(token); + } + + public Task GetPublicKey(CancellationToken token) + { + return _publicKeySource.GetAsync(token); + } + } +} diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Runners/MonitorRunner.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Runners/MonitorRunner.cs index 634e3bd4164..bcb8fe61cc6 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Runners/MonitorRunner.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Runners/MonitorRunner.cs @@ -3,19 +3,15 @@ // See the LICENSE file in the project root for more information. using Microsoft.Diagnostics.Monitoring.TestCommon; -using Microsoft.Diagnostics.Monitoring.TestCommon.Options; using Microsoft.Diagnostics.Monitoring.TestCommon.Runners; -using Microsoft.Diagnostics.Monitoring.WebApi; using Microsoft.Diagnostics.Tools.Monitor; using System; using System.Collections.Generic; using System.IO; using System.Reflection; using System.Text.Json; -using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; -using Xunit; using Xunit.Abstractions; namespace Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests.Runners @@ -23,34 +19,32 @@ namespace Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests.Runners /// /// Runner for the dotnet-monitor tool. /// - internal sealed class MonitorRunner : IAsyncDisposable + internal class MonitorRunner : IAsyncDisposable { - private readonly LoggingRunnerAdapter _adapter; - - // Completion source containing the bound address of the default URL (e.g. provided by --urls argument) - private readonly TaskCompletionSource _defaultAddressSource = - new(TaskCreationOptions.RunContinuationsAsynchronously); - - // Completion source containing the bound address of the metrics URL (e.g. provided by --metricUrls argument) - private readonly TaskCompletionSource _metricsAddressSource = - new(TaskCreationOptions.RunContinuationsAsynchronously); + protected readonly object _lock = new(); - // Completion source containing the string representing the base64 encoded MonitorApiKey for accessing the monitor (e.g. provided by --temp-apikey argument) - private readonly TaskCompletionSource _monitorApiKeySource = - new(TaskCreationOptions.RunContinuationsAsynchronously); - - private readonly ITestOutputHelper _outputHelper; - - private readonly TaskCompletionSource _readySource = - new(TaskCreationOptions.RunContinuationsAsynchronously); + protected readonly ITestOutputHelper _outputHelper; private readonly DotNetRunner _runner = new(); + private readonly LoggingRunnerAdapter _adapter; + private readonly string _runnerTmpPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("D")); private bool _isDisposed; + /// + /// Sets configuration values via environment variables. + /// + public RootOptions ConfigurationFromEnvironment { get; } = new(); + + /// + /// Gets the task for the underlying 's + /// which is used to wait for process exit. + /// + protected Task RunnerExitedTask => _runner.ExitedTask; + /// /// The path of the currently executing assembly. /// @@ -72,45 +66,6 @@ internal sealed class MonitorRunner : IAsyncDisposable .Replace(Assembly.GetExecutingAssembly().GetName().Name, "dotnet-monitor") .Replace(CurrentTargetFrameworkFolderName, "netcoreapp3.1"); - /// - /// Sets configuration values via environment variables. - /// - public RootOptions ConfigurationFromEnvironment { get; } = new(); - - /// - /// The mode of the diagnostic port connection. Default is - /// (the tool is searching for apps that are in listen mode). - /// - /// - /// Set to if tool needs to establish the diagnostic port listener. - /// - public DiagnosticPortConnectionMode ConnectionMode { get; set; } = DiagnosticPortConnectionMode.Connect; - - /// - /// Path of the diagnostic port to establish when is . - /// - public string DiagnosticPortPath { get; set; } - - /// - /// Determines whether authentication is disabled when starting dotnet-monitor. - /// - public bool DisableAuthentication { get; set; } - - /// - /// Determines whether HTTP egress is disabled when starting dotnet-monitor. - /// - public bool DisableHttpEgress { get; set; } - - /// - /// Determines whether a temporary api key should be generated while starting dotnet-monitor. - /// - public bool UseTempApiKey { get; set; } - - /// - /// Determines whether metrics are disabled via the command line when starting dotnet-monitor. - /// - public bool DisableMetricsViaCommandLine { get; set; } - private string SharedConfigDirectoryPath => Path.Combine(_runnerTmpPath, "SharedConfig"); @@ -136,9 +91,9 @@ public MonitorRunner(ITestOutputHelper outputHelper) Directory.CreateDirectory(UserConfigDirectoryPath); } - public async ValueTask DisposeAsync() + public virtual async ValueTask DisposeAsync() { - lock (_adapter) + lock (_lock) { if (_isDisposed) { @@ -148,11 +103,8 @@ public async ValueTask DisposeAsync() } _adapter.ReceivedStandardOutputLine -= StandardOutputCallback; - await _adapter.DisposeAsync().ConfigureAwait(false); - CancelCompletionSources(CancellationToken.None); - _runner.Dispose(); try @@ -165,48 +117,18 @@ public async ValueTask DisposeAsync() } } - public async Task StartAsync(CancellationToken token) + public virtual async Task StartAsync(string command, string[] args, CancellationToken token) { List argsList = new(); - argsList.Add("collect"); - - argsList.Add("--urls"); - argsList.Add("http://127.0.0.1:0"); - - if (DisableMetricsViaCommandLine) - { - argsList.Add("--metrics:false"); - } - else - { - argsList.Add("--metricUrls"); - argsList.Add("http://127.0.0.1:0"); - } - - if (ConnectionMode == DiagnosticPortConnectionMode.Listen) - { - argsList.Add("--diagnostic-port"); - if (string.IsNullOrEmpty(DiagnosticPortPath)) - { - throw new ArgumentNullException(nameof(DiagnosticPortPath)); - } - argsList.Add(DiagnosticPortPath); - } - - if (DisableAuthentication) - { - argsList.Add("--no-auth"); - } - - if (DisableHttpEgress) + if (!string.IsNullOrEmpty(command)) { - argsList.Add("--no-http-egress"); + argsList.Add(command); } - if (UseTempApiKey) + if (args != null) { - argsList.Add("--temp-apikey"); + argsList.AddRange(args); } _runner.EntrypointAssemblyPath = DotNetMonitorPath; @@ -227,7 +149,7 @@ public async Task StartAsync(CancellationToken token) _adapter.Environment.Add("DotnetMonitorTestSettings__UserConfigDirectoryOverride", UserConfigDirectoryPath); // Set configuration via environment variables - var configurationViaEnvironment = ConfigurationFromEnvironment.ToEnvironmentConfiguration(); + var configurationViaEnvironment = ConfigurationFromEnvironment.ToEnvironmentConfiguration(useDotnetMonitorPrefix: true); if (configurationViaEnvironment.Count > 0) { // Set additional environment variables from configuration @@ -240,54 +162,16 @@ public async Task StartAsync(CancellationToken token) _outputHelper.WriteLine("User Settings Path: {0}", UserSettingsFilePath); await _adapter.StartAsync(token); - - using IDisposable _ = token.Register(() => CancelCompletionSources(token)); - - // Await ready and exited tasks in case process exits before it is ready. - if (_runner.ExitedTask == await Task.WhenAny(_readySource.Task, _runner.ExitedTask)) - { - throw new InvalidOperationException("Process exited before it was ready."); - } - - // Await ready task to check if it faulted or cancelled. - await _readySource.Task; } - private void CancelCompletionSources(CancellationToken token) + public virtual async Task WaitForExitAsync(CancellationToken token) { - _defaultAddressSource.TrySetCanceled(token); - _metricsAddressSource.TrySetCanceled(token); - _readySource.TrySetCanceled(token); + await RunnerExitedTask.WithCancellation(token).ConfigureAwait(false); + await _adapter.ReadToEnd(token).ConfigureAwait(false); } - private void StandardOutputCallback(string line) + protected virtual void StandardOutputCallback(string line) { - ConsoleLogEvent logEvent = JsonSerializer.Deserialize(line); - - switch (logEvent.Category) - { - case "Microsoft.Hosting.Lifetime": - HandleLifetimeEvent(logEvent); - break; - case "Microsoft.Diagnostics.Tools.Monitor.Startup": - HandleStartupEvent(logEvent); - break; - } - } - - public Task GetDefaultAddressAsync(CancellationToken token) - { - return _defaultAddressSource.GetAsync(token); - } - - public Task GetMetricsAddressAsync(CancellationToken token) - { - return _metricsAddressSource.GetAsync(token); - } - - public Task GetMonitorApiKey(CancellationToken token) - { - return _monitorApiKeySource.GetAsync(token); } public void WriteKeyPerValueConfiguration(RootOptions options) @@ -306,15 +190,7 @@ public async Task WriteUserSettingsAsync(RootOptions options, CancellationToken { using FileStream stream = new(UserSettingsFilePath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.ReadWrite); - JsonSerializerOptions serializerOptions = new() - { -#if NET6_0_OR_GREATER - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull -#else - IgnoreNullValues = true -#endif - }; - + JsonSerializerOptions serializerOptions = JsonSerializerOptionsFactory.Create(JsonSerializerOptionsFactory.JsonIgnoreCondition.WhenWritingNull); await JsonSerializer.SerializeAsync(stream, options, serializerOptions).ConfigureAwait(false); _outputHelper.WriteLine("Wrote user settings."); @@ -325,48 +201,5 @@ public async Task WriteUserSettingsAsync(RootOptions options, TimeSpan timeout) using CancellationTokenSource cancellation = new(timeout); await WriteUserSettingsAsync(options, cancellation.Token).ConfigureAwait(false); } - - private void HandleLifetimeEvent(ConsoleLogEvent logEvent) - { - // Lifetime events do not have unique EventIds, thus use the format - // string to differentiate the individual events. - if (logEvent.State.TryGetValue("{OriginalFormat}", out string format)) - { - switch (format) - { - case "Application started. Press Ctrl+C to shut down.": - Assert.True(_readySource.TrySetResult(null)); - break; - } - } - } - - private void HandleStartupEvent(ConsoleLogEvent logEvent) - { - switch (logEvent.EventId) - { - case 16: // Bound default address: {address} - if (logEvent.State.TryGetValue("address", out string defaultAddress)) - { - _outputHelper.WriteLine("Default Address: {0}", defaultAddress); - Assert.True(_defaultAddressSource.TrySetResult(defaultAddress)); - } - break; - case 17: // Bound metrics address: {address} - if (logEvent.State.TryGetValue("address", out string metricsAddress)) - { - _outputHelper.WriteLine("Metrics Address: {0}", metricsAddress); - Assert.True(_metricsAddressSource.TrySetResult(metricsAddress)); - } - break; - case 23: - if (logEvent.State.TryGetValue("MonitorApiKey", out string monitorApiKey)) - { - _outputHelper.WriteLine("MonitorApiKey: {0}", monitorApiKey); - Assert.True(_monitorApiKeySource.TrySetResult(monitorApiKey)); - } - break; - } - } } } diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Runners/MonitorRunnerExtensions.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Runners/MonitorRunnerExtensions.cs index e9b0dc90637..1f0303bc2c4 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Runners/MonitorRunnerExtensions.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Runners/MonitorRunnerExtensions.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using Microsoft.Diagnostics.Monitoring.TestCommon; +using Microsoft.Diagnostics.Monitoring.WebApi; using System; using System.Net.Http; using System.Net.Http.Headers; @@ -11,12 +12,12 @@ namespace Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests.Runners { - internal static class MonitorRunnerExtensions + internal static class MonitorCollectRunnerExtensions { /// /// Creates a over the default address of the . /// - public static Task CreateHttpClientDefaultAddressAsync(this MonitorRunner runner, IHttpClientFactory factory) + public static Task CreateHttpClientDefaultAddressAsync(this MonitorCollectRunner runner, IHttpClientFactory factory) { return runner.CreateHttpClientDefaultAddressAsync(factory, Extensions.Options.Options.DefaultName, TestTimeouts.HttpApi); } @@ -24,7 +25,7 @@ public static Task CreateHttpClientDefaultAddressAsync(this MonitorR /// /// Creates a named over the default address of the . /// - public static Task CreateHttpClientDefaultAddressAsync(this MonitorRunner runner, IHttpClientFactory factory, string name) + public static Task CreateHttpClientDefaultAddressAsync(this MonitorCollectRunner runner, IHttpClientFactory factory, string name) { return runner.CreateHttpClientDefaultAddressAsync(factory, name, TestTimeouts.HttpApi); } @@ -32,7 +33,7 @@ public static Task CreateHttpClientDefaultAddressAsync(this MonitorR /// /// Creates a over the default address of the . /// - public static Task CreateHttpClientDefaultAddressAsync(this MonitorRunner runner, IHttpClientFactory factory, TimeSpan timeout) + public static Task CreateHttpClientDefaultAddressAsync(this MonitorCollectRunner runner, IHttpClientFactory factory, TimeSpan timeout) { return runner.CreateHttpClientDefaultAddressAsync(factory, Extensions.Options.Options.DefaultName, timeout); } @@ -40,7 +41,7 @@ public static Task CreateHttpClientDefaultAddressAsync(this MonitorR /// /// Creates a named over the default address of the . /// - public static async Task CreateHttpClientDefaultAddressAsync(this MonitorRunner runner, IHttpClientFactory factory, string name, TimeSpan timeout) + public static async Task CreateHttpClientDefaultAddressAsync(this MonitorCollectRunner runner, IHttpClientFactory factory, string name, TimeSpan timeout) { HttpClient client = factory.CreateClient(name); @@ -50,7 +51,7 @@ public static async Task CreateHttpClientDefaultAddressAsync(this Mo if (runner.UseTempApiKey) { string monitorApiKey = await runner.GetMonitorApiKey(cancellation.Token); - client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(AuthenticationTests.ApiKeyScheme, monitorApiKey); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(AuthConstants.ApiKeySchema, monitorApiKey); } return client; @@ -59,7 +60,7 @@ public static async Task CreateHttpClientDefaultAddressAsync(this Mo /// /// Creates a over the metrics address of the . /// - public static Task CreateHttpClientMetricsAddressAsync(this MonitorRunner runner, IHttpClientFactory factory) + public static Task CreateHttpClientMetricsAddressAsync(this MonitorCollectRunner runner, IHttpClientFactory factory) { return runner.CreateHttpClientMetricsAddressAsync(factory, TestTimeouts.HttpApi); } @@ -67,7 +68,7 @@ public static Task CreateHttpClientMetricsAddressAsync(this MonitorR /// /// Creates a over the metrics address of the . /// - public static async Task CreateHttpClientMetricsAddressAsync(this MonitorRunner runner, IHttpClientFactory factory, TimeSpan timeout) + public static async Task CreateHttpClientMetricsAddressAsync(this MonitorCollectRunner runner, IHttpClientFactory factory, TimeSpan timeout) { HttpClient client = factory.CreateClient(); @@ -77,12 +78,12 @@ public static async Task CreateHttpClientMetricsAddressAsync(this Mo return client; } - public static Task StartAsync(this MonitorRunner runner) + public static Task StartAsync(this MonitorCollectRunner runner) { return runner.StartAsync(CommonTestTimeouts.StartProcess); } - public static async Task StartAsync(this MonitorRunner runner, TimeSpan timeout) + public static async Task StartAsync(this MonitorCollectRunner runner, TimeSpan timeout) { using CancellationTokenSource cancellation = new(timeout); await runner.StartAsync(cancellation.Token); diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Runners/ScenarioRunner.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Runners/ScenarioRunner.cs index 81cbed82f2e..51ee104ad7f 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Runners/ScenarioRunner.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Runners/ScenarioRunner.cs @@ -26,7 +26,7 @@ public static async Task SingleTarget( Func appValidate, Func postAppValidate = null, Action configureApp = null, - Action configureTool = null, + Action configureTool = null, bool disableHttpEgress = false) { DiagnosticPortHelper.Generate( @@ -34,7 +34,7 @@ public static async Task SingleTarget( out DiagnosticPortConnectionMode appConnectionMode, out string diagnosticPortPath); - await using MonitorRunner toolRunner = new(outputHelper); + await using MonitorCollectRunner toolRunner = new(outputHelper); toolRunner.ConnectionMode = mode; toolRunner.DiagnosticPortPath = diagnosticPortPath; toolRunner.DisableAuthentication = true; diff --git a/src/Tools/dotnet-monitor/Auth/ApiKeyAuthenticationHandler.cs b/src/Tools/dotnet-monitor/Auth/ApiKeyAuthenticationHandler.cs deleted file mode 100644 index 1c70122a28b..00000000000 --- a/src/Tools/dotnet-monitor/Auth/ApiKeyAuthenticationHandler.cs +++ /dev/null @@ -1,119 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using Microsoft.AspNetCore.Authentication; -using Microsoft.Diagnostics.Monitoring.WebApi; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.Extensions.Primitives; -using Microsoft.Net.Http.Headers; -using System; -using System.Buffers.Text; -using System.Diagnostics; -using System.Linq; -using System.Net.Http.Headers; -using System.Security.Claims; -using System.Security.Cryptography; -using System.Text.Encodings.Web; -using System.Threading.Tasks; - -namespace Microsoft.Diagnostics.Tools.Monitor -{ - /// - /// Authenticates against the ApiKey stored on the server. - /// - internal sealed class ApiKeyAuthenticationHandler : AuthenticationHandler - { - public const int ApiKeyByteMinLength = 32; - public const int ApiKeyByteMaxLength = 2048; - // This is the max length to efficiently encode an ApiKey of the length ApiKeyByteMaxLength. - private readonly int ApiKeyBase64MaxLength = Base64.GetMaxEncodedToUtf8Length(ApiKeyByteMaxLength); - - public ApiKeyAuthenticationHandler( - IOptionsMonitor options, - ILoggerFactory loggerFactory, - UrlEncoder encoder, - ISystemClock clock) - : base(options, loggerFactory, encoder, clock) - { - } - - protected override Task HandleAuthenticateAsync() - { - ApiKeyAuthenticationOptions options = OptionsMonitor.CurrentValue; - if (options.ValidationErrors.Any()) - { - Logger.ApiKeyValidationFailures(options.ValidationErrors); - - return Task.FromResult(AuthenticateResult.Fail(Strings.ErrorMessage_ApiKeyNotConfigured)); - } - - //We are expecting a header such as Authorization: - //If this is not present, we will return NoResult and move on to the next authentication handler. - if (!Request.Headers.TryGetValue(HeaderNames.Authorization, out StringValues values) || - !values.Any()) - { - return Task.FromResult(AuthenticateResult.NoResult()); - } - - if (!AuthenticationHeaderValue.TryParse(values.First(), out AuthenticationHeaderValue authHeader)) - { - return Task.FromResult(AuthenticateResult.Fail(Strings.ErrorMessage_InvalidAuthHeader)); - } - - if (!string.Equals(authHeader.Scheme, Scheme.Name, StringComparison.OrdinalIgnoreCase)) - { - return Task.FromResult(AuthenticateResult.NoResult()); - } - - if (authHeader.Parameter == null) - { - return Task.FromResult(AuthenticateResult.Fail(Strings.ErrorMessage_InvalidApiKeyFormat)); - } - - if (authHeader.Parameter.Length > ApiKeyBase64MaxLength) - { - return Task.FromResult(AuthenticateResult.Fail(Strings.ErrorMessage_InvalidApiKeyFormat)); - } - - // Calculate the max length of the base64 encoded value, - // the rules to decode this are really complex and allow white space in the - // string which should be ignored, simply calculate the max it can be. - int byteLen = Base64.GetMaxDecodedFromUtf8Length(authHeader.Parameter.Length); - - if (byteLen < ApiKeyByteMinLength) - { - return Task.FromResult(AuthenticateResult.Fail(Strings.ErrorMessage_InvalidApiKeyFormat)); - } - - // The user is passing a base 64-encoded version of the secret - // We will be hash this and compare it to the secret in our configuration. - byte[] buffer = new byte[byteLen]; - Span span = new Span(buffer); - if (!Convert.TryFromBase64String(authHeader.Parameter, span, out int bytesWritten) || bytesWritten < ApiKeyByteMinLength || bytesWritten > ApiKeyByteMaxLength) - { - return Task.FromResult(AuthenticateResult.Fail(Strings.ErrorMessage_InvalidApiKeyFormat)); - } - - Debug.Assert(null != options.HashAlgorithm); - using HashAlgorithm algorithm = HashAlgorithm.Create(options.HashAlgorithm); - Debug.Assert(null != algorithm); - - byte[] hashedSecret = algorithm.ComputeHash(buffer, 0, bytesWritten); - - Debug.Assert(null != options.HashValue); - if (hashedSecret.SequenceEqual(options.HashValue)) - { - return Task.FromResult(AuthenticateResult.Success( - new AuthenticationTicket( - new ClaimsPrincipal(new[] { new ClaimsIdentity(AuthConstants.ApiKeySchema) }), - AuthConstants.ApiKeySchema))); - } - else - { - return Task.FromResult(AuthenticateResult.Fail(Strings.ErrorMessage_InvalidApiKey)); - } - } - } -} diff --git a/src/Tools/dotnet-monitor/Auth/ApiKeyAuthenticationPostConfigureOptions.cs b/src/Tools/dotnet-monitor/Auth/ApiKeyAuthenticationPostConfigureOptions.cs deleted file mode 100644 index 682804e28fe..00000000000 --- a/src/Tools/dotnet-monitor/Auth/ApiKeyAuthenticationPostConfigureOptions.cs +++ /dev/null @@ -1,117 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using Microsoft.Extensions.Options; -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Globalization; -using System.Linq; -using System.Security.Cryptography; - -namespace Microsoft.Diagnostics.Tools.Monitor -{ - /// - /// Sets options on ApiKeyAuthenticationOptions based on ApiAuthenticationOptions. - /// - internal sealed class ApiKeyAuthenticationPostConfigureOptions : - IPostConfigureOptions - { - private readonly IOptionsMonitor _apiAuthOptions; - - public ApiKeyAuthenticationPostConfigureOptions( - IOptionsMonitor apiAuthOptions) - { - _apiAuthOptions = apiAuthOptions; - } - - public void PostConfigure(string name, ApiKeyAuthenticationOptions options) - { - ApiAuthenticationOptions sourceOptions = _apiAuthOptions.CurrentValue; - - IList errors = new List(); - - Validator.TryValidateObject( - sourceOptions, - new ValidationContext(sourceOptions, null, null), - errors, - validateAllProperties: true); - - // Validate hash algorithm is allowed and is supported. - if (!string.IsNullOrEmpty(sourceOptions.ApiKeyHashType)) - { - if (!HashAlgorithmChecker.IsAllowedAlgorithm(sourceOptions.ApiKeyHashType)) - { - errors.Add( - new ValidationResult( - string.Format( - Strings.ErrorMessage_FieldNotAllowed, - nameof(ApiAuthenticationOptions.ApiKeyHashType), - sourceOptions.ApiKeyHashType), - new string[] { nameof(ApiAuthenticationOptions.ApiKeyHashType) })); - } - else - { - using HashAlgorithm algorithm = HashAlgorithm.Create(sourceOptions.ApiKeyHashType); - if (null == algorithm) - { - errors.Add( - new ValidationResult( - string.Format( - Strings.ErrorMessage_FieldNotAllowed, - nameof(ApiAuthenticationOptions.ApiKeyHashType), - sourceOptions.ApiKeyHashType), - new string[] { nameof(ApiAuthenticationOptions.ApiKeyHashType) })); - } - } - } - - byte[] apiKeyHashBytes = null; - if (!string.IsNullOrEmpty(sourceOptions.ApiKeyHash)) - { - // ApiKeyHash is represented as a hex string. e.g. AABBCCDDEEFF - if (sourceOptions.ApiKeyHash.Length % 2 == 0) - { - apiKeyHashBytes = new byte[sourceOptions.ApiKeyHash.Length / 2]; - for (int i = 0; i < sourceOptions.ApiKeyHash.Length; i += 2) - { - if (!byte.TryParse(sourceOptions.ApiKeyHash.AsSpan(i, 2), NumberStyles.HexNumber, provider: NumberFormatInfo.InvariantInfo, result: out byte resultByte)) - { - errors.Add( - new ValidationResult( - string.Format( - Strings.ErrorMessage_FieldNotHex, - nameof(ApiAuthenticationOptions.ApiKeyHash)), - new string[] { nameof(ApiAuthenticationOptions.ApiKeyHash) })); - errors.Add(new ValidationResult($"The {nameof(ApiAuthenticationOptions.ApiKeyHash)} field could not be decoded as hex string.", new string[] { nameof(ApiAuthenticationOptions.ApiKeyHash) })); - break; - } - apiKeyHashBytes[i / 2] = resultByte; - } - } - else - { - errors.Add( - new ValidationResult( - string.Format( - Strings.ErrorMessage_FieldOddLengh, - nameof(ApiAuthenticationOptions.ApiKeyHash)), - new string[] { nameof(ApiAuthenticationOptions.ApiKeyHash) })); - } - } - - options.ValidationErrors = errors; - if (errors.Any()) - { - options.HashAlgorithm = null; - options.HashValue = null; - } - else - { - options.HashAlgorithm = sourceOptions.ApiKeyHashType; - options.HashValue = apiKeyHashBytes; - } - } - } -} diff --git a/src/Tools/dotnet-monitor/Auth/AuthOptions.cs b/src/Tools/dotnet-monitor/Auth/AuthConfiguration.cs similarity index 80% rename from src/Tools/dotnet-monitor/Auth/AuthOptions.cs rename to src/Tools/dotnet-monitor/Auth/AuthConfiguration.cs index 9e95395f131..287fea0a5ef 100644 --- a/src/Tools/dotnet-monitor/Auth/AuthOptions.cs +++ b/src/Tools/dotnet-monitor/Auth/AuthConfiguration.cs @@ -8,7 +8,7 @@ namespace Microsoft.Diagnostics.Tools.Monitor { - internal sealed class AuthOptions : IAuthOptions + internal sealed class AuthConfiguration : IAuthConfiguration { public bool EnableNegotiate => RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && KeyAuthenticationMode != KeyAuthenticationMode.NoAuth; @@ -17,15 +17,15 @@ internal sealed class AuthOptions : IAuthOptions public bool EnableKeyAuth => (KeyAuthenticationMode == KeyAuthenticationMode.StoredKey) || (KeyAuthenticationMode == KeyAuthenticationMode.TemporaryKey); - public GeneratedApiKey TemporaryKey { get; } + public GeneratedJwtKey TemporaryJwtKey { get; } - public AuthOptions(KeyAuthenticationMode mode) + public AuthConfiguration(KeyAuthenticationMode mode) { KeyAuthenticationMode = mode; if (mode == KeyAuthenticationMode.TemporaryKey) { - TemporaryKey = GeneratedApiKey.Create(); + TemporaryJwtKey = GeneratedJwtKey.Create(); } } } diff --git a/src/Tools/dotnet-monitor/Auth/AuthorizedUserRequirement.cs b/src/Tools/dotnet-monitor/Auth/AuthorizedUserRequirement.cs index c6696c3a5dc..9bce3a5c763 100644 --- a/src/Tools/dotnet-monitor/Auth/AuthorizedUserRequirement.cs +++ b/src/Tools/dotnet-monitor/Auth/AuthorizedUserRequirement.cs @@ -3,9 +3,7 @@ // See the LICENSE file in the project root for more information. using Microsoft.AspNetCore.Authorization; -using System; -using System.Collections.Generic; -using System.Text; +using Microsoft.Extensions.Options; namespace Microsoft.Diagnostics.Tools.Monitor { diff --git a/src/Tools/dotnet-monitor/Auth/GeneratedApiKey.cs b/src/Tools/dotnet-monitor/Auth/GeneratedApiKey.cs deleted file mode 100644 index cc484f21850..00000000000 --- a/src/Tools/dotnet-monitor/Auth/GeneratedApiKey.cs +++ /dev/null @@ -1,65 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Linq; -using System.Security.Cryptography; -using System.Text; - -namespace Microsoft.Diagnostics.Tools.Monitor -{ - internal sealed class GeneratedApiKey - { - public const int DefaultKeyLength = 32; - public const string DefaultHashAlgorithm = "SHA256"; - - public readonly string MonitorApiKey; - public readonly string HashAlgorithm; - public readonly string HashValue; - - private GeneratedApiKey(string monitorApiKey, string hashAlgorithm, string hashValue) - { - this.MonitorApiKey = monitorApiKey; - this.HashAlgorithm = hashAlgorithm; - this.HashValue = hashValue; - } - - public static GeneratedApiKey Create() - { - return GeneratedApiKey.Create(DefaultKeyLength, DefaultHashAlgorithm); - } - - public static GeneratedApiKey Create(int keyLength, string hashAlgorithm) - { - if (!HashAlgorithmChecker.IsAllowedAlgorithm(hashAlgorithm)) - { - throw new ArgumentOutOfRangeException(nameof(hashAlgorithm)); - } - - if (keyLength < ApiKeyAuthenticationHandler.ApiKeyByteMinLength || keyLength > ApiKeyAuthenticationHandler.ApiKeyByteMaxLength) - { - throw new ArgumentOutOfRangeException(nameof(keyLength)); - } - - using RandomNumberGenerator rng = RandomNumberGenerator.Create(); - using HashAlgorithm hasher = System.Security.Cryptography.HashAlgorithm.Create(hashAlgorithm); - - byte[] secret = new byte[keyLength]; - rng.GetBytes(secret); - - byte[] hash = hasher.ComputeHash(secret); - StringBuilder outHash = new StringBuilder(hash.Length * 2); - - foreach (byte b in hash) - { - outHash.AppendFormat("{0:X2}", b); - } - - string apiKey = Convert.ToBase64String(secret); - - GeneratedApiKey result = new GeneratedApiKey(apiKey, hashAlgorithm, outHash.ToString()); - return result; - } - } -} diff --git a/src/Tools/dotnet-monitor/Auth/GeneratedJwtKey.cs b/src/Tools/dotnet-monitor/Auth/GeneratedJwtKey.cs new file mode 100644 index 00000000000..cd902c50b59 --- /dev/null +++ b/src/Tools/dotnet-monitor/Auth/GeneratedJwtKey.cs @@ -0,0 +1,59 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Diagnostics.Monitoring.WebApi; +using Microsoft.IdentityModel.Tokens; +using System; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Microsoft.Diagnostics.Tools.Monitor +{ + internal sealed class GeneratedJwtKey + { + public readonly string Token; + public readonly string Subject; + public readonly string PublicKey; + + private GeneratedJwtKey(string token, string subject, string publicKey) + { + Token = token; + Subject = subject; + PublicKey = publicKey; + } + + public static GeneratedJwtKey Create() + { + Guid subjectId = Guid.NewGuid(); + string subjectStr = subjectId.ToString("D"); + + ECDsa dsa = ECDsa.Create(); + dsa.GenerateKey(ECCurve.NamedCurves.nistP384); + ECDsaSecurityKey secKey = new ECDsaSecurityKey(dsa); + SigningCredentials signingCreds = new SigningCredentials(secKey, SecurityAlgorithms.EcdsaSha384); + JwtHeader newHeader = new JwtHeader(signingCreds, null, JwtConstants.HeaderType); + + Claim audClaim = new Claim(AuthConstants.ClaimAudienceStr, AuthConstants.ApiKeyJwtAudience); + Claim issClaim = new Claim(AuthConstants.ClaimIssuerStr, AuthConstants.ApiKeyJwtInternalIssuer); + Claim subClaim = new Claim(AuthConstants.ClaimSubjectStr, subjectStr); + JwtPayload newPayload = new JwtPayload(new Claim[] { audClaim, issClaim, subClaim }); + + JwtSecurityToken newToken = new JwtSecurityToken(newHeader, newPayload); + + ECDsa pubDsa = ECDsa.Create(dsa.ExportParameters(includePrivateParameters: false)); + ECDsaSecurityKey pubSecKey = new ECDsaSecurityKey(pubDsa); + JsonWebKey jwk = JsonWebKeyConverter.ConvertFromECDsaSecurityKey(pubSecKey); + string publicKeyJson = JsonSerializer.Serialize(jwk, new JsonSerializerOptions() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }); + string publicKeyEncoded = Base64UrlEncoder.Encode(publicKeyJson); + + JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler(); + string output = tokenHandler.WriteToken(newToken); + + return new GeneratedJwtKey(output, subjectStr, publicKeyEncoded); + } + } +} diff --git a/src/Tools/dotnet-monitor/Auth/HashAlgorithmChecker.cs b/src/Tools/dotnet-monitor/Auth/HashAlgorithmChecker.cs deleted file mode 100644 index fc4de152b02..00000000000 --- a/src/Tools/dotnet-monitor/Auth/HashAlgorithmChecker.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using System.Linq; - -namespace Microsoft.Diagnostics.Tools.Monitor -{ - class HashAlgorithmChecker - { - private static readonly string[] DisallowedHashAlgorithms = new string[] - { - // ------------------ SHA1 ------------------ - "SHA", - "SHA1", - "System.Security.Cryptography.SHA1", - "System.Security.Cryptography.SHA1Cng", - "System.Security.Cryptography.HashAlgorithm", - "http://www.w3.org/2000/09/xmldsig#sha1", - // These give a KeyedHashAlgorith based on SHA1 - "System.Security.Cryptography.HMAC", - "System.Security.Cryptography.KeyedHashAlgorithm", - "HMACSHA1", - "System.Security.Cryptography.HMACSHA1", - "http://www.w3.org/2000/09/xmldsig#hmac-sha1", - - // ------------------ MD5 ------------------ - "MD5", - "System.Security.Cryptography.MD5", - "System.Security.Cryptography.MD5Cng", - "http://www.w3.org/2001/04/xmldsig-more#md5", - // These give a KeyedHashAlgorith based on MD5 - "HMACMD5", - "System.Security.Cryptography.HMACMD5", - "http://www.w3.org/2001/04/xmldsig-more#hmac-md5", - }; - - public static bool IsAllowedAlgorithm(string hashAlgorithmName) - { - return !DisallowedHashAlgorithms.Contains(hashAlgorithmName, StringComparer.OrdinalIgnoreCase); - } - } -} diff --git a/src/Tools/dotnet-monitor/Auth/IAuthOptions.cs b/src/Tools/dotnet-monitor/Auth/IAuthConfiguration.cs similarity index 84% rename from src/Tools/dotnet-monitor/Auth/IAuthOptions.cs rename to src/Tools/dotnet-monitor/Auth/IAuthConfiguration.cs index f2ee4b49969..0ebfc8b2ae4 100644 --- a/src/Tools/dotnet-monitor/Auth/IAuthOptions.cs +++ b/src/Tools/dotnet-monitor/Auth/IAuthConfiguration.cs @@ -13,10 +13,10 @@ internal enum KeyAuthenticationMode NoAuth, } - internal interface IAuthOptions + internal interface IAuthConfiguration { bool EnableNegotiate { get; } KeyAuthenticationMode KeyAuthenticationMode { get; } - GeneratedApiKey TemporaryKey { get; } + GeneratedJwtKey TemporaryJwtKey { get; } } } diff --git a/src/Tools/dotnet-monitor/Auth/JwtAlgorithmChecker.cs b/src/Tools/dotnet-monitor/Auth/JwtAlgorithmChecker.cs new file mode 100644 index 00000000000..9535df877df --- /dev/null +++ b/src/Tools/dotnet-monitor/Auth/JwtAlgorithmChecker.cs @@ -0,0 +1,63 @@ +using Microsoft.IdentityModel.Tokens; +using System; +using System.Collections.Generic; +using System.Linq; + +#if UNITTEST +namespace Microsoft.Diagnostics.Monitoring.TestCommon.Options +#else +namespace Microsoft.Diagnostics.Tools.Monitor +#endif +{ + internal static class JwtAlgorithmChecker + { + /// + /// This is the list of allowed algorithms for JWS that are public/private key based. + /// We also reject RSASSA-PSS because .net core does not have support for this algorithm. + /// + /// + /// We enforce this list to prevent storage of private key information in configuration. + /// Pulled from: https://datatracker.ietf.org/doc/html/rfc7518#section-3.1 + /// + private static readonly string[] AllowedJwtAlgos = new string[] + { + // ECDSA using curves P-X and SHA-X + SecurityAlgorithms.EcdsaSha256, SecurityAlgorithms.EcdsaSha256Signature, + SecurityAlgorithms.EcdsaSha384, SecurityAlgorithms.EcdsaSha384Signature, + SecurityAlgorithms.EcdsaSha512, SecurityAlgorithms.EcdsaSha512Signature, + + // RSASSA-PKCS1-v1_5 using SHA-x + SecurityAlgorithms.RsaSha256, SecurityAlgorithms.RsaSha256Signature, + SecurityAlgorithms.RsaSha384, SecurityAlgorithms.RsaSha384Signature, + SecurityAlgorithms.RsaSha512, SecurityAlgorithms.RsaSha512Signature, + }; + + /// + /// This is the list of JSON Web Key Key Types that we support. Specifically these are the values that are + /// valid for the 'kty' field. + /// + /// + /// Again we allow all the algorithms that are public/private to prevent private key information in configuration. + /// Pulled from: https://datatracker.ietf.org/doc/html/rfc7518#section-7.4.2 + /// + private static readonly string[] AllowedJwkKeyTypes = new string[] + { + // Elliptic curve + JsonWebAlgorithmsKeyTypes.EllipticCurve, + // RSA + JsonWebAlgorithmsKeyTypes.RSA + }; + + public static IReadOnlyList GetAllowedJwsAlgorithmList() + { + return new List(AllowedJwtAlgos); + } + + public static bool IsValidJwk(JsonWebKey key) + { + return + !string.IsNullOrEmpty(key.Kty) + && AllowedJwkKeyTypes.Contains(key.Kty, StringComparer.OrdinalIgnoreCase); + } + } +} diff --git a/src/Tools/dotnet-monitor/Auth/JwtBearerChangeTokenSource.cs b/src/Tools/dotnet-monitor/Auth/JwtBearerChangeTokenSource.cs new file mode 100644 index 00000000000..eb29ba3acd5 --- /dev/null +++ b/src/Tools/dotnet-monitor/Auth/JwtBearerChangeTokenSource.cs @@ -0,0 +1,54 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; +using System; +using System.Threading; + +namespace Microsoft.Diagnostics.Tools.Monitor +{ + /// + /// Notifies that JwtBearerOptions changes when MonitorApiKeyConfiguration changes. + /// + internal sealed class JwtBearerChangeTokenSource : + IOptionsChangeTokenSource, + IDisposable + { + private readonly IOptionsMonitor _optionsMonitor; + private readonly IDisposable _changeRegistration; + + private ConfigurationReloadToken _reloadToken = new ConfigurationReloadToken(); + + public JwtBearerChangeTokenSource( + IOptionsMonitor optionsMonitor) + { + _optionsMonitor = optionsMonitor; + _changeRegistration = _optionsMonitor.OnChange(OnReload); + } + + /// + /// Returns Named config instance. expects + /// its configuration to be named after the AuthenticationScheme it's using, not . + /// + public string Name => JwtBearerDefaults.AuthenticationScheme; + + public IChangeToken GetChangeToken() + { + return _reloadToken; + } + + public void Dispose() + { + _changeRegistration.Dispose(); + } + + private void OnReload(MonitorApiKeyConfiguration options) + { + Interlocked.Exchange(ref _reloadToken, new ConfigurationReloadToken()).OnReload(); + } + } +} diff --git a/src/Tools/dotnet-monitor/Auth/JwtBearerPostConfigure.cs b/src/Tools/dotnet-monitor/Auth/JwtBearerPostConfigure.cs new file mode 100644 index 00000000000..ef8a3c1372a --- /dev/null +++ b/src/Tools/dotnet-monitor/Auth/JwtBearerPostConfigure.cs @@ -0,0 +1,61 @@ +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Diagnostics.Monitoring.WebApi; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Text; + +namespace Microsoft.Diagnostics.Tools.Monitor +{ + /// + /// Configures based on configuration. + /// + internal sealed class JwtBearerPostConfigure : + IPostConfigureOptions + { + private readonly IOptionsMonitor _apiKeyConfig; + + public JwtBearerPostConfigure( + IOptionsMonitor apiKeyConfig) + { + _apiKeyConfig = apiKeyConfig; + } + + public void PostConfigure(string name, JwtBearerOptions options) + { + MonitorApiKeyConfiguration configSnapshot = _apiKeyConfig.CurrentValue; + if (configSnapshot.ValidationErrors.Any()) + { + options.SecurityTokenValidators.Add(new RejectAllSecurityValidator()); + return; + } + + TokenValidationParameters tokenValidationParameters = new TokenValidationParameters + { + // Signing Settings + RequireSignedTokens = true, + ValidAlgorithms = JwtAlgorithmChecker.GetAllowedJwsAlgorithmList(), + + // Issuer Settings + ValidateIssuer = false, + ValidateIssuerSigningKey = true, + IssuerSigningKeys = new SecurityKey[] { configSnapshot.PublicKey }, + TryAllIssuerSigningKeys = true, + + // Audience Settings + ValidateAudience = true, + ValidAudiences = new string[] { AuthConstants.ApiKeyJwtAudience }, + + // Other Settings + ValidateActor = false, + ValidateLifetime = false, + }; + options.TokenValidationParameters = tokenValidationParameters; + } + } +} diff --git a/src/Tools/dotnet-monitor/Auth/LiveJwtBearerHandler.cs b/src/Tools/dotnet-monitor/Auth/LiveJwtBearerHandler.cs new file mode 100644 index 00000000000..ee0ec666bc0 --- /dev/null +++ b/src/Tools/dotnet-monitor/Auth/LiveJwtBearerHandler.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System.Text.Encodings.Web; +using Microsoft.AspNetCore.Authentication.JwtBearer; + +namespace Microsoft.Diagnostics.Tools.Monitor +{ + /// + /// This is a version of that will refresh the + /// stored JwtBearerOptions options when the settings change. + /// + internal sealed class LiveJwtBearerHandler : JwtBearerHandler + { + public LiveJwtBearerHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) + : base(options, logger, encoder, clock) + { + options.OnChange((JwtBearerOptions opts, string name) => + { + // This is required to get AuthenticationHandler to reload options. + // Once InitializeAsync is called, the value of the JwtBearerOptions is stored in Options and updates are never taken. + base.InitializeAsync(Scheme, Context); + }); + } + } +} diff --git a/src/Tools/dotnet-monitor/Auth/ApiKeyAuthenticationOptionsChangeTokenSource.cs b/src/Tools/dotnet-monitor/Auth/MonitorApiKeyChangeTokenSource.cs similarity index 68% rename from src/Tools/dotnet-monitor/Auth/ApiKeyAuthenticationOptionsChangeTokenSource.cs rename to src/Tools/dotnet-monitor/Auth/MonitorApiKeyChangeTokenSource.cs index 3766d69ec41..c01a9cffac0 100644 --- a/src/Tools/dotnet-monitor/Auth/ApiKeyAuthenticationOptionsChangeTokenSource.cs +++ b/src/Tools/dotnet-monitor/Auth/MonitorApiKeyChangeTokenSource.cs @@ -11,19 +11,19 @@ namespace Microsoft.Diagnostics.Tools.Monitor { /// - /// Notifies that ApiKeyAuthenticationOptions changes when ApiAuthenticationOptions changes. + /// Notifies that changes when changes. /// - internal sealed class ApiKeyAuthenticationOptionsChangeTokenSource : - IOptionsChangeTokenSource, + internal sealed class MonitorApiKeyChangeTokenSource : + IOptionsChangeTokenSource, IDisposable { - private readonly IOptionsMonitor _optionsMonitor; + private readonly IOptionsMonitor _optionsMonitor; private readonly IDisposable _changeRegistration; private ConfigurationReloadToken _reloadToken = new ConfigurationReloadToken(); - public ApiKeyAuthenticationOptionsChangeTokenSource( - IOptionsMonitor optionsMonitor) + public MonitorApiKeyChangeTokenSource( + IOptionsMonitor optionsMonitor) { _optionsMonitor = optionsMonitor; _changeRegistration = _optionsMonitor.OnChange(OnReload); @@ -41,7 +41,7 @@ public void Dispose() _changeRegistration.Dispose(); } - private void OnReload(ApiAuthenticationOptions options) + private void OnReload(MonitorApiKeyOptions options) { Interlocked.Exchange(ref _reloadToken, new ConfigurationReloadToken()).OnReload(); } diff --git a/src/Tools/dotnet-monitor/Auth/ApiKeyAuthenticationOptions.cs b/src/Tools/dotnet-monitor/Auth/MonitorApiKeyConfiguration.cs similarity index 51% rename from src/Tools/dotnet-monitor/Auth/ApiKeyAuthenticationOptions.cs rename to src/Tools/dotnet-monitor/Auth/MonitorApiKeyConfiguration.cs index e91a4324d25..79a2583be67 100644 --- a/src/Tools/dotnet-monitor/Auth/ApiKeyAuthenticationOptions.cs +++ b/src/Tools/dotnet-monitor/Auth/MonitorApiKeyConfiguration.cs @@ -3,17 +3,22 @@ // See the LICENSE file in the project root for more information. using Microsoft.AspNetCore.Authentication; +using Microsoft.IdentityModel.Tokens; +using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; namespace Microsoft.Diagnostics.Tools.Monitor { - internal sealed class ApiKeyAuthenticationOptions : AuthenticationSchemeOptions + /// + /// Represents internal version of . + /// This object contains the validation state of the object and the decoded + /// Json Web Key. + /// + internal class MonitorApiKeyConfiguration : AuthenticationSchemeOptions { - public string HashAlgorithm { get; set; } - - public byte[] HashValue { get; set; } - + public string Subject { get; set; } + public SecurityKey PublicKey { get; set; } public IEnumerable ValidationErrors { get; set; } } -} +} \ No newline at end of file diff --git a/src/Tools/dotnet-monitor/Auth/ApiKeyAuthenticationOptionsObserver.cs b/src/Tools/dotnet-monitor/Auth/MonitorApiKeyConfigurationObserver.cs similarity index 58% rename from src/Tools/dotnet-monitor/Auth/ApiKeyAuthenticationOptionsObserver.cs rename to src/Tools/dotnet-monitor/Auth/MonitorApiKeyConfigurationObserver.cs index eccf994dbb7..b3dc7954453 100644 --- a/src/Tools/dotnet-monitor/Auth/ApiKeyAuthenticationOptionsObserver.cs +++ b/src/Tools/dotnet-monitor/Auth/MonitorApiKeyConfigurationObserver.cs @@ -12,17 +12,17 @@ namespace Microsoft.Diagnostics.Tools.Monitor /// /// Service that monitors API Key authentication options changes and logs issues with the specified options. /// - internal class ApiKeyAuthenticationOptionsObserver : + internal class MonitorApiKeyConfigurationObserver : IDisposable { - private readonly ILogger _logger; - private readonly IOptionsMonitor _options; + private readonly ILogger _logger; + private readonly IOptionsMonitor _options; private IDisposable _changeRegistration; - public ApiKeyAuthenticationOptionsObserver( - ILogger logger, - IOptionsMonitor options + public MonitorApiKeyConfigurationObserver( + ILogger logger, + IOptionsMonitor options ) { _logger = logger; @@ -31,10 +31,10 @@ IOptionsMonitor options public void Initialize() { - _changeRegistration = _options.OnChange(OnApiKeyAuthenticationOptionsChanged); + _changeRegistration = _options.OnChange(OnMonitorApiKeyOptionsChanged); // Write out current validation state of options when starting the tool. - CheckApiKeyAuthenticationOptions(_options.CurrentValue); + CheckMonitorApiKeyOptions(_options.CurrentValue); } public void Dispose() @@ -42,20 +42,22 @@ public void Dispose() _changeRegistration?.Dispose(); } - private void OnApiKeyAuthenticationOptionsChanged(ApiKeyAuthenticationOptions options) + private void OnMonitorApiKeyOptionsChanged(MonitorApiKeyConfiguration options) { - _logger.ApiKeyAuthenticationOptionsChanged(); - - CheckApiKeyAuthenticationOptions(options); + CheckMonitorApiKeyOptions(options); } - private void CheckApiKeyAuthenticationOptions(ApiKeyAuthenticationOptions options) + private void CheckMonitorApiKeyOptions(MonitorApiKeyConfiguration options) { // ValidationErrors will be null if API key authentication is not enabled. if (null != options.ValidationErrors && options.ValidationErrors.Any()) { _logger.ApiKeyValidationFailures(options.ValidationErrors); } + else + { + _logger.ApiKeyAuthenticationOptionsValidated(); + } } } } diff --git a/src/Tools/dotnet-monitor/Auth/MonitorApiKeyPostConfigure.cs b/src/Tools/dotnet-monitor/Auth/MonitorApiKeyPostConfigure.cs new file mode 100644 index 00000000000..907cb3addb6 --- /dev/null +++ b/src/Tools/dotnet-monitor/Auth/MonitorApiKeyPostConfigure.cs @@ -0,0 +1,113 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; + +namespace Microsoft.Diagnostics.Tools.Monitor +{ + /// + /// Sets options on based on . + /// This class is responsible for decoding the value provided in and validating it. + /// + internal sealed class MonitorApiKeyPostConfigure : + IPostConfigureOptions + { + private readonly ILogger _logger; + private readonly IOptionsMonitor _apiKeyOptions; + + public MonitorApiKeyPostConfigure( + ILogger logger, + IOptionsMonitor apiKeyOptions) + { + _logger = logger; + _apiKeyOptions = apiKeyOptions; + } + + public void PostConfigure(string name, MonitorApiKeyConfiguration options) + { + MonitorApiKeyOptions sourceOptions = _apiKeyOptions.CurrentValue; + + IList errors = new List(); + + Validator.TryValidateObject( + sourceOptions, + new ValidationContext(sourceOptions, null, null), + errors, + validateAllProperties: true); + + string jwkJson = null; + try + { + jwkJson = Base64UrlEncoder.Decode(sourceOptions.PublicKey); + } + catch (Exception) + { + errors.Add( + new ValidationResult( + string.Format( + Strings.ErrorMessage_NotBase64, + nameof(MonitorApiKeyOptions.PublicKey), + sourceOptions.PublicKey), + new string[] { nameof(MonitorApiKeyOptions.PublicKey) })); + } + + JsonWebKey jwk = null; + if (!string.IsNullOrEmpty(jwkJson)) + { + try + { + jwk = JsonWebKey.Create(jwkJson); + } + // JsonWebKey will throw only throw ArgumentException or a derived class. + catch (ArgumentException ex) + { + errors.Add( + new ValidationResult( + string.Format( + Strings.ErrorMessage_InvalidJwk, + nameof(MonitorApiKeyOptions.PublicKey), + sourceOptions.PublicKey, + ex.Message), + new string[] { nameof(MonitorApiKeyOptions.PublicKey) })); + } + } + + if (null != jwk) + { + if(!JwtAlgorithmChecker.IsValidJwk(jwk)) + { + errors.Add( + new ValidationResult( + string.Format( + Strings.ErrorMessage_RejectedJwk, + nameof(MonitorApiKeyOptions.PublicKey)), + new string[] { nameof(MonitorApiKeyOptions.PublicKey) })); + } + // We will let the algorithm work with private key but we should produce a warning message + else if (jwk.HasPrivateKey) + { + _logger.NotifyPrivateKey(nameof(MonitorApiKeyOptions.PublicKey)); + } + } + + options.ValidationErrors = errors; + if (errors.Any()) + { + options.Subject = string.Empty; + options.PublicKey = null; + } + else + { + options.Subject = sourceOptions.Subject; + options.PublicKey = jwk; + } + } + } +} diff --git a/src/Tools/dotnet-monitor/Auth/RejectAllSecurityValidator.cs b/src/Tools/dotnet-monitor/Auth/RejectAllSecurityValidator.cs new file mode 100644 index 00000000000..8dd75cc6be6 --- /dev/null +++ b/src/Tools/dotnet-monitor/Auth/RejectAllSecurityValidator.cs @@ -0,0 +1,33 @@ +using Microsoft.AspNetCore.Authentication; +using Microsoft.IdentityModel.Tokens; +using System; +using System.Collections.Generic; +using System.Security.Claims; +using System.Text; + +namespace Microsoft.Diagnostics.Tools.Monitor +{ + /// + /// This will reject all validations and is used when configuration is invalid and + /// all attempts to authenticate should be rejected. + /// + internal sealed class RejectAllSecurityValidator : ISecurityTokenValidator + { + public bool CanValidateToken => true; + + public int MaximumTokenSizeInBytes + { + // We need to provide a maximum token size, so we pick the same as the default used by everything derived from TokenHandler + get => TokenValidationParameters.DefaultMaximumTokenSizeInBytes; + set => throw new NotImplementedException(); + } + + public bool CanReadToken(string securityToken) => true; + + public ClaimsPrincipal ValidateToken(string securityToken, TokenValidationParameters validationParameters, out SecurityToken validatedToken) + { + validatedToken = null; + throw new InvalidOperationException(Strings.ErrorMessage_ApiKeyNotConfigured); + } + } +} diff --git a/src/Tools/dotnet-monitor/Auth/UserAuthorizationHandler.cs b/src/Tools/dotnet-monitor/Auth/UserAuthorizationHandler.cs index 2d7184752d7..d0bd9dfd25a 100644 --- a/src/Tools/dotnet-monitor/Auth/UserAuthorizationHandler.cs +++ b/src/Tools/dotnet-monitor/Auth/UserAuthorizationHandler.cs @@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Authentication.Negotiate; using Microsoft.AspNetCore.Authorization; using Microsoft.Diagnostics.Monitoring.WebApi; +using Microsoft.Extensions.Options; using System; using System.Collections.Generic; using System.Linq; @@ -20,12 +21,22 @@ namespace Microsoft.Diagnostics.Tools.Monitor /// internal sealed class UserAuthorizationHandler : AuthorizationHandler { + private readonly IOptionsMonitor _apiKeyConfig; + public UserAuthorizationHandler(IOptionsMonitor apiKeyConfig) + { + _apiKeyConfig = apiKeyConfig; + } + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, AuthorizedUserRequirement requirement) { - // If the schema type is ApiKey, we do not need further authorization. - if (context.User.Identity.AuthenticationType == AuthConstants.ApiKeySchema) + if (context.User.Identity.AuthenticationType == AuthConstants.FederationAuthType) { - context.Succeed(requirement); + // If we get a FederationAuthType (Bearer from a Jwt Token) we need to check that the user has the specified subject claim. + MonitorApiKeyConfiguration configSnapshot = _apiKeyConfig.CurrentValue; + if (context.User.HasClaim(ClaimTypes.NameIdentifier, configSnapshot.Subject)) + { + context.Succeed(requirement); + } } else if ((context.User.Identity.AuthenticationType == AuthConstants.NtlmSchema) || (context.User.Identity.AuthenticationType == AuthConstants.KerberosSchema) || diff --git a/src/Tools/dotnet-monitor/CommonOptionsExtensions.cs b/src/Tools/dotnet-monitor/CommonOptionsExtensions.cs new file mode 100644 index 00000000000..801cd6e05c8 --- /dev/null +++ b/src/Tools/dotnet-monitor/CommonOptionsExtensions.cs @@ -0,0 +1,128 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace Microsoft.Diagnostics.Tools.Monitor +{ + internal static class CommonOptionsExtensions + { + /// + /// Generates an environment variable map of the options. + /// + /// + /// Each key is the variable name; each value is the variable value. + /// + public static IDictionary ToEnvironmentConfiguration(this RootOptions options, bool useDotnetMonitorPrefix = false) + { + Dictionary variables = new(StringComparer.OrdinalIgnoreCase); + MapObject(options, useDotnetMonitorPrefix ? "DotNetMonitor_" : string.Empty, variables); + return variables; + } + + /// + /// Generates a key-per-file map of the options. + /// + /// + /// Each key is the file name; each value is the file content. + /// + public static IDictionary ToKeyPerFileConfiguration(this RootOptions options) + { + Dictionary variables = new(StringComparer.OrdinalIgnoreCase); + MapObject(options, string.Empty, variables); + return variables; + } + + private static void MapDictionary(IDictionary dictionary, string prefix, IDictionary map) + { + foreach (var key in dictionary.Keys) + { + object value = dictionary[key]; + if (null != value) + { + string keyString = Convert.ToString(key, CultureInfo.InvariantCulture); + MapValue( + value, + FormattableString.Invariant($"{prefix}{keyString}"), + map); + } + } + } + + private static void MapList(IList list, string prefix, IDictionary map) + { + for (int index = 0; index < list.Count; index++) + { + object value = list[index]; + if (null != value) + { + MapValue( + value, + FormattableString.Invariant($"{prefix}{index}"), + map); + } + } + } + + private static void MapObject(object obj, string prefix, IDictionary map) + { + foreach (PropertyInfo property in obj.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)) + { + if (!property.GetIndexParameters().Any()) + { + MapValue( + property.GetValue(obj), + FormattableString.Invariant($"{prefix}{property.Name}"), + map); + } + } + } + + private static void MapValue(object value, string valueName, IDictionary map) + { + if (null != value) + { + Type valueType = value.GetType(); + if (valueType.IsPrimitive || typeof(string) == valueType || typeof(Guid) == valueType) + { + map.Add( + valueName, + Convert.ToString(value, CultureInfo.InvariantCulture)); + } + else + { + string prefix = FormattableString.Invariant($"{valueName}__"); + if (value is IDictionary dictionary) + { + MapDictionary(dictionary, prefix, map); + } + else if (value is IList list) + { + MapList(list, prefix, map); + } + else + { + MapObject(value, prefix, map); + } + } + } + } + + private static string ToHexString(byte[] data) + { + StringBuilder builder = new(2 * data.Length); + foreach (byte b in data) + { + builder.Append(b.ToString("X2", CultureInfo.InvariantCulture)); + } + return builder.ToString(); + } + } +} diff --git a/src/Tools/dotnet-monitor/ConfigurationJsonWriter.cs b/src/Tools/dotnet-monitor/ConfigurationJsonWriter.cs index 1b4c1d6fd51..ea4bf17710f 100644 --- a/src/Tools/dotnet-monitor/ConfigurationJsonWriter.cs +++ b/src/Tools/dotnet-monitor/ConfigurationJsonWriter.cs @@ -68,21 +68,26 @@ public void Write(IConfiguration configuration, bool full) if (full) { - ProcessChildSection(configuration, ConfigurationKeys.ApiAuthentication, includeChildSections: true); + ProcessChildSection(configuration, ConfigurationKeys.Authentication, includeChildSections: true); ProcessChildSection(configuration, ConfigurationKeys.Egress, includeChildSections: true); } else { - //Do not emit ApiKeyHash - IConfigurationSection authSection = ProcessChildSection(configuration, ConfigurationKeys.ApiAuthentication, includeChildSections: false); - if (authSection != null) + IConfigurationSection auth = ProcessChildSection(configuration, ConfigurationKeys.Authentication, includeChildSections: false); + if (null != auth) { _writer.WriteStartObject(); - ProcessChildSection(authSection, nameof(ApiAuthenticationOptions.ApiKeyHash), includeChildSections: false, redact: true); - ProcessChildSection(authSection, nameof(ApiAuthenticationOptions.ApiKeyHashType), includeChildSections: false, redact: false); - _writer.WriteEndObject(); + IConfigurationSection monitorApiKey = ProcessChildSection(auth, ConfigurationKeys.MonitorApiKey, includeChildSections: false); + if (null != monitorApiKey) + { + _writer.WriteStartObject(); + ProcessChildSection(monitorApiKey, nameof(MonitorApiKeyOptions.Subject), includeChildSections: false, redact: false); + // The PublicKey should only ever contain the public key, however we expect that accidents may occur and we should + // redact this field in the event the JWK contains the private key information. + ProcessChildSection(monitorApiKey, nameof(MonitorApiKeyOptions.PublicKey), includeChildSections: false, redact: true); + } } - + IConfigurationSection egress = ProcessChildSection(configuration, ConfigurationKeys.Egress, includeChildSections: false); if (egress != null) { diff --git a/src/Tools/dotnet-monitor/ConfigurationKeys.cs b/src/Tools/dotnet-monitor/ConfigurationKeys.cs index c052e7b0297..b9dc2b4f13c 100644 --- a/src/Tools/dotnet-monitor/ConfigurationKeys.cs +++ b/src/Tools/dotnet-monitor/ConfigurationKeys.cs @@ -6,18 +6,20 @@ namespace Microsoft.Diagnostics.Tools.Monitor { internal static class ConfigurationKeys { - public const string ApiAuthentication = nameof(ApiAuthentication); + public const string Authentication = nameof(RootOptions.Authentication); - public const string CorsConfiguration = nameof(CorsConfiguration); + public const string MonitorApiKey = nameof(AuthenticationOptions.MonitorApiKey); - public const string DiagnosticPort = nameof(DiagnosticPort); + public const string CorsConfiguration = nameof(RootOptions.CorsConfiguration); - public const string Egress = nameof(Egress); + public const string DiagnosticPort = nameof(RootOptions.DiagnosticPort); - public const string Metrics = nameof(Metrics); + public const string Egress = nameof(RootOptions.Egress); - public const string Storage = nameof(Storage); + public const string Metrics = nameof(RootOptions.Metrics); + public const string Storage = nameof(RootOptions.Storage); + public const string DefaultProcess = nameof(DefaultProcess); public const string Logging = nameof(Logging); diff --git a/src/Tools/dotnet-monitor/DiagnosticsMonitorCommandHandler.cs b/src/Tools/dotnet-monitor/DiagnosticsMonitorCommandHandler.cs index 861ef53ce66..373fdfb2922 100644 --- a/src/Tools/dotnet-monitor/DiagnosticsMonitorCommandHandler.cs +++ b/src/Tools/dotnet-monitor/DiagnosticsMonitorCommandHandler.cs @@ -3,12 +3,14 @@ // See the LICENSE file in the project root for more information. using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Hosting; using Microsoft.Diagnostics.Monitoring; using Microsoft.Diagnostics.Monitoring.WebApi; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -128,7 +130,7 @@ public static IHostBuilder CreateHostBuilder(IConsole console, string[] urls, st IHostBuilder hostBuilder = Host.CreateDefaultBuilder(); KeyAuthenticationMode authMode = noAuth ? KeyAuthenticationMode.NoAuth : tempApiKey ? KeyAuthenticationMode.TemporaryKey : KeyAuthenticationMode.StoredKey; - AuthOptions authenticationOptions = new AuthOptions(authMode); + AuthConfiguration authenticationOptions = new AuthConfiguration(authMode); EgressOutputConfiguration egressConfiguration = new EgressOutputConfiguration(httpEgressEnabled: !noHttpEgress); @@ -176,7 +178,7 @@ public static IHostBuilder CreateHostBuilder(IConsole console, string[] urls, st { //TODO Many of these service additions should be done through extension methods - services.AddSingleton(authenticationOptions); + services.AddSingleton(authenticationOptions); services.AddSingleton(egressConfiguration); @@ -185,21 +187,13 @@ public static IHostBuilder CreateHostBuilder(IConsole console, string[] urls, st // to observe other options in the future, at which point it might be good to // refactor the options observers for each into separate implementations and are // orchestrated by this single service. - services.AddSingleton(); + services.AddSingleton(); List authSchemas = null; if (authenticationOptions.EnableKeyAuth) { - services.ConfigureApiKeyConfiguration(context.Configuration); - - //Add support for Authentication and Authorization. - AuthenticationBuilder authBuilder = services.AddAuthentication(options => - { - options.DefaultAuthenticateScheme = AuthConstants.ApiKeySchema; - options.DefaultChallengeScheme = AuthConstants.ApiKeySchema; - }) - .AddScheme(AuthConstants.ApiKeySchema, _ => { }); - + AuthenticationBuilder authBuilder = services.ConfigureMonitorApiKeyAuthentication(context.Configuration); + authSchemas = new List { AuthConstants.ApiKeySchema }; if (authenticationOptions.EnableNegotiate) @@ -222,7 +216,6 @@ public static IHostBuilder CreateHostBuilder(IConsole console, string[] urls, st builder.AddRequirements(new AuthorizedUserRequirement()); builder.RequireAuthenticatedUser(); builder.AddAuthenticationSchemes(authSchemas.ToArray()); - }); } else @@ -299,14 +292,14 @@ public static IHostBuilder CreateHostBuilder(IConsole console, string[] urls, st return hostBuilder; } - private static void ConfigureTempApiHashKey(IConfigurationBuilder builder, AuthOptions authenticationOptions) + private static void ConfigureTempApiHashKey(IConfigurationBuilder builder, AuthConfiguration authenticationOptions) { - if (authenticationOptions.TemporaryKey != null) + if (authenticationOptions.TemporaryJwtKey != null) { builder.AddInMemoryCollection(new Dictionary { - { ConfigurationPath.Combine(ConfigurationKeys.ApiAuthentication, nameof(ApiAuthenticationOptions.ApiKeyHashType)), authenticationOptions.TemporaryKey.HashAlgorithm }, - { ConfigurationPath.Combine(ConfigurationKeys.ApiAuthentication, nameof(ApiAuthenticationOptions.ApiKeyHash)), authenticationOptions.TemporaryKey.HashValue }, + { ConfigurationPath.Combine(ConfigurationKeys.Authentication, ConfigurationKeys.MonitorApiKey, nameof(MonitorApiKeyOptions.Subject)), authenticationOptions.TemporaryJwtKey.Subject }, + { ConfigurationPath.Combine(ConfigurationKeys.Authentication, ConfigurationKeys.MonitorApiKey, nameof(MonitorApiKeyOptions.PublicKey)), authenticationOptions.TemporaryJwtKey.PublicKey }, }); } } diff --git a/src/Tools/dotnet-monitor/GenerateApiKeyCommandHandler.cs b/src/Tools/dotnet-monitor/GenerateApiKeyCommandHandler.cs index d3a00f97566..2670ca9c182 100644 --- a/src/Tools/dotnet-monitor/GenerateApiKeyCommandHandler.cs +++ b/src/Tools/dotnet-monitor/GenerateApiKeyCommandHandler.cs @@ -2,10 +2,15 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using Microsoft.Diagnostics.Monitoring.WebApi; +using Microsoft.Net.Http.Headers; using System; +using System.Collections.Generic; using System.CommandLine; -using System.CommandLine.IO; using System.Globalization; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; @@ -18,39 +23,76 @@ namespace Microsoft.Diagnostics.Tools.Monitor /// internal sealed class GenerateApiKeyCommandHandler { - public Task GenerateApiKey(CancellationToken token, int keyLength, string hashAlgorithm, IConsole console) + public Task GenerateApiKey(CancellationToken token, OutputFormat output, IConsole console) { - if (!HashAlgorithmChecker.IsAllowedAlgorithm(hashAlgorithm)) + GeneratedJwtKey newJwt = GeneratedJwtKey.Create(); + + StringBuilder outputBldr = new StringBuilder(); + + outputBldr.AppendLine(Strings.Message_GenerateApiKey); + outputBldr.AppendLine(); + outputBldr.AppendLine(string.Format(Strings.Message_GeneratedAuthorizationHeader, HeaderNames.Authorization, AuthConstants.ApiKeySchema, newJwt.Token)); + outputBldr.AppendLine(); + + RootOptions opts = new() { - console.Error.WriteLine( - string.Format( - CultureInfo.CurrentCulture, - Strings.ErrorMessage_ParameterNotAllowed, - nameof(hashAlgorithm), - hashAlgorithm)); - return Task.FromResult(1); - } + Authentication = new AuthenticationOptions() + { + MonitorApiKey = new MonitorApiKeyOptions() + { + Subject = newJwt.Subject, + PublicKey = newJwt.PublicKey, + } + } + }; - if (keyLength < ApiKeyAuthenticationHandler.ApiKeyByteMinLength || keyLength > ApiKeyAuthenticationHandler.ApiKeyByteMaxLength) + outputBldr.AppendFormat(CultureInfo.CurrentCulture, Strings.Message_SettingsDump, output); + outputBldr.AppendLine(); + switch (output) { - console.Error.WriteLine( - string.Format( - CultureInfo.CurrentCulture, - Strings.ErrorMessage_ParameterNotAllowedByteRange, - nameof(hashAlgorithm), - hashAlgorithm, - ApiKeyAuthenticationHandler.ApiKeyByteMinLength, - ApiKeyAuthenticationHandler.ApiKeyByteMaxLength)); - return Task.FromResult(1); + case OutputFormat.Json: + string optsJson = JsonSerializer.Serialize(opts, new JsonSerializerOptions() + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + WriteIndented = true + }); + outputBldr.AppendLine(optsJson); + break; + case OutputFormat.Text: + outputBldr.AppendLine(string.Format(Strings.Message_GeneratekeySubject, newJwt.Subject)); + outputBldr.AppendLine(string.Format(Strings.Message_GeneratekeyPublicKey, newJwt.PublicKey)); + break; + case OutputFormat.Cmd: + case OutputFormat.PowerShell: + case OutputFormat.Shell: + IDictionary optList = opts.ToEnvironmentConfiguration(); + foreach ((string name, string value) in optList) + { + outputBldr.AppendFormat(CultureInfo.InvariantCulture, GetFormatString(output), name, value); + outputBldr.AppendLine(); + } + break; } - GeneratedApiKey newKey = GeneratedApiKey.Create(keyLength, hashAlgorithm); - - console.Out.WriteLine(FormattableString.Invariant($"Authorization: {Monitoring.WebApi.AuthConstants.ApiKeySchema} {newKey.MonitorApiKey}")); - console.Out.WriteLine(FormattableString.Invariant($"ApiKeyHash: {newKey.HashValue}")); - console.Out.WriteLine(FormattableString.Invariant($"ApiKeyHashType: {newKey.HashAlgorithm}")); + outputBldr.AppendLine(); + console.Out.Write(outputBldr.ToString()); return Task.FromResult(0); } + + private string GetFormatString(OutputFormat output) + { + switch (output) + { + case OutputFormat.Cmd: + return "set {0}={1}"; + case OutputFormat.PowerShell: + return "$env:{0}=\"{1}\""; + case OutputFormat.Shell: + return "export {0}=\"{1}\""; + default: + throw new InvalidOperationException(string.Format(Strings.ErrorMessage_UnknownFormat, output)); + } + } } } diff --git a/src/Tools/dotnet-monitor/LoggingExtensions.cs b/src/Tools/dotnet-monitor/LoggingExtensions.cs index 8d4f8e9e859..afeb178f59c 100644 --- a/src/Tools/dotnet-monitor/LoggingExtensions.cs +++ b/src/Tools/dotnet-monitor/LoggingExtensions.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using System.Text.Json; namespace Microsoft.Diagnostics.Tools.Monitor { @@ -115,11 +116,7 @@ internal static class LoggingExtensions logLevel: LogLevel.Warning, formatString: Strings.LogFormatString_ApiKeyValidationFailure); - private static readonly Action _apiKeyAuthenticationOptionsChanged = - LoggerMessage.Define( - eventId: new EventId(22, "ApiKeyAuthenticationOptionsChanged"), - logLevel: LogLevel.Information, - formatString: Strings.LogFormatString_ApiKeyAuthenticationOptionsChanged); + // 22:ApiKeyAuthenticationOptionsChanged private static readonly Action _logTempKey = LoggerMessage.Define( @@ -133,10 +130,23 @@ internal static class LoggingExtensions logLevel: LogLevel.Warning, formatString: Strings.LogFormatString_DuplicateEgressProviderIgnored); + private static readonly Action _apiKeyAuthenticationOptionsValidated = + LoggerMessage.Define( + eventId: new EventId(25, "ApiKeyAuthenticationOptionsValidated"), + logLevel: LogLevel.Information, + formatString: Strings.LogFormatString_ApiKeyAuthenticationOptionsValidated); + + private static readonly Action _notifyPrivateKey = + LoggerMessage.Define( + eventId: new EventId(26, "NotifyPrivateKey"), + logLevel: LogLevel.Warning, + formatString: Strings.LogFormatString_NotifyPrivateKey); + public static void EgressProviderInvalidOptions(this ILogger logger, string providerName) { _egressProviderInvalidOptions(logger, providerName, null); } + public static void EgressCopyActionStreamToEgressStream(this ILogger logger, int bufferSize) { _egressCopyActionStreamToEgressStream(logger, bufferSize, null); @@ -207,15 +217,10 @@ public static void ApiKeyValidationFailures(this ILogger logger, IEnumerable name: "generatekey", description: Strings.HelpDescription_CommandGenerateKey) { - CommandHandler.Create(new GenerateApiKeyCommandHandler().GenerateApiKey), - HashAlgorithm(), KeyLength() + CommandHandler.Create(new GenerateApiKeyCommandHandler().GenerateApiKey), + Output() }; private static Command CollectCommand() => @@ -124,20 +124,12 @@ private static Option TempApiKey() => Argument = new Argument(name: "tempApiKey", getDefaultValue: () => false) }; - private static Option HashAlgorithm() => + private static Option Output() => new Option( - aliases: new[] { "-h", "--hash-algorithm" }, - description: Strings.HelpDescription_HashAlgorithm) + aliases: new[] { "-o", "--output" }, + description: Strings.HelpDescription_OutputFormat) { - Argument = new Argument(name: "hashAlgorithm", getDefaultValue: () => GeneratedApiKey.DefaultHashAlgorithm) - }; - - private static Option KeyLength() => - new Option( - aliases: new[] { "-l", "--key-length" }, - description: Strings.HelpDescription_KeyLength) - { - Argument = new Argument(name: "keyLength", getDefaultValue: () => GeneratedApiKey.DefaultKeyLength) + Argument = new Argument(name: "output", getDefaultValue: () => OutputFormat.Json) }; private static Option ConfigLevel() => @@ -170,6 +162,7 @@ public static Task Main(string[] args) return parser.InvokeAsync(args); } } + internal enum ConfigDisplayLevel { Redacted, diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Options/RootOptions.cs b/src/Tools/dotnet-monitor/RootOptions.cs similarity index 83% rename from src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Options/RootOptions.cs rename to src/Tools/dotnet-monitor/RootOptions.cs index e0b8482e346..5a062aca2b1 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Options/RootOptions.cs +++ b/src/Tools/dotnet-monitor/RootOptions.cs @@ -8,9 +8,9 @@ namespace Microsoft.Diagnostics.Tools.Monitor { internal partial class RootOptions { - public ApiAuthenticationOptions ApiAuthentication { get; set; } + public AuthenticationOptions Authentication { get; set; } - public CorsConfiguration CorsConfiguration { get; set; } + public CorsConfigurationOptions CorsConfiguration { get; set; } public DiagnosticPortOptions DiagnosticPort { get; set; } diff --git a/src/Tools/dotnet-monitor/ServiceCollectionExtensions.cs b/src/Tools/dotnet-monitor/ServiceCollectionExtensions.cs index d44058a28ab..eb43d122d1d 100644 --- a/src/Tools/dotnet-monitor/ServiceCollectionExtensions.cs +++ b/src/Tools/dotnet-monitor/ServiceCollectionExtensions.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.Diagnostics.Monitoring.WebApi; using Microsoft.Diagnostics.Tools.Monitor.Egress; using Microsoft.Diagnostics.Tools.Monitor.Egress.AzureBlob; @@ -22,13 +24,34 @@ public static IServiceCollection ConfigureMetrics(this IServiceCollection servic return ConfigureOptions(services, configuration, ConfigurationKeys.Metrics); } - public static IServiceCollection ConfigureApiKeyConfiguration(this IServiceCollection services, IConfiguration configuration) + public static IServiceCollection ConfigureMonitorApiKeyOptions(this IServiceCollection services, IConfiguration configuration) { - return ConfigureOptions(services, configuration, ConfigurationKeys.ApiAuthentication) - // Loads and validates ApiAuthenticationOptions into ApiKeyAuthenticationOptions - .AddSingleton, ApiKeyAuthenticationPostConfigureOptions>() - // Notifies that ApiKeyAuthenticationOptions is changed when ApiAuthenticationOptions is changed. - .AddSingleton, ApiKeyAuthenticationOptionsChangeTokenSource>(); + ConfigureOptions(services, configuration, ConfigurationKeys.MonitorApiKey); + + // Loads and validates MonitorApiKeyOptions into MonitorApiKeyConfiguration + services.AddSingleton, MonitorApiKeyPostConfigure>(); + // Notifies that MonitorApiKeyConfiguration is changed when MonitorApiKeyOptions is changed. + services.AddSingleton, MonitorApiKeyChangeTokenSource>(); + + return services; + } + + public static AuthenticationBuilder ConfigureMonitorApiKeyAuthentication(this IServiceCollection services, IConfiguration configuration) + { + IConfigurationSection authSection = configuration.GetSection(ConfigurationKeys.Authentication); + services.ConfigureMonitorApiKeyOptions(authSection); + + // Notifies that the JwtBearerOptions change when MonitorApiKeyConfiguration gets changed. + services.AddSingleton, JwtBearerChangeTokenSource>(); + // Adds the JwtBearerOptions configuration source, which will provide the updated JwtBearerOptions when MonitorApiKeyConfiguration updates + services.TryAddEnumerable(ServiceDescriptor.Singleton, JwtBearerPostConfigure>()); + + // AddJwtBearer will consume the JwtBearerOptions generated by ConfigureMonitorApiKeyConfiguration + AuthenticationBuilder builder = services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme); + // We need to provide a slightly modified JwtBearerHandler to do auth, the basic JwtBearerHandler does not accept option changes after initialization + builder.AddScheme(JwtBearerDefaults.AuthenticationScheme, null, _ => { }); + + return builder; } public static IServiceCollection ConfigureStorage(this IServiceCollection services, IConfiguration configuration) diff --git a/src/Tools/dotnet-monitor/Startup.cs b/src/Tools/dotnet-monitor/Startup.cs index 0f2435bda1d..f29c5d1bfb9 100644 --- a/src/Tools/dotnet-monitor/Startup.cs +++ b/src/Tools/dotnet-monitor/Startup.cs @@ -99,9 +99,9 @@ public void Configure( IApplicationBuilder app, IHostApplicationLifetime lifetime, IWebHostEnvironment env, - IAuthOptions options, + IAuthConfiguration options, AddressListenResults listenResults, - ApiKeyAuthenticationOptionsObserver optionsObserver, + MonitorApiKeyConfigurationObserver optionsObserver, ILogger logger) { // These errors are populated before Startup.Configure is called because @@ -141,7 +141,7 @@ public void Configure( { if (options.KeyAuthenticationMode == KeyAuthenticationMode.TemporaryKey) { - logger.LogTempKey(options.TemporaryKey.MonitorApiKey); + logger.LogTempKey(options.TemporaryJwtKey.Token); } //Auth is enabled and we are binding on http. Make sure we log a warning. @@ -181,8 +181,8 @@ public void Configure( app.UseAuthentication(); app.UseAuthorization(); - CorsConfiguration corsConfiguration = new CorsConfiguration(); - Configuration.Bind(nameof(CorsConfiguration), corsConfiguration); + CorsConfigurationOptions corsConfiguration = new CorsConfigurationOptions(); + Configuration.Bind(ConfigurationKeys.CorsConfiguration, corsConfiguration); if (!string.IsNullOrEmpty(corsConfiguration.AllowedOrigins)) { app.UseCors(builder => builder.WithOrigins(corsConfiguration.GetOrigins()).AllowAnyHeader().AllowAnyMethod()); @@ -217,7 +217,7 @@ private static void LogBoundAddresses(IFeatureCollection features, AddressListen } } - private static void LogElevatedPermissions(IAuthOptions options, ILogger logger) + private static void LogElevatedPermissions(IAuthConfiguration options, ILogger logger) { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { diff --git a/src/Tools/dotnet-monitor/Strings.Designer.cs b/src/Tools/dotnet-monitor/Strings.Designer.cs index 0a6a6ae2160..26eae97fee2 100644 --- a/src/Tools/dotnet-monitor/Strings.Designer.cs +++ b/src/Tools/dotnet-monitor/Strings.Designer.cs @@ -150,15 +150,6 @@ internal static string ErrorMessage_EgressUnableToCreateIntermediateFile { } } - /// - /// Looks up a localized string similar to The {0} field value '{1}' is not allowed.. - /// - internal static string ErrorMessage_FieldNotAllowed { - get { - return ResourceManager.GetString("ErrorMessage_FieldNotAllowed", resourceCulture); - } - } - /// /// Looks up a localized string similar to The {0} field could not be decoded as hex string.. /// @@ -204,6 +195,24 @@ internal static string ErrorMessage_InvalidAuthHeader { } } + /// + /// Looks up a localized string similar to The configuration parameter {0} must be contain a valid jwk, the value '{1}' could not be parsed as a Json Web Key. The expected format is a Json Web Key written as JSON which is base64Url encoded. {2}. + /// + internal static string ErrorMessage_InvalidJwk { + get { + return ResourceManager.GetString("ErrorMessage_InvalidJwk", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The configuration parameter {0} must be base64Url encoded, the value '{1}' could not be parsed as a base64Url-encoded string.. + /// + internal static string ErrorMessage_NotBase64 { + get { + return ResourceManager.GetString("ErrorMessage_NotBase64", resourceCulture); + } + } + /// /// Looks up a localized string similar to The {0} parameter value '{1}' is not allowed.. /// @@ -222,6 +231,24 @@ internal static string ErrorMessage_ParameterNotAllowedByteRange { } } + /// + /// Looks up a localized string similar to The configuration parameter {0} must be contain a jwk that is valid for use with dotnet-monitor. The provided Json Web Key must be have a key-type of EC or RSA and must-not have private key information because this this key is only used for signature verification.. + /// + internal static string ErrorMessage_RejectedJwk { + get { + return ResourceManager.GetString("ErrorMessage_RejectedJwk", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The {0} field or the {1} field is required.. + /// + internal static string ErrorMessage_TwoFieldsMissing { + get { + return ResourceManager.GetString("ErrorMessage_TwoFieldsMissing", resourceCulture); + } + } + /// /// Looks up a localized string similar to Unable to bind any urls.. /// @@ -240,6 +267,15 @@ internal static string ErrorMessage_UnhandledConnectionMode { } } + /// + /// Looks up a localized string similar to Unknown output format type: {0}. + /// + internal static string ErrorMessage_UnknownFormat { + get { + return ResourceManager.GetString("ErrorMessage_UnknownFormat", resourceCulture); + } + } + /// /// Looks up a localized string similar to Monitor logs and metrics in a .NET application send the results to a chosen destination.. /// @@ -276,24 +312,6 @@ internal static string HelpDescription_CommandShow { } } - /// - /// Looks up a localized string similar to The string representing the hash algorithm used to compute ApiKeyHash store in configuration, typically SHA256.. - /// - internal static string HelpDescription_HashAlgorithm { - get { - return ResourceManager.GetString("HelpDescription_HashAlgorithm", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The length of the MonitorApiKey in bytes.. - /// - internal static string HelpDescription_KeyLength { - get { - return ResourceManager.GetString("HelpDescription_KeyLength", resourceCulture); - } - } - /// /// Looks up a localized string similar to The fully qualified path and filename of the diagnostic port to which runtime instances can connect.. /// @@ -367,11 +385,29 @@ internal static string HelpDescription_OptionUrls { } /// - /// Looks up a localized string similar to {apiAuthenticationConfigKey} settings have changed.. + /// Looks up a localized string similar to The format to display the output in. Valid values are Json, Text, and EnvVar.. /// - internal static string LogFormatString_ApiKeyAuthenticationOptionsChanged { + internal static string HelpDescription_OutputFormat { get { - return ResourceManager.GetString("LogFormatString_ApiKeyAuthenticationOptionsChanged", resourceCulture); + return ResourceManager.GetString("HelpDescription_OutputFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {apiAuthenticationConfigKey} settings have changed. The new settings have passed validation.. + /// + internal static string LogFormatString_ApiKeyAuthenticationOptionsValidated { + get { + return ResourceManager.GetString("LogFormatString_ApiKeyAuthenticationOptionsValidated", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The {configName} have changed, new values: Subject={subject}, PublicKey={publicKey}. + /// + internal static string LogFormatString_ApiKeyOptionsChanged { + get { + return ResourceManager.GetString("LogFormatString_ApiKeyOptionsChanged", resourceCulture); } } @@ -501,6 +537,15 @@ internal static string LogFormatString_NoAuthentication { } } + /// + /// Looks up a localized string similar to The configuration field {fieldName} contains private key information. The private key information is not required for dotnet-monitor to verify a token signature and it is strongly recomended to only provide the public key.. + /// + internal static string LogFormatString_NotifyPrivateKey { + get { + return ResourceManager.GetString("LogFormatString_NotifyPrivateKey", resourceCulture); + } + } + /// /// Looks up a localized string similar to {failure}. /// @@ -528,6 +573,51 @@ internal static string LogFormatString_UnableToListenToAddress { } } + /// + /// Looks up a localized string similar to Generated ApiKey for dotnet-monitor; use the following header for authorization:. + /// + internal static string Message_GenerateApiKey { + get { + return ResourceManager.GetString("Message_GenerateApiKey", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0}: {1} {2}. + /// + internal static string Message_GeneratedAuthorizationHeader { + get { + return ResourceManager.GetString("Message_GeneratedAuthorizationHeader", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Public Key: {0}. + /// + internal static string Message_GeneratekeyPublicKey { + get { + return ResourceManager.GetString("Message_GeneratekeyPublicKey", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Subject: {0}. + /// + internal static string Message_GeneratekeySubject { + get { + return ResourceManager.GetString("Message_GeneratekeySubject", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Settings in {0} format:. + /// + internal static string Message_SettingsDump { + get { + return ResourceManager.GetString("Message_SettingsDump", resourceCulture); + } + } + /// /// Looks up a localized string similar to :NOT PRESENT:. /// diff --git a/src/Tools/dotnet-monitor/Strings.resx b/src/Tools/dotnet-monitor/Strings.resx index 56a946f8101..a6dde0b6ecb 100644 --- a/src/Tools/dotnet-monitor/Strings.resx +++ b/src/Tools/dotnet-monitor/Strings.resx @@ -166,13 +166,6 @@ Gets the format string for file system egress provider failure due to inability to create an intermediate file. 1 Format Parameter: 0. intermediateFileDirectory: The directory where the intermediate file was attempted to be created. - - - The {0} field value '{1}' is not allowed. - Gets the format string for rejecting a field value. -2 Format Parameters: -0. fieldName: The name of the field that failed validation -1. fieldValue: The value in the field that was rejected The {0} field could not be decoded as hex string. @@ -198,6 +191,21 @@ Invalid authentication header. Gets a string similar to "Invalid authentication header.". + + The configuration parameter {0} must be contain a valid jwk, the value '{1}' could not be parsed as a Json Web Key. The expected format is a Json Web Key written as JSON which is base64Url encoded. {2} + Gets the format string for rejecting a config param because it is not a json-encoded jwk. +3 Format Parameters: +0. configName: The variable name for the provided string. +1. value: The value of the string that could not be parsed. +2. errorMessage: Additional error message text. + + + The configuration parameter {0} must be base64Url encoded, the value '{1}' could not be parsed as a base64Url-encoded string. + Gets the format string for rejecting a string because it is not base64url encoded. +2 Format Parameters: +0. stringName: The variable name for the provided string. +1. stringValue: The value of the string that could not be parsed. + The {0} parameter value '{1}' is not allowed. Gets the format string for rejecting a parameter value. @@ -213,6 +221,19 @@ 1. parameterValue: The value in the parameter that was rejected 2. minBytes: The minimum number of bytes acceptable 3. maxBytes: The maximum number of bytes acceptable + + + The configuration parameter {0} must be contain a jwk that is valid for use with dotnet-monitor. The provided Json Web Key must be have a key-type of EC or RSA and must-not have private key information because this this key is only used for signature verification. + Gets the format string for rejecting a config param because it is not a jwk type that is accepted. +1 Format Parameter: +0. configName: The variable name for the provided string. + + + The {0} field or the {1} field is required. + Gets the format string for rejecting validation due to 2 missing fields where at least one is required. +2 Format Parameters: +0. fieldNameOne: The name of the first field that is missing +1. fieldNameTwo: The name of the second field that is missing Unable to bind any urls. @@ -223,6 +244,12 @@ Gets the format string for egress provider failure due to missing provider. 1 Format Parameter: 0. connectionMode: The provided DiagnosticPortConnectionMode that could not be parsed. + + + Unknown output format type: {0} + Gets the format string for an unknown output format. +1 Format Parameter: +0. formatType: The value of the unknown OutputFormat type. Monitor logs and metrics in a .NET application send the results to a chosen destination. @@ -240,14 +267,6 @@ Shows configuration, as if dotnet-monitor collect was executed with these parameters. Gets the string to display in help that explains what the 'show' command does. - - The string representing the hash algorithm used to compute ApiKeyHash store in configuration, typically SHA256. - Gets the string to display in help that explains what the '--hash-algorithm' option does. - - - The length of the MonitorApiKey in bytes. - Gets the string to display in help that explains what the '--key-length' option does. - The fully qualified path and filename of the diagnostic port to which runtime instances can connect. Gets the string to display in help that explains what the '--diagnostic-port' option does. @@ -280,11 +299,23 @@ Bindings for the REST api. Gets the string to display in help that explains what the '--urls' option does. - - {apiAuthenticationConfigKey} settings have changed. + + The format to display the output in. Valid values are Json, Text, and EnvVar. + Gets the string to display in help that explains what the '--output' option does. + + + {apiAuthenticationConfigKey} settings have changed. The new settings have passed validation. Gets the format string that is printed in the 22:ApiKeyAuthenticationOptionsChanged event. 1 Format Parameter: 1. apiAuthenticationConfigKey: The string key of ApiAuthentication object in configuration. + + + The {configName} have changed, new values: Subject={subject}, PublicKey={publicKey} + Gets the format string that is printed in the 21:ApiKeyValidationFailure event. +3 Format Parameters: +1. configName: The string key of MonitorApiKey object in configuration. +2. subject: The value of the Subject property from the MonitorApiKeyOptions object. +3. publicKey: The value of the PublicKey property from the MonitorApiKeyOptions object. {apiAuthenticationConfigKey} settings are invalid: {validationFailure} @@ -376,6 +407,9 @@ Gets the format string that is printed in the 13:NoAuthentication event. 0 Format Parameters + + The configuration field {fieldName} contains private key information. The private key information is not required for dotnet-monitor to verify a token signature and it is strongly recomended to only provide the public key. + {failure} Gets the format string that is printed in the 18:OptionsValidationFailure event. @@ -393,6 +427,26 @@ 1 Format Parameter: 1. url: The URL that could not have a listener attached to it + + Generated ApiKey for dotnet-monitor; use the following header for authorization: + Gets a header string for the output of GenerateApiKeyCommand. + + + {0}: {1} {2} + Gets the string for writing the http Authorization header. + + + Public Key: {0} + Gets the string for displaying the PublicKey configuration value + + + Subject: {0} + Gets the string for displaying the Subject configuration value + + + Settings in {0} format: + Gets a header string for the configuration output of GenerateApiKeyCommand. + :NOT PRESENT: Gets a string similar to ":NOT PRESENT:". diff --git a/src/Tools/dotnet-monitor/dotnet-monitor.csproj b/src/Tools/dotnet-monitor/dotnet-monitor.csproj index 04fe3e9aa47..74dda206f54 100644 --- a/src/Tools/dotnet-monitor/dotnet-monitor.csproj +++ b/src/Tools/dotnet-monitor/dotnet-monitor.csproj @@ -22,10 +22,12 @@ + + From d251001ff9b192461a41c4753a2bc69b8a50d167 Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Fri, 13 Aug 2021 13:00:52 +0000 Subject: [PATCH 09/23] Update dependencies from https://github.com/dotnet/runtime build 20210813.5 (#717) [main] Update dependencies from dotnet/runtime --- eng/Version.Details.xml | 4 ++-- eng/Versions.props | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 3e476aeda9a..a14ad2a4890 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -30,9 +30,9 @@ https://github.com/dotnet/aspnetcore 9c2b65a8f9ac334db5575160b2e07a35c25d0585 - + https://github.com/dotnet/runtime - 0ea8653e1f0ada5c7a15515430c6f16585911af4 + f66b142980b2b0df738158457458e003944dc7f6 diff --git a/eng/Versions.props b/eng/Versions.props index 280ddcfadbc..c3537fbc6b6 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -34,7 +34,7 @@ 5.0.0-preview.21411.1 5.0.0-preview.21411.1 - 6.0.0-rc.1.21411.5 + 6.0.0-rc.1.21413.5 1.0.240901 From 6fed2b429b88652c795dddea2ffc4caf21dc4d08 Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Fri, 13 Aug 2021 13:01:00 +0000 Subject: [PATCH 10/23] Update dependencies from https://github.com/dotnet/aspnetcore build 20210812.9 (#718) [main] Update dependencies from dotnet/aspnetcore --- eng/Version.Details.xml | 4 ++-- eng/Versions.props | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index a14ad2a4890..eda33fb07c0 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -26,9 +26,9 @@ https://github.com/dotnet/symstore d8e2990b89c53632653d7d67f3481cc72773f25c - + https://github.com/dotnet/aspnetcore - 9c2b65a8f9ac334db5575160b2e07a35c25d0585 + 24ea7ef4b753e1d1d56d13ffa4791af242bcefd0 https://github.com/dotnet/runtime diff --git a/eng/Versions.props b/eng/Versions.props index c3537fbc6b6..747ac120226 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -29,7 +29,7 @@ 6.0.0-beta.21410.8 - 6.0.0-rc.1.21411.15 + 6.0.0-rc.1.21412.9 5.0.0-preview.21411.1 5.0.0-preview.21411.1 From a364f7e68b67f3ba987aed77e54c991fb6fee793 Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Fri, 13 Aug 2021 17:33:04 +0000 Subject: [PATCH 11/23] Update dependencies from https://github.com/dotnet/diagnostics build 20210812.1 (#716) [main] Update dependencies from dotnet/diagnostics --- eng/Version.Details.xml | 8 ++++---- eng/Versions.props | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index eda33fb07c0..ec37c4f52bd 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -4,13 +4,13 @@ https://github.com/dotnet/command-line-api 166610c56ff732093f0145a2911d4f6c40b786da - + https://github.com/dotnet/diagnostics - 626b7f8f23053672f989a8174336897fe1b57434 + 7ffbade45f5eba01ee700fd43f5d7e78f2ca6d5a - + https://github.com/dotnet/diagnostics - 626b7f8f23053672f989a8174336897fe1b57434 + 7ffbade45f5eba01ee700fd43f5d7e78f2ca6d5a diff --git a/eng/Versions.props b/eng/Versions.props index 747ac120226..100f5f8f3b4 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -31,8 +31,8 @@ 6.0.0-rc.1.21412.9 - 5.0.0-preview.21411.1 - 5.0.0-preview.21411.1 + 5.0.0-preview.21412.1 + 5.0.0-preview.21412.1 6.0.0-rc.1.21413.5 From ed78b4842d7b5bb794330e9a26bc1936421b202e Mon Sep 17 00:00:00 2001 From: Patrick Fenelon Date: Fri, 13 Aug 2021 11:40:08 -0700 Subject: [PATCH 12/23] Add detailed message to output in TestSchemaBaseline (#715) * Add detailed message to output in TestSchemaBaseline. This shows context when the baseline differs from the generated schema. --- .../SchemaGenerationTests.cs | 31 +++++++++++++------ 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.ConfigurationSchema.UnitTests/SchemaGenerationTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.ConfigurationSchema.UnitTests/SchemaGenerationTests.cs index 5d5073c19ca..32f06cbf40d 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.ConfigurationSchema.UnitTests/SchemaGenerationTests.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.ConfigurationSchema.UnitTests/SchemaGenerationTests.cs @@ -5,6 +5,7 @@ using Microsoft.Diagnostics.Monitoring.TestCommon.Runners; using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Reflection; using System.Threading; @@ -77,31 +78,43 @@ private async Task GenerateSchema() return new FileStream(tempSchema, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, 4096, FileOptions.DeleteOnClose); } - private async Task CompareLines(TextReader first, TextReader second) + private async Task CompareLines(TextReader baseline, TextReader generated) { - IList firstLines = await ReadAllLines(first); - IList secondLines = await ReadAllLines(second); + IList baselineLines = await ReadAllLines(baseline); + IList generatedLines = await ReadAllLines(generated); - for (int i = 0; i < Math.Min(firstLines.Count, secondLines.Count); i++) + for (int i = 0; i < Math.Min(baselineLines.Count, generatedLines.Count); i++) { - if (!string.Equals(firstLines[i], secondLines[i], StringComparison.Ordinal)) + if (!string.Equals(baselineLines[i], generatedLines[i], StringComparison.Ordinal)) { _outputHelper.WriteLine($"Differs from baseline on line {i + 1}:"); - _outputHelper.WriteLine(firstLines[i]); - _outputHelper.WriteLine(secondLines[i]); + PrintSection(_outputHelper, nameof(baseline), baselineLines, i); + PrintSection(_outputHelper, nameof(generated), generatedLines, i); return false; } } - if (firstLines.Count != secondLines.Count) + if (baselineLines.Count != generatedLines.Count) { - _outputHelper.WriteLine($"Count differs from baseline: {firstLines.Count} {secondLines.Count}"); + _outputHelper.WriteLine($"Count differs from baseline: {baselineLines.Count} {generatedLines.Count}"); return false; } return true; } + private static void PrintSection(ITestOutputHelper outputHelper, string header, IList lines, int lineHighlighted, int contextQty = 7) + { + outputHelper.WriteLine($"-----{header}-----"); + int startLine = Math.Max(0, lineHighlighted - contextQty); + int endLine = Math.Min(lines.Count, lineHighlighted + contextQty); + int formatQty = (endLine + 1).ToString("D").Length; // Get the length of the biggest number (add 1 for the 1-based index) + for (int i = startLine; i <= endLine; i++) + { + outputHelper.WriteLine("{0}:{1}{2}", (i+1).ToString("D" + formatQty.ToString(CultureInfo.InvariantCulture)), (i == lineHighlighted) ? " >" : " ", lines[i]); + } + } + private async Task> ReadAllLines(TextReader reader) { var lines = new List(); From d9280b3b7db5b0cffb94b0c17906ca08b1813bd0 Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Sat, 14 Aug 2021 12:42:09 +0000 Subject: [PATCH 13/23] Update dependencies from https://github.com/dotnet/arcade build 20210812.1 (#721) [main] Update dependencies from dotnet/arcade --- eng/Version.Details.xml | 8 ++++---- eng/Versions.props | 2 +- global.json | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index ec37c4f52bd..2f2b76efb9e 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -14,13 +14,13 @@ - + https://github.com/dotnet/arcade - e10772e3594e46a031574c20a4145441737ac56d + 58ac7035021bd7277ef7582338afd25403fc9aea - + https://github.com/dotnet/arcade - e10772e3594e46a031574c20a4145441737ac56d + 58ac7035021bd7277ef7582338afd25403fc9aea https://github.com/dotnet/symstore diff --git a/eng/Versions.props b/eng/Versions.props index 100f5f8f3b4..410904a1c6e 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -27,7 +27,7 @@ --> - 6.0.0-beta.21410.8 + 6.0.0-beta.21412.1 6.0.0-rc.1.21412.9 diff --git a/global.json b/global.json index 1d09eebba7e..4571c406869 100644 --- a/global.json +++ b/global.json @@ -16,6 +16,6 @@ }, "msbuild-sdks": { "Microsoft.Build.NoTargets": "2.0.1", - "Microsoft.DotNet.Arcade.Sdk": "6.0.0-beta.21410.8" + "Microsoft.DotNet.Arcade.Sdk": "6.0.0-beta.21412.1" } } From b358d9fbe03bf622e90ef80d5c755cc6b52de2bb Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Sat, 14 Aug 2021 12:58:23 +0000 Subject: [PATCH 14/23] Update dependencies from https://github.com/dotnet/runtime build 20210814.1 (#722) [main] Update dependencies from dotnet/runtime --- eng/Version.Details.xml | 4 ++-- eng/Versions.props | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 2f2b76efb9e..e81039b3548 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -30,9 +30,9 @@ https://github.com/dotnet/aspnetcore 24ea7ef4b753e1d1d56d13ffa4791af242bcefd0 - + https://github.com/dotnet/runtime - f66b142980b2b0df738158457458e003944dc7f6 + 1f26d24c0ffba88504d66736d5000e7f750f0f58 diff --git a/eng/Versions.props b/eng/Versions.props index 410904a1c6e..8a0fa20d0be 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -34,7 +34,7 @@ 5.0.0-preview.21412.1 5.0.0-preview.21412.1 - 6.0.0-rc.1.21413.5 + 6.0.0-rc.1.21414.1 1.0.240901 From 203216d4be5ee3ef09303ba3940199d0e3557f51 Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Sun, 15 Aug 2021 12:51:05 +0000 Subject: [PATCH 15/23] Update dependencies from https://github.com/dotnet/runtime build 20210814.4 (#724) [main] Update dependencies from dotnet/runtime --- eng/Version.Details.xml | 4 ++-- eng/Versions.props | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index e81039b3548..c4dd058d5f8 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -30,9 +30,9 @@ https://github.com/dotnet/aspnetcore 24ea7ef4b753e1d1d56d13ffa4791af242bcefd0 - + https://github.com/dotnet/runtime - 1f26d24c0ffba88504d66736d5000e7f750f0f58 + 6c39236e9f8353758b5b841c3ce171051dc71579 diff --git a/eng/Versions.props b/eng/Versions.props index 8a0fa20d0be..d79da6c27bb 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -34,7 +34,7 @@ 5.0.0-preview.21412.1 5.0.0-preview.21412.1 - 6.0.0-rc.1.21414.1 + 6.0.0-rc.1.21414.4 1.0.240901 From 1430b931d9321e07b8bef77088901fcbdbef0eca Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Mon, 16 Aug 2021 12:45:43 +0000 Subject: [PATCH 16/23] Update dependencies from https://github.com/dotnet/runtime build 20210815.6 (#725) [main] Update dependencies from dotnet/runtime --- eng/Version.Details.xml | 4 ++-- eng/Versions.props | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index c4dd058d5f8..9ef1763c21c 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -30,9 +30,9 @@ https://github.com/dotnet/aspnetcore 24ea7ef4b753e1d1d56d13ffa4791af242bcefd0 - + https://github.com/dotnet/runtime - 6c39236e9f8353758b5b841c3ce171051dc71579 + fde6b37e985605d862c070256de7c97e2a3f3342 diff --git a/eng/Versions.props b/eng/Versions.props index d79da6c27bb..bb13f33184c 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -34,7 +34,7 @@ 5.0.0-preview.21412.1 5.0.0-preview.21412.1 - 6.0.0-rc.1.21414.4 + 6.0.0-rc.1.21415.6 1.0.240901 From 6f5de0c7e97f22cfd11edf58ecca35659e25b744 Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Tue, 17 Aug 2021 00:48:41 +0000 Subject: [PATCH 17/23] Update dependencies from https://github.com/dotnet/aspnetcore build 20210813.12 (#723) [main] Update dependencies from dotnet/aspnetcore --- eng/Version.Details.xml | 4 ++-- eng/Versions.props | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 9ef1763c21c..c94c9a460e4 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -26,9 +26,9 @@ https://github.com/dotnet/symstore d8e2990b89c53632653d7d67f3481cc72773f25c - + https://github.com/dotnet/aspnetcore - 24ea7ef4b753e1d1d56d13ffa4791af242bcefd0 + bcfbd5cc47dde7f2be50a24721f24a020dc77356 https://github.com/dotnet/runtime diff --git a/eng/Versions.props b/eng/Versions.props index bb13f33184c..93912c61eb8 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -29,7 +29,7 @@ 6.0.0-beta.21412.1 - 6.0.0-rc.1.21412.9 + 6.0.0-rc.1.21413.12 5.0.0-preview.21412.1 5.0.0-preview.21412.1 From caa59a5ebde8f0cc3eb82f306a902c6f2b4ba78d Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Tue, 17 Aug 2021 12:42:20 +0000 Subject: [PATCH 18/23] Update dependencies from https://github.com/dotnet/diagnostics build 20210813.1 (#727) [main] Update dependencies from dotnet/diagnostics --- eng/Version.Details.xml | 8 ++++---- eng/Versions.props | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index c94c9a460e4..038f4486a35 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -4,13 +4,13 @@ https://github.com/dotnet/command-line-api 166610c56ff732093f0145a2911d4f6c40b786da - + https://github.com/dotnet/diagnostics - 7ffbade45f5eba01ee700fd43f5d7e78f2ca6d5a + 9ed8c2fce33fe4136d77e019e319510ba77cc4c6 - + https://github.com/dotnet/diagnostics - 7ffbade45f5eba01ee700fd43f5d7e78f2ca6d5a + 9ed8c2fce33fe4136d77e019e319510ba77cc4c6 diff --git a/eng/Versions.props b/eng/Versions.props index 93912c61eb8..8fd0402a77e 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -31,8 +31,8 @@ 6.0.0-rc.1.21413.12 - 5.0.0-preview.21412.1 - 5.0.0-preview.21412.1 + 5.0.0-preview.21413.1 + 5.0.0-preview.21413.1 6.0.0-rc.1.21415.6 From 7ed86ad55e7806c2bc6eeaf920fcbc8c1dd5fadb Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Tue, 17 Aug 2021 12:52:42 +0000 Subject: [PATCH 19/23] Update dependencies from https://github.com/dotnet/arcade build 20210813.4 (#728) [main] Update dependencies from dotnet/arcade --- eng/Version.Details.xml | 8 ++++---- eng/Versions.props | 2 +- global.json | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 038f4486a35..b2f596e03ed 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -14,13 +14,13 @@ - + https://github.com/dotnet/arcade - 58ac7035021bd7277ef7582338afd25403fc9aea + 9b7027ba718462aa6410cef61a8be5a4283e7528 - + https://github.com/dotnet/arcade - 58ac7035021bd7277ef7582338afd25403fc9aea + 9b7027ba718462aa6410cef61a8be5a4283e7528 https://github.com/dotnet/symstore diff --git a/eng/Versions.props b/eng/Versions.props index 8fd0402a77e..90c9b4129a3 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -27,7 +27,7 @@ --> - 6.0.0-beta.21412.1 + 6.0.0-beta.21413.4 6.0.0-rc.1.21413.12 diff --git a/global.json b/global.json index 4571c406869..afdac051c9c 100644 --- a/global.json +++ b/global.json @@ -16,6 +16,6 @@ }, "msbuild-sdks": { "Microsoft.Build.NoTargets": "2.0.1", - "Microsoft.DotNet.Arcade.Sdk": "6.0.0-beta.21412.1" + "Microsoft.DotNet.Arcade.Sdk": "6.0.0-beta.21413.4" } } From 384898ab45cefccbcb0976dd014da3ca48520ab3 Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Tue, 17 Aug 2021 13:04:10 +0000 Subject: [PATCH 20/23] Update dependencies from https://github.com/dotnet/runtime build 20210817.1 (#729) [main] Update dependencies from dotnet/runtime --- eng/Version.Details.xml | 4 ++-- eng/Versions.props | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index b2f596e03ed..e0bc438eb95 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -30,9 +30,9 @@ https://github.com/dotnet/aspnetcore bcfbd5cc47dde7f2be50a24721f24a020dc77356 - + https://github.com/dotnet/runtime - fde6b37e985605d862c070256de7c97e2a3f3342 + 14b34eb02bc8969b77c0d3a1e39fb38f450625cf diff --git a/eng/Versions.props b/eng/Versions.props index 90c9b4129a3..943ba02d69b 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -34,7 +34,7 @@ 5.0.0-preview.21413.1 5.0.0-preview.21413.1 - 6.0.0-rc.1.21415.6 + 6.0.0-rc.1.21417.1 1.0.240901 From 860cfcd20ec597d5c37bd567c1344a29073219d1 Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Tue, 17 Aug 2021 15:56:26 +0000 Subject: [PATCH 21/23] Update dependencies from https://github.com/dotnet/aspnetcore build 20210817.1 (#730) [main] Update dependencies from dotnet/aspnetcore --- eng/Version.Details.xml | 4 ++-- eng/Versions.props | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index e0bc438eb95..7a9846368f5 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -26,9 +26,9 @@ https://github.com/dotnet/symstore d8e2990b89c53632653d7d67f3481cc72773f25c - + https://github.com/dotnet/aspnetcore - bcfbd5cc47dde7f2be50a24721f24a020dc77356 + 0ca2ed9af69e7e334b8e3c1de1d015017f138988 https://github.com/dotnet/runtime diff --git a/eng/Versions.props b/eng/Versions.props index 943ba02d69b..327bebfade5 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -29,7 +29,7 @@ 6.0.0-beta.21413.4 - 6.0.0-rc.1.21413.12 + 6.0.0-rc.1.21417.1 5.0.0-preview.21413.1 5.0.0-preview.21413.1 From 89a0e84e34201e20552829361325c32a7c277611 Mon Sep 17 00:00:00 2001 From: Justin Anderson Date: Tue, 17 Aug 2021 11:52:43 -0700 Subject: [PATCH 22/23] Small changes to the WithCancellation extension method. (#719) --- .../TaskExtensions.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/TaskExtensions.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/TaskExtensions.cs index a928aef0a8c..9ba05e008e3 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/TaskExtensions.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/TaskExtensions.cs @@ -44,7 +44,7 @@ public static async Task SafeAwait(this Task task, ITestOutputHelper outputHelpe public static async Task WithCancellation(this Task task, CancellationToken token) { - CancellationTokenSource localTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token); + using CancellationTokenSource localTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token); try { @@ -56,11 +56,8 @@ public static async Task WithCancellation(this Task task, CancellationToken toke } finally { - // If the token provided wasn't cancelled, cancel our own token - if (!token.IsCancellationRequested) - { - localTokenSource.Cancel(); - } + // Cancel to make sure Task.Delay token registration is removed. + localTokenSource.Cancel(); } } } From 6145472dd4b6579f952544c4c891987ec752d460 Mon Sep 17 00:00:00 2001 From: Justin Anderson Date: Tue, 17 Aug 2021 14:30:58 -0700 Subject: [PATCH 23/23] Move *EndpointInfo implementations to dotnet-monitor. (#726) Move *EndpointInfo implementations to dotnet-monitor. Add dotnet-monitor unit test assembly and move *EndpointInfo tests to it. Consolidate calculating entry point assembly path into single utility method. --- dotnet-monitor.sln | 7 ++ ...soft.Diagnostics.Monitoring.Options.csproj | 2 +- ...osoft.Diagnostics.Monitoring.WebApi.csproj | 1 + .../SchemaGenerationTests.cs | 3 +- .../OpenApiGeneratorTests.cs | 3 +- .../AssemblyHelper.cs | 31 +++++++++ .../DotNetHost.cs | 15 +++++ .../DotNetHost.cs.template | 4 ++ .../GenerateDotNetHost.targets | 3 + .../Runners/AppRunner.cs | 13 ++-- .../Runners/DotNetRunner.cs | 11 +++- .../TargetFrameworkMoniker.cs | 64 +++++++++++++++++++ .../Runners/MonitorRunner.cs | 21 ++---- .../EndpointInfoSourceTests.cs | 30 +++++---- ...agnostics.Monitoring.Tool.UnitTests.csproj | 13 ++++ .../EndpointInfo/ClientEndpointInfoSource.cs | 3 +- .../EndpointInfo/EndpointInfo.cs | 3 +- .../EndpointInfo/ServerEndpointInfoSource.cs | 3 +- 18 files changed, 185 insertions(+), 45 deletions(-) create mode 100644 src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/AssemblyHelper.cs create mode 100644 src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/TargetFrameworkMoniker.cs rename src/Tests/{Microsoft.Diagnostics.Monitoring.WebApi.UnitTests => Microsoft.Diagnostics.Monitoring.Tool.UnitTests}/EndpointInfoSourceTests.cs (93%) create mode 100644 src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests.csproj rename src/{Microsoft.Diagnostics.Monitoring.WebApi => Tools/dotnet-monitor}/EndpointInfo/ClientEndpointInfoSource.cs (95%) rename src/{Microsoft.Diagnostics.Monitoring.WebApi => Tools/dotnet-monitor}/EndpointInfo/EndpointInfo.cs (97%) rename src/{Microsoft.Diagnostics.Monitoring.WebApi => Tools/dotnet-monitor}/EndpointInfo/ServerEndpointInfoSource.cs (99%) diff --git a/dotnet-monitor.sln b/dotnet-monitor.sln index e7e8bd71012..f6849d81e37 100644 --- a/dotnet-monitor.sln +++ b/dotnet-monitor.sln @@ -36,6 +36,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Diagnostics.Monit EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Diagnostics.Monitoring.Options", "src\Microsoft.Diagnostics.Monitoring.Options\Microsoft.Diagnostics.Monitoring.Options.csproj", "{173F959B-231B-45D1-8328-9460D4C5BC71}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Diagnostics.Monitoring.Tool.UnitTests", "src\Tests\Microsoft.Diagnostics.Monitoring.Tool.UnitTests\Microsoft.Diagnostics.Monitoring.Tool.UnitTests.csproj", "{C5D14E28-DACA-4884-B513-9246B788BC22}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -86,6 +88,10 @@ Global {173F959B-231B-45D1-8328-9460D4C5BC71}.Debug|Any CPU.Build.0 = Debug|Any CPU {173F959B-231B-45D1-8328-9460D4C5BC71}.Release|Any CPU.ActiveCfg = Release|Any CPU {173F959B-231B-45D1-8328-9460D4C5BC71}.Release|Any CPU.Build.0 = Release|Any CPU + {C5D14E28-DACA-4884-B513-9246B788BC22}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C5D14E28-DACA-4884-B513-9246B788BC22}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C5D14E28-DACA-4884-B513-9246B788BC22}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C5D14E28-DACA-4884-B513-9246B788BC22}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -105,6 +111,7 @@ Global {422ABBF6-6236-4042-AACA-09531DBDFBAA} = {C7568468-1C79-4944-8136-18812A7F9EA7} {3AD0A40B-C569-4712-9764-7A788B9CD811} = {C7568468-1C79-4944-8136-18812A7F9EA7} {173F959B-231B-45D1-8328-9460D4C5BC71} = {19FAB78C-3351-4911-8F0C-8C6056401740} + {C5D14E28-DACA-4884-B513-9246B788BC22} = {C7568468-1C79-4944-8136-18812A7F9EA7} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {46465737-C938-44FC-BE1A-4CE139EBB5E0} diff --git a/src/Microsoft.Diagnostics.Monitoring.Options/Microsoft.Diagnostics.Monitoring.Options.csproj b/src/Microsoft.Diagnostics.Monitoring.Options/Microsoft.Diagnostics.Monitoring.Options.csproj index a369689f274..6b640ff0314 100644 --- a/src/Microsoft.Diagnostics.Monitoring.Options/Microsoft.Diagnostics.Monitoring.Options.csproj +++ b/src/Microsoft.Diagnostics.Monitoring.Options/Microsoft.Diagnostics.Monitoring.Options.csproj @@ -1,4 +1,4 @@ - + netstandard2.0 diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Microsoft.Diagnostics.Monitoring.WebApi.csproj b/src/Microsoft.Diagnostics.Monitoring.WebApi/Microsoft.Diagnostics.Monitoring.WebApi.csproj index d370f21f973..391f981ec3d 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Microsoft.Diagnostics.Monitoring.WebApi.csproj +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Microsoft.Diagnostics.Monitoring.WebApi.csproj @@ -42,6 +42,7 @@ + diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.ConfigurationSchema.UnitTests/SchemaGenerationTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.ConfigurationSchema.UnitTests/SchemaGenerationTests.cs index 32f06cbf40d..7540213983c 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.ConfigurationSchema.UnitTests/SchemaGenerationTests.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.ConfigurationSchema.UnitTests/SchemaGenerationTests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using Microsoft.Diagnostics.Monitoring.TestCommon; using Microsoft.Diagnostics.Monitoring.TestCommon.Runners; using System; using System.Collections.Generic; @@ -31,7 +32,7 @@ public class SchemaGenerationTests private const string SchemaGeneratorName = "Microsoft.Diagnostics.Monitoring.ConfigurationSchema"; private static readonly string SchemaGeneratorPath = - CurrentExecutingAssemblyPath.Replace(Assembly.GetExecutingAssembly().GetName().Name, SchemaGeneratorName); + AssemblyHelper.GetAssemblyArtifactBinPath(Assembly.GetExecutingAssembly(), SchemaGeneratorName); public SchemaGenerationTests(ITestOutputHelper outputHelper) { diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.OpenApiGen.UnitTests/OpenApiGeneratorTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.OpenApiGen.UnitTests/OpenApiGeneratorTests.cs index 53354591f41..6b329f727c3 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.OpenApiGen.UnitTests/OpenApiGeneratorTests.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.OpenApiGen.UnitTests/OpenApiGeneratorTests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using Microsoft.Diagnostics.Monitoring.TestCommon; using Microsoft.Diagnostics.Monitoring.TestCommon.Runners; using Microsoft.OpenApi.Extensions; using Microsoft.OpenApi.Models; @@ -32,7 +33,7 @@ public class OpenApiGeneratorTests Path.Combine(Path.GetDirectoryName(CurrentExecutingAssemblyPath), OpenApiBaselineName); private static readonly string OpenApiGenPath = - CurrentExecutingAssemblyPath.Replace(Assembly.GetExecutingAssembly().GetName().Name, OpenApiGenName); + AssemblyHelper.GetAssemblyArtifactBinPath(Assembly.GetExecutingAssembly(), OpenApiGenName); private readonly ITestOutputHelper _outputHelper; diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/AssemblyHelper.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/AssemblyHelper.cs new file mode 100644 index 00000000000..c279d938f9a --- /dev/null +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/AssemblyHelper.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Reflection; + +namespace Microsoft.Diagnostics.Monitoring.TestCommon +{ + public static class AssemblyHelper + { + public static string GetAssemblyArtifactBinPath( + Assembly testAssembly, + string assemblyName, + TargetFrameworkMoniker tfm = TargetFrameworkMoniker.Current) + { + string assemblyPath = testAssembly.Location + .Replace(testAssembly.GetName().Name, assemblyName); + + if (tfm != TargetFrameworkMoniker.Current) + { + string currentFolderName = DotNetHost.BuiltTargetFrameworkMoniker.ToFolderName(); + string targetFolderName = tfm.ToFolderName(); + + assemblyPath = assemblyPath.Replace(currentFolderName, targetFolderName); + } + + return assemblyPath; + } + } +} diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/DotNetHost.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/DotNetHost.cs index 075ae74b3cb..1940bb9c9d2 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/DotNetHost.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/DotNetHost.cs @@ -20,5 +20,20 @@ public partial class DotNetHost public static string HostExePath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? @"..\..\..\..\..\.dotnet\dotnet.exe" : "../../../../../.dotnet/dotnet"; + + public static TargetFrameworkMoniker BuiltTargetFrameworkMoniker + { + get + { + // Update with specific TFM when building this assembly for a new target framework. +#if NETCOREAPP3_1 + return TargetFrameworkMoniker.NetCoreApp31; +#elif NET5_0 + return TargetFrameworkMoniker.Net50; +#elif NET6_0 + return TargetFrameworkMoniker.Net60; +#endif + } + } } } diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/DotNetHost.cs.template b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/DotNetHost.cs.template index 7567dc0123f..3266e0dd1f7 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/DotNetHost.cs.template +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/DotNetHost.cs.template @@ -10,5 +10,9 @@ namespace Microsoft.Diagnostics.Monitoring.TestCommon { public static readonly string CurrentAspNetCoreVersionString = "$MicrosoftAspNetCoreAppVersion$"; public static readonly string CurrentNetCoreVersionString = "$MicrosoftNetCoreAppVersion$"; + + public static readonly string NetCore31VersionString = "$MicrosoftNetCoreApp31Version$"; + public static readonly string NetCore50VersionString = "$MicrosoftNetCoreApp50Version$"; + public static readonly string NetCore60VersionString = "$MicrosoftNetCoreApp60Version$"; } } \ No newline at end of file diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/GenerateDotNetHost.targets b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/GenerateDotNetHost.targets index 675e35cc6e5..1f727be2d92 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/GenerateDotNetHost.targets +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/GenerateDotNetHost.targets @@ -21,6 +21,9 @@ $([System.IO.File]::ReadAllText('DotNetHost.cs.template')) $(TemplateContent.Replace('$MicrosoftNetCoreAppVersion$', '$(NetCoreAppVersion)')) $(TransformedContent.Replace('$MicrosoftAspNetCoreAppVersion$', '$(AspNetCoreAppVersion)')) + $(TransformedContent.Replace('$MicrosoftNetCoreApp31Version$', '$(MicrosoftNETCoreApp31Version)')) + $(TransformedContent.Replace('$MicrosoftNetCoreApp50Version$', '$(MicrosoftNETCoreApp50Version)')) + $(TransformedContent.Replace('$MicrosoftNetCoreApp60Version$', '$(MicrosoftNETCoreApp60Version)')) diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/Runners/AppRunner.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/Runners/AppRunner.cs index 49dcd2a8c14..279ab12f631 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/Runners/AppRunner.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/Runners/AppRunner.cs @@ -35,17 +35,10 @@ public sealed class AppRunner : IAsyncDisposable private bool _isDiposed; - /// - /// The path of the current test assembly. - /// - private string CurrentTestAssembly => _testAssembly.Location; - /// /// The path to the application. /// - private string AppPath => - CurrentTestAssembly - .Replace(_testAssembly.GetName().Name, "Microsoft.Diagnostics.Monitoring.UnitTestApp"); + private string AppPath => AssemblyHelper.GetAssemblyArtifactBinPath(_testAssembly, "Microsoft.Diagnostics.Monitoring.UnitTestApp"); /// /// The mode of the diagnostic port connection. Default is @@ -74,13 +67,15 @@ public sealed class AppRunner : IAsyncDisposable public int AppId { get; } - public AppRunner(ITestOutputHelper outputHelper, Assembly testAssembly, int appId = 1) + public AppRunner(ITestOutputHelper outputHelper, Assembly testAssembly, int appId = 1, TargetFrameworkMoniker tfm = TargetFrameworkMoniker.Current) { AppId = appId; _testAssembly = testAssembly; _outputHelper = new PrefixedOutputHelper(outputHelper, FormattableString.Invariant($"[App{appId}] ")); + _runner.TargetFramework = tfm; + _adapter = new LoggingRunnerAdapter(_outputHelper, _runner); _adapter.ReceivedStandardOutputLine += StandardOutputCallback; } diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/Runners/DotNetRunner.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/Runners/DotNetRunner.cs index 855806001d7..2bb8e562c7a 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/Runners/DotNetRunner.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/Runners/DotNetRunner.cs @@ -70,7 +70,7 @@ public sealed class DotNetRunner : IDisposable /// Gets the process ID of the running process. /// public int ProcessId => _process.Id; - + /// /// Gets a that reads stderr. /// @@ -86,6 +86,11 @@ public sealed class DotNetRunner : IDisposable /// public StreamReader StandardOutput => _process.StandardOutput; + /// + /// Get or set the target framework on which the application should run. + /// + public TargetFrameworkMoniker TargetFramework { get; set; } = TargetFrameworkMoniker.Current; + /// /// Determines if should wait for the diagnostic pipe to be available. /// @@ -123,10 +128,10 @@ public async Task StartAsync(CancellationToken token) switch (FrameworkReference) { case DotNetFrameworkReference.Microsoft_AspNetCore_App: - frameworkVersion = DotNetHost.CurrentAspNetCoreVersionString; + frameworkVersion = TargetFramework.GetAspNetCoreFrameworkVersion(); break; case DotNetFrameworkReference.Microsoft_NetCore_App: - frameworkVersion = DotNetHost.CurrentNetCoreVersionString; + frameworkVersion = TargetFramework.GetNetCoreAppFrameworkVersion(); break; default: throw new InvalidOperationException($"Unsupported framework reference: {FrameworkReference}"); diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/TargetFrameworkMoniker.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/TargetFrameworkMoniker.cs new file mode 100644 index 00000000000..54f7ca93c59 --- /dev/null +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/TargetFrameworkMoniker.cs @@ -0,0 +1,64 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace Microsoft.Diagnostics.Monitoring.TestCommon +{ + public enum TargetFrameworkMoniker + { + Current, + NetCoreApp31, + Net50, + Net60 + } + + public static class TargetFrameworkMonikerExtensions + { + public static string GetAspNetCoreFrameworkVersion(this TargetFrameworkMoniker moniker) + { + switch (moniker) + { + case TargetFrameworkMoniker.Current: + return DotNetHost.CurrentAspNetCoreVersionString; + } + throw CreateUnsupportedException(moniker); + } + + public static string GetNetCoreAppFrameworkVersion(this TargetFrameworkMoniker moniker) + { + switch (moniker) + { + case TargetFrameworkMoniker.Current: + return DotNetHost.CurrentNetCoreVersionString; + case TargetFrameworkMoniker.NetCoreApp31: + return DotNetHost.NetCore31VersionString; + case TargetFrameworkMoniker.Net50: + return DotNetHost.NetCore50VersionString; + case TargetFrameworkMoniker.Net60: + return DotNetHost.NetCore60VersionString; + } + throw CreateUnsupportedException(moniker); + } + + public static string ToFolderName(this TargetFrameworkMoniker moniker) + { + switch (moniker) + { + case TargetFrameworkMoniker.Net50: + return "net5.0"; + case TargetFrameworkMoniker.NetCoreApp31: + return "netcoreapp3.1"; + case TargetFrameworkMoniker.Net60: + return "net6.0"; + } + throw CreateUnsupportedException(moniker); + } + + private static ArgumentException CreateUnsupportedException(TargetFrameworkMoniker moniker) + { + return new ArgumentException($"Unsupported target framework moniker: {moniker:G}"); + } + } +} diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Runners/MonitorRunner.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Runners/MonitorRunner.cs index bcb8fe61cc6..564ddf19bdf 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Runners/MonitorRunner.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Runners/MonitorRunner.cs @@ -46,25 +46,14 @@ internal class MonitorRunner : IAsyncDisposable protected Task RunnerExitedTask => _runner.ExitedTask; /// - /// The path of the currently executing assembly. - /// - private static string CurrentExecutingAssemblyPath => - Assembly.GetExecutingAssembly().Location; - - /// - /// The target framework name of the currently executing assembly. - /// - private static string CurrentTargetFrameworkFolderName => - new FileInfo(CurrentExecutingAssemblyPath).Directory.Name; - - /// - /// The path to dotnet-monitor. It is currently only build for the + /// The path to dotnet-monitor. It is currently only built for the /// netcoreapp3.1 target framework. /// private static string DotNetMonitorPath => - CurrentExecutingAssemblyPath - .Replace(Assembly.GetExecutingAssembly().GetName().Name, "dotnet-monitor") - .Replace(CurrentTargetFrameworkFolderName, "netcoreapp3.1"); + AssemblyHelper.GetAssemblyArtifactBinPath( + Assembly.GetExecutingAssembly(), + "dotnet-monitor", + TargetFrameworkMoniker.NetCoreApp31); private string SharedConfigDirectoryPath => Path.Combine(_runnerTmpPath, "SharedConfig"); diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.WebApi.UnitTests/EndpointInfoSourceTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/EndpointInfoSourceTests.cs similarity index 93% rename from src/Tests/Microsoft.Diagnostics.Monitoring.WebApi.UnitTests/EndpointInfoSourceTests.cs rename to src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/EndpointInfoSourceTests.cs index ea82bc88d65..ff08632e164 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.WebApi.UnitTests/EndpointInfoSourceTests.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/EndpointInfoSourceTests.cs @@ -4,6 +4,8 @@ using Microsoft.Diagnostics.Monitoring.TestCommon; using Microsoft.Diagnostics.Monitoring.TestCommon.Runners; +using Microsoft.Diagnostics.Monitoring.WebApi; +using Microsoft.Diagnostics.Tools.Monitor; using System; using System.Collections.Generic; using System.Linq; @@ -13,9 +15,8 @@ using Xunit; using Xunit.Abstractions; -namespace Microsoft.Diagnostics.Monitoring.WebApi.UnitTests +namespace Microsoft.Diagnostics.Monitoring.Tool.UnitTests { -#if NET5_0_OR_GREATER public class EndpointInfoSourceTests { private static readonly TimeSpan DefaultNegativeVerificationTimeout = TimeSpan.FromSeconds(2); @@ -104,8 +105,9 @@ public async Task ServerSourceThrowsWhenMultipleStartTest() /// Tests that the server endpoint info source can properly enumerate endpoint infos when a single /// target connects to it and "disconnects" from it. /// - [Fact] - public async Task ServerSourceAddRemoveSingleConnectionTest() + [Theory] + [MemberData(nameof(GetTfmsSupportingPortListener))] + public async Task ServerSourceAddRemoveSingleConnectionTest(TargetFrameworkMoniker appTfm) { await using var source = CreateServerSource(out string transportName); source.Start(); @@ -113,7 +115,7 @@ public async Task ServerSourceAddRemoveSingleConnectionTest() var endpointInfos = await GetEndpointInfoAsync(source); Assert.Empty(endpointInfos); - AppRunner runner = CreateAppRunner(transportName); + AppRunner runner = CreateAppRunner(transportName, appTfm); Task newEndpointInfoTask = source.WaitForNewEndpointInfoAsync(runner, CommonTestTimeouts.StartProcess); @@ -143,8 +145,9 @@ await runner.ExecuteAsync(async () => /// Tests that the server endpoint info source can properly enumerate endpoint infos when multiple /// targets connect to it and "disconnect" from it. /// - [Fact] - public async Task ServerSourceAddRemoveMultipleConnectionTest() + [Theory] + [MemberData(nameof(GetTfmsSupportingPortListener))] + public async Task ServerSourceAddRemoveMultipleConnectionTest(TargetFrameworkMoniker appTfm) { await using var source = CreateServerSource(out string transportName); source.Start(); @@ -159,7 +162,7 @@ public async Task ServerSourceAddRemoveMultipleConnectionTest() // Start all app instances for (int i = 0; i < appCount; i++) { - runners[i] = CreateAppRunner(transportName, appId: i + 1); + runners[i] = CreateAppRunner(transportName, appTfm, appId: i + 1); newEndpointInfoTasks[i] = source.WaitForNewEndpointInfoAsync(runners[i], CommonTestTimeouts.StartProcess); } @@ -199,6 +202,12 @@ await runners.ExecuteAsync(async () => Assert.Empty(endpointInfos); } + public static IEnumerable GetTfmsSupportingPortListener() + { + yield return new object[] { TargetFrameworkMoniker.Net50 }; + yield return new object[] { TargetFrameworkMoniker.Net60 }; + } + private TestServerEndpointInfoSource CreateServerSource(out string transportName) { DiagnosticPortHelper.Generate(DiagnosticPortConnectionMode.Listen, out _, out transportName); @@ -206,9 +215,9 @@ private TestServerEndpointInfoSource CreateServerSource(out string transportName return new TestServerEndpointInfoSource(transportName, _outputHelper); } - private AppRunner CreateAppRunner(string transportName = null, int appId = 1) + private AppRunner CreateAppRunner(string transportName, TargetFrameworkMoniker tfm, int appId = 1) { - AppRunner appRunner = new(_outputHelper, Assembly.GetExecutingAssembly(), appId); + AppRunner appRunner = new(_outputHelper, Assembly.GetExecutingAssembly(), appId, tfm); appRunner.ConnectionMode = DiagnosticPortConnectionMode.Connect; appRunner.DiagnosticPortPath = transportName; appRunner.ScenarioName = TestAppScenarios.AsyncWait.Name; @@ -305,5 +314,4 @@ internal override void OnRemovedEndpointInfo(EndpointInfo info) } } } -#endif } diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests.csproj b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests.csproj new file mode 100644 index 00000000000..7af05c77ac3 --- /dev/null +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests.csproj @@ -0,0 +1,13 @@ + + + + netcoreapp3.1 + + + + + + + + + diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/EndpointInfo/ClientEndpointInfoSource.cs b/src/Tools/dotnet-monitor/EndpointInfo/ClientEndpointInfoSource.cs similarity index 95% rename from src/Microsoft.Diagnostics.Monitoring.WebApi/EndpointInfo/ClientEndpointInfoSource.cs rename to src/Tools/dotnet-monitor/EndpointInfo/ClientEndpointInfoSource.cs index 686d6ec61e7..cf42d89f119 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/EndpointInfo/ClientEndpointInfoSource.cs +++ b/src/Tools/dotnet-monitor/EndpointInfo/ClientEndpointInfoSource.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using Microsoft.Diagnostics.Monitoring.WebApi; using Microsoft.Diagnostics.NETCore.Client; using System; using System.Collections.Generic; @@ -10,7 +11,7 @@ using System.Threading; using System.Threading.Tasks; -namespace Microsoft.Diagnostics.Monitoring.WebApi +namespace Microsoft.Diagnostics.Tools.Monitor { internal sealed class ClientEndpointInfoSource : IEndpointInfoSourceInternal { diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/EndpointInfo/EndpointInfo.cs b/src/Tools/dotnet-monitor/EndpointInfo/EndpointInfo.cs similarity index 97% rename from src/Microsoft.Diagnostics.Monitoring.WebApi/EndpointInfo/EndpointInfo.cs rename to src/Tools/dotnet-monitor/EndpointInfo/EndpointInfo.cs index c6a0dac8190..6ed52508230 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/EndpointInfo/EndpointInfo.cs +++ b/src/Tools/dotnet-monitor/EndpointInfo/EndpointInfo.cs @@ -2,11 +2,12 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using Microsoft.Diagnostics.Monitoring.WebApi; using Microsoft.Diagnostics.NETCore.Client; using System; using System.Diagnostics; -namespace Microsoft.Diagnostics.Monitoring.WebApi +namespace Microsoft.Diagnostics.Tools.Monitor { [DebuggerDisplay("{DebuggerDisplay,nq}")] internal class EndpointInfo : IEndpointInfo diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/EndpointInfo/ServerEndpointInfoSource.cs b/src/Tools/dotnet-monitor/EndpointInfo/ServerEndpointInfoSource.cs similarity index 99% rename from src/Microsoft.Diagnostics.Monitoring.WebApi/EndpointInfo/ServerEndpointInfoSource.cs rename to src/Tools/dotnet-monitor/EndpointInfo/ServerEndpointInfoSource.cs index 67b4b1e65b3..eb1857e9355 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/EndpointInfo/ServerEndpointInfoSource.cs +++ b/src/Tools/dotnet-monitor/EndpointInfo/ServerEndpointInfoSource.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using Microsoft.Diagnostics.Monitoring.WebApi; using Microsoft.Diagnostics.NETCore.Client; using System; using System.Collections.Generic; @@ -10,7 +11,7 @@ using System.Threading; using System.Threading.Tasks; -namespace Microsoft.Diagnostics.Monitoring.WebApi +namespace Microsoft.Diagnostics.Tools.Monitor { /// /// Aggregates diagnostic endpoints that are established at a transport path via a reversed server.