From 2eb7ba21525b206e7d1dadc84ed3e9eb0d5f5e7c Mon Sep 17 00:00:00 2001 From: devlooped-bot Date: Wed, 12 Nov 2025 00:28:45 +0000 Subject: [PATCH] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Bump=20files=20with=20dotn?= =?UTF-8?q?et-file=20sync=20=EF=BB=BF#=20devlooped/oss?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add explicit write permissions from caller workflow https://github.com/devlooped/oss/commit/8fa147d - Fix improper first / in gh api repos https://github.com/devlooped/oss/commit/f2b690c - Ensure lf for Scriban templates always https://github.com/devlooped/oss/commit/4a9aa32 - Disable warnings for non-packable test projects https://github.com/devlooped/oss/commit/95b338b - Improve default Product metadata, remove .git from user-facing URLs https://github.com/devlooped/oss/commit/4339749 - Switch to dotnet-env for .NET SDK setup https://github.com/devlooped/oss/commit/56c2b85 - Use GH_TOKEN if available for PR https://github.com/devlooped/oss/commit/77e83f2 - Group MEAI packages together https://github.com/devlooped/oss/commit/e733294 - Change label from 'docs' to 'dependencies' https://github.com/devlooped/oss/commit/2d1fb4e - Ignore .env files recursively https://github.com/devlooped/oss/commit/3776526 - Revert indent size for project files https://github.com/devlooped/oss/commit/a62c459 # devlooped/SponsorLink - Introduce standalone SponsorManifest for read/validate https://github.com/devlooped/SponsorLink/commit/a755e4b - Fix version/override for CPV https://github.com/devlooped/SponsorLink/commit/8a40822 - Improve wording on editor usage requiring sponsorship https://github.com/devlooped/SponsorLink/commit/21d8dac - Bump JWT also in samples https://github.com/devlooped/SponsorLink/commit/e8ec200 --- .editorconfig | 5 + .gitattributes | 1 + .github/actions/dotnet/action.yml | 35 ++++ .github/dependabot.yml | 8 + .github/workflows/build.yml | 11 +- .github/workflows/dotnet-env.yml | 44 +++++ .github/workflows/dotnet-file.yml | 7 +- .github/workflows/includes.yml | 26 ++- .github/workflows/publish.yml | 10 +- .gitignore | 1 + .netconfig | 100 ++++++----- readme.md | 61 ++++--- src/Directory.Build.props | 17 ++ src/Directory.Build.targets | 7 +- .../SponsorLink.Analyzer.Tests.targets | 4 +- src/SponsorLink/SponsorLink.Analyzer.targets | 8 +- src/SponsorLink/SponsorLink/ManifestStatus.cs | 25 --- src/SponsorLink/SponsorLink/Resources.es.resx | 3 + src/SponsorLink/SponsorLink/Resources.resx | 2 +- src/SponsorLink/SponsorLink/SponsorLink.cs | 78 --------- .../SponsorLink/SponsorLink.csproj | 2 +- .../SponsorLink/SponsorManifest.cs | 160 ++++++++++++++++++ ...orLinkTests.cs => SponsorManifestTests.cs} | 59 ++++--- src/SponsorLink/Tests/Tests.csproj | 4 +- 24 files changed, 440 insertions(+), 238 deletions(-) create mode 100644 .github/actions/dotnet/action.yml create mode 100644 .github/workflows/dotnet-env.yml delete mode 100644 src/SponsorLink/SponsorLink/ManifestStatus.cs create mode 100644 src/SponsorLink/SponsorLink/SponsorManifest.cs rename src/SponsorLink/Tests/{SponsorLinkTests.cs => SponsorManifestTests.cs} (63%) diff --git a/.editorconfig b/.editorconfig index 4cab270..3ba6837 100644 --- a/.editorconfig +++ b/.editorconfig @@ -30,6 +30,8 @@ indent_size = 2 # Dotnet code style settings: [*.{cs,vb}] +tab_width = 4 + # Sort using and Import directives with System.* appearing first dotnet_sort_system_directives_first = true # Avoid "this." and "Me." if not necessary @@ -57,6 +59,9 @@ dotnet_style_require_accessibility_modifiers = omit_if_default:error # IDE0040: Add accessibility modifiers dotnet_diagnostic.IDE0040.severity = error +# IDE1100: Error reading content of source file 'Project.TargetFrameworkMoniker' (i.e. from ThisAssembly) +dotnet_diagnostic.IDE1100.severity = none + [*.cs] # Top-level files are definitely OK csharp_using_directive_placement = outside_namespace:silent diff --git a/.gitattributes b/.gitattributes index 4f89148..3095556 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,6 +1,7 @@ # normalize by default * text=auto encoding=UTF-8 *.sh text eol=lf +*.sbn eol=lf # These are windows specific files which we may as well ensure are # always crlf on checkout diff --git a/.github/actions/dotnet/action.yml b/.github/actions/dotnet/action.yml new file mode 100644 index 0000000..c876ae3 --- /dev/null +++ b/.github/actions/dotnet/action.yml @@ -0,0 +1,35 @@ +name: ⚙ dotnet +description: Configures dotnet if the repo/org defines the DOTNET custom property + +runs: + using: composite + steps: + - name: 🔎 dotnet + id: dotnet + shell: bash + run: | + VERSIONS=$(gh api repos/${{ github.repository }}/properties/values | jq -r '.[] | select(.property_name == "DOTNET") | .value') + # Remove extra whitespace from VERSIONS + VERSIONS=$(echo "$VERSIONS" | tr -s ' ' | tr -d ' ') + # Convert comma-separated to newline-separated + NEWLINE_VERSIONS=$(echo "$VERSIONS" | tr ',' '\n') + # Validate versions + while IFS= read -r version; do + if ! [[ $version =~ ^[0-9]+(\.[0-9]+(\.[0-9]+)?)?(\.x)?$ ]]; then + echo "Error: Invalid version format: $version" + exit 1 + fi + done <<< "$NEWLINE_VERSIONS" + # Write multiline output to $GITHUB_OUTPUT + { + echo 'versions<> $GITHUB_OUTPUT + + - name: ⚙ dotnet + if: steps.dotnet.outputs.versions != '' + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + ${{ steps.dotnet.outputs.versions }} diff --git a/.github/dependabot.yml b/.github/dependabot.yml index c95eb73..11c5d7d 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -24,6 +24,11 @@ updates: Extensions: patterns: - "Microsoft.Extensions*" + exclude-patterns: + - "Microsoft.Extensions.AI*" + ExtensionsAI: + patterns: + - "Microsoft.Extensions.AI*" Web: patterns: - "Microsoft.AspNetCore*" @@ -38,3 +43,6 @@ updates: ProtoBuf: patterns: - "protobuf-*" + Spectre: + patterns: + - "Spectre.Console*" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6653044..d6bd793 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -28,6 +28,7 @@ env: GH_TOKEN: ${{ secrets.GH_TOKEN }} MSBUILDTERMINALLOGGER: auto Configuration: ${{ github.event.inputs.configuration || 'Release' }} + SLEET_FEED_URL: ${{ vars.SLEET_FEED_URL }} defaults: run: @@ -65,12 +66,7 @@ jobs: fetch-depth: 0 - name: ⚙ dotnet - uses: actions/setup-dotnet@v4 - with: - dotnet-version: | - 6.x - 8.x - 9.x + uses: devlooped/actions-dotnet-env@v1 - name: 🙏 build run: dotnet build -m:1 -bl:build.binlog @@ -104,6 +100,9 @@ jobs: submodules: recursive fetch-depth: 0 + - name: ⚙ dotnet + uses: devlooped/actions-dotnet-env@v1 + - name: ✓ ensure format run: | dotnet format whitespace --verify-no-changes -v:diag --exclude ~/.nuget diff --git a/.github/workflows/dotnet-env.yml b/.github/workflows/dotnet-env.yml new file mode 100644 index 0000000..a76d0fd --- /dev/null +++ b/.github/workflows/dotnet-env.yml @@ -0,0 +1,44 @@ +name: dotnet-env +on: + workflow_dispatch: + push: + branches: + - main + paths: + - '**/*.*proj' + +jobs: + which-dotnet: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + + steps: + - name: 🤖 defaults + uses: devlooped/actions-bot@v1 + with: + name: ${{ secrets.BOT_NAME }} + email: ${{ secrets.BOT_EMAIL }} + gh_token: ${{ secrets.GH_TOKEN }} + github_token: ${{ secrets.GITHUB_TOKEN }} + + - name: 🤘 checkout + uses: actions/checkout@v4 + with: + token: ${{ env.GH_TOKEN }} + + - name: 🤌 dotnet + uses: devlooped/actions-which-dotnet@v1 + + - name: ✍ pull request + uses: peter-evans/create-pull-request@v7 + with: + base: main + branch: which-dotnet + delete-branch: true + labels: dependencies + title: "⚙ Update dotnet versions" + body: "Update dotnet versions" + commit-message: "Update dotnet versions" + token: ${{ env.GH_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/dotnet-file.yml b/.github/workflows/dotnet-file.yml index 083f24d..402f6ba 100644 --- a/.github/workflows/dotnet-file.yml +++ b/.github/workflows/dotnet-file.yml @@ -12,5 +12,10 @@ env: jobs: run: + permissions: + contents: write uses: devlooped/oss/.github/workflows/dotnet-file-core.yml@main - secrets: inherit \ No newline at end of file + secrets: + BOT_NAME: ${{ secrets.BOT_NAME }} + BOT_EMAIL: ${{ secrets.BOT_EMAIL }} + GH_TOKEN: ${{ secrets.GH_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/includes.yml b/.github/workflows/includes.yml index d787ccb..b591d40 100644 --- a/.github/workflows/includes.yml +++ b/.github/workflows/includes.yml @@ -5,8 +5,9 @@ on: branches: - 'main' paths: - - '**.md' + - '**.md' - '!changelog.md' + - 'osmfeula.txt' jobs: includes: @@ -31,14 +32,33 @@ jobs: - name: +Mᐁ includes uses: devlooped/actions-includes@v1 + - name: 📝 OSMF EULA + shell: pwsh + run: | + $file = "osmfeula.txt" + $props = "src/Directory.Build.props" + if (-not (test-path $file) -or -not (test-path $props)) { + exit 0 + } + + $product = dotnet msbuild $props -getproperty:Product + if (-not $product) { + write-error 'To use OSMF EULA, ensure the $(Product) property is set in Directory.props' + exit 1 + } + + ((get-content -raw $file) -replace '\$product\$',$product).trim() | set-content $file + - name: ✍ pull request uses: peter-evans/create-pull-request@v6 with: - add-paths: '**.md' + add-paths: | + **.md + osmfeula.txt base: main branch: markdown-includes delete-branch: true - labels: docs + labels: dependencies author: ${{ env.BOT_AUTHOR }} committer: ${{ env.BOT_AUTHOR }} commit-message: +Mᐁ includes diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 8b065d7..03e57d9 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -15,7 +15,8 @@ env: VersionLabel: ${{ github.ref }} GH_TOKEN: ${{ secrets.GH_TOKEN }} MSBUILDTERMINALLOGGER: auto - + SLEET_FEED_URL: https://api.nuget.org/v3/index.json + jobs: publish: runs-on: ${{ vars.PUBLISH_AGENT || 'ubuntu-latest' }} @@ -27,12 +28,7 @@ jobs: fetch-depth: 0 - name: ⚙ dotnet - uses: actions/setup-dotnet@v4 - with: - dotnet-version: | - 6.x - 8.x - 9.x + uses: devlooped/actions-dotnet-env@v1 - name: 🙏 build run: dotnet build -m:1 -bl:build.binlog diff --git a/.gitignore b/.gitignore index 2ac54a7..0fe79fb 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ BenchmarkDotNet.Artifacts .genaiscript .idea local.settings.json +.env *.suo *.sdf diff --git a/.netconfig b/.netconfig index 50be3fe..2b49a71 100644 --- a/.netconfig +++ b/.netconfig @@ -24,26 +24,26 @@ skip [file ".editorconfig"] url = https://github.com/devlooped/oss/blob/main/.editorconfig - sha = c779d3d4e468358106dea03e93ba2cd35bb01ecb - etag = 7298c6450967975a8782b5c74f3071e1910fc59686e48f9c9d5cd7c68213cf59 + sha = a62c45934ac2952f2f5d54d8aea4a7ebc1babaff + etag = b5e919b472a52d4b522f86494f0f2c0ba74a6d9601454e20e4cbaf744317ff62 weak [file ".gitattributes"] url = https://github.com/devlooped/oss/blob/main/.gitattributes - sha = 5f92a68e302bae675b394ef343114139c075993e + sha = 4a9aa321c4982b83c185cf8dffed181ff84667d5 - etag = 338ba6d92c8d1774363396739c2be4257bfc58026f4b0fe92cb0ae4460e1eff7 + etag = 09cad18280ed04b67f7f87591e5481510df04d44c3403231b8af885664d8fd58 weak [file ".github/dependabot.yml"] url = https://github.com/devlooped/oss/blob/main/.github/dependabot.yml - sha = 49661dbf0720cde93eb5569be7523b5912351560 + sha = e733294084fb3e75d517a2e961e87df8faae7dc6 - etag = c147ea2f3431ca0338c315c4a45b56ee233c4d30f8d6ab698d0e1980a257fd6a + etag = 3bf8d9214a15c049ca5cfe80d212a8cbe4753b8a638a9804ef73d34c7def9618 weak [file ".github/workflows/build.yml"] url = https://github.com/devlooped/oss/blob/main/.github/workflows/build.yml - sha = 06e898ccba692566ebf845fa7c8833ac6c318c0a + sha = 56c2b8532c2f86235a0f5bd00ba6eba126f199cf - etag = 0a4b3f0a875cd8c9434742b4046558aecf610d3fa3d490cfd2099266e95e9195 + etag = bf99c19427f4372ecfe38ec56aa8c411058684fb717da5661f17ac00388b3602 weak [file ".github/workflows/changelog.yml"] url = https://github.com/devlooped/oss/blob/main/.github/workflows/changelog.yml @@ -53,21 +53,21 @@ weak [file ".github/workflows/dotnet-file.yml"] url = https://github.com/devlooped/oss/blob/main/.github/workflows/dotnet-file.yml - sha = 59aaf432369b5ea597831d4feec5a6ac4024c2e3 + sha = 8fa147d4799d73819040736c399d0b1db2c2d86c - etag = 1374e3f8c9b7af69c443605c03f7262300dcb7d783738d9eb9fe84268ed2d10c + etag = 1ca805a23656e99c03f9d478dba8ccef6e571f5de2ac0e9bb7e3c5216c99a694 weak [file ".github/workflows/publish.yml"] url = https://github.com/devlooped/oss/blob/main/.github/workflows/publish.yml - sha = 06e898ccba692566ebf845fa7c8833ac6c318c0a + sha = 56c2b8532c2f86235a0f5bd00ba6eba126f199cf - etag = 2f64f75ad01f735fd05290370fb8a826111ac8dd7e74ce04226bb627a54a62ba + etag = 2ef43521627aa3a91dd55bdc2856ec0c6a93b42485d4fe9d6b181f9ee42c8e18 weak [file ".gitignore"] url = https://github.com/devlooped/oss/blob/main/.gitignore - sha = e0be248fff1d39133345283b8227372b36574b75 + sha = 3776526342afb3f57da7e80f2095e5fdca3c31c9 - etag = c449ec6f76803e1891357ca2b8b4fcb5b2e5deeff8311622fd92ca9fbf1e6575 + etag = 11767f73556aa4c6c8bcc153b77ee8e8114f99fa3b885b0a7d66d082f91e77b3 weak [file "Directory.Build.rsp"] url = https://github.com/devlooped/oss/blob/main/Directory.Build.rsp @@ -92,15 +92,15 @@ etag = 2c6335b37e4ae05eea7c01f5d0c9d82b49c488f868a8b5ba7bff7c6ff01f3994 [file "src/Directory.Build.props"] url = https://github.com/devlooped/oss/blob/main/src/Directory.Build.props - sha = b76de49afb376aa48eb172963ed70663b59b31d3 + sha = 95b338b9780de6b8ed11f060fed2d300f136ec60 - etag = c8b56f3860cc7ccb8773b7bd6189f5c7a6e3a2c27e9104c1ee201fbdc5af9873 + etag = 5e73ffebd7aef6fecae5a73d4fb044bf46f3a593ee2858ff1648d9c337e546ad weak [file "src/Directory.Build.targets"] url = https://github.com/devlooped/oss/blob/main/src/Directory.Build.targets - sha = a8b208093599263b7f2d1fe3854634c588ea5199 + sha = 4339749ef4b8f66def75931df09ef99c149f8421 - etag = 19087699f05396205e6b050d999a43b175bd242f6e8fac86f6df936310178b03 + etag = 8b4492765755c030c4c351e058a92f53ab493cab440c1c0ef431f6635c4dae0e weak [file "src/kzu.snk"] url = https://github.com/devlooped/oss/blob/main/src/kzu.snk @@ -120,9 +120,9 @@ weak [file ".github/workflows/includes.yml"] url = https://github.com/devlooped/oss/blob/main/.github/workflows/includes.yml - sha = 85829f2510f335f4a411867f3dbaaa116c3ab3de + sha = 2d1fb4ed52b63689f2b20b994512ebac28721243 - etag = 086f6b6316cc6ea7089c0dcc6980be519e6ed6e6201e65042ef41b82634ec0ee + etag = 34ade86f020dea717c6a27ad7dcd0069c35be2832c58b0ba961278a1efe34089 weak [file ".github/workflows/combine-prs.yml"] url = https://github.com/devlooped/oss/blob/main/.github/workflows/combine-prs.yml @@ -225,16 +225,16 @@ weak [file "src/SponsorLink/SponsorLink.Analyzer.Tests.targets"] url = https://github.com/devlooped/SponsorLink/blob/main/samples/dotnet/SponsorLink.Analyzer.Tests.targets - sha = df44ccc14cc11b5674c55aca9ba8596bdbcf8438 + sha = 8a4082211918b604ad95ef0f3da3cd414747c46a - etag = a3e9cbcc227dd56a7bed236eaded136f1b80f9f36a4fabce8be695ee844bf881 + etag = ac4e82c24d5a812eb7a1ad20d2d076b7aeedddd90c8196eaea0c227693a2ede6 weak [file "src/SponsorLink/SponsorLink.Analyzer.targets"] url = https://github.com/devlooped/SponsorLink/blob/main/samples/dotnet/SponsorLink.Analyzer.targets - sha = 0f551e3be564625ee4d078649c55363bf35954ba + sha = 8a4082211918b604ad95ef0f3da3cd414747c46a - etag = 7276d81830e29f8d5f3e27ee62cadaf7aef02a0162b9a05c88e1daef9cc4875e + etag = b75dd01945453c3ccd9eb96f65959ff1607a2cf11226fac5014b01b7cb6314d7 weak [file "src/SponsorLink/SponsorLink/AnalyzerOptionsExtensions.cs"] url = https://github.com/devlooped/SponsorLink/blob/main/samples/dotnet/SponsorLink/AnalyzerOptionsExtensions.cs @@ -254,12 +254,6 @@ etag = a5d79dbc0ed9fac4fb1879fb3790b9ebab18e47c14c454554ce9f53f21487bb5 weak -[file "src/SponsorLink/SponsorLink/ManifestStatus.cs"] - url = https://github.com/devlooped/SponsorLink/blob/main/samples/dotnet/SponsorLink/ManifestStatus.cs - sha = f47528874a6d9192b5546f84b455f5ccc474a707 - - etag = e46848f83c0436ba33a1c09a4060ad627a74db41bab66bb37ca40fce8a6532a7 - weak [file "src/SponsorLink/SponsorLink/Resources.es-AR.resx"] url = https://github.com/devlooped/SponsorLink/blob/main/samples/dotnet/SponsorLink/Resources.es-AR.resx sha = 586398c3e650495f36601ecc8983a14ed745e058 @@ -268,28 +262,28 @@ weak [file "src/SponsorLink/SponsorLink/Resources.es.resx"] url = https://github.com/devlooped/SponsorLink/blob/main/samples/dotnet/SponsorLink/Resources.es.resx - sha = 29921560c73bb91c2a21a21800daf0b250773598 + sha = 21d8dac3077c75cd07d7cc7f9e10f2620afce834 - etag = feb9dc86e4d9c0c4a294cd6e03c5b914943e8d206b88a125abd1b0f882ddb247 + etag = 89a7bb797aeacca43e043196a00eea91f282df4caf9bbe937749026a03f707ad weak [file "src/SponsorLink/SponsorLink/Resources.resx"] url = https://github.com/devlooped/SponsorLink/blob/main/samples/dotnet/SponsorLink/Resources.resx - sha = 29921560c73bb91c2a21a21800daf0b250773598 + sha = 21d8dac3077c75cd07d7cc7f9e10f2620afce834 - etag = 7665a3be17cd224b1c413ade6a9c1c5a822dace1e7f9daae33a2e52d8bca15bb + etag = 8902652b8907de2fbccf73f3738d0fce503fc667a084171d6b88bf3373e559e7 weak [file "src/SponsorLink/SponsorLink/SponsorLink.cs"] url = https://github.com/devlooped/SponsorLink/blob/main/samples/dotnet/SponsorLink/SponsorLink.cs - sha = 3f72a9fd35274a659dd380a7d5b747d71b9732a1 + sha = a755e4be0f7cb73cfde208857e28f7cfeba2dcc3 - etag = 616598e0ecb6d2ce97660aa6ac049e2a31a1c953669743b7b612b61d40c37706 + etag = 402e2beb11cf64c07be3d0fc3e89115fd09fc24133c08a8951bf0e784909c510 weak [file "src/SponsorLink/SponsorLink/SponsorLink.csproj"] url = https://github.com/devlooped/SponsorLink/blob/main/samples/dotnet/SponsorLink/SponsorLink.csproj - sha = 0f551e3be564625ee4d078649c55363bf35954ba + sha = e8ec200934a3b3788c2e31d7022c717f5fd152fa - etag = 27db7c8288b721804b52a719a9218ab1198f4db5b7a7d06bce4e1770def2d4a0 + etag = 1a58baf82b1813f68610272aa6161a18a70d5c619154734039a0d48fce6d735a weak [file "src/SponsorLink/SponsorLink/SponsorLinkAnalyzer.cs"] url = https://github.com/devlooped/SponsorLink/blob/main/samples/dotnet/SponsorLink/SponsorLinkAnalyzer.cs @@ -375,12 +369,6 @@ etag = 1875555adb7eab21acf1e730b6baeb8c095d9f6f9f07303a87ad9c16e0f6490d weak -[file "src/SponsorLink/Tests/SponsorLinkTests.cs"] - url = https://github.com/devlooped/SponsorLink/blob/main/samples/dotnet/Tests/SponsorLinkTests.cs - sha = f47528874a6d9192b5546f84b455f5ccc474a707 - - etag = 1fa41250bd984e8aa840a966d34ce0e94f2111d1422d7f50b864c38364fcf4a4 - weak [file "src/SponsorLink/Tests/SponsorableManifest.cs"] url = https://github.com/devlooped/SponsorLink/blob/main/samples/dotnet/Tests/SponsorableManifest.cs sha = f47528874a6d9192b5546f84b455f5ccc474a707 @@ -389,10 +377,10 @@ weak [file "src/SponsorLink/Tests/Tests.csproj"] url = https://github.com/devlooped/SponsorLink/blob/main/samples/dotnet/Tests/Tests.csproj - sha = 0f551e3be564625ee4d078649c55363bf35954ba + sha = e8ec200934a3b3788c2e31d7022c717f5fd152fa - etag = 7d27c17944c61da196f11f904383b25b3f40579fbeb0cacb367bf05ec184ad7f + etag = eb34fc9fe25b0169f069ff692379a19c59673727d8abb6f45816012661329df5 weak [file "src/SponsorLink/Tests/keys/kzu.key"] url = https://github.com/devlooped/SponsorLink/blob/main/samples/dotnet/Tests/keys/kzu.key @@ -456,3 +444,23 @@ sha = 032439dbf180fca0539a5bd3a019f18ab3484b76 etag = da7c0104131bd474b52fc9bc9f9bda6470e24ae38d4fb9f5c4f719bc01370ab5 weak +[file ".github/actions/dotnet/action.yml"] + url = https://github.com/devlooped/oss/blob/main/.github/actions/dotnet/action.yml + sha = f2b690ce307acb76c5b8d7faec1a5b971a93653e + etag = 27ea11baa2397b3ec9e643a935832da97719c4e44215cfd135c49cad4c29373f + weak +[file ".github/workflows/dotnet-env.yml"] + url = https://github.com/devlooped/oss/blob/main/.github/workflows/dotnet-env.yml + sha = 77e83f238196d2723640abef0c7b6f43994f9747 + etag = fcb9759a96966df40dcd24906fd328ddec05953b7e747a6bb8d0d1e4c3865274 + weak +[file "src/SponsorLink/SponsorLink/SponsorManifest.cs"] + url = https://github.com/devlooped/SponsorLink/blob/main/samples/dotnet/SponsorLink/SponsorManifest.cs + sha = a755e4be0f7cb73cfde208857e28f7cfeba2dcc3 + etag = 55ef89e8441156541c1c74a50675b7f56633b56493031f0ffa877460839e3536 + weak +[file "src/SponsorLink/Tests/SponsorManifestTests.cs"] + url = https://github.com/devlooped/SponsorLink/blob/main/samples/dotnet/Tests/SponsorManifestTests.cs + sha = a755e4be0f7cb73cfde208857e28f7cfeba2dcc3 + etag = 82ae1c417265f2e136544980b4f687a1cc2c1bfb24df93d354c259053550f4a3 + weak diff --git a/readme.md b/readme.md index a8281da..bf9cc8e 100644 --- a/readme.md +++ b/readme.md @@ -70,41 +70,38 @@ The versioning scheme for packages is: # Sponsors -[![Clarius Org](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/clarius.png "Clarius Org")](https://github.com/clarius) -[![MFB Technologies, Inc.](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/MFB-Technologies-Inc.png "MFB Technologies, Inc.")](https://github.com/MFB-Technologies-Inc) -[![Torutek](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/torutek-gh.png "Torutek")](https://github.com/torutek-gh) -[![DRIVE.NET, Inc.](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/drivenet.png "DRIVE.NET, Inc.")](https://github.com/drivenet) -[![Keith Pickford](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/Keflon.png "Keith Pickford")](https://github.com/Keflon) -[![Thomas Bolon](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/tbolon.png "Thomas Bolon")](https://github.com/tbolon) -[![Kori Francis](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/kfrancis.png "Kori Francis")](https://github.com/kfrancis) -[![Toni Wenzel](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/twenzel.png "Toni Wenzel")](https://github.com/twenzel) -[![Uno Platform](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/unoplatform.png "Uno Platform")](https://github.com/unoplatform) -[![Dan Siegel](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/dansiegel.png "Dan Siegel")](https://github.com/dansiegel) -[![Reuben Swartz](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/rbnswartz.png "Reuben Swartz")](https://github.com/rbnswartz) -[![Jacob Foshee](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/jfoshee.png "Jacob Foshee")](https://github.com/jfoshee) -[![](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/Mrxx99.png "")](https://github.com/Mrxx99) -[![Eric Johnson](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/eajhnsn1.png "Eric Johnson")](https://github.com/eajhnsn1) -[![Ix Technologies B.V.](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/IxTechnologies.png "Ix Technologies B.V.")](https://github.com/IxTechnologies) -[![David JENNI](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/davidjenni.png "David JENNI")](https://github.com/davidjenni) -[![Jonathan ](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/Jonathan-Hickey.png "Jonathan ")](https://github.com/Jonathan-Hickey) -[![Charley Wu](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/akunzai.png "Charley Wu")](https://github.com/akunzai) -[![Ken Bonny](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/KenBonny.png "Ken Bonny")](https://github.com/KenBonny) -[![Simon Cropp](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/SimonCropp.png "Simon Cropp")](https://github.com/SimonCropp) -[![agileworks-eu](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/agileworks-eu.png "agileworks-eu")](https://github.com/agileworks-eu) -[![sorahex](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/sorahex.png "sorahex")](https://github.com/sorahex) -[![Zheyu Shen](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/arsdragonfly.png "Zheyu Shen")](https://github.com/arsdragonfly) -[![Vezel](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/vezel-dev.png "Vezel")](https://github.com/vezel-dev) -[![ChilliCream](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/ChilliCream.png "ChilliCream")](https://github.com/ChilliCream) -[![4OTC](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/4OTC.png "4OTC")](https://github.com/4OTC) -[![Vincent Limo](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/v-limo.png "Vincent Limo")](https://github.com/v-limo) -[![Jordan S. Jones](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/jordansjones.png "Jordan S. Jones")](https://github.com/jordansjones) -[![domischell](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/DominicSchell.png "domischell")](https://github.com/DominicSchell) +[![Clarius Org](https://avatars.githubusercontent.com/u/71888636?v=4&s=39 "Clarius Org")](https://github.com/clarius) +[![MFB Technologies, Inc.](https://avatars.githubusercontent.com/u/87181630?v=4&s=39 "MFB Technologies, Inc.")](https://github.com/MFB-Technologies-Inc) +[![DRIVE.NET, Inc.](https://avatars.githubusercontent.com/u/15047123?v=4&s=39 "DRIVE.NET, Inc.")](https://github.com/drivenet) +[![Keith Pickford](https://avatars.githubusercontent.com/u/16598898?u=64416b80caf7092a885f60bb31612270bffc9598&v=4&s=39 "Keith Pickford")](https://github.com/Keflon) +[![Thomas Bolon](https://avatars.githubusercontent.com/u/127185?u=7f50babfc888675e37feb80851a4e9708f573386&v=4&s=39 "Thomas Bolon")](https://github.com/tbolon) +[![Kori Francis](https://avatars.githubusercontent.com/u/67574?u=3991fb983e1c399edf39aebc00a9f9cd425703bd&v=4&s=39 "Kori Francis")](https://github.com/kfrancis) +[![Uno Platform](https://avatars.githubusercontent.com/u/52228309?v=4&s=39 "Uno Platform")](https://github.com/unoplatform) +[![Reuben Swartz](https://avatars.githubusercontent.com/u/724704?u=2076fe336f9f6ad678009f1595cbea434b0c5a41&v=4&s=39 "Reuben Swartz")](https://github.com/rbnswartz) +[![Jacob Foshee](https://avatars.githubusercontent.com/u/480334?v=4&s=39 "Jacob Foshee")](https://github.com/jfoshee) +[![](https://avatars.githubusercontent.com/u/33566379?u=bf62e2b46435a267fa246a64537870fd2449410f&v=4&s=39 "")](https://github.com/Mrxx99) +[![Eric Johnson](https://avatars.githubusercontent.com/u/26369281?u=41b560c2bc493149b32d384b960e0948c78767ab&v=4&s=39 "Eric Johnson")](https://github.com/eajhnsn1) +[![David JENNI](https://avatars.githubusercontent.com/u/3200210?v=4&s=39 "David JENNI")](https://github.com/davidjenni) +[![Jonathan ](https://avatars.githubusercontent.com/u/5510103?u=98dcfbef3f32de629d30f1f418a095bf09e14891&v=4&s=39 "Jonathan ")](https://github.com/Jonathan-Hickey) +[![Charley Wu](https://avatars.githubusercontent.com/u/574719?u=ea7c743490c83e8e4b36af76000f2c71f75d636e&v=4&s=39 "Charley Wu")](https://github.com/akunzai) +[![Ken Bonny](https://avatars.githubusercontent.com/u/6417376?u=569af445b6f387917029ffb5129e9cf9f6f68421&v=4&s=39 "Ken Bonny")](https://github.com/KenBonny) +[![Simon Cropp](https://avatars.githubusercontent.com/u/122666?v=4&s=39 "Simon Cropp")](https://github.com/SimonCropp) +[![agileworks-eu](https://avatars.githubusercontent.com/u/5989304?v=4&s=39 "agileworks-eu")](https://github.com/agileworks-eu) +[![Zheyu Shen](https://avatars.githubusercontent.com/u/4067473?v=4&s=39 "Zheyu Shen")](https://github.com/arsdragonfly) +[![Vezel](https://avatars.githubusercontent.com/u/87844133?v=4&s=39 "Vezel")](https://github.com/vezel-dev) +[![ChilliCream](https://avatars.githubusercontent.com/u/16239022?v=4&s=39 "ChilliCream")](https://github.com/ChilliCream) +[![4OTC](https://avatars.githubusercontent.com/u/68428092?v=4&s=39 "4OTC")](https://github.com/4OTC) +[![Vincent Limo](https://avatars.githubusercontent.com/devlooped-user?s=39 "Vincent Limo")](https://github.com/v-limo) +[![domischell](https://avatars.githubusercontent.com/u/66068846?u=0a5c5e2e7d90f15ea657bc660f175605935c5bea&v=4&s=39 "domischell")](https://github.com/DominicSchell) +[![Justin Wendlandt](https://avatars.githubusercontent.com/u/1068431?u=f7715ed6a8bf926d96ec286f0f1c65f94bf86928&v=4&s=39 "Justin Wendlandt")](https://github.com/jwendl) +[![Adrian Alonso](https://avatars.githubusercontent.com/u/2027083?u=129cf516d99f5cb2fd0f4a0787a069f3446b7522&v=4&s=39 "Adrian Alonso")](https://github.com/adalon) +[![Michael Hagedorn](https://avatars.githubusercontent.com/u/61711586?u=8f653dfcb641e8c18cc5f78692ebc6bb3a0c92be&v=4&s=39 "Michael Hagedorn")](https://github.com/Eule02) +[![torutek](https://avatars.githubusercontent.com/u/33917059?v=4&s=39 "torutek")](https://github.com/torutek) +[![mccaffers](https://avatars.githubusercontent.com/u/16667079?u=739e110e62a75870c981640447efa5eb2cb3bc8f&v=4&s=39 "mccaffers")](https://github.com/mccaffers) - -[![Sponsor this project](https://raw.githubusercontent.com/devlooped/sponsors/main/sponsor.png "Sponsor this project")](https://github.com/sponsors/devlooped) -  +[![Sponsor this project](https://avatars.githubusercontent.com/devlooped-sponsor?s=118 "Sponsor this project")](https://github.com/sponsors/devlooped) [Learn more about GitHub Sponsors](https://github.com/sponsors) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 381c383..a17c0a8 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -20,6 +20,7 @@ Daniel Cazzulino + Devlooped Copyright (C) Daniel Cazzulino and Contributors. All rights reserved. false MIT @@ -42,6 +43,8 @@ true + + false @@ -126,6 +129,8 @@ <_VersionLabel>$(_VersionLabel.Replace('/merge', '')) <_VersionLabel>$(_VersionLabel.Replace('/', '-')) + + <_VersionLabel>$(_VersionLabel.Replace('_', '-')) $(_VersionLabel) @@ -153,6 +158,18 @@ + + + 1.0.0 + $(VersionPrefix)-$(VersionSuffix) + $(VersionPrefix) + + diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets index 6232750..083afa6 100644 --- a/src/Directory.Build.targets +++ b/src/Directory.Build.targets @@ -165,6 +165,9 @@ @(_GitSourceRoot) + + $([System.IO.Path]::GetFileNameWithoutExtension($(PrivateRepositoryUrl))) + $(ProductFromUrl) @@ -175,9 +178,9 @@ Condition="'$(SourceControlInformationFeatureSupported)' == 'true' And '$(IsPackable)' == 'true'"> - $(RepositoryUrl) + $(RepositoryUrl.Replace('.git', '')) $(Description) - $(RepositoryUrl)/blob/main/changelog.md + $(RepositoryUrl.Replace('.git', ''))/blob/main/changelog.md diff --git a/src/SponsorLink/SponsorLink.Analyzer.Tests.targets b/src/SponsorLink/SponsorLink.Analyzer.Tests.targets index 1ce67f6..4687e1e 100644 --- a/src/SponsorLink/SponsorLink.Analyzer.Tests.targets +++ b/src/SponsorLink/SponsorLink.Analyzer.Tests.targets @@ -7,12 +7,12 @@ - + - + diff --git a/src/SponsorLink/SponsorLink.Analyzer.targets b/src/SponsorLink/SponsorLink.Analyzer.targets index 8bf120e..6a78464 100644 --- a/src/SponsorLink/SponsorLink.Analyzer.targets +++ b/src/SponsorLink/SponsorLink.Analyzer.targets @@ -84,15 +84,15 @@ - - + + - - + + diff --git a/src/SponsorLink/SponsorLink/ManifestStatus.cs b/src/SponsorLink/SponsorLink/ManifestStatus.cs deleted file mode 100644 index 0960e5a..0000000 --- a/src/SponsorLink/SponsorLink/ManifestStatus.cs +++ /dev/null @@ -1,25 +0,0 @@ -// -namespace Devlooped.Sponsors; - -/// -/// The resulting status from validation. -/// -public enum ManifestStatus -{ - /// - /// The manifest couldn't be read at all. - /// - Unknown, - /// - /// The manifest was read and is valid (not expired and properly signed). - /// - Valid, - /// - /// The manifest was read but has expired. - /// - Expired, - /// - /// The manifest was read, but its signature is invalid. - /// - Invalid, -} diff --git a/src/SponsorLink/SponsorLink/Resources.es.resx b/src/SponsorLink/SponsorLink/Resources.es.resx index b9ff562..17a7de0 100644 --- a/src/SponsorLink/SponsorLink/Resources.es.resx +++ b/src/SponsorLink/SponsorLink/Resources.es.resx @@ -169,6 +169,9 @@ Por favor considera apoyar el proyecto patrocinando en {0} y ejecutando posterio Eres un contribuidor al proyecto, eres lo máximo 💟! + + El uso de {0} sin warnings en el editor requiere un patrocinio activo. Ver mas en {1}. + Patrocinar los proyectos en que dependes asegura que se mantengan activos, y que recibas el apoyo que necesitas. También es muy económico y está disponible en todo el mundo! Por favor considera apoyar el proyecto patrocinando en {0} y ejecutando posteriormente 'sponsor sync {1}'. diff --git a/src/SponsorLink/SponsorLink/Resources.resx b/src/SponsorLink/SponsorLink/Resources.resx index 7b1dfa9..93b75bc 100644 --- a/src/SponsorLink/SponsorLink/Resources.resx +++ b/src/SponsorLink/SponsorLink/Resources.resx @@ -171,7 +171,7 @@ Please consider supporting the project by sponsoring at {0} and running 'sponsor You are a contributor to the project, you rock 💟! - Editor usage of {0} requires an active sponsorship. Learn more at {1}. + Editor usage of {0} without warnings requires an active sponsorship. Learn more at {1}. Sponsoring projects you depend on ensures they remain active, and that you get the support you need. It's also super affordable and available worldwide! diff --git a/src/SponsorLink/SponsorLink/SponsorLink.cs b/src/SponsorLink/SponsorLink/SponsorLink.cs index eec50c8..2c1e6db 100644 --- a/src/SponsorLink/SponsorLink/SponsorLink.cs +++ b/src/SponsorLink/SponsorLink/SponsorLink.cs @@ -165,82 +165,4 @@ public static bool TryRead([NotNullWhen(true)] out ClaimsPrincipal? principal, I return principal != null; } - - /// - /// Validates the manifest signature and optional expiration. - /// - /// The JWT to validate. - /// The key to validate the manifest signature with. - /// Except when returning , returns the security token read from the JWT, even if signature check failed. - /// The associated claims, only when return value is not . - /// Whether to check for expiration. - /// The status of the validation. - public static ManifestStatus Validate(string jwt, string jwk, out SecurityToken? token, out ClaimsIdentity? identity, bool validateExpiration) - { - token = default; - identity = default; - - SecurityKey key; - try - { - key = JsonWebKey.Create(jwk); - } - catch (ArgumentException) - { - return ManifestStatus.Unknown; - } - - var handler = new JsonWebTokenHandler { MapInboundClaims = false }; - - if (!handler.CanReadToken(jwt)) - return ManifestStatus.Unknown; - - var validation = new TokenValidationParameters - { - RequireExpirationTime = false, - ValidateLifetime = false, - ValidateAudience = false, - ValidateIssuer = false, - ValidateIssuerSigningKey = true, - IssuerSigningKey = key, - RoleClaimType = "roles", - NameClaimType = "sub", - }; - - var result = handler.ValidateTokenAsync(jwt, validation).Result; - if (result.Exception != null) - { - if (result.Exception is SecurityTokenInvalidSignatureException) - { - var jwtToken = handler.ReadJsonWebToken(jwt); - token = jwtToken; - identity = new ClaimsIdentity(jwtToken.Claims); - return ManifestStatus.Invalid; - } - else - { - var jwtToken = handler.ReadJsonWebToken(jwt); - token = jwtToken; - identity = new ClaimsIdentity(jwtToken.Claims); - return ManifestStatus.Invalid; - } - } - - token = result.SecurityToken; - identity = new ClaimsIdentity(result.ClaimsIdentity.Claims, "JWT"); - - if (validateExpiration && token.ValidTo == DateTime.MinValue) - return ManifestStatus.Invalid; - - // The sponsorable manifest does not have an expiration time. - if (validateExpiration && token.ValidTo < DateTimeOffset.UtcNow) - return ManifestStatus.Expired; - - return ManifestStatus.Valid; - } - - class JwtRolesPrincipal(ClaimsIdentity identity) : ClaimsPrincipal([identity]) - { - public override bool IsInRole(string role) => HasClaim("roles", role) || base.IsInRole(role); - } } diff --git a/src/SponsorLink/SponsorLink/SponsorLink.csproj b/src/SponsorLink/SponsorLink/SponsorLink.csproj index 46691cc..25a474c 100644 --- a/src/SponsorLink/SponsorLink/SponsorLink.csproj +++ b/src/SponsorLink/SponsorLink/SponsorLink.csproj @@ -28,7 +28,7 @@ - + diff --git a/src/SponsorLink/SponsorLink/SponsorManifest.cs b/src/SponsorLink/SponsorLink/SponsorManifest.cs new file mode 100644 index 0000000..b4aa9d7 --- /dev/null +++ b/src/SponsorLink/SponsorLink/SponsorManifest.cs @@ -0,0 +1,160 @@ +// +#nullable enable +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Security.Claims; +using Microsoft.IdentityModel.JsonWebTokens; +using Microsoft.IdentityModel.Tokens; + +namespace Devlooped.Sponsors; + +/// +/// The resulting status from validation. +/// +public enum ManifestStatus +{ + /// + /// The manifest couldn't be read at all. + /// + Unknown, + /// + /// The manifest was read and is valid (not expired and properly signed). + /// + Valid, + /// + /// The manifest was read but has expired. + /// + Expired, + /// + /// The manifest was read, but its signature is invalid. + /// + Invalid, +} + +/// +/// Represents the sponsorship status of a user. +/// +/// The status. +/// The principal potentially containing roles validated from the manifest. +/// The security token from the validated manifest. +public record SponsorManifest(ManifestStatus Status, ClaimsPrincipal Principal, SecurityToken? SecurityToken) +{ + /// + /// Whether the manifest is . + /// + public bool IsValid => Status == ManifestStatus.Valid; +} + +static partial class SponsorLink +{ + /// + /// Reads the local manifest (if present) for the specified sponsorable account and validates it + /// against the given JWK key. + /// + /// The sponsorable account to read. + /// The public key to validate the signature on the manifest JWT if found. + /// Whether to validate the manifest expiration. If , + /// an expired manifest will be reported as . The expiration date + /// can be checked in that case via the . + /// A manifest that represents the user status. + public static SponsorManifest GetManifest(string sponsorable, string jwk, bool validateExpiration = true) + { + var path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + ".sponsorlink", "github", sponsorable + ".jwt"); + + if (!File.Exists(path)) + return new SponsorManifest(ManifestStatus.Unknown, new ClaimsPrincipal(), null); + + return ParseManifest(File.ReadAllText(path), jwk, validateExpiration); + } + + internal static SponsorManifest ParseManifest(string jwt, string jwk, bool validateExpiration) + { + var status = Validate(jwt, jwk, out var token, out var identity, validateExpiration); + + if (status == ManifestStatus.Unknown || identity == null) + return new SponsorManifest(status, new ClaimsPrincipal(), token); + + return new SponsorManifest(status, new JwtRolesPrincipal(identity), token); + } + + /// + /// Validates the manifest signature and optional expiration. + /// + /// The JWT to validate. + /// The key to validate the manifest signature with. + /// Except when returning , returns the security token read from the JWT, even if signature check failed. + /// The associated claims, only when return value is not . + /// Whether to check for expiration. + /// The status of the validation. + public static ManifestStatus Validate(string jwt, string jwk, out SecurityToken? token, out ClaimsIdentity? identity, bool validateExpiration) + { + token = default; + identity = default; + + SecurityKey key; + try + { + key = JsonWebKey.Create(jwk); + } + catch (ArgumentException) + { + return ManifestStatus.Unknown; + } + + var handler = new JsonWebTokenHandler { MapInboundClaims = false }; + + if (!handler.CanReadToken(jwt)) + return ManifestStatus.Unknown; + + var validation = new TokenValidationParameters + { + RequireExpirationTime = false, + ValidateLifetime = false, + ValidateAudience = false, + ValidateIssuer = false, + ValidateIssuerSigningKey = true, + IssuerSigningKey = key, + RoleClaimType = "roles", + NameClaimType = "sub", + }; + + var result = handler.ValidateTokenAsync(jwt, validation).Result; + if (!result.IsValid || result.Exception != null) + { + if (result.Exception is SecurityTokenInvalidSignatureException) + { + var jwtToken = handler.ReadJsonWebToken(jwt); + token = jwtToken; + identity = new ClaimsIdentity(jwtToken.Claims); + return ManifestStatus.Invalid; + } + else + { + var jwtToken = handler.ReadJsonWebToken(jwt); + token = jwtToken; + identity = new ClaimsIdentity(jwtToken.Claims); + return ManifestStatus.Invalid; + } + } + + token = result.SecurityToken; + identity = new ClaimsIdentity(result.ClaimsIdentity.Claims, "JWT"); + + if (validateExpiration && token.ValidTo == DateTime.MinValue) + return ManifestStatus.Invalid; + + // The sponsorable manifest does not have an expiration time. + if (validateExpiration && token.ValidTo < DateTimeOffset.UtcNow) + return ManifestStatus.Expired; + + return ManifestStatus.Valid; + } + + class JwtRolesPrincipal(ClaimsIdentity identity) : ClaimsPrincipal([identity]) + { + public override bool IsInRole(string role) => HasClaim("roles", role) || base.IsInRole(role); + } +} diff --git a/src/SponsorLink/Tests/SponsorLinkTests.cs b/src/SponsorLink/Tests/SponsorManifestTests.cs similarity index 63% rename from src/SponsorLink/Tests/SponsorLinkTests.cs rename to src/SponsorLink/Tests/SponsorManifestTests.cs index 7625e2c..7895a81 100644 --- a/src/SponsorLink/Tests/SponsorLinkTests.cs +++ b/src/SponsorLink/Tests/SponsorManifestTests.cs @@ -8,7 +8,7 @@ namespace Devlooped.Tests; -public class SponsorLinkTests +public class SponsorManifestTests { // We need to convert to jwk string since the analyzer project has merged the JWT assembly and types. public static string ToJwk(SecurityKey key) @@ -19,64 +19,67 @@ public static string ToJwk(SecurityKey key) [Fact] public void ValidateSponsorable() { - var manifest = SponsorableManifest.Create(new Uri("https://foo.com"), [new Uri("https://github.com/sponsors/bar")], "ASDF1234"); - var jwt = manifest.ToJwt(); - var jwk = ToJwk(manifest.SecurityKey); + var sponsorable = SponsorableManifest.Create(new Uri("https://foo.com"), [new Uri("https://github.com/sponsors/bar")], "ASDF1234"); + var jwt = sponsorable.ToJwt(); + var jwk = ToJwk(sponsorable.SecurityKey); // NOTE: sponsorable manifest doesn't have expiration date. - var status = SponsorLink.Validate(jwt, jwk, out var token, out var principal, false); + var manifest = SponsorLink.ParseManifest(jwt, jwk, false); - Assert.Equal(ManifestStatus.Valid, status); + Assert.True(manifest.IsValid); + Assert.Equal(ManifestStatus.Valid, manifest.Status); } [Fact] public void ValidateWrongKey() { - var manifest = SponsorableManifest.Create(new Uri("https://foo.com"), [new Uri("https://github.com/sponsors/bar")], "ASDF1234"); - var jwt = manifest.ToJwt(); + var sponsorable = SponsorableManifest.Create(new Uri("https://foo.com"), [new Uri("https://github.com/sponsors/bar")], "ASDF1234"); + var jwt = sponsorable.ToJwt(); var jwk = ToJwk(new RsaSecurityKey(RSA.Create())); - var status = SponsorLink.Validate(jwt, jwk, out var token, out var principal, false); + var manifest = SponsorLink.ParseManifest(jwt, jwk, false); - Assert.Equal(ManifestStatus.Invalid, status); + Assert.Equal(ManifestStatus.Invalid, manifest.Status); // We should still be a able to read the data, knowing it may have been tampered with. - Assert.NotNull(principal); - Assert.NotNull(token); + Assert.NotNull(manifest.Principal); + Assert.NotNull(manifest.SecurityToken); } [Fact] public void ValidateExpiredSponsor() { - var manifest = SponsorableManifest.Create(new Uri("https://foo.com"), [new Uri("https://github.com/sponsors/bar")], "ASDF1234"); - var jwk = ToJwk(manifest.SecurityKey); - var sponsor = manifest.Sign([], expiration: TimeSpan.Zero); + var sponsorable = SponsorableManifest.Create(new Uri("https://foo.com"), [new Uri("https://github.com/sponsors/bar")], "ASDF1234"); + var jwk = ToJwk(sponsorable.SecurityKey); + var sponsor = sponsorable.Sign([], expiration: TimeSpan.Zero); // Will be expired after this. Thread.Sleep(1000); - var status = SponsorLink.Validate(sponsor, jwk, out var token, out var principal, true); + var manifest = SponsorLink.ParseManifest(sponsor, jwk, true); - Assert.Equal(ManifestStatus.Expired, status); + Assert.Equal(ManifestStatus.Expired, manifest.Status); // We should still be a able to read the data, even if expired (but not tampered with). - Assert.NotNull(principal); - Assert.NotNull(token); + Assert.NotNull(manifest.Principal); + Assert.NotNull(manifest.SecurityToken); } [Fact] public void ValidateUnknownFormat() { - var manifest = SponsorableManifest.Create(new Uri("https://foo.com"), [new Uri("https://github.com/sponsors/bar")], "ASDF1234"); - var jwk = ToJwk(manifest.SecurityKey); + var sponsorable = SponsorableManifest.Create(new Uri("https://foo.com"), [new Uri("https://github.com/sponsors/bar")], "ASDF1234"); + var jwk = ToJwk(sponsorable.SecurityKey); - var status = SponsorLink.Validate("asdfasdf", jwk, out var token, out var principal, false); + var manifest = SponsorLink.ParseManifest("asdfasdf", jwk, false); - Assert.Equal(ManifestStatus.Unknown, status); + Assert.Equal(ManifestStatus.Unknown, manifest.Status); // Nothing could be read at all. - Assert.Null(principal); - Assert.Null(token); + Assert.False(manifest.IsValid); + Assert.NotNull(manifest.Principal); + Assert.Null(manifest.Principal.Identity); + Assert.Null(manifest.SecurityToken); } [Fact] @@ -111,7 +114,7 @@ public void ValidateCachedManifest() var jwt = File.ReadAllText(path); - var status = SponsorLink.Validate(jwt, + var manifest = SponsorLink.ParseManifest(jwt, """ { "e": "AQAB", @@ -119,8 +122,8 @@ public void ValidateCachedManifest() "n": "5inhv8QymaDBOihNi1eY-6-hcIB5qSONFZxbxxXAyOtxAdjFCPM-94gIZqM9CDrX3pyg1lTJfml_a_FZSU9dB1ii5mSX_mNHBFXn1_l_gi1ErdbkIF5YbW6oxWFxf3G5mwVXwnPfxHTyQdmWQ3YJR-A3EB4kaFwLqA6Ha5lb2ObGpMTQJNakD4oTAGDhqHMGhu6PupGq5ie4qZcQ7N8ANw8xH7nicTkbqEhQABHWOTmLBWq5f5F6RYGF8P7cl0IWl_w4YcIZkGm2vX2fi26F9F60cU1v13GZEVDTXpJ9kzvYeM9sYk6fWaoyY2jhE51qbv0B0u6hScZiLREtm3n7ClJbIGXhkUppFS2JlNaX3rgQ6t-4LK8gUTyLt3zDs2H8OZyCwlCpfmGmdsUMkm1xX6t2r-95U3zywynxoWZfjBCJf41leM9OMKYwNWZ6LQMyo83HWw1PBIrX4ZLClFwqBcSYsXDyT8_ZLd1cdYmPfmtllIXxZhLClwT5qbCWv73V" } """ - , out var token, out var principal, false); + , false); - Assert.Equal(ManifestStatus.Valid, status); + Assert.Equal(ManifestStatus.Valid, manifest.Status); } } diff --git a/src/SponsorLink/Tests/Tests.csproj b/src/SponsorLink/Tests/Tests.csproj index b14b666..df8bc07 100644 --- a/src/SponsorLink/Tests/Tests.csproj +++ b/src/SponsorLink/Tests/Tests.csproj @@ -10,8 +10,8 @@ - - + +