diff --git a/.github/workflows/check-pr-has-milestone.yml b/.github/workflows/check-pr-has-milestone.yml index ebf3832622..056ddc7b82 100644 --- a/.github/workflows/check-pr-has-milestone.yml +++ b/.github/workflows/check-pr-has-milestone.yml @@ -14,6 +14,7 @@ concurrency: jobs: fail-on-bad-milestone: + if: github.event.pull_request.draft != true name: Fail if Pull Request has no Associated Version Milestone runs-on: ubuntu-latest steps: diff --git a/.github/workflows/ci-pipeline.yml b/.github/workflows/ci-pipeline.yml index 4651095d0a..fdc0778370 100644 --- a/.github/workflows/ci-pipeline.yml +++ b/.github/workflows/ci-pipeline.yml @@ -54,6 +54,7 @@ jobs: start-gate: name: CI Start Gate runs-on: ubuntu-latest + timeout-minutes: 1 steps: - name: GitHub Requires at Least One Step for a Job run: exit 0 @@ -62,6 +63,7 @@ jobs: name: Build ReleaseNotes for Other Jobs needs: start-gate runs-on: ubuntu-latest + timeout-minutes: 5 steps: - name: Install Native Dependencies run: | @@ -97,8 +99,7 @@ jobs: name: Validate Nix Flake needs: start-gate runs-on: ubuntu-latest - env: - TEST_TGS_VERSION: "6.17.0" # Version we use here doesn't matter as it won't be executed. Just used to download a zip and calc hash + timeout-minutes: 90 steps: - name: Install Native Packages # Name checked in rerunFlakyTests.js run: | @@ -120,24 +121,6 @@ jobs: with: ref: "refs/pull/${{ inputs.pull_request_number }}/merge" - - name: Replace current TGS version with test version - run: | - CURRENT_TGS_VERSION="$(xmlstarlet sel -N X="http://schemas.microsoft.com/developer/msbuild/2003" --template --value-of /X:Project/X:PropertyGroup/X:TgsCoreVersion build/Version.props)" - sed -i -e "s/${CURRENT_TGS_VERSION}/${{ env.TEST_TGS_VERSION }}/g" build/Version.props - - - name: Retrieve ServerConsole.zip Artifact - run: | - mkdir release - curl -L https://github.com/tgstation/tgstation-server/releases/download/tgstation-server-v${{ env.TEST_TGS_VERSION }}/ServerConsole.zip -f -o release/ServerConsole.zip - - - name: Regenerate Nix Hash - run: | - nix hash path ./release > build/package/nix/ServerConsole.sha256 - cat build/package/nix/ServerConsole.sha256 - - - name: Cleanup Download - run: rm -rf ./release - - name: Check Flake run: | cd build/package/nix @@ -147,10 +130,9 @@ jobs: name: Run CodeQL needs: start-gate runs-on: ubuntu-latest + timeout-minutes: 15 permissions: security-events: write - env: - TGS_TELEMETRY_KEY_FILE: /tmp/tgs_telemetry_key.txt steps: - name: Install Native Dependencies run: | @@ -192,9 +174,6 @@ jobs: with: languages: csharp - - name: Setup Telemetry Key File - run: echo "fake_telemetry_key" > ${{ env.TGS_TELEMETRY_KEY_FILE }} - - name: Build # Name checked in rerunFlakyTests.js run: dotnet build -c ReleaseNoWindows -p:TGS_HOST_NO_WEBPANEL=true @@ -206,6 +185,7 @@ jobs: dmapi-build: name: Build DMAPI needs: start-gate + timeout-minutes: 5 strategy: fail-fast: false matrix: @@ -305,6 +285,7 @@ jobs: opendream-build: name: Build DMAPI (OpenDream) needs: start-gate + timeout-minutes: 10 strategy: fail-fast: false matrix: @@ -362,6 +343,7 @@ jobs: name: Check Nuget Versions Match Tools runs-on: ubuntu-latest needs: start-gate + timeout-minutes: 1 steps: - name: Checkout (Branch) uses: actions/checkout@v4 @@ -381,11 +363,19 @@ jobs: id: wix-tool run: echo "version=$(cat build/package/winget/.config/dotnet-tools.json | jq -r '.tools.wix.version')" >> $GITHUB_OUTPUT + - name: Retrieve StrawberryShake Tool Version 1 + id: strawberry-tool-1 + run: echo "version=$(cat src/Tgstation.Server.Host.Utils.GitLab.GraphQL/.config/dotnet-tools.json | jq -r '.tools."strawberryshake.tools".version')" >> $GITHUB_OUTPUT + + - name: Retrieve StrawberryShake Tool Version 2 + id: strawberry-tool-2 + run: echo "version=$(cat src/Tgstation.Server.Client.GraphQL/.config/dotnet-tools.json | jq -r '.tools."strawberryshake.tools".version')" >> $GITHUB_OUTPUT + - name: Retrieve dotnet-ef Nuget Version id: dotnet-ef-nuget run: | - regex='\s+' - if [[ $(cat src/Tgstation.Server.Host/Tgstation.Server.Host.csproj) =~ $regex ]]; then + regex='\s+' + if [[ $(cat Directory.Packages.props) =~ $regex ]]; then echo "version=${BASH_REMATCH[1]}" >> $GITHUB_OUTPUT else echo "Regex search failed!" @@ -403,6 +393,17 @@ jobs: exit 1 fi + - name: Retrieve StrawberryShake Nuget Version + id: strawberry-nuget + run: | + regex='\s+' + if [[ $(cat Directory.Packages.props) =~ $regex ]]; then + echo "version=${BASH_REMATCH[1]}" >> $GITHUB_OUTPUT + else + echo "Regex search failed!" + exit 1 + fi + - name: Fail if dotnet-ef Versions Don't Match if: ${{ steps.dotnet-ef-tool.outputs.version != steps.dotnet-ef-nuget.outputs.version }} run: | @@ -415,10 +416,17 @@ jobs: echo "${{ steps.wix-tool.outputs.version }} != ${{ steps.wix-nuget.outputs.version }}" exit 1 + - name: Fail if StrawberryShake Versions Don't Match + if: ${{ steps.strawberry-tool-1.outputs.version != steps.strawberry-nuget.outputs.version || steps.strawberry-tool-2.outputs.version != steps.strawberry-nuget.outputs.version }} + run: | + echo "${{ steps.strawberry-tool-1.outputs.version }} != ${{ steps.strawberry-nuget.outputs.version }} || ${{ steps.strawberry-tool-2.outputs.version }} != ${{ steps.strawberry-nuget.outputs.version }}" + exit 1 + pages-build: name: Build gh-pages needs: build-releasenotes runs-on: ubuntu-latest + timeout-minutes: 10 steps: - name: Setup dotnet uses: actions/setup-dotnet@v4 @@ -497,8 +505,7 @@ jobs: name: Build Docker Image runs-on: ubuntu-latest needs: start-gate - env: - TGS_TELEMETRY_KEY_FILE: tgs_telemetry_key.txt + timeout-minutes: 10 steps: - name: Checkout (Branch) uses: actions/checkout@v4 @@ -510,26 +517,17 @@ jobs: with: ref: "refs/pull/${{ inputs.pull_request_number }}/merge" - - name: Setup Telemetry Key File - shell: bash - run: echo "${{ secrets.TGS_TELEMETRY_KEY }}" > ${{ env.TGS_TELEMETRY_KEY_FILE }} - - name: Build Docker Image # Name checked in rerunFlakyTests.js - run: docker build . -f build/Dockerfile --build-arg TGS_TELEMETRY_KEY_FILE=${{ env.TGS_TELEMETRY_KEY_FILE }} - - - name: Delete Telemetry Key File - if: always() - run: rm -f ${{ env.TGS_TELEMETRY_KEY_FILE }} + run: docker build . -f build/Dockerfile linux-unit-tests: name: Linux Tests needs: start-gate + timeout-minutes: 10 strategy: fail-fast: false matrix: configuration: ["Debug", "Release"] - env: - TGS_TELEMETRY_KEY_FILE: /tmp/tgs_telemetry_key.txt runs-on: ubuntu-latest steps: - name: Install Native x86 libc Dependencies # Name checked in rerunFlakyTests.js @@ -562,16 +560,9 @@ jobs: - name: Enable Corepack run: corepack enable - - name: Setup Telemetry Key File - run: echo "${{ secrets.TGS_TELEMETRY_KEY }}" > ${{ env.TGS_TELEMETRY_KEY_FILE }} - - name: Build # Name checked in rerunFlakyTests.js run: dotnet build -c ${{ matrix.configuration }}NoWindows - - name: Delete Telemetry Key File - if: always() - run: rm -f ${{ env.TGS_TELEMETRY_KEY_FILE }} - - name: Run Unit Tests run: sudo dotnet test --no-build --logger "GitHubActions;summary.includePassedTests=true;summary.includeSkippedTests=true" --filter TestCategory!=RequiresDatabase -c ${{ matrix.configuration }}NoWindows --collect:"XPlat Code Coverage" --settings build/ci.runsettings --results-directory ./TestResults tgstation-server.sln env: @@ -590,12 +581,11 @@ jobs: windows-unit-tests: name: Windows Tests needs: start-gate + timeout-minutes: 15 strategy: fail-fast: false matrix: configuration: ["Debug", "Release"] - env: - TGS_TELEMETRY_KEY_FILE: C:/tgs_telemetry_key.txt runs-on: windows-2025 steps: - name: Setup dotnet @@ -622,18 +612,9 @@ jobs: - name: Enable Corepack run: corepack enable - - name: Setup Telemetry Key File - shell: bash - run: echo "${{ secrets.TGS_TELEMETRY_KEY }}" > ${{ env.TGS_TELEMETRY_KEY_FILE }} - - name: Build # Name checked in rerunFlakyTests.js run: dotnet build -c ${{ matrix.configuration }}NoWix - - name: Delete Telemetry Key File - shell: bash - if: always() - run: rm -f ${{ env.TGS_TELEMETRY_KEY_FILE }} - - name: Run Unit Tests run: dotnet test --no-build --logger "GitHubActions;summary.includePassedTests=true;summary.includeSkippedTests=true" --filter TestCategory!=RequiresDatabase -c ${{ matrix.configuration }}NoWix --collect:"XPlat Code Coverage" --settings build/ci.runsettings --results-directory ./TestResults tgstation-server.sln env: @@ -651,6 +632,7 @@ jobs: name: Prepare Live Tests Cache of EDGE Versions needs: start-gate runs-on: ubuntu-latest + timeout-minutes: 1 steps: - name: Cache BYOND .zips (Linux) uses: actions/cache@v4 @@ -691,6 +673,7 @@ jobs: windows-integration-tests: name: Windows Live Tests needs: [dmapi-build, opendream-build, prep-edge-versions] + timeout-minutes: 60 strategy: fail-fast: false matrix: @@ -698,8 +681,6 @@ jobs: ["SqlServer", "Sqlite", "PostgresSql", "MariaDB", "MySql"] watchdog-type: ["Basic", "Advanced"] configuration: ["Debug", "Release"] - env: - TGS_TELEMETRY_KEY_FILE: C:/tgs_telemetry_key.txt runs-on: windows-2025 steps: - name: Setup dotnet @@ -801,18 +782,9 @@ jobs: - name: Enable Corepack run: corepack enable - - name: Setup Telemetry Key File - shell: bash - run: echo "${{ secrets.TGS_TELEMETRY_KEY }}" > ${{ env.TGS_TELEMETRY_KEY_FILE }} - - name: Build # Name checked in rerunFlakyTests.js run: dotnet build -c ${{ matrix.configuration }} tests/Tgstation.Server.Tests/Tgstation.Server.Tests.csproj - - name: Delete Telemetry Key File - shell: bash - if: always() - run: rm -f ${{ env.TGS_TELEMETRY_KEY_FILE }} - - name: Run Live Tests # Logging here is weird because printing massive amounts of text on Windows runners is SLOW AS SHIT!!! id: live-tests shell: bash @@ -906,6 +878,7 @@ jobs: linux-integration-tests: name: Linux Live Tests needs: [dmapi-build, opendream-build, prep-edge-versions] + timeout-minutes: 60 services: # We start all dbs here so we can just code the stuff once mssql: image: ${{ (matrix.database-type == 'SqlServer') && 'mcr.microsoft.com/mssql/server:2019-latest' || '' }} @@ -953,8 +926,6 @@ jobs: database-type: ["Sqlite", "PostgresSql", "MariaDB", "MySql"] watchdog-type: ["Basic", "Advanced"] configuration: ["Debug", "Release"] - env: - TGS_TELEMETRY_KEY_FILE: /tmp/tgs_telemetry_key.txt runs-on: ubuntu-latest steps: - name: Setup dotnet @@ -1030,16 +1001,9 @@ jobs: - name: Enable Corepack run: corepack enable - - name: Setup Telemetry Key File - run: echo "${{ secrets.TGS_TELEMETRY_KEY }}" > ${{ env.TGS_TELEMETRY_KEY_FILE }} - - name: Build # Name checked in rerunFlakyTests.js run: dotnet build -c ${{ matrix.configuration }}NoWindows tests/Tgstation.Server.Tests/Tgstation.Server.Tests.csproj - - name: Delete Telemetry Key File - if: always() - run: rm -f ${{ env.TGS_TELEMETRY_KEY_FILE }} - - name: Run Live Tests run: | cd tests/Tgstation.Server.Tests @@ -1091,6 +1055,7 @@ jobs: name: OpenAPI Spec Validation needs: windows-integration-tests runs-on: ubuntu-latest + timeout-minutes: 5 steps: - name: Install IBM OpenAPI Validator run: npm i -g ibm-openapi-validator@0.51.3 @@ -1116,6 +1081,7 @@ jobs: upload-code-coverage: name: Upload Code Coverage + timeout-minutes: 5 needs: [ linux-unit-tests, @@ -1382,8 +1348,7 @@ jobs: name: Build .deb Package # Can't do i386 due to https://github.com/dotnet/core/issues/4595 needs: build-releasenotes runs-on: ubuntu-latest - env: - TGS_TELEMETRY_KEY_FILE: /tmp/tgs_telemetry_key.txt + timeout-minutes: 30 steps: - name: Install Native Dependencies # Name checked in rerunFlakyTests.js run: | @@ -1430,9 +1395,6 @@ jobs: - name: Grab Most Recent Changelog run: curl -L https://raw.githubusercontent.com/tgstation/tgstation-server/gh-pages/changelog.yml -o changelog.yml - - name: Setup Telemetry Key File - run: echo "${{ secrets.TGS_TELEMETRY_KEY }}" > ${{ env.TGS_TELEMETRY_KEY_FILE }} - - name: Retrieve ReleaseNotes Binaries uses: actions/download-artifact@v4 with: @@ -1472,10 +1434,6 @@ jobs: gpg --verify tgstation-server_${{ env.TGS_VERSION }}-1_amd64.changes gpg --verify tgstation-server_${{ env.TGS_VERSION }}-1_amd64.buildinfo - - name: Delete Telemetry Key File - if: always() - run: rm -f ${{ env.TGS_TELEMETRY_KEY_FILE }} - - name: Test Install run: | sudo mkdir /etc/tgstation-server @@ -1539,8 +1497,7 @@ jobs: name: Build Windows Installer .exe runs-on: windows-2025 needs: start-gate - env: - TGS_TELEMETRY_KEY_FILE: C:/tgs_telemetry_key.txt + timeout-minutes: 15 steps: - name: Setup dotnet uses: actions/setup-dotnet@v4 @@ -1574,18 +1531,9 @@ jobs: - name: Enable Corepack run: corepack enable - - name: Setup Telemetry Key File - shell: bash - run: echo "${{ secrets.TGS_TELEMETRY_KEY }}" > ${{ env.TGS_TELEMETRY_KEY_FILE }} - - name: Build Host # Name checked in rerunFlakyTests.js run: dotnet build -c Release src/Tgstation.Server.Host/Tgstation.Server.Host.csproj - - name: Delete Telemetry Key File - shell: bash - if: always() - run: rm -f ${{ env.TGS_TELEMETRY_KEY_FILE }} - - name: Build Service # Name checked in rerunFlakyTests.js run: dotnet build -c Release src/Tgstation.Server.Host.Service/Tgstation.Server.Host.Service.csproj @@ -1684,6 +1632,7 @@ jobs: name: Check winget-pkgs Pull Request Template is up to date needs: build-releasenotes runs-on: ubuntu-latest + timeout-minutes: 5 steps: - name: Setup dotnet uses: actions/setup-dotnet@v4 @@ -1734,6 +1683,7 @@ jobs: ci-completion-gate: if: always() && !cancelled() name: CI Completion Gate + timeout-minutes: 1 needs: [ pages-build, @@ -1823,26 +1773,22 @@ jobs: TGS_RELEASE_NOTES_TOKEN: ${{ steps.app-token-generation.outputs.token }} run: dotnet release_notes_bins/Tgstation.Server.ReleaseNotes.dll ${{ env.TGS_API_VERSION }} --restapi - - name: Create GitHub Release - uses: actions/create-release@v1 - id: create_release - env: - GITHUB_TOKEN: ${{ steps.app-token-generation.outputs.token }} - with: - tag_name: api-v${{ env.TGS_API_VERSION }} - release_name: tgstation-server REST API v${{ env.TGS_API_VERSION }} - body_path: release_notes.md - commitish: ${{ github.event.head_commit.id }} + - name: Setup Release Artifacts + run: | + mkdir release_assets + cp ./swaggger/tgs_api.json ./release_assets/swagger.json - - name: Upload OpenApi Spec - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ steps.app-token-generation.outputs.token }} + - name: Create GitHub Release + uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./swagger/tgs_api.json - asset_name: swagger.json - asset_content_type: application/json + token: ${{ steps.app-token-generation.outputs.token }} + tag_name: api-v${{ env.TGS_API_VERSION }} + files: | + ./release_assets/* + name: tgstation-server REST API v${{ env.TGS_API_VERSION }} + body_path: release_notes.md + target_commitish: ${{ github.event.head_commit.id }} + make_latest: false deploy-gql: name: Deploy GraphQL API @@ -1910,27 +1856,23 @@ jobs: TGS_RELEASE_NOTES_TOKEN: ${{ steps.app-token-generation.outputs.token }} run: dotnet release_notes_bins/Tgstation.Server.ReleaseNotes.dll ${{ env.TGS_API_VERSION }} --graphqlapi - - name: Create GitHub Release - uses: actions/create-release@v1 - id: create_release - env: - GITHUB_TOKEN: ${{ steps.app-token-generation.outputs.token }} - with: - tag_name: graphql-v${{ env.TGS_API_VERSION }} - release_name: tgstation-server GraphQL API v${{ env.TGS_API_VERSION }} - body_path: release_notes.md - commitish: ${{ github.event.head_commit.id }} - prerelease: ${{ env.TGS_GRAPHQL_PRERELEASE }} + - name: Setup Release Artifacts + run: | + mkdir release_assets + cp ./schema/tgs-api.graphql ./release_assets/tgs-api.graphql - - name: Upload GraphQL Schema - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ steps.app-token-generation.outputs.token }} + - name: Create GitHub Release + uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./schema/tgs-api.graphql - asset_name: tgs-api.graphql - asset_content_type: text/plain + token: ${{ steps.app-token-generation.outputs.token }} + tag_name: graphql-v${{ env.TGS_API_VERSION }} + files: | + ./release_assets/* + name: tgstation-server GraphQL API v${{ env.TGS_API_VERSION }} + body_path: release_notes.md + target_commitish: ${{ github.event.head_commit.id }} + make_latest: false + prerelease: ${{ env.TGS_GRAPHQL_PRERELEASE }} deploy-dm: name: Deploy DreamMaker API @@ -1991,26 +1933,22 @@ jobs: TGS_RELEASE_NOTES_TOKEN: ${{ steps.app-token-generation.outputs.token }} run: dotnet release_notes_bins/Tgstation.Server.ReleaseNotes.dll ${{ env.TGS_DM_VERSION }} --dmapi - - name: Create GitHub Release - uses: actions/create-release@v1 - id: create_release - env: - GITHUB_TOKEN: ${{ steps.app-token-generation.outputs.token }} - with: - tag_name: dmapi-v${{ env.TGS_DM_VERSION }} - release_name: tgstation-server DMAPI v${{ env.TGS_DM_VERSION }} - body_path: release_notes.md - commitish: ${{ github.event.head_commit.id }} + - name: Setup Release Artifacts + run: | + mkdir release_assets + cp ./DMAPI.zip ./release_assets/DMAPI.zip - - name: Upload DMAPI Artifact - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ steps.app-token-generation.outputs.token }} + - name: Create GitHub Release + uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./DMAPI.zip - asset_name: DMAPI.zip - asset_content_type: application/zip + token: ${{ steps.app-token-generation.outputs.token }} + tag_name: dmapi-v${{ env.TGS_DM_VERSION }} + files: | + ./release_assets/* + name: tgstation-server DMAPI v${{ env.TGS_DM_VERSION }} + body_path: release_notes.md + target_commitish: ${{ github.event.head_commit.id }} + make_latest: false deploy-client: name: Deploy Nuget Packages @@ -2107,8 +2045,6 @@ jobs: needs: [deploy-dm, deploy-rest, deploy-gql, deployment-gate] runs-on: windows-2025 if: (!(cancelled() || failure())) && github.event.ref == 'refs/heads/master' && contains(github.event.head_commit.message, '[TGSDeploy]') && needs.deployment-gate.result == 'success' - env: - TGS_TELEMETRY_KEY_FILE: C:/tgs_telemetry_key.txt permissions: id-token: write attestations: write @@ -2135,19 +2071,10 @@ jobs: - name: Enable Corepack run: corepack enable - - name: Setup Telemetry Key File - shell: bash - run: echo "${{ secrets.TGS_TELEMETRY_KEY }}" > ${{ env.TGS_TELEMETRY_KEY_FILE }} - # We need to rebuild the installer.exe so it can be properly signed - name: Build Host # Name checked in rerunFlakyTests.js run: dotnet build -c Release src/Tgstation.Server.Host/Tgstation.Server.Host.csproj - - name: Delete Telemetry Key File - shell: bash - if: always() - run: rm -f ${{ env.TGS_TELEMETRY_KEY_FILE }} - - name: Build Service # Name checked in rerunFlakyTests.js run: dotnet build -c Release src/Tgstation.Server.Host.Service/Tgstation.Server.Host.Service.csproj @@ -2310,113 +2237,30 @@ jobs: TGS_RELEASE_NOTES_TOKEN: ${{ steps.app-token-generation.outputs.token }} run: dotnet release_notes_bins/Tgstation.Server.ReleaseNotes.dll ${{ env.TGS_VERSION }} + - name: Setup Release Artifacts + run: | + mkdir release_assets + cp ./ServerConsole.zip ./release_assets/ServerConsole.zip + cp ./ServerService.zip ./release_assets/ServerService.zip + cp ./DMAPI.zip ./release_assets/DMAPI.zip + cp ./swagger/tgs_api.json ./release_assets/swagger.json + cp ./schema/tgs-api.graphql ./release_assets/tgs-api.graphql + cp ./ServerUpdatePackage.zip ./release_assets/ServerUpdatePackage.zip + cp ./packaging-debian/tgstation-server-v${{ env.TGS_VERSION }}.debian.packaging.tar.xz ./release_assets/tgstation-server-v${{ env.TGS_VERSION }}.debian.packaging.tar.xz + cp ./build/package/winget/Tgstation.Server.Host.Service.Wix.Bundle/bin/Release/mariadb.msi ./release_assets/mariadb-${{ env.MARIADB_VERSION }}-winx64.msi + cp ./build/package/winget/tgstation-server-installer.exe ./release_assets/tgstation-server-installer.exe + - name: Create GitHub Release - uses: actions/create-release@v1 - id: create_release - env: - GITHUB_TOKEN: ${{ steps.app-token-generation.outputs.token }} + uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b with: + token: ${{ steps.app-token-generation.outputs.token }} tag_name: tgstation-server-v${{ env.TGS_VERSION }} - release_name: tgstation-server-v${{ env.TGS_VERSION }} + files: | + ./release_assets/* + name: tgstation-server-v${{ env.TGS_VERSION }} body_path: release_notes.md - commitish: ${{ github.event.head_commit.id }} - - - name: Upload Server Console Artifact to Release - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ steps.app-token-generation.outputs.token }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./ServerConsole.zip - asset_name: ServerConsole.zip - asset_content_type: application/zip - - - name: Upload Server Service Artifact - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ steps.app-token-generation.outputs.token }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./ServerService.zip - asset_name: ServerService.zip - asset_content_type: application/zip - - - name: Upload DMAPI Artifact - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ steps.app-token-generation.outputs.token }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./DMAPI.zip - asset_name: DMAPI.zip - asset_content_type: application/zip - - - name: Upload REST API Artifact - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ steps.app-token-generation.outputs.token }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./swagger/tgs_api.json - asset_name: swagger.json - asset_content_type: application/json - - - name: Upload GraphQL API Artifact - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ steps.app-token-generation.outputs.token }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./schema/tgs-api.graphql - asset_name: tgs-api.graphql - asset_content_type: text/plain - - - name: Upload Server Update Package Artifact - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ steps.app-token-generation.outputs.token }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./ServerUpdatePackage.zip - asset_name: ServerUpdatePackage.zip - asset_content_type: application/zip - - - name: Upload Debian Packaging Artifact - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ steps.app-token-generation.outputs.token }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./packaging-debian/tgstation-server-v${{ env.TGS_VERSION }}.debian.packaging.tar.xz - asset_name: tgstation-server-v${{ env.TGS_VERSION }}.debian.packaging.tar.xz - asset_content_type: application/x-tar - - - name: Upload MariaDB .msi - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ steps.app-token-generation.outputs.token }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./build/package/winget/Tgstation.Server.Host.Service.Wix.Bundle/bin/Release/mariadb.msi - asset_name: mariadb-${{ env.MARIADB_VERSION }}-winx64.msi - asset_content_type: application/octet-stream - - - name: Upload Installer .exe - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ steps.app-token-generation.outputs.token }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./build/package/winget/tgstation-server-installer.exe - asset_name: tgstation-server-installer.exe - asset_content_type: application/octet-stream - - update-nix: - name: Nix Deployment - needs: changelog-regen - if: (!(cancelled() || failure())) && needs.changelog-regen.result == 'success' - uses: ./.github/workflows/nix-deployment.yml - secrets: inherit + target_commitish: ${{ github.event.head_commit.id }} + make_latest: true changelog-regen: name: Regenerate Changelog diff --git a/.github/workflows/ci-security.yml b/.github/workflows/ci-security.yml index eb7806f7d1..279b8bc2b4 100644 --- a/.github/workflows/ci-security.yml +++ b/.github/workflows/ci-security.yml @@ -24,7 +24,7 @@ concurrency: jobs: security-checkpoint: name: Check CI Clearance - if: github.event_name == 'pull_request_target' && (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id || github.event.pull_request.user.id == 49699333) && github.event.pull_request.state == 'open' + if: github.event_name == 'pull_request_target' && (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id || github.event.pull_request.user.id == 49699333) && github.event.pull_request.state == 'open' && github.event.pull_request.draft != true runs-on: ubuntu-latest steps: - name: Generate App Token @@ -62,7 +62,7 @@ jobs: ci-pipline-workflow-call: name: CI Pipeline needs: security-checkpoint - if: (!(cancelled() || failure()) && (needs.security-checkpoint.result == 'success' || (github.event_name != 'pull_request_target' && github.event.pull_request.head.repo.id == github.event.pull_request.base.repo.id && github.event.pull_request.user.id != 49699333))) + if: (!(cancelled() || failure()) && (needs.security-checkpoint.result == 'success' || (github.event_name != 'pull_request_target' && github.event.pull_request.head.repo.id == github.event.pull_request.base.repo.id && github.event.pull_request.user.id != 49699333)) && github.event.pull_request.draft != true) uses: ./.github/workflows/ci-pipeline.yml secrets: inherit with: diff --git a/.github/workflows/nix-deployment.yml b/.github/workflows/nix-deployment.yml deleted file mode 100644 index 470913346e..0000000000 --- a/.github/workflows/nix-deployment.yml +++ /dev/null @@ -1,68 +0,0 @@ -name: Nix Deployment - -on: - workflow_call: - workflow_dispatch: - -jobs: - update-nix: - name: Update Nix SHA (master) - runs-on: ubuntu-latest - steps: - - name: Install Native Packages # Name checked in rerunFlakyTests.js - run: | - sudo apt-get update - sudo apt-get install -y xmlstarlet - - - name: Setup Nix - uses: cachix/install-nix-action@v31 - with: - nix_path: nixpkgs=channel:nixos-unstable - - - name: Generate App Token - id: app-token-generation - uses: actions/create-github-app-token@v1 - with: - app-id: ${{ secrets.APP_ID }} - private-key: ${{ secrets.APP_PRIVATE_KEY }} - - - name: Checkout - uses: actions/checkout@v4 - with: - token: ${{ steps.app-token-generation.outputs.token }} - ref: master - fetch-depth: 0 - # fetch-tags: true Doesn't work, see https://github.com/actions/checkout/issues/1471 - - - name: Parse TGS version - run: echo "TGS_VERSION=$(xmlstarlet sel -N X="http://schemas.microsoft.com/developer/msbuild/2003" --template --value-of /X:Project/X:PropertyGroup/X:TgsCoreVersion build/Version.props)" >> $GITHUB_ENV - - - name: Retrieve ServerConsole.zip Artifact - run: | - mkdir release - curl -L https://github.com/tgstation/tgstation-server/releases/download/tgstation-server-v${{ env.TGS_VERSION }}/ServerConsole.zip -f -o release/ServerConsole.zip - - - name: Regenerate Nix Hash - run: | - nix hash path ./release > build/package/nix/ServerConsole.sha256 - cat build/package/nix/ServerConsole.sha256 - rm -rf ./release - - - name: Commit - run: | - git config --global push.default simple - git config user.name "tgstation-server-ci[bot]" - git config user.email "161980869+tgstation-server-ci[bot]@users.noreply.github.com" - git add build/package/nix/ServerConsole.sha256 - git commit -m "Update nix SHA256 for TGS v${{ env.TGS_VERSION }}" - - - name: Re-tag - run: | - git tag -d tgstation-server-v${{ env.TGS_VERSION }} - git tag -a tgstation-server-v${{ env.TGS_VERSION }} -m tgstation-server-v${{ env.TGS_VERSION }} - - - name: Push Commit - run: git push - - - name: Force Push Tags - run: git push -f origin tag tgstation-server-v${{ env.TGS_VERSION }} diff --git a/.github/workflows/regenerate-nix-deps.yml b/.github/workflows/regenerate-nix-deps.yml new file mode 100644 index 0000000000..bb35638f82 --- /dev/null +++ b/.github/workflows/regenerate-nix-deps.yml @@ -0,0 +1,54 @@ +name: Regenerate Nix Nuget Dependencies + +on: + pull_request_target: + paths: + - '**.csproj' + - '**.props' + - '**.yml' + branches: + - dev + - master + - V7 + +concurrency: + group: "regenerate-nix-dependencies-${{ github.head_ref || github.run_id }}-${{ github.event_name }}" + cancel-in-progress: true + +jobs: + regenerate-nix-dependencies: + name: Regenerate Nix Dependencies + runs-on: ubuntu-latest + if: github.event.pull_request.head.repo.id == github.event.pull_request.base.repo.id + steps: + - name: Setup Nix + uses: cachix/install-nix-action@v31 + with: + nix_path: nixpkgs=channel:nixos-unstable + + - name: Generate App Token + id: app-token-generation + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + + - name: Clone PR Branch + run: git clone -b ${{ github.event.pull_request.head.ref }} --depth 1 "https://tgstation-server-ci:${{ steps.app-token-generation.outputs.token }}@github.com/tgstation/tgstation-server" . + + - name: Regenerate Dependencies + run: | + cd build/package/nix + nix shell nixpkgs#nuget-to-json nixpkgs#dotnetCorePackages.sdk_8_0 -c "./GenDeps.sh" + + - name: Commit + run: | + git config user.name "tgstation-server-ci[bot]" + git config user.email "161980869+tgstation-server-ci[bot]@users.noreply.github.com" + git add build/package/nix/deps.json + git diff-index --quiet HEAD || git commit -m "Regenerate Nix Hashes based on Nuget Packages" + + - name: Push + run: | + git config --global push.default simple + git push -u origin ${{ github.event.pull_request.head.ref }} 2>&1 diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 0000000000..ac2af192bd --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index 9b3eb0ad9b..8cd929bca6 100644 --- a/README.md +++ b/README.md @@ -169,6 +169,7 @@ The following dependencies are required. - aspnetcore-runtime-8.0 (See Prerequisites under the `Ubuntu/Debian Package` section) - libc6-i386 - libstdc++6:i386 +- libcurl4:i386 - gcc-multilib (Only on 64-bit systems) - gdb (for using gcore to create core dumps) @@ -321,12 +322,6 @@ Create an `appsettings.Production.yml` file next to `appsettings.yml`. This will - `Swarm:UpdateRequiredNodeCount`: Should be set to the total number of servers in your swarm minus 1. Prevents updates from occurring unless the non-controller server count in the swarm is greater than or equal to this value. -- `Telemetry:DisableVersionReporting`: Prevents you installation and the version you're using from being reported on the source repository's deployments list - -- `Telemetry:ServerFriendlyName`: Prevents anonymous TGS version usage statistics from being sent to be displayed on the repository. - -- `Telemetry:VersionReportingRepositoryId`: The repository telemetry is sent to. For security reasons, this is not the main TGS repo. See the [tgstation-server-deployments](https://github.com/tgstation/tgstation-server-deployments) repository for more information. - #### OAuth Configuration - `Security:OAuth:`: Sets the OAuth client ID and secret for a given ``. The currently supported providers are `GitHub`, `Discord`, and `InvisionCommunity`. Setting these fields to `null` disables logins AND gateway auth with the provider, but does not stop users from associating their accounts using the API. Sample Entry: diff --git a/build/Common.props b/build/Common.props index d826c09025..3f04e9db66 100644 --- a/build/Common.props +++ b/build/Common.props @@ -5,6 +5,7 @@ net$(TgsNetMajorVersion).0 latest true + true diff --git a/build/Dockerfile b/build/Dockerfile index 8a3a1d24f7..cfdf9bcaed 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -1,17 +1,14 @@ FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim AS build -# Set in CI -ARG TGS_TELEMETRY_KEY_FILE= - # install node and npm # replace shell with bash so we can source files RUN curl --silent -o- https://raw.githubusercontent.com/creationix/nvm/v0.39.1/install.sh | sh -ENV NODE_VERSION 20.5.1 +ENV NODE_VERSION=20.5.1 -ENV NVM_DIR /root/.nvm -ENV NODE_PATH $NVM_DIR/v$NODE_VERSION/lib/node_modules -ENV PATH $NVM_DIR/versions/node/v$NODE_VERSION/bin:$PATH +ENV NVM_DIR=/root/.nvm +ENV NODE_PATH=$NVM_DIR/v$NODE_VERSION/lib/node_modules +ENV PATH=$NVM_DIR/versions/node/v$NODE_VERSION/bin:$PATH RUN . $NVM_DIR/nvm.sh \ && nvm install $NODE_VERSION \ @@ -50,8 +47,7 @@ RUN dotnet publish -c Release -o /app \ WORKDIR /repo/src/Tgstation.Server.Host -RUN export TGS_TELEMETRY_KEY_FILE="../../${TGS_TELEMETRY_KEY_FILE}" \ - && dotnet publish -c Release -o /app/lib/Default \ +RUN dotnet publish -c Release -o /app/lib/Default \ && cd ../.. \ && build/RemoveUnsupportedRuntimes.sh /app/lib/Default \ && mv /app/lib/Default/appsettings* /app @@ -70,9 +66,9 @@ RUN dpkg --add-architecture i386 \ EXPOSE 5000 -ENV General__ValidInstancePaths__0 /tgs_instances -ENV FileLogging__Directory /tgs_logs -ENV Internal__UsingDocker true +ENV General__ValidInstancePaths__0=/tgs_instances +ENV FileLogging__Directory=/tgs_logs +ENV Internal__UsingDocker=true WORKDIR /app diff --git a/build/NewtonsoftJson.props b/build/NewtonsoftJson.props index da775538fb..3180130e0f 100644 --- a/build/NewtonsoftJson.props +++ b/build/NewtonsoftJson.props @@ -1,6 +1,6 @@ - + diff --git a/build/NugetCommon.props b/build/NugetCommon.props index f07a00a656..a215932944 100644 --- a/build/NugetCommon.props +++ b/build/NugetCommon.props @@ -25,7 +25,7 @@ - + diff --git a/build/SrcCommon.props b/build/SrcCommon.props index a98bf6bad8..97402d3a22 100644 --- a/build/SrcCommon.props +++ b/build/SrcCommon.props @@ -17,7 +17,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/build/TestCommon.props b/build/TestCommon.props index 8a6c5d675f..701289e318 100644 --- a/build/TestCommon.props +++ b/build/TestCommon.props @@ -3,24 +3,24 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + - + - + diff --git a/build/Version.props b/build/Version.props index 80ba2e95de..76db731fcd 100644 --- a/build/Version.props +++ b/build/Version.props @@ -3,22 +3,22 @@ - 6.18.2 - 5.8.0 - 10.13.0 - 0.6.0 + 6.19.0 + 5.9.0 + 10.14.0 + 0.7.0 7.0.0 - 18.0.0 - 21.0.0 - 7.3.3 - 5.10.1 + 18.2.0 + 21.2.0 + 7.4.0 + 5.11.0 1.6.0 9.0.0 1.2.1 netstandard2.0 8 - https://download.visualstudio.microsoft.com/download/pr/751d3fcd-72db-4da2-b8d0-709c19442225/33cc492bde704bfd6d70a2b9109005a0/dotnet-hosting-8.0.6-win.exe + https://builds.dotnet.microsoft.com/dotnet/aspnetcore/Runtime/8.0.19/dotnet-hosting-8.0.19-win.exe 11.4.2 diff --git a/build/analyzers.ruleset b/build/analyzers.ruleset index 15ba71dc89..214eba9ecc 100644 --- a/build/analyzers.ruleset +++ b/build/analyzers.ruleset @@ -1,10 +1,10 @@ - + - + diff --git a/build/package/nix/GenDeps.sh b/build/package/nix/GenDeps.sh new file mode 100755 index 0000000000..5a7deed97c --- /dev/null +++ b/build/package/nix/GenDeps.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +cd ../../../src/Tgstation.Server.Host.Console +dotnet restore --packages out +nuget-to-json out > ../../build/package/nix/deps.json +rm -rf out diff --git a/build/package/nix/deps.json b/build/package/nix/deps.json new file mode 100644 index 0000000000..a1ac3aa37f --- /dev/null +++ b/build/package/nix/deps.json @@ -0,0 +1,227 @@ +[ + { + "pname": "Microsoft.Build.Tasks.Git", + "version": "8.0.0", + "hash": "sha256-vX6/kPij8vNAu8f7rrvHHhPrNph20IcufmrBgZNxpQA=" + }, + { + "pname": "Microsoft.Extensions.Configuration", + "version": "9.0.8", + "hash": "sha256-GnD1Ar/yZfCZQw2k/2jKteLG1lF/Dk7S3tgMvn+SFqc=" + }, + { + "pname": "Microsoft.Extensions.Configuration.Abstractions", + "version": "9.0.8", + "hash": "sha256-hes+QZM3DQ1R/8CDOdWObk6s1oGhzFqka8Qc7Baf9PY=" + }, + { + "pname": "Microsoft.Extensions.Configuration.Binder", + "version": "9.0.8", + "hash": "sha256-N8WMvnbCKsUtpK08B1CYi5LOuq6Sbv3Nois4CKjDQJ8=" + }, + { + "pname": "Microsoft.Extensions.Configuration.CommandLine", + "version": "9.0.8", + "hash": "sha256-U8YasTaEsniloNaSPvlcXmGPfgTzjP8RvCfnLVx1onE=" + }, + { + "pname": "Microsoft.Extensions.Configuration.EnvironmentVariables", + "version": "9.0.8", + "hash": "sha256-g3FgfS11nS02dwDnpjpiD+r7nyUZ2eifGl8r2rfwanY=" + }, + { + "pname": "Microsoft.Extensions.Configuration.FileExtensions", + "version": "9.0.8", + "hash": "sha256-W7PnvqPcdJnJIPaEh1qRDh/WCVSz/KQy+GAMhMNhKE4=" + }, + { + "pname": "Microsoft.Extensions.Configuration.Json", + "version": "9.0.8", + "hash": "sha256-/QFT/SksJcsZ2Cjw0WkJzLnp+mT2m+38avEOgttrAaM=" + }, + { + "pname": "Microsoft.Extensions.Configuration.UserSecrets", + "version": "9.0.8", + "hash": "sha256-QcSfPQku3Hh5UIvuR3r3JYFDo2DFIZOx/i0/JmtOPWE=" + }, + { + "pname": "Microsoft.Extensions.DependencyInjection", + "version": "9.0.8", + "hash": "sha256-fJOwbtlmP6mXGYqHRCqtb7e08h5mFza6Wmd1NbNq3ug=" + }, + { + "pname": "Microsoft.Extensions.DependencyInjection.Abstractions", + "version": "9.0.8", + "hash": "sha256-uFBeyx8WTgDX2z8paf6ZAQ45WexaWG8uzO5x+qGrPRU=" + }, + { + "pname": "Microsoft.Extensions.Diagnostics", + "version": "9.0.8", + "hash": "sha256-jV71HdeEU/T60f5qr2ND5GY6/Qk4iPiMUbnjEq8S6Qo=" + }, + { + "pname": "Microsoft.Extensions.Diagnostics.Abstractions", + "version": "9.0.8", + "hash": "sha256-nsgRtkUUC5q+Wc76lu3xxRgOT0dw+EXGa4pFCeI0iEo=" + }, + { + "pname": "Microsoft.Extensions.FileProviders.Abstractions", + "version": "9.0.8", + "hash": "sha256-9X3roHvoAFzlTwVSlkbksB9EosKjVHeXuR5Jm682Wvk=" + }, + { + "pname": "Microsoft.Extensions.FileProviders.Physical", + "version": "9.0.8", + "hash": "sha256-lVnOgpxjO5VaCgviGeQ0R8kAIiDN1nKqpbj8CrCDpic=" + }, + { + "pname": "Microsoft.Extensions.FileSystemGlobbing", + "version": "9.0.8", + "hash": "sha256-1dmTABLD1Zo2vdZFsASTx8T8MRI8emN//KuNP3OiWKw=" + }, + { + "pname": "Microsoft.Extensions.Hosting", + "version": "9.0.8", + "hash": "sha256-0tnVesvcSrqvLarEVEf0kqJXdveLQCY7rCLis8b206Q=" + }, + { + "pname": "Microsoft.Extensions.Hosting.Abstractions", + "version": "9.0.8", + "hash": "sha256-N1XwGfMh2a9grBfObp9md7YPSm7rlNZO5NG8OmHn8W4=" + }, + { + "pname": "Microsoft.Extensions.Hosting.Systemd", + "version": "9.0.8", + "hash": "sha256-yFVkcy8UK36wAvVkrjg77y/8xFo0rvQNH0OrKApVNWE=" + }, + { + "pname": "Microsoft.Extensions.Logging", + "version": "9.0.8", + "hash": "sha256-SEVCMpVwjcQtTSs4lirb89A36MxLQwwqdDFWbr1VvP8=" + }, + { + "pname": "Microsoft.Extensions.Logging.Abstractions", + "version": "9.0.8", + "hash": "sha256-vaUApbwsqKt7+AItgusbCKKdTyOg/5KCdSZjDZarw20=" + }, + { + "pname": "Microsoft.Extensions.Logging.Configuration", + "version": "9.0.8", + "hash": "sha256-/6sIAQlXXDdWnERGXN9XqqOFf1Q71uFSMiThwdIPzEY=" + }, + { + "pname": "Microsoft.Extensions.Logging.Console", + "version": "9.0.8", + "hash": "sha256-8cz7l8ubkRIBd6gwDJcpJd66MYNU0LsvDH9+PU+mTJo=" + }, + { + "pname": "Microsoft.Extensions.Logging.Debug", + "version": "9.0.8", + "hash": "sha256-IPMzdLmY/l0IP25DTQ13qdA/wwX0V3wq+sTHp8bX0UM=" + }, + { + "pname": "Microsoft.Extensions.Logging.EventLog", + "version": "9.0.8", + "hash": "sha256-W/yr3lXCwbtY5DTq50q5vM4Jeibj8j//xZF2qa6lgcc=" + }, + { + "pname": "Microsoft.Extensions.Logging.EventSource", + "version": "9.0.8", + "hash": "sha256-AFjHG4fiHgbJbgcr6u0/M83vf0XtdoeA5ZGSrIV31Co=" + }, + { + "pname": "Microsoft.Extensions.Options", + "version": "9.0.8", + "hash": "sha256-AbwIL8sSZ/qDBKbvabHp1tbExBFr73fYjuXJiV6On1U=" + }, + { + "pname": "Microsoft.Extensions.Options.ConfigurationExtensions", + "version": "9.0.8", + "hash": "sha256-MwTPdC6kJ6Ff/Tw7BHa9JzRwBeCVdRS1gMz17agSjaY=" + }, + { + "pname": "Microsoft.Extensions.Primitives", + "version": "9.0.8", + "hash": "sha256-K3T8krgXZmvQg87AQQrn9kiH2sDyKzRUMDyuB/ItmPc=" + }, + { + "pname": "Microsoft.NETCore.Platforms", + "version": "1.1.0", + "hash": "sha256-FeM40ktcObQJk4nMYShB61H/E8B7tIKfl9ObJ0IOcCM=" + }, + { + "pname": "Microsoft.NETFramework.ReferenceAssemblies", + "version": "1.0.3", + "hash": "sha256-FBoJP5DHZF0QHM0xLm9yd4HJZVQOuSpSKA+VQRpphEE=" + }, + { + "pname": "Microsoft.NETFramework.ReferenceAssemblies.net20", + "version": "1.0.3", + "hash": "sha256-f3SZf+wzjd5w9ajJ/Qmqb+IShnMeexM8wTPWROTjxeg=" + }, + { + "pname": "Microsoft.SourceLink.Common", + "version": "8.0.0", + "hash": "sha256-AfUqleVEqWuHE7z2hNiwOLnquBJ3tuYtbkdGMppHOXc=" + }, + { + "pname": "Microsoft.SourceLink.GitHub", + "version": "8.0.0", + "hash": "sha256-hNTkpKdCLY5kIuOmznD1mY+pRdJ0PKu2HypyXog9vb0=" + }, + { + "pname": "Mono.Posix.NETStandard", + "version": "1.0.0", + "hash": "sha256-/F61k7MY/fu2FcfW7CkyjuUroKwlYAXPQFVeDs1QknY=" + }, + { + "pname": "NETStandard.Library", + "version": "2.0.3", + "hash": "sha256-Prh2RPebz/s8AzHb2sPHg3Jl8s31inv9k+Qxd293ybo=" + }, + { + "pname": "StyleCop.Analyzers", + "version": "1.2.0-beta.556", + "hash": "sha256-97YYQcr5vZxTvi36608eUkA1wb6xllZQ7UcXbjrYIfU=" + }, + { + "pname": "StyleCop.Analyzers.Unstable", + "version": "1.2.0.556", + "hash": "sha256-aVop7a9r+X2RsZETgngBm3qQPEIiPBWgHzicGSTEymc=" + }, + { + "pname": "System.Diagnostics.DiagnosticSource", + "version": "9.0.8", + "hash": "sha256-bVGcjEcZUVeY2jOvZeFnG+ym8ebUKOftx+gErNXeiK4=" + }, + { + "pname": "System.Diagnostics.EventLog", + "version": "9.0.8", + "hash": "sha256-u10dcgug0Pwp83YNagVto8Pu3ieuByflYLNwAdX9Fm0=" + }, + { + "pname": "System.IO.Pipelines", + "version": "9.0.8", + "hash": "sha256-dO84rUcpDgj6k8rTZq3Kmy/y+6c/cP1Fa1dsCV9JIlA=" + }, + { + "pname": "System.Runtime.CompilerServices.Unsafe", + "version": "6.1.2", + "hash": "sha256-X2p/U680Zfkr622oc+vg5JYgbDEzE7mLre5DVaayWTc=" + }, + { + "pname": "System.Text.Encodings.Web", + "version": "9.0.8", + "hash": "sha256-2Fy5/VAqBHuwcxHMVevyzQDRIRlyTZlT+5pwr6OtuuU=" + }, + { + "pname": "System.Text.Json", + "version": "9.0.8", + "hash": "sha256-CEoLOj0KeuctK2jXd6yZ+/5yx4apsEh7+xsJH95h/1c=" + }, + { + "pname": "System.Threading.Tasks.Extensions", + "version": "4.6.3", + "hash": "sha256-GrySx1F6Ah6tfnnQt/PHC+dbzg+sfP47OOFX0yJF/xo=" + } +] diff --git a/build/package/nix/package.nix b/build/package/nix/package.nix index 848ebe91f8..b198175138 100644 --- a/build/package/nix/package.nix +++ b/build/package/nix/package.nix @@ -27,44 +27,26 @@ let installPhase = '' mkdir -p $out xmlstarlet sel -N X="http://schemas.microsoft.com/developer/msbuild/2003" --template --value-of /X:Project/X:PropertyGroup/X:TgsCoreVersion ./Version.props > $out/tgs_version.txt + xmlstarlet sel -N X="http://schemas.microsoft.com/developer/msbuild/2003" --template --value-of /X:Project/X:PropertyGroup/X:TgsHostWatchdogVersion ./Version.props > $out/watchdog_version.txt ''; }; - version = (builtins.readFile "${versionParse}/tgs_version.txt"); - fixedOutput = stdenv.mkDerivation { - pname = "tgstation-server-release-server-console-zip"; - inherit version; + tgstation-server-host-console = pkgs.buildDotnetModule { + pname = "Tgstation.Server.Host.Console"; + version = (builtins.readFile "${versionParse}/watchdog_version.txt"); # Be careful, this influences the assembly version - meta = with pkgs.lib; { - description = "Host watchdog binaries for tgstation-server"; - homepage = "https://github.com/tgstation/tgstation-server"; - changelog = "https://github.com/tgstation/tgstation-server/releases/tag/tgstation-server-v${version}"; - license = licenses.agpl3Plus; - platforms = platforms.x86_64; - }; + src = ./../../..; - nativeBuildInputs = with pkgs; [ - curl - cacert - versionParse - ]; + projectFile = "src/Tgstation.Server.Host.Console/Tgstation.Server.Host.Console.csproj"; + nugetDeps = ./deps.json; # see "Generating and updating NuGet dependencies" section for details - src = ./.; + TGS_NIX_BUILD = "yes"; - buildPhase = '' - curl -L https://github.com/tgstation/tgstation-server/releases/download/tgstation-server-v${version}/ServerConsole.zip -o ServerConsole.zip - ''; + executables = []; - installPhase = '' - mkdir -p $out - mv ServerConsole.zip $out/ServerConsole.zip - ''; - - outputHashAlgo = "sha256"; - outputHashMode = "recursive"; - outputHash = (builtins.readFile ./ServerConsole.sha256); + dotnet-sdk = pkgs.dotnetCorePackages.sdk_8_0; + dotnet-runtime = pkgs.dotnetCorePackages.runtime_8_0; }; - rpath = lib.makeLibraryPath [ pkgs.stdenv_32bit.cc.cc.lib ]; in stdenv.mkDerivation { pname = "tgstation-server"; @@ -79,18 +61,17 @@ stdenv.mkDerivation { }; buildInputs = with pkgs; [ - dotnetCorePackages.sdk_8_0 + pkgs.dotnetCorePackages.sdk_8_0 gdb systemd zlib gcc_multi glibc bash + tgstation-server-host-console ]; nativeBuildInputs = with pkgs; [ makeWrapper - unzip - fixedOutput versionParse ]; @@ -98,13 +79,11 @@ stdenv.mkDerivation { installPhase = '' mkdir -p $out/bin - unzip "${fixedOutput}/ServerConsole.zip" -d $out/bin - rm -rf $out/bin/lib makeWrapper ${pkgs.dotnetCorePackages.sdk_8_0}/bin/dotnet $out/bin/tgstation-server --suffix PATH : ${ lib.makeBinPath ( with pkgs; [ - dotnetCorePackages.sdk_8_0 + pkgs.dotnetCorePackages.sdk_8_0 gdb bash ] @@ -117,6 +96,6 @@ stdenv.mkDerivation { zlib ] ) - } --add-flags "$out/bin/Tgstation.Server.Host.Console.dll --bootstrap" + } --add-flags "${tgstation-server-host-console}/lib/Tgstation.Server.Host.Console/Tgstation.Server.Host.Console.dll --bootstrap" ''; } diff --git a/build/package/nix/tgstation-server.nix b/build/package/nix/tgstation-server.nix index 3d08c4263c..f66603a63f 100644 --- a/build/package/nix/tgstation-server.nix +++ b/build/package/nix/tgstation-server.nix @@ -130,7 +130,7 @@ in environment.etc = { "tgstation-server.d/appsettings.yml" = { - text = (builtins.readFile "${package}/bin/appsettings.yml"); + text = (builtins.readFile ./../../../src/Tgstation.Server.Host/appsettings.yml); group = cfg.groupname; mode = "0644"; }; diff --git a/build/package/winget/Tgstation.Server.Host.Service.Wix.Bundle/Tgstation.Server.Host.Service.Wix.Bundle.wixproj b/build/package/winget/Tgstation.Server.Host.Service.Wix.Bundle/Tgstation.Server.Host.Service.Wix.Bundle.wixproj index df2363b5ac..c9525c1183 100644 --- a/build/package/winget/Tgstation.Server.Host.Service.Wix.Bundle/Tgstation.Server.Host.Service.Wix.Bundle.wixproj +++ b/build/package/winget/Tgstation.Server.Host.Service.Wix.Bundle/Tgstation.Server.Host.Service.Wix.Bundle.wixproj @@ -24,8 +24,8 @@ - - + + diff --git a/build/package/winget/Tgstation.Server.Host.Service.Wix.Extensions/Tgstation.Server.Host.Service.Wix.Extensions.csproj b/build/package/winget/Tgstation.Server.Host.Service.Wix.Extensions/Tgstation.Server.Host.Service.Wix.Extensions.csproj index fcdbd512a6..b6b9d3a807 100644 --- a/build/package/winget/Tgstation.Server.Host.Service.Wix.Extensions/Tgstation.Server.Host.Service.Wix.Extensions.csproj +++ b/build/package/winget/Tgstation.Server.Host.Service.Wix.Extensions/Tgstation.Server.Host.Service.Wix.Extensions.csproj @@ -7,7 +7,7 @@ - + diff --git a/build/package/winget/Tgstation.Server.Host.Service.Wix/Tgstation.Server.Host.Service.Wix.wixproj b/build/package/winget/Tgstation.Server.Host.Service.Wix/Tgstation.Server.Host.Service.Wix.wixproj index 272694a58e..0e3ab3e9e9 100644 --- a/build/package/winget/Tgstation.Server.Host.Service.Wix/Tgstation.Server.Host.Service.Wix.wixproj +++ b/build/package/winget/Tgstation.Server.Host.Service.Wix/Tgstation.Server.Host.Service.Wix.wixproj @@ -20,8 +20,8 @@ - - + + diff --git a/global.json b/global.json index 9c97a32878..ddc8b64cb7 100644 --- a/global.json +++ b/global.json @@ -1,7 +1,7 @@ { "sdk": { "version": "8.0.0", - "rollForward": "latestMajor", + "rollForward": "latestMinor", "allowPrerelease": false } } diff --git a/src/DMAPI/tgs.dm b/src/DMAPI/tgs.dm index b9b539c32e..9e04917b24 100644 --- a/src/DMAPI/tgs.dm +++ b/src/DMAPI/tgs.dm @@ -1,7 +1,7 @@ // tgstation-server DMAPI // The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in IETF RFC 2119. -#define TGS_DMAPI_VERSION "7.3.3" +#define TGS_DMAPI_VERSION "7.4.0" // All functions and datums outside this document are subject to change with any version and should not be relied on. @@ -584,11 +584,17 @@ /world/proc/TgsTriggerEvent(event_name, list/parameters, wait_for_completion = FALSE) CAN_BE_REDEFINED(TRUE) return + + +/// Trigger a TGS deployment for the current instance. The current state of the repository will not be changed. +/world/proc/TgsTriggerDeployment() + CAN_BE_REDEFINED(TRUE) + return /* The MIT License -Copyright (c) 2017-2024 Jordan Brown +Copyright (c) 2017-2026 Jordan Brown Permission is hereby granted, free of charge, to any person obtaining a copy of this software and diff --git a/src/DMAPI/tgs/README.md b/src/DMAPI/tgs/README.md index 35ca73d7e9..473442fd11 100644 --- a/src/DMAPI/tgs/README.md +++ b/src/DMAPI/tgs/README.md @@ -5,9 +5,9 @@ This folder should be placed on its own inside a codebase that wishes to use the - [includes.dm](./includes.dm) is the file that should be included by DM code, it handles including the rest. - The [core](./core) folder includes all code not directly part of any API version. - The other versioned folders contain code for the different DMAPI versions. - - [v3210](./v3210) contains the final TGS3 API. - - [v4](./v4) is the legacy DMAPI 4 (Used in TGS 4.0.X versions). - - [v5](./v5) is the current DMAPI version used by TGS >=4.1. + - [v3210](./v3210) contains the final TGS3 API. + - [v4](./v4) is the legacy DMAPI 4 (Used in TGS 4.0.X versions). + - [v5](./v5) is the current DMAPI version used by TGS >=4.1. - [LICENSE](./LICENSE) is the MIT license for the DMAPI. APIs communicate with TGS in two ways. All versions implement TGS -> DM communication using /world/Topic. DM -> TGS communication, called the bridge method, is different for each version. diff --git a/src/DMAPI/tgs/core/README.md b/src/DMAPI/tgs/core/README.md index 965e21b549..7886a85714 100644 --- a/src/DMAPI/tgs/core/README.md +++ b/src/DMAPI/tgs/core/README.md @@ -2,7 +2,7 @@ This folder contains all DMAPI code not directly involved in an API. -- [_definitions.dm](./definitions.dm) contains defines needed across DMAPI internals. +- [\_definitions.dm](./definitions.dm) contains defines needed across DMAPI internals. - [byond_world_export.dm](./byond_world_export.dm) contains the default `/datum/tgs_http_handler` implementation which uses `world.Export()`. - [core.dm](./core.dm) contains the implementations of the `/world/proc/TgsXXX()` procs. Many map directly to the `/datum/tgs_api` functions. It also contains the /datum selection and setup code. - [datum.dm](./datum.dm) contains the `/datum/tgs_api` declarations that all APIs must implement. diff --git a/src/DMAPI/tgs/core/core.dm b/src/DMAPI/tgs/core/core.dm index 63cb5a2c35..f70d07aaed 100644 --- a/src/DMAPI/tgs/core/core.dm +++ b/src/DMAPI/tgs/core/core.dm @@ -177,3 +177,8 @@ parameters = list() return api.TriggerEvent(event_name, parameters, wait_for_completion) + +/world/TgsTriggerDeployment() + var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs) + if(api) + return api.TriggerDeployment() diff --git a/src/DMAPI/tgs/core/datum.dm b/src/DMAPI/tgs/core/datum.dm index 3ca53e9bf7..fae924929d 100644 --- a/src/DMAPI/tgs/core/datum.dm +++ b/src/DMAPI/tgs/core/datum.dm @@ -72,3 +72,6 @@ TGS_PROTECT_DATUM(/datum/tgs_api) /datum/tgs_api/proc/TriggerEvent(event_name, list/parameters, wait_for_completion) return FALSE + +/datum/tgs_api/proc/TriggerDeployment() + return TGS_UNIMPLEMENTED diff --git a/src/DMAPI/tgs/v5/README.md b/src/DMAPI/tgs/v5/README.md index a8a0c748e7..fb374c2f87 100644 --- a/src/DMAPI/tgs/v5/README.md +++ b/src/DMAPI/tgs/v5/README.md @@ -2,8 +2,8 @@ This DMAPI implements bridge requests using HTTP GET requests to TGS. It has no security restrictions. -- [__interop_version.dm](./__interop_version.dm) contains the version of the API used between the DMAPI and TGS. -- [_defines.dm](./_defines.dm) contains constant definitions. +- [\_\_interop_version.dm](./__interop_version.dm) contains the version of the API used between the DMAPI and TGS. +- [\_defines.dm](./_defines.dm) contains constant definitions. - [api.dm](./api.dm) contains the bulk of the API code. - [bridge.dm](./bridge.dm) contains functions related to making bridge requests. - [chunking.dm](./chunking.dm) contains common function for splitting large raw data sets into chunks BYOND can natively process. diff --git a/src/DMAPI/tgs/v5/__interop_version.dm b/src/DMAPI/tgs/v5/__interop_version.dm index 9589cbf31b..e6ebf62db1 100644 --- a/src/DMAPI/tgs/v5/__interop_version.dm +++ b/src/DMAPI/tgs/v5/__interop_version.dm @@ -1 +1 @@ -"5.10.1" +"5.11.0" diff --git a/src/DMAPI/tgs/v5/_defines.dm b/src/DMAPI/tgs/v5/_defines.dm index a47bfd7800..d4bd50353e 100644 --- a/src/DMAPI/tgs/v5/_defines.dm +++ b/src/DMAPI/tgs/v5/_defines.dm @@ -15,6 +15,7 @@ #define DMAPI5_BRIDGE_COMMAND_CHAT_SEND 5 #define DMAPI5_BRIDGE_COMMAND_CHUNK 6 #define DMAPI5_BRIDGE_COMMAND_EVENT 7 +#define DMAPI5_BRIDGE_COMMAND_DEPLOY 8 #define DMAPI5_PARAMETER_ACCESS_IDENTIFIER "accessIdentifier" #define DMAPI5_PARAMETER_CUSTOM_COMMANDS "customCommands" diff --git a/src/DMAPI/tgs/v5/api.dm b/src/DMAPI/tgs/v5/api.dm index 4ca9036b79..8ade67d28e 100644 --- a/src/DMAPI/tgs/v5/api.dm +++ b/src/DMAPI/tgs/v5/api.dm @@ -294,6 +294,19 @@ return TRUE +/datum/tgs_api/v5/TriggerDeployment(event_name, list/parameters, wait_for_completion) + RequireInitialBridgeResponse() + WaitForReattach(TRUE) + + if(interop_version.minor < 11) + TGS_WARNING_LOG("Interop version too low for triggering deployments!") + return FALSE + + var response = Bridge(DMAPI5_BRIDGE_COMMAND_DEPLOY) + if(!response) + return FALSE + return TRUE + /datum/tgs_api/v5/proc/DecodeChannels(chat_update_json) TGS_DEBUG_LOG("DecodeChannels()") var/list/chat_channels_json = chat_update_json[DMAPI5_CHAT_UPDATE_CHANNELS] diff --git a/src/DMAPI/tgs/v5/undefs.dm b/src/DMAPI/tgs/v5/undefs.dm index ca49e46cdf..9fb845c91d 100644 --- a/src/DMAPI/tgs/v5/undefs.dm +++ b/src/DMAPI/tgs/v5/undefs.dm @@ -15,6 +15,7 @@ #undef DMAPI5_BRIDGE_COMMAND_CHAT_SEND #undef DMAPI5_BRIDGE_COMMAND_CHUNK #undef DMAPI5_BRIDGE_COMMAND_EVENT +#undef DMAPI5_BRIDGE_COMMAND_DEPLOY #undef DMAPI5_PARAMETER_ACCESS_IDENTIFIER #undef DMAPI5_PARAMETER_CUSTOM_COMMANDS diff --git a/src/Tgstation.Server.Api/Models/ErrorCode.cs b/src/Tgstation.Server.Api/Models/ErrorCode.cs index ea2c9ebd67..b7673ff4e9 100644 --- a/src/Tgstation.Server.Api/Models/ErrorCode.cs +++ b/src/Tgstation.Server.Api/Models/ErrorCode.cs @@ -493,7 +493,7 @@ namespace Tgstation.Server.Api.Models /// /// Attempted to restart a stopped watchdog. /// - [Description("Cannot restart the watchdog as it is not running!")] + [Description("Cannot perform watchdog operation as it is not running!")] WatchdogNotRunning, /// diff --git a/src/Tgstation.Server.Api/Models/Internal/DreamDaemonApiBase.cs b/src/Tgstation.Server.Api/Models/Internal/DreamDaemonApiBase.cs index 116f27425d..d97e8c3102 100644 --- a/src/Tgstation.Server.Api/Models/Internal/DreamDaemonApiBase.cs +++ b/src/Tgstation.Server.Api/Models/Internal/DreamDaemonApiBase.cs @@ -14,6 +14,13 @@ namespace Tgstation.Server.Api.Models.Internal [ResponseOptions] public long? SessionId { get; set; } + /// + /// A incrementing ID for representing current iteration of servers world (i.e. after calling /world/proc/Reboot). Only unique within the current . Only tracked in game sessions with the DMAPI enabled. + /// + /// 1 + [ResponseOptions] + public long? WorldIteration { get; set; } + /// /// When the current server execution was started. /// diff --git a/src/Tgstation.Server.Api/Rights/RightsHelper.cs b/src/Tgstation.Server.Api/Rights/RightsHelper.cs index 424d4608d5..ec1d0fad08 100644 --- a/src/Tgstation.Server.Api/Rights/RightsHelper.cs +++ b/src/Tgstation.Server.Api/Rights/RightsHelper.cs @@ -33,6 +33,12 @@ namespace Tgstation.Server.Api.Rights /// The of the given . public static Type RightToType(RightsType rightsType) => TypeMap[rightsType]; + /// + /// Iterate the of each right. + /// + /// An of each of right. + public static IEnumerable AllRightTypes() => TypeMap.Values; + /// /// Map a given to its respective . /// diff --git a/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj b/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj index 6f32743090..7ca98fa6f8 100644 --- a/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj +++ b/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj @@ -26,11 +26,11 @@ - + - + - + diff --git a/src/Tgstation.Server.Client.GraphQL/.config/dotnet-tools.json b/src/Tgstation.Server.Client.GraphQL/.config/dotnet-tools.json index 17a88ea077..6cbce338fa 100644 --- a/src/Tgstation.Server.Client.GraphQL/.config/dotnet-tools.json +++ b/src/Tgstation.Server.Client.GraphQL/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "strawberryshake.tools": { - "version": "13.9.12", + "version": "15.1.9", "commands": [ "dotnet-graphql" ] diff --git a/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/CreateSystemUserWithPermissionSet.graphql b/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/CreateSystemUserWithPermissionSet.graphql index 5c9e176dc4..eede94e90b 100644 --- a/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/CreateSystemUserWithPermissionSet.graphql +++ b/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/CreateSystemUserWithPermissionSet.graphql @@ -9,7 +9,7 @@ mutation CreateSystemUserWithPermissionSet($systemIdentifier: String!) { message } } - user { + updatedUser { id } } diff --git a/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/CreateUserFromOAuthConnection.graphql b/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/CreateUserFromOAuthConnection.graphql index dc69b9ef29..50140af490 100644 --- a/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/CreateUserFromOAuthConnection.graphql +++ b/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/CreateUserFromOAuthConnection.graphql @@ -7,7 +7,7 @@ mutation CreateUserFromOAuthConnection($name: String!, $oAuthConnections: [OAuth message } } - user { + updatedUser { id } } diff --git a/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/CreateUserGroupWithInstanceListPerm.graphql b/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/CreateUserGroupWithInstanceListPerm.graphql index c4f88b6770..0f8ed8a366 100644 --- a/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/CreateUserGroupWithInstanceListPerm.graphql +++ b/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/CreateUserGroupWithInstanceListPerm.graphql @@ -1,5 +1,24 @@ mutation CreateUserGroupWithInstanceListPerm($name: String!) { - createUserGroup(input: { name: $name, permissionSet: { instanceManagerRights: { canList: true } } }) { + createUserGroup(input: { + name: $name + permissionSet: { + instanceManagerRights: { + canList: true + canCreate: false + canDelete: false + canGrantPermissions: false + canRead: false + canRelocate: false + canRename: false + canSetAutoUpdate: false + canSetChatBotLimit: false + canSetConfiguration: false + canSetOnline: false + canSetAutoStart: false + canSetAutoStop: false + } + } + }) { userGroup { id name diff --git a/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/CreateUserWithPassword.graphql b/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/CreateUserWithPassword.graphql index 8f330b0364..69e3cbe5d3 100644 --- a/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/CreateUserWithPassword.graphql +++ b/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/CreateUserWithPassword.graphql @@ -7,7 +7,7 @@ mutation CreateUserWithPassword($name: String!, $password: String!) { message } } - user { + updatedUser { id } } diff --git a/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/CreateUserWithPasswordSelectOAuthConnections.graphql b/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/CreateUserWithPasswordSelectOAuthConnections.graphql index 2d6b335889..86874a4834 100644 --- a/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/CreateUserWithPasswordSelectOAuthConnections.graphql +++ b/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/CreateUserWithPasswordSelectOAuthConnections.graphql @@ -1,10 +1,12 @@ mutation CreateUserWithPasswordSelectOAuthConnections($name: String!, $password: String!) { createUserByPasswordAndPermissionSet(input: { name: $name, password: $password }) { - user { + updatedUser { id - oAuthConnections { - externalUserId - provider + user { + oAuthConnections { + externalUserId + provider + } } } errors { diff --git a/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/SetFullPermsOnUserGroup.graphql b/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/SetFullPermsOnUserGroup.graphql index 8c77199901..baad139418 100644 --- a/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/SetFullPermsOnUserGroup.graphql +++ b/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/SetFullPermsOnUserGroup.graphql @@ -25,6 +25,8 @@ mutation SetFullPermsOnUserGroup($id: ID!) { canRead: true canList: true canRelocate: true + canSetAutoStart: true + canSetAutoStop: true } } } diff --git a/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/SetUserGroup.graphql b/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/SetUserGroup.graphql index 4299d5b537..db77e0c601 100644 --- a/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/SetUserGroup.graphql +++ b/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/SetUserGroup.graphql @@ -7,35 +7,37 @@ mutation SetUserGroup($id: ID!, $newGroupId: ID!) { message } } - user { - ownedPermissionSet { - instanceManagerRights { - canCreate - canDelete - canGrantPermissions - canList - canRead - canRelocate - canRename - canSetAutoUpdate - canSetChatBotLimit - canSetConfiguration - canSetOnline + updatedUser { + user { + ownedPermissionSet { + instanceManagerRights { + canCreate + canDelete + canGrantPermissions + canList + canRead + canRelocate + canRename + canSetAutoUpdate + canSetChatBotLimit + canSetConfiguration + canSetOnline + } + administrationRights { + canChangeVersion + canDownloadLogs + canEditOwnServiceConnections + canEditOwnPassword + canReadUsers + canRestartHost + canUploadVersion + canWriteUsers + } } - administrationRights { - canChangeVersion - canDownloadLogs - canEditOwnServiceConnections - canEditOwnPassword - canReadUsers - canRestartHost - canUploadVersion - canWriteUsers + group { + id } } - group { - id - } } } } diff --git a/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/SetUserOAuthConnections.graphql b/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/SetUserOAuthConnections.graphql index 81746cf0b4..34f74f64bf 100644 --- a/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/SetUserOAuthConnections.graphql +++ b/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/SetUserOAuthConnections.graphql @@ -2,17 +2,19 @@ mutation SetUserOAuthConnections($id: ID!, $newOAuthConnections: [OAuthConnectio updateUser( input: { id: $id, newOAuthConnections: $newOAuthConnections } ) { - user { - canonicalName - createdAt - enabled - id - name - systemIdentifier - oAuthConnections { - externalUserId - provider - } + updatedUser { + user { + canonicalName + createdAt + enabled + id + name + systemIdentifier + oAuthConnections { + externalUserId + provider + } + } } errors { ... on ErrorMessageError { diff --git a/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/SetUserPermissionSet.graphql b/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/SetUserPermissionSet.graphql index fa2fa558fe..ae1a58b5fc 100644 --- a/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/SetUserPermissionSet.graphql +++ b/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/SetUserPermissionSet.graphql @@ -7,60 +7,62 @@ mutation SetUserPermissionSet($id: ID!, $permissionSet: PermissionSetInput!) { message } } - user { - effectivePermissionSet { - administrationRights { - canChangeVersion - canDownloadLogs - canEditOwnServiceConnections - canEditOwnPassword - canReadUsers - canRestartHost - canUploadVersion - canWriteUsers + updatedUser { + user { + effectivePermissionSet { + administrationRights { + canChangeVersion + canDownloadLogs + canEditOwnServiceConnections + canEditOwnPassword + canReadUsers + canRestartHost + canUploadVersion + canWriteUsers + } + instanceManagerRights { + canCreate + canDelete + canGrantPermissions + canList + canRead + canRelocate + canRename + canSetAutoUpdate + canSetChatBotLimit + canSetConfiguration + canSetOnline + } + } + ownedPermissionSet { + administrationRights { + canChangeVersion + canDownloadLogs + canEditOwnServiceConnections + canEditOwnPassword + canReadUsers + canRestartHost + canUploadVersion + canWriteUsers + } + instanceManagerRights { + canCreate + canDelete + canGrantPermissions + canList + canRead + canRelocate + canRename + canSetAutoUpdate + canSetChatBotLimit + canSetConfiguration + canSetOnline + } + } + group { + id + } } - instanceManagerRights { - canCreate - canDelete - canGrantPermissions - canList - canRead - canRelocate - canRename - canSetAutoUpdate - canSetChatBotLimit - canSetConfiguration - canSetOnline - } - } - ownedPermissionSet { - administrationRights { - canChangeVersion - canDownloadLogs - canEditOwnServiceConnections - canEditOwnPassword - canReadUsers - canRestartHost - canUploadVersion - canWriteUsers - } - instanceManagerRights { - canCreate - canDelete - canGrantPermissions - canList - canRead - canRelocate - canRename - canSetAutoUpdate - canSetChatBotLimit - canSetConfiguration - canSetOnline - } - } - group { - id - } } } } diff --git a/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/UpdateUserOAuthConnections.graphql b/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/UpdateUserOAuthConnections.graphql index 8cd18d2dc5..ad0fdfdbad 100644 --- a/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/UpdateUserOAuthConnections.graphql +++ b/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/UpdateUserOAuthConnections.graphql @@ -1,10 +1,12 @@ mutation UpdateUserOAuthConnections($id: ID!, $newOAuthConnections: [OAuthConnectionInput!]) { updateUser(input: { id: $id, newOAuthConnections: $newOAuthConnections }) { - user { + updatedUser { id - oAuthConnections { - externalUserId - provider + user { + oAuthConnections { + externalUserId + provider + } } } errors { diff --git a/src/Tgstation.Server.Client.GraphQL/GQL/Queries/GetSomeGroupInfo.graphql b/src/Tgstation.Server.Client.GraphQL/GQL/Queries/GetSomeGroupInfo.graphql index 5d37e33b07..6ed50da05f 100644 --- a/src/Tgstation.Server.Client.GraphQL/GQL/Queries/GetSomeGroupInfo.graphql +++ b/src/Tgstation.Server.Client.GraphQL/GQL/Queries/GetSomeGroupInfo.graphql @@ -1,38 +1,36 @@ query GetSomeGroupInfo($id: ID!) { swarm { - users { - groups { - byId(id: $id) { - permissionSet { - instanceManagerRights { - canCreate - canDelete - canGrantPermissions - canList - canRead - canRelocate - canRename - canSetAutoUpdate - canSetChatBotLimit - canSetConfiguration - canSetOnline - } - administrationRights { - canChangeVersion - canDownloadLogs - canEditOwnServiceConnections - canEditOwnPassword - canReadUsers - canRestartHost - canUploadVersion - canWriteUsers - } + userGroups { + byId(id: $id) { + permissionSet { + instanceManagerRights { + canCreate + canDelete + canGrantPermissions + canList + canRead + canRelocate + canRename + canSetAutoUpdate + canSetChatBotLimit + canSetConfiguration + canSetOnline } - queryableUsersByGroup(first: 1) { - totalCount - nodes { - id - } + administrationRights { + canChangeVersion + canDownloadLogs + canEditOwnServiceConnections + canEditOwnPassword + canReadUsers + canRestartHost + canUploadVersion + canWriteUsers + } + } + queryableUsersByGroup(first: 1) { + totalCount + nodes { + id } } } diff --git a/src/Tgstation.Server.Client.GraphQL/GQL/Queries/ListUserGroups.graphql b/src/Tgstation.Server.Client.GraphQL/GQL/Queries/ListUserGroups.graphql index 08bc93b4fd..3c043fa184 100644 --- a/src/Tgstation.Server.Client.GraphQL/GQL/Queries/ListUserGroups.graphql +++ b/src/Tgstation.Server.Client.GraphQL/GQL/Queries/ListUserGroups.graphql @@ -1,12 +1,10 @@ query ListUserGroups { swarm { - users { - groups { - queryableGroups { - totalCount - nodes { - id - } + userGroups { + queryableGroups { + totalCount + nodes { + id } } } diff --git a/src/Tgstation.Server.Client.GraphQL/Tgstation.Server.Client.GraphQL.csproj b/src/Tgstation.Server.Client.GraphQL/Tgstation.Server.Client.GraphQL.csproj index 67e9d74754..04d5ae2baf 100644 --- a/src/Tgstation.Server.Client.GraphQL/Tgstation.Server.Client.GraphQL.csproj +++ b/src/Tgstation.Server.Client.GraphQL/Tgstation.Server.Client.GraphQL.csproj @@ -9,28 +9,24 @@ - + + - - - - - - + - + $(IntermediateOutputPath)berry/GraphQLClient.Client.cs $(IntermediateOutputPath)berry/GraphQLClient.Client.cs diff --git a/src/Tgstation.Server.Client/ApiClient.cs b/src/Tgstation.Server.Client/ApiClient.cs index 0a7214e3cd..ffa5731f35 100644 --- a/src/Tgstation.Server.Client/ApiClient.cs +++ b/src/Tgstation.Server.Client/ApiClient.cs @@ -69,9 +69,9 @@ namespace Tgstation.Server.Client }; /// - /// The for the . + /// The for the . /// - readonly IHttpClient httpClient; + readonly HttpClient httpClient; /// /// The s used by the . @@ -166,7 +166,7 @@ namespace Tgstation.Server.Client /// The value of . /// The value of . public ApiClient( - IHttpClient httpClient, + HttpClient httpClient, Uri url, ApiHeaders apiHeaders, ApiHeaders? tokenRefreshHeaders, diff --git a/src/Tgstation.Server.Client/ApiClientFactory.cs b/src/Tgstation.Server.Client/ApiClientFactory.cs index 0429f7d649..469010c153 100644 --- a/src/Tgstation.Server.Client/ApiClientFactory.cs +++ b/src/Tgstation.Server.Client/ApiClientFactory.cs @@ -1,7 +1,7 @@ using System; +using System.Net.Http; using Tgstation.Server.Api; -using Tgstation.Server.Common.Http; namespace Tgstation.Server.Client { @@ -19,5 +19,19 @@ namespace Tgstation.Server.Client apiHeaders, tokenRefreshHeaders, authless); + + /// + public IApiClient CreateApiClient( + Uri url, + ApiHeaders apiHeaders, + ApiHeaders? tokenRefreshHeaders, + HttpMessageHandler handler, + bool disposeHandler, + bool authless) => new ApiClient( + new HttpClient(handler, disposeHandler), + url, + apiHeaders, + tokenRefreshHeaders, + authless); } } diff --git a/src/Tgstation.Server.Client/IApiClientFactory.cs b/src/Tgstation.Server.Client/IApiClientFactory.cs index e49385dcf9..f0fa49a42a 100644 --- a/src/Tgstation.Server.Client/IApiClientFactory.cs +++ b/src/Tgstation.Server.Client/IApiClientFactory.cs @@ -1,4 +1,5 @@ using System; +using System.Net.Http; using Tgstation.Server.Api; @@ -22,5 +23,23 @@ namespace Tgstation.Server.Client ApiHeaders apiHeaders, ApiHeaders? tokenRefreshHeaders, bool authless); + + /// + /// Create an . + /// + /// The base . + /// The for the . + /// The to use to generate a new . + /// The to use with the internal . + /// If should be disposed with the created . + /// If there should be no authentication performed. + /// A new . + public IApiClient CreateApiClient( + Uri url, + ApiHeaders apiHeaders, + ApiHeaders? tokenRefreshHeaders, + HttpMessageHandler handler, + bool disposeHandler, + bool authless); } } diff --git a/src/Tgstation.Server.Client/Tgstation.Server.Client.csproj b/src/Tgstation.Server.Client/Tgstation.Server.Client.csproj index f681ecff8f..88aa1055c9 100644 --- a/src/Tgstation.Server.Client/Tgstation.Server.Client.csproj +++ b/src/Tgstation.Server.Client/Tgstation.Server.Client.csproj @@ -11,9 +11,9 @@ - + - + diff --git a/src/Tgstation.Server.Common/Http/CachedResponseStream.cs b/src/Tgstation.Server.Common/Http/CachedResponseStream.cs index eb007b54a5..14361b6ef8 100644 --- a/src/Tgstation.Server.Common/Http/CachedResponseStream.cs +++ b/src/Tgstation.Server.Common/Http/CachedResponseStream.cs @@ -36,7 +36,7 @@ namespace Tgstation.Server.Common.Http response.Content = null; try { - // don't cry about the missing CancellationToken overload: https://github.com/dotnet/runtime/issues/916 + // don't cry about the missing CancellationToken overload: https://github.com/dotnet/corefx/issues/32615#issuecomment-562083237 var responseStream = await content.ReadAsStreamAsync().ConfigureAwait(false); return new CachedResponseStream(content, responseStream); } diff --git a/src/Tgstation.Server.Common/Http/HttpClient.cs b/src/Tgstation.Server.Common/Http/HttpClient.cs deleted file mode 100644 index 692c10a304..0000000000 --- a/src/Tgstation.Server.Common/Http/HttpClient.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Threading; -using System.Threading.Tasks; - -namespace Tgstation.Server.Common.Http -{ - /// - public sealed class HttpClient : IHttpClient - { - /// - public TimeSpan Timeout - { - get => httpClient.Timeout; - set => httpClient.Timeout = value; - } - - /// - public HttpRequestHeaders DefaultRequestHeaders => httpClient.DefaultRequestHeaders; - - /// - /// The real . - /// - readonly System.Net.Http.HttpClient httpClient; - - /// - /// Initializes a new instance of the class. - /// - /// The to wrap. - public HttpClient(System.Net.Http.HttpClient implementation) - { - httpClient = implementation ?? throw new ArgumentNullException(nameof(implementation)); - } - - /// - /// Initializes a new instance of the class. - /// - public HttpClient() - : this(new System.Net.Http.HttpClient()) - { - } - - /// - public void Dispose() => httpClient.Dispose(); - - /// - public Task SendAsync(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationToken cancellationToken) - => httpClient.SendAsync(request, completionOption, cancellationToken); - } -} diff --git a/src/Tgstation.Server.Common/Http/HttpClientFactory.cs b/src/Tgstation.Server.Common/Http/HttpClientFactory.cs deleted file mode 100644 index 93659471b5..0000000000 --- a/src/Tgstation.Server.Common/Http/HttpClientFactory.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using System.Net.Http.Headers; - -namespace Tgstation.Server.Common.Http -{ - /// - /// that creates s. - /// - public sealed class HttpClientFactory : IAbstractHttpClientFactory - { - /// - public IHttpClient CreateClient() - { - var client = new HttpClient(); - try - { - client.DefaultRequestHeaders.UserAgent.Add(userAgent); - return client; - } - catch - { - client.Dispose(); - throw; - } - } - - /// - /// The used as created client's User-Agent header on request. - /// - readonly ProductInfoHeaderValue userAgent; - - /// - /// Initializes a new instance of the class. - /// - /// The value of . - public HttpClientFactory(ProductInfoHeaderValue userAgent) - { - this.userAgent = userAgent ?? throw new ArgumentNullException(nameof(userAgent)); - } - } -} diff --git a/src/Tgstation.Server.Common/Http/IAbstractHttpClientFactory.cs b/src/Tgstation.Server.Common/Http/IAbstractHttpClientFactory.cs deleted file mode 100644 index 120712f2bb..0000000000 --- a/src/Tgstation.Server.Common/Http/IAbstractHttpClientFactory.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Tgstation.Server.Common.Http -{ - /// - /// Creates s. - /// - public interface IAbstractHttpClientFactory - { - /// - /// Create a . - /// - /// A new . - IHttpClient CreateClient(); - } -} diff --git a/src/Tgstation.Server.Common/Http/IHttpClient.cs b/src/Tgstation.Server.Common/Http/IHttpClient.cs deleted file mode 100644 index d0ade7050d..0000000000 --- a/src/Tgstation.Server.Common/Http/IHttpClient.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Threading; -using System.Threading.Tasks; - -namespace Tgstation.Server.Common.Http -{ - /// - /// For sending HTTP requests. - /// - public interface IHttpClient : IDisposable - { - /// - /// The request timeout. - /// - TimeSpan Timeout { get; set; } - - /// - /// The used on every request. - /// - HttpRequestHeaders DefaultRequestHeaders { get; } - - /// - /// Send an HTTP request. - /// - /// The . - /// The . - /// The for the operation. - /// A resulting in the of the request. - Task SendAsync(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationToken cancellationToken); - } -} diff --git a/src/Tgstation.Server.Common/Tgstation.Server.Common.csproj b/src/Tgstation.Server.Common/Tgstation.Server.Common.csproj index 97766617d4..acdff03504 100644 --- a/src/Tgstation.Server.Common/Tgstation.Server.Common.csproj +++ b/src/Tgstation.Server.Common/Tgstation.Server.Common.csproj @@ -12,10 +12,10 @@ - + - + diff --git a/src/Tgstation.Server.Host.Console/Tgstation.Server.Host.Console.csproj b/src/Tgstation.Server.Host.Console/Tgstation.Server.Host.Console.csproj index 75f657b562..e12827c195 100644 --- a/src/Tgstation.Server.Host.Console/Tgstation.Server.Host.Console.csproj +++ b/src/Tgstation.Server.Host.Console/Tgstation.Server.Host.Console.csproj @@ -13,9 +13,9 @@ - + - + diff --git a/src/Tgstation.Server.Host.Service/Tgstation.Server.Host.Service.csproj b/src/Tgstation.Server.Host.Service/Tgstation.Server.Host.Service.csproj index 59ab045d5a..9e250c2c8d 100644 --- a/src/Tgstation.Server.Host.Service/Tgstation.Server.Host.Service.csproj +++ b/src/Tgstation.Server.Host.Service/Tgstation.Server.Host.Service.csproj @@ -17,25 +17,25 @@ - + - + - + - + - + - + - + - + - + - + diff --git a/src/Tgstation.Server.Host.Utils.GitLab.GraphQL/.config/dotnet-tools.json b/src/Tgstation.Server.Host.Utils.GitLab.GraphQL/.config/dotnet-tools.json new file mode 100644 index 0000000000..4b619e4d2c --- /dev/null +++ b/src/Tgstation.Server.Host.Utils.GitLab.GraphQL/.config/dotnet-tools.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "strawberryshake.tools": { + "version": "15.1.9", + "commands": [ + "dotnet-graphql" + ], + "rollForward": false + } + } +} diff --git a/src/Tgstation.Server.Host.Utils.GitLab.GraphQL/.graphqlrc.json b/src/Tgstation.Server.Host.Utils.GitLab.GraphQL/.graphqlrc.json index 3ed674432b..be863532b4 100644 --- a/src/Tgstation.Server.Host.Utils.GitLab.GraphQL/.graphqlrc.json +++ b/src/Tgstation.Server.Host.Utils.GitLab.GraphQL/.graphqlrc.json @@ -4,12 +4,9 @@ "extensions": { "strawberryShake": { "name": "GraphQLClient", - "url": "../../artifacts/gitlab-api.graphql", + "url": "https://gitlab.com/api/graphql", "namespace": "Tgstation.Server.Host.Utils.GitLab.GraphQL", - "records": { - "inputs": false, - "entities": false - }, + "dependencyInjection": true, "transportProfiles": [ { "default": "Http", diff --git a/src/Tgstation.Server.Host.Utils.GitLab.GraphQL/Tgstation.Server.Host.Utils.GitLab.GraphQL.csproj b/src/Tgstation.Server.Host.Utils.GitLab.GraphQL/Tgstation.Server.Host.Utils.GitLab.GraphQL.csproj index ab2f3f9dbc..a6b397bbbd 100644 --- a/src/Tgstation.Server.Host.Utils.GitLab.GraphQL/Tgstation.Server.Host.Utils.GitLab.GraphQL.csproj +++ b/src/Tgstation.Server.Host.Utils.GitLab.GraphQL/Tgstation.Server.Host.Utils.GitLab.GraphQL.csproj @@ -1,4 +1,4 @@ - + @@ -7,28 +7,24 @@ enable - - - - - - + - + + - + $(IntermediateOutputPath)berry/GraphQLClient.Client.cs @@ -39,7 +35,7 @@ - + diff --git a/src/Tgstation.Server.Host.Utils.GitLab.GraphQL/package.json b/src/Tgstation.Server.Host.Utils.GitLab.GraphQL/package.json deleted file mode 100644 index 79610fdd4e..0000000000 --- a/src/Tgstation.Server.Host.Utils.GitLab.GraphQL/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "license": "AGPL-3.0-only", - "devDependencies": { - "apollo": "^2.34.0" - }, - "packageManager": "yarn@4.7.0" -} diff --git a/src/Tgstation.Server.Host.Utils.GitLab.GraphQL/yarn.lock b/src/Tgstation.Server.Host.Utils.GitLab.GraphQL/yarn.lock deleted file mode 100644 index 5e4dca122d..0000000000 --- a/src/Tgstation.Server.Host.Utils.GitLab.GraphQL/yarn.lock +++ /dev/null @@ -1,4532 +0,0 @@ -# This file is generated by running "yarn install" inside your project. -# Manual changes might be lost - proceed with caution! - -__metadata: - version: 8 - cacheKey: 10 - -"@apollo/federation@npm:0.27.0": - version: 0.27.0 - resolution: "@apollo/federation@npm:0.27.0" - dependencies: - apollo-graphql: "npm:^0.9.3" - lodash.xorby: "npm:^4.7.0" - peerDependencies: - graphql: ^14.5.0 || ^15.0.0 - checksum: 10/fcec6e6c5cd358fcab3d1830e5bf4641960024529a2fca07ae5bc0ee4740afe3a0dfa5c93dc4a9d232b020dc7a380a2aec4c6afc640df3140235e4c572ddacaf - languageName: node - linkType: hard - -"@apollo/utils.keyvaluecache@npm:^1.0.1": - version: 1.0.2 - resolution: "@apollo/utils.keyvaluecache@npm:1.0.2" - dependencies: - "@apollo/utils.logger": "npm:^1.0.0" - lru-cache: "npm:7.10.1 - 7.13.1" - checksum: 10/353794482ad8c476e36c2152d6a647244a8cffbbc26a9b2b28986b3651aaff16b73df1dfed9edc8eb151fe7bd4c59d06b3b1b4c6b1aa516fceb8119a46fa8f72 - languageName: node - linkType: hard - -"@apollo/utils.logger@npm:^1.0.0": - version: 1.0.1 - resolution: "@apollo/utils.logger@npm:1.0.1" - checksum: 10/621bd80ce43a73f97342568b712fd46fee9041212d4c7264a63676e29d17ab292773c3c21b91f8a2dffb1fe7931ece3954886bd04e3100e1765c6d05e231e2a7 - languageName: node - linkType: hard - -"@apollographql/apollo-tools@npm:0.5.4, @apollographql/apollo-tools@npm:^0.5.4": - version: 0.5.4 - resolution: "@apollographql/apollo-tools@npm:0.5.4" - peerDependencies: - graphql: ^14.2.1 || ^15.0.0 || ^16.0.0 - checksum: 10/4f69566d23ffb77ffedd87c679dcab608400f297e4cd5423151977b917737c427015485a8e0436feeb5154574171742ab626fb1a8f5ae2739070757976fd49f2 - languageName: node - linkType: hard - -"@apollographql/graphql-language-service-interface@npm:^2.0.2": - version: 2.0.2 - resolution: "@apollographql/graphql-language-service-interface@npm:2.0.2" - dependencies: - "@apollographql/graphql-language-service-parser": "npm:^2.0.0" - "@apollographql/graphql-language-service-types": "npm:^2.0.0" - "@apollographql/graphql-language-service-utils": "npm:^2.0.2" - peerDependencies: - graphql: ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 - checksum: 10/1c7a8260a7cc93e84f513b8c7f03f156bb3719d9b52f0ef7a1cf58c118e9fe6c8db2c2ff60ad3d5ecf16d222bd83dceee806e62df0304538cbc80af5a55db134 - languageName: node - linkType: hard - -"@apollographql/graphql-language-service-parser@npm:^2.0.0": - version: 2.0.2 - resolution: "@apollographql/graphql-language-service-parser@npm:2.0.2" - dependencies: - "@apollographql/graphql-language-service-types": "npm:^2.0.0" - peerDependencies: - graphql: ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 - checksum: 10/47b5507b72951f030739592666b3957c7f12a36333b9f89669e152ddea86f5a064813168e08ac81c5a74e3f5bf43aa10fdadec3d4597ddc0680cd97b388729a6 - languageName: node - linkType: hard - -"@apollographql/graphql-language-service-types@npm:^2.0.0": - version: 2.0.2 - resolution: "@apollographql/graphql-language-service-types@npm:2.0.2" - peerDependencies: - graphql: ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 - checksum: 10/d2648de9775bb70920ad5cc6c77ba1f331020c4118a5863e3b163348c6c1cbca4f742611a1ec6a0064a9413260675e37c8b2544d02e4c2a525caf893780360f6 - languageName: node - linkType: hard - -"@apollographql/graphql-language-service-utils@npm:^2.0.2": - version: 2.0.2 - resolution: "@apollographql/graphql-language-service-utils@npm:2.0.2" - dependencies: - "@apollographql/graphql-language-service-types": "npm:^2.0.0" - peerDependencies: - graphql: ^0.12.0 || ^0.13.0 || ^14.0.0 - checksum: 10/ace741509b992d402adcc4fa7cbfcea37aa0f5c62b6eb64f20427449428ebf6c25be0e3f939fc8173191a0e76a32d0d308113e3e2c8ed031a37847e2114712b3 - languageName: node - linkType: hard - -"@babel/code-frame@npm:^7.0.0": - version: 7.26.2 - resolution: "@babel/code-frame@npm:7.26.2" - dependencies: - "@babel/helper-validator-identifier": "npm:^7.25.9" - js-tokens: "npm:^4.0.0" - picocolors: "npm:^1.0.0" - checksum: 10/db2c2122af79d31ca916755331bb4bac96feb2b334cdaca5097a6b467fdd41963b89b14b6836a14f083de7ff887fc78fa1b3c10b14e743d33e12dbfe5ee3d223 - languageName: node - linkType: hard - -"@babel/generator@npm:7.17.10": - version: 7.17.10 - resolution: "@babel/generator@npm:7.17.10" - dependencies: - "@babel/types": "npm:^7.17.10" - "@jridgewell/gen-mapping": "npm:^0.1.0" - jsesc: "npm:^2.5.1" - checksum: 10/32fa924433681d8122aa30a0833d4b25d400d075d143920f559352c066db4e656362d1863cfce316034d424f6345df8a64909cb2a8386e96198cc0ca18d6c4aa - languageName: node - linkType: hard - -"@babel/helper-string-parser@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/helper-string-parser@npm:7.25.9" - checksum: 10/c28656c52bd48e8c1d9f3e8e68ecafd09d949c57755b0d353739eb4eae7ba4f7e67e92e4036f1cd43378cc1397a2c943ed7bcaf5949b04ab48607def0258b775 - languageName: node - linkType: hard - -"@babel/helper-validator-identifier@npm:^7.16.7, @babel/helper-validator-identifier@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/helper-validator-identifier@npm:7.25.9" - checksum: 10/3f9b649be0c2fd457fa1957b694b4e69532a668866b8a0d81eabfa34ba16dbf3107b39e0e7144c55c3c652bf773ec816af8df4a61273a2bb4eb3145ca9cf478e - languageName: node - linkType: hard - -"@babel/parser@npm:^7.1.3": - version: 7.26.10 - resolution: "@babel/parser@npm:7.26.10" - dependencies: - "@babel/types": "npm:^7.26.10" - bin: - parser: ./bin/babel-parser.js - checksum: 10/3f87781f46795ba72448168061d9e99c394fdf9cd4aa3ddf053a06334247da4d25d0923ccc89195937d3360d384cee181e99711763c1e8fe81d4f17ee22541fc - languageName: node - linkType: hard - -"@babel/types@npm:7.17.10": - version: 7.17.10 - resolution: "@babel/types@npm:7.17.10" - dependencies: - "@babel/helper-validator-identifier": "npm:^7.16.7" - to-fast-properties: "npm:^2.0.0" - checksum: 10/e128cc776b7c7e48884c0c3665f475cb8432a4404f2fc10ab7edc831998ed6ec423411191171a72a1e08058c7d8faa5b535b5cc9c3bb2677fe35ac3505792045 - languageName: node - linkType: hard - -"@babel/types@npm:^7.17.10, @babel/types@npm:^7.26.10": - version: 7.26.10 - resolution: "@babel/types@npm:7.26.10" - dependencies: - "@babel/helper-string-parser": "npm:^7.25.9" - "@babel/helper-validator-identifier": "npm:^7.25.9" - checksum: 10/6b4f24ee77af853c2126eaabb65328cd44a7d6f439685131cf929c30567e01b6ea2e5d5653b2c304a09c63a5a6199968f0e27228a007acf35032036d79a9dee6 - languageName: node - linkType: hard - -"@endemolshinegroup/cosmiconfig-typescript-loader@npm:^3.0.2": - version: 3.0.2 - resolution: "@endemolshinegroup/cosmiconfig-typescript-loader@npm:3.0.2" - dependencies: - lodash.get: "npm:^4" - make-error: "npm:^1" - ts-node: "npm:^9" - tslib: "npm:^2" - peerDependencies: - cosmiconfig: ">=6" - checksum: 10/a7f5d9e4e262338833f07810fe602dfaf179bd3241aa1d9da2d24a2bfd7df1c43841f2b8b9d4f0cc9e0ef672a71e853a312c80f5d125d707bcb43c0d3fefce35 - languageName: node - linkType: hard - -"@jridgewell/gen-mapping@npm:^0.1.0": - version: 0.1.1 - resolution: "@jridgewell/gen-mapping@npm:0.1.1" - dependencies: - "@jridgewell/set-array": "npm:^1.0.0" - "@jridgewell/sourcemap-codec": "npm:^1.4.10" - checksum: 10/ba76fae1d8ea52b181474518c705a8eac36405dfc836fb07e9c25730a84d29e05fd6d954f121057742639f3128a24ea45d205c9c989efd464d1114671c19fa6c - languageName: node - linkType: hard - -"@jridgewell/set-array@npm:^1.0.0": - version: 1.2.1 - resolution: "@jridgewell/set-array@npm:1.2.1" - checksum: 10/832e513a85a588f8ed4f27d1279420d8547743cc37fcad5a5a76fc74bb895b013dfe614d0eed9cb860048e6546b798f8f2652020b4b2ba0561b05caa8c654b10 - languageName: node - linkType: hard - -"@jridgewell/sourcemap-codec@npm:^1.4.10": - version: 1.5.0 - resolution: "@jridgewell/sourcemap-codec@npm:1.5.0" - checksum: 10/4ed6123217569a1484419ac53f6ea0d9f3b57e5b57ab30d7c267bdb27792a27eb0e4b08e84a2680aa55cc2f2b411ffd6ec3db01c44fdc6dc43aca4b55f8374fd - languageName: node - linkType: hard - -"@nodelib/fs.scandir@npm:2.1.5": - version: 2.1.5 - resolution: "@nodelib/fs.scandir@npm:2.1.5" - dependencies: - "@nodelib/fs.stat": "npm:2.0.5" - run-parallel: "npm:^1.1.9" - checksum: 10/6ab2a9b8a1d67b067922c36f259e3b3dfd6b97b219c540877a4944549a4d49ea5ceba5663905ab5289682f1f3c15ff441d02f0447f620a42e1cb5e1937174d4b - languageName: node - linkType: hard - -"@nodelib/fs.stat@npm:2.0.5, @nodelib/fs.stat@npm:^2.0.2": - version: 2.0.5 - resolution: "@nodelib/fs.stat@npm:2.0.5" - checksum: 10/012480b5ca9d97bff9261571dbbec7bbc6033f69cc92908bc1ecfad0792361a5a1994bc48674b9ef76419d056a03efadfce5a6cf6dbc0a36559571a7a483f6f0 - languageName: node - linkType: hard - -"@nodelib/fs.walk@npm:^1.2.3": - version: 1.2.8 - resolution: "@nodelib/fs.walk@npm:1.2.8" - dependencies: - "@nodelib/fs.scandir": "npm:2.1.5" - fastq: "npm:^1.6.0" - checksum: 10/40033e33e96e97d77fba5a238e4bba4487b8284678906a9f616b5579ddaf868a18874c0054a75402c9fbaaa033a25ceae093af58c9c30278e35c23c9479e79b0 - languageName: node - linkType: hard - -"@oclif/color@npm:^1.0.0, @oclif/color@npm:^1.0.1": - version: 1.0.13 - resolution: "@oclif/color@npm:1.0.13" - dependencies: - ansi-styles: "npm:^4.2.1" - chalk: "npm:^4.1.0" - strip-ansi: "npm:^6.0.1" - supports-color: "npm:^8.1.1" - tslib: "npm:^2" - checksum: 10/885a6ba4a7d296fef559ba1a7f04a6e67dba92d7aeb46dd23b18d99551d3716710c077d2f3180ff0a4a6d18998b920f723b92865bcd21970ae03dbaff57ba480 - languageName: node - linkType: hard - -"@oclif/command@npm:1.8.16": - version: 1.8.16 - resolution: "@oclif/command@npm:1.8.16" - dependencies: - "@oclif/config": "npm:^1.18.2" - "@oclif/errors": "npm:^1.3.5" - "@oclif/help": "npm:^1.0.1" - "@oclif/parser": "npm:^3.8.6" - debug: "npm:^4.1.1" - semver: "npm:^7.3.2" - peerDependencies: - "@oclif/config": ^1 - checksum: 10/a276315eeda8b375fd7958c95a8757795f4fcfb59eabc88b00254dd40c441b018e39204aff2a80dfa708fc54f2643c2aef846ab7f7f20c641919fc5586f133da - languageName: node - linkType: hard - -"@oclif/config@npm:1.18.16": - version: 1.18.16 - resolution: "@oclif/config@npm:1.18.16" - dependencies: - "@oclif/errors": "npm:^1.3.6" - "@oclif/parser": "npm:^3.8.16" - debug: "npm:^4.3.4" - globby: "npm:^11.1.0" - is-wsl: "npm:^2.1.1" - tslib: "npm:^2.6.1" - checksum: 10/f3985ecf92980063258f2c1f9c2791f228c033aeb6cd424cd857d920d2776a25484dbd3b9f9e71ec38399e438d5f5e7b8f616fe7fab3721fc1f6a203aa438781 - languageName: node - linkType: hard - -"@oclif/config@npm:1.18.3": - version: 1.18.3 - resolution: "@oclif/config@npm:1.18.3" - dependencies: - "@oclif/errors": "npm:^1.3.5" - "@oclif/parser": "npm:^3.8.0" - debug: "npm:^4.1.1" - globby: "npm:^11.0.1" - is-wsl: "npm:^2.1.1" - tslib: "npm:^2.3.1" - checksum: 10/8a414a6abf3bf604f7fe94fe2d46331a2f5debafc77f8f1837641b13221642adebd598b70f69be639051852bfab84aa3b3e51fac8639873a74f8f43cb4434c67 - languageName: node - linkType: hard - -"@oclif/config@npm:^1.18.2": - version: 1.18.17 - resolution: "@oclif/config@npm:1.18.17" - dependencies: - "@oclif/errors": "npm:^1.3.6" - "@oclif/parser": "npm:^3.8.17" - debug: "npm:^4.3.4" - globby: "npm:^11.1.0" - is-wsl: "npm:^2.1.1" - tslib: "npm:^2.6.1" - checksum: 10/ba78114673e22860c6444e6950e4bf3e7266015353cc8acec253556ca44eeb0245ab3a44d14561ebb99bc2cb91e664d5eceba231090f701f922800c7494fa2b4 - languageName: node - linkType: hard - -"@oclif/core@npm:^1.0.8, @oclif/core@npm:^1.1.1, @oclif/core@npm:^1.2.0, @oclif/core@npm:^1.2.1, @oclif/core@npm:^1.3.6, @oclif/core@npm:^1.7.0": - version: 1.26.2 - resolution: "@oclif/core@npm:1.26.2" - dependencies: - "@oclif/linewrap": "npm:^1.0.0" - "@oclif/screen": "npm:^3.0.4" - ansi-escapes: "npm:^4.3.2" - ansi-styles: "npm:^4.3.0" - cardinal: "npm:^2.1.1" - chalk: "npm:^4.1.2" - clean-stack: "npm:^3.0.1" - cli-progress: "npm:^3.10.0" - debug: "npm:^4.3.4" - ejs: "npm:^3.1.6" - fs-extra: "npm:^9.1.0" - get-package-type: "npm:^0.1.0" - globby: "npm:^11.1.0" - hyperlinker: "npm:^1.0.0" - indent-string: "npm:^4.0.0" - is-wsl: "npm:^2.2.0" - js-yaml: "npm:^3.14.1" - natural-orderby: "npm:^2.0.3" - object-treeify: "npm:^1.1.33" - password-prompt: "npm:^1.1.2" - semver: "npm:^7.3.7" - string-width: "npm:^4.2.3" - strip-ansi: "npm:^6.0.1" - supports-color: "npm:^8.1.1" - supports-hyperlinks: "npm:^2.2.0" - tslib: "npm:^2.4.1" - widest-line: "npm:^3.1.0" - wrap-ansi: "npm:^7.0.0" - checksum: 10/1d4d1e1914e6ef673e7dd36a674b4ac9d40cf08ad09233d908e61c75e4fdb1dbf7f2c8cde4c9b8c7a64c23ff81e56b1fd2e0b0bd2cfcb8f1422b12828f7b9255 - languageName: node - linkType: hard - -"@oclif/errors@npm:1.3.5": - version: 1.3.5 - resolution: "@oclif/errors@npm:1.3.5" - dependencies: - clean-stack: "npm:^3.0.0" - fs-extra: "npm:^8.1" - indent-string: "npm:^4.0.0" - strip-ansi: "npm:^6.0.0" - wrap-ansi: "npm:^7.0.0" - checksum: 10/bac7aa3fc4f9a08588453ca365962499e9ae9a2e0638dd65c3875db3136c82335041e9938a0adf461eb5d350023efd386a8ca8d31f74a2bf6eac4cac77ec4cc8 - languageName: node - linkType: hard - -"@oclif/errors@npm:1.3.6, @oclif/errors@npm:^1.3.5, @oclif/errors@npm:^1.3.6": - version: 1.3.6 - resolution: "@oclif/errors@npm:1.3.6" - dependencies: - clean-stack: "npm:^3.0.0" - fs-extra: "npm:^8.1" - indent-string: "npm:^4.0.0" - strip-ansi: "npm:^6.0.1" - wrap-ansi: "npm:^7.0.0" - checksum: 10/6240bd1f1893ea93b0efec1bb33865aa51ccf4feddb798a0596b497d6afaa927232a475ca618ab561685a82fb44327bc29bdceba7124ebf5ca8544972907c154 - languageName: node - linkType: hard - -"@oclif/help@npm:^1.0.1": - version: 1.0.15 - resolution: "@oclif/help@npm:1.0.15" - dependencies: - "@oclif/config": "npm:1.18.16" - "@oclif/errors": "npm:1.3.6" - chalk: "npm:^4.1.2" - indent-string: "npm:^4.0.0" - lodash: "npm:^4.17.21" - string-width: "npm:^4.2.0" - strip-ansi: "npm:^6.0.0" - widest-line: "npm:^3.1.0" - wrap-ansi: "npm:^6.2.0" - checksum: 10/5281fa9dba7f414197ad406543b8c431637c9a1fd08a229ed466eca6a8a4a8c4d6b593e339c137bcc6dbd95b1e8b46d4e9a775988c5c0e5b34828a88b069839e - languageName: node - linkType: hard - -"@oclif/linewrap@npm:^1.0.0": - version: 1.0.0 - resolution: "@oclif/linewrap@npm:1.0.0" - checksum: 10/210edd1aac4a0a2b68ec71a2b62e5c3a15f88ac0d5af31aae126fff1d147921c5de06d611999d4958699bafe1298cad9f62a9ff45fc55e4846bf35fcc7a6f331 - languageName: node - linkType: hard - -"@oclif/parser@npm:^3.8.0, @oclif/parser@npm:^3.8.16, @oclif/parser@npm:^3.8.17, @oclif/parser@npm:^3.8.6": - version: 3.8.17 - resolution: "@oclif/parser@npm:3.8.17" - dependencies: - "@oclif/errors": "npm:^1.3.6" - "@oclif/linewrap": "npm:^1.0.0" - chalk: "npm:^4.1.0" - tslib: "npm:^2.6.2" - checksum: 10/6ac2afa1094ad150ed77d2eb1cdb4233552c3c3c0c9e657e029714d1e4523fa1f717b13ebc0aca16f681a648282378d874b2650340dd9817540efa542d4fa0bf - languageName: node - linkType: hard - -"@oclif/plugin-autocomplete@npm:1.3.0": - version: 1.3.0 - resolution: "@oclif/plugin-autocomplete@npm:1.3.0" - dependencies: - "@oclif/core": "npm:^1.7.0" - chalk: "npm:^4.1.0" - debug: "npm:^4.3.4" - fs-extra: "npm:^9.0.1" - checksum: 10/2aa61f820e5e9a1011066a35ecc28f071541786865506fa310b58b4f44eff313222a7c4599921a3030681e494b0d117a0fb03a83d605e8edc19cf0393716b45f - languageName: node - linkType: hard - -"@oclif/plugin-help@npm:5.1.12": - version: 5.1.12 - resolution: "@oclif/plugin-help@npm:5.1.12" - dependencies: - "@oclif/core": "npm:^1.3.6" - checksum: 10/595d4459bd5ab7e4a24c0ca3667ef652a32a8253570a38e19d8c24be24d09b7eb3018a4c4825ce43b5cc6224a4b3f73a5eecdabf07587e017b0ddf38557b04d3 - languageName: node - linkType: hard - -"@oclif/plugin-not-found@npm:2.3.1": - version: 2.3.1 - resolution: "@oclif/plugin-not-found@npm:2.3.1" - dependencies: - "@oclif/color": "npm:^1.0.0" - "@oclif/core": "npm:^1.2.1" - fast-levenshtein: "npm:^3.0.0" - lodash: "npm:^4.17.21" - checksum: 10/1b19d792eb72a40c7f205a3bd988ab2efad91812f8184a68e95928288fd2a2b79d86062b8409cbe491f73af8997dd6823f6e16aa840be3dafb8c15ed3db8316b - languageName: node - linkType: hard - -"@oclif/plugin-plugins@npm:2.1.0": - version: 2.1.0 - resolution: "@oclif/plugin-plugins@npm:2.1.0" - dependencies: - "@oclif/color": "npm:^1.0.1" - "@oclif/core": "npm:^1.2.0" - chalk: "npm:^4.1.2" - debug: "npm:^4.1.0" - fs-extra: "npm:^9.0" - http-call: "npm:^5.2.2" - load-json-file: "npm:^5.3.0" - npm-run-path: "npm:^4.0.1" - semver: "npm:^7.3.2" - tslib: "npm:^2.0.0" - yarn: "npm:^1.22.17" - checksum: 10/019af0a52c915e223b40eaae2f77ad5451c5e554626af99ad071dc836cb658c98db86ce387538b0bd0ca9901ae96c2c1a4aff1acd121e4834e9479ca86a98674 - languageName: node - linkType: hard - -"@oclif/plugin-warn-if-update-available@npm:2.0.4": - version: 2.0.4 - resolution: "@oclif/plugin-warn-if-update-available@npm:2.0.4" - dependencies: - "@oclif/core": "npm:^1.0.8" - chalk: "npm:^4.1.0" - debug: "npm:^4.1.0" - fs-extra: "npm:^9.0.1" - http-call: "npm:^5.2.2" - lodash: "npm:^4.17.21" - semver: "npm:^7.3.2" - checksum: 10/5318a6666a8e47cba5cff0195bb38e13de361ee9d9d3e3ce05e49f7d168e3bab5138d8f657c575cb512e9a1f83c226b095ac20fc4424e60e710aaf6153d2431d - languageName: node - linkType: hard - -"@oclif/screen@npm:^1.0.4 ": - version: 1.0.4 - resolution: "@oclif/screen@npm:1.0.4" - checksum: 10/fb0027fe8c46c68a14258f7c87add28258844faf51033d5c5546be39747d64ef043feb71cfd71b0576d1c9e00892ca5aed2e280419d072177645962564609d53 - languageName: node - linkType: hard - -"@oclif/screen@npm:^3.0.4": - version: 3.0.8 - resolution: "@oclif/screen@npm:3.0.8" - checksum: 10/6a93c701b4daed2f17f1d7149d8accc3c4294ee1c2741e0ba9a039eeed5db6ddf2a182a3ea36cf9dc47a1875d073f053a32f2b19c8b81165dc855b2f47d86071 - languageName: node - linkType: hard - -"@samverschueren/stream-to-observable@npm:^0.3.0": - version: 0.3.1 - resolution: "@samverschueren/stream-to-observable@npm:0.3.1" - dependencies: - any-observable: "npm:^0.3.0" - peerDependenciesMeta: - rxjs: - optional: true - zen-observable: - optional: true - checksum: 10/2b62bff492d58b4fdc8339ecc29ac3d8e1c37ae920c9d41fcb490a574422c3df1eae26b07103198b97b586c5e7106d47440ce24580a2a919aa5f9359d9914f2c - languageName: node - linkType: hard - -"@types/node-fetch@npm:^2.5.10": - version: 2.6.12 - resolution: "@types/node-fetch@npm:2.6.12" - dependencies: - "@types/node": "npm:*" - form-data: "npm:^4.0.0" - checksum: 10/8107c479da83a3114fcbfa882eba95ee5175cccb5e4dd53f737a96f2559ae6262f662176b8457c1656de09ec393cc7b20a266c077e4bfb21e929976e1cf4d0f9 - languageName: node - linkType: hard - -"@types/node@npm:*": - version: 22.13.10 - resolution: "@types/node@npm:22.13.10" - dependencies: - undici-types: "npm:~6.20.0" - checksum: 10/57dc6a5e0110ca9edea8d7047082e649fa7fa813f79e4a901653b9174141c622f4336435648baced5b38d9f39843f404fa2d8d7a10981610da26066bc8caab48 - languageName: node - linkType: hard - -"@types/parse-json@npm:^4.0.0": - version: 4.0.2 - resolution: "@types/parse-json@npm:4.0.2" - checksum: 10/5bf62eec37c332ad10059252fc0dab7e7da730764869c980b0714777ad3d065e490627be9f40fc52f238ffa3ac4199b19de4127196910576c2fe34dd47c7a470 - languageName: node - linkType: hard - -"@wry/equality@npm:^0.1.2": - version: 0.1.11 - resolution: "@wry/equality@npm:0.1.11" - dependencies: - tslib: "npm:^1.9.3" - checksum: 10/b3672e3c1be3b19589eff5135af835dc84ba57c1caddd6d37ec2f1910fb4054ac2693c48d39750bcad21e733d7652169b6842ebb2cbf7a1a835502fec7f86480 - languageName: node - linkType: hard - -"ajv@npm:^8.0.1": - version: 8.17.1 - resolution: "ajv@npm:8.17.1" - dependencies: - fast-deep-equal: "npm:^3.1.3" - fast-uri: "npm:^3.0.1" - json-schema-traverse: "npm:^1.0.0" - require-from-string: "npm:^2.0.2" - checksum: 10/ee3c62162c953e91986c838f004132b6a253d700f1e51253b99791e2dbfdb39161bc950ebdc2f156f8568035bb5ed8be7bd78289cd9ecbf3381fe8f5b82e3f33 - languageName: node - linkType: hard - -"ansi-escapes@npm:^3.0.0": - version: 3.2.0 - resolution: "ansi-escapes@npm:3.2.0" - checksum: 10/0f94695b677ea742f7f1eed961f7fd8d05670f744c6ad1f8f635362f6681dcfbc1575cb05b43abc7bb6d67e25a75fb8c7ea8f2a57330eb2c76b33f18cb2cef0a - languageName: node - linkType: hard - -"ansi-escapes@npm:^4.3.0, ansi-escapes@npm:^4.3.2": - version: 4.3.2 - resolution: "ansi-escapes@npm:4.3.2" - dependencies: - type-fest: "npm:^0.21.3" - checksum: 10/8661034456193ffeda0c15c8c564a9636b0c04094b7f78bd01517929c17c504090a60f7a75f949f5af91289c264d3e1001d91492c1bd58efc8e100500ce04de2 - languageName: node - linkType: hard - -"ansi-regex@npm:^2.0.0": - version: 2.1.1 - resolution: "ansi-regex@npm:2.1.1" - checksum: 10/190abd03e4ff86794f338a31795d262c1dfe8c91f7e01d04f13f646f1dcb16c5800818f886047876f1272f065570ab86b24b99089f8b68a0e11ff19aed4ca8f1 - languageName: node - linkType: hard - -"ansi-regex@npm:^3.0.0": - version: 3.0.1 - resolution: "ansi-regex@npm:3.0.1" - checksum: 10/09daf180c5f59af9850c7ac1bd7fda85ba596cc8cbeb210826e90755f06c818af86d9fa1e6e8322fab2c3b9e9b03f56c537b42241139f824dd75066a1e7257cc - languageName: node - linkType: hard - -"ansi-regex@npm:^5.0.1": - version: 5.0.1 - resolution: "ansi-regex@npm:5.0.1" - checksum: 10/2aa4bb54caf2d622f1afdad09441695af2a83aa3fe8b8afa581d205e57ed4261c183c4d3877cee25794443fde5876417d859c108078ab788d6af7e4fe52eb66b - languageName: node - linkType: hard - -"ansi-styles@npm:^2.2.1": - version: 2.2.1 - resolution: "ansi-styles@npm:2.2.1" - checksum: 10/ebc0e00381f2a29000d1dac8466a640ce11943cef3bda3cd0020dc042e31e1058ab59bf6169cd794a54c3a7338a61ebc404b7c91e004092dd20e028c432c9c2c - languageName: node - linkType: hard - -"ansi-styles@npm:^3.2.1": - version: 3.2.1 - resolution: "ansi-styles@npm:3.2.1" - dependencies: - color-convert: "npm:^1.9.0" - checksum: 10/d85ade01c10e5dd77b6c89f34ed7531da5830d2cb5882c645f330079975b716438cd7ebb81d0d6e6b4f9c577f19ae41ab55f07f19786b02f9dfd9e0377395665 - languageName: node - linkType: hard - -"ansi-styles@npm:^4.0.0, ansi-styles@npm:^4.1.0, ansi-styles@npm:^4.2.0, ansi-styles@npm:^4.2.1, ansi-styles@npm:^4.3.0": - version: 4.3.0 - resolution: "ansi-styles@npm:4.3.0" - dependencies: - color-convert: "npm:^2.0.1" - checksum: 10/b4494dfbfc7e4591b4711a396bd27e540f8153914123dccb4cdbbcb514015ada63a3809f362b9d8d4f6b17a706f1d7bea3c6f974b15fa5ae76b5b502070889ff - languageName: node - linkType: hard - -"ansicolors@npm:~0.3.2": - version: 0.3.2 - resolution: "ansicolors@npm:0.3.2" - checksum: 10/0704d1485d84d65a47aacd3d2d26f501f21aeeb509922c8f2496d0ec5d346dc948efa64f3151aef0571d73e5c44eb10fd02f27f59762e9292fe123bb1ea9ff7d - languageName: node - linkType: hard - -"any-observable@npm:^0.3.0": - version: 0.3.0 - resolution: "any-observable@npm:0.3.0" - checksum: 10/21f27ed714c54aac6db4c1200674933f93416b832433cd14e5071db53f7d480de66a4c529181655dee52371be7f73ebeb0880b02a95571d70152fd6b226c11e9 - languageName: node - linkType: hard - -"apollo-codegen-core@npm:0.40.9, apollo-codegen-core@npm:^0.40.9": - version: 0.40.9 - resolution: "apollo-codegen-core@npm:0.40.9" - dependencies: - "@babel/generator": "npm:7.17.10" - "@babel/parser": "npm:^7.1.3" - "@babel/types": "npm:7.17.10" - apollo-env: "npm:^0.10.2" - apollo-language-server: "npm:^1.26.9" - ast-types: "npm:^0.14.0" - common-tags: "npm:^1.5.1" - recast: "npm:^0.21.0" - checksum: 10/52328d7d7e684d409243737cd200f98b759eb039498aa750518cd10ed0baa82d11d9ee4639b74b5c5c29a3c613eb21ab3e5447c47c460aba349fd0a5f2793608 - languageName: node - linkType: hard - -"apollo-codegen-flow@npm:0.38.9": - version: 0.38.9 - resolution: "apollo-codegen-flow@npm:0.38.9" - dependencies: - "@babel/generator": "npm:7.17.10" - "@babel/types": "npm:7.17.10" - apollo-codegen-core: "npm:^0.40.9" - change-case: "npm:^4.0.0" - common-tags: "npm:^1.5.1" - inflected: "npm:^2.0.3" - checksum: 10/f4bee85c4ccda639ca595e4733480f558a21970e18c36c3c213475084751dbc5f2a8b9c1d0b8174686338831b925b24bdf2d8d67318ab97155ed0855f386bbe6 - languageName: node - linkType: hard - -"apollo-codegen-scala@npm:0.39.9": - version: 0.39.9 - resolution: "apollo-codegen-scala@npm:0.39.9" - dependencies: - apollo-codegen-core: "npm:^0.40.9" - change-case: "npm:^4.0.0" - common-tags: "npm:^1.5.1" - inflected: "npm:^2.0.3" - checksum: 10/8a970a665bf58f3bcbf5e9124d8015ce8ebb056549a6c3f23bf0e8e915b39e08451fc92169f55b202fe358c3692bed518fa3048547cf0c386dc0f4ed7bc66f32 - languageName: node - linkType: hard - -"apollo-codegen-swift@npm:0.40.9": - version: 0.40.9 - resolution: "apollo-codegen-swift@npm:0.40.9" - dependencies: - apollo-codegen-core: "npm:^0.40.9" - change-case: "npm:^4.0.0" - common-tags: "npm:^1.5.1" - inflected: "npm:^2.0.3" - checksum: 10/ee7ca516776c97e6fbafa0949e84fa75ba40e299f2454931da729dcd70e547d64528bb5b166554ece9f0204b36d3654996ccfb2c06314213936e9cd30751322b - languageName: node - linkType: hard - -"apollo-codegen-typescript@npm:0.40.9": - version: 0.40.9 - resolution: "apollo-codegen-typescript@npm:0.40.9" - dependencies: - "@babel/generator": "npm:7.17.10" - "@babel/types": "npm:7.17.10" - apollo-codegen-core: "npm:^0.40.9" - change-case: "npm:^4.0.0" - common-tags: "npm:^1.5.1" - inflected: "npm:^2.0.3" - checksum: 10/5dfef8fdd3c7312b8c08c3ed9da317dce0e38f8fee51dc96a9e20d5daf29fd9aff3e981b127258926dc10e0c37995a8a2bc63726e4584b6ffbd130713be33280 - languageName: node - linkType: hard - -"apollo-datasource@npm:^3.0.0": - version: 3.3.2 - resolution: "apollo-datasource@npm:3.3.2" - dependencies: - "@apollo/utils.keyvaluecache": "npm:^1.0.1" - apollo-server-env: "npm:^4.2.1" - checksum: 10/70244e792655b357213b92e9dd0e8ca553724857847c9bedb53a1dbf7a92fc0d8b05a60d77203d6c30331599b44c5d7cc5f4d94c934465fa05b146b681ed2293 - languageName: node - linkType: hard - -"apollo-env@npm:0.10.2, apollo-env@npm:^0.10.2": - version: 0.10.2 - resolution: "apollo-env@npm:0.10.2" - dependencies: - "@types/node-fetch": "npm:^2.5.10" - core-js: "npm:^3.0.1" - node-fetch: "npm:^2.6.1" - sha.js: "npm:^2.4.11" - checksum: 10/142a440fa1a27e51055e3dcd4e6617a00eb97ddcb75c142d00c68bbff4adb011a94b77fd8aa1d75f00c3f3991c1ce40ce3caa37e2aaebdff6beec02beb9aaeae - languageName: node - linkType: hard - -"apollo-graphql@npm:0.9.7, apollo-graphql@npm:^0.9.3, apollo-graphql@npm:^0.9.7": - version: 0.9.7 - resolution: "apollo-graphql@npm:0.9.7" - dependencies: - core-js-pure: "npm:^3.10.2" - lodash.sortby: "npm:^4.7.0" - sha.js: "npm:^2.4.11" - peerDependencies: - graphql: ^14.2.1 || ^15.0.0 - checksum: 10/7236488b75f5cb3b9297e59779d54aab0010eece6a5424632a3bf150a88941c3d15be3c2f2aa9110099829df0be689294ef7820529419814a425f05ef5e105d0 - languageName: node - linkType: hard - -"apollo-language-server@npm:1.26.9, apollo-language-server@npm:^1.26.9": - version: 1.26.9 - resolution: "apollo-language-server@npm:1.26.9" - dependencies: - "@apollo/federation": "npm:0.27.0" - "@apollographql/apollo-tools": "npm:^0.5.4" - "@apollographql/graphql-language-service-interface": "npm:^2.0.2" - "@endemolshinegroup/cosmiconfig-typescript-loader": "npm:^3.0.2" - apollo-datasource: "npm:^3.0.0" - apollo-env: "npm:^0.10.2" - apollo-graphql: "npm:^0.9.7" - apollo-link: "npm:^1.2.3" - apollo-link-context: "npm:^1.0.9" - apollo-link-error: "npm:^1.1.1" - apollo-link-http: "npm:^1.5.5" - apollo-server-errors: "npm:^2.0.2" - await-to-js: "npm:^3.0.0" - core-js: "npm:^3.0.1" - cosmiconfig: "npm:^7.0.1" - dotenv: "npm:^16.0.0" - glob: "npm:^8.0.0" - graphql: "npm:14.0.2 - 14.2.0 || ^14.3.1 || ^15.0.0" - graphql-tag: "npm:^2.10.1" - lodash.debounce: "npm:^4.0.8" - lodash.merge: "npm:^4.6.1" - minimatch: "npm:^5.0.0" - vscode-languageserver: "npm:^7.0.0" - vscode-languageserver-textdocument: "npm:^1.0.4" - vscode-uri: "npm:1.0.6" - checksum: 10/c4e64e0f138531d18b69048d03f8396d68162e6ed65e1585c1d4e27b911538480b6e27b2a3b58c4a8003f167be3aa97e82731a701bf9347358677823de383e59 - languageName: node - linkType: hard - -"apollo-link-context@npm:^1.0.9": - version: 1.0.20 - resolution: "apollo-link-context@npm:1.0.20" - dependencies: - apollo-link: "npm:^1.2.14" - tslib: "npm:^1.9.3" - checksum: 10/5e905e3dd364dafb8683beb07d60cc84378a96247f21b735194bd2aa70c8f0f8b7b066177a9a7e5549ca08f60901c58243e56fa61a1969436a61fdb9d0cb582b - languageName: node - linkType: hard - -"apollo-link-error@npm:^1.1.1": - version: 1.1.13 - resolution: "apollo-link-error@npm:1.1.13" - dependencies: - apollo-link: "npm:^1.2.14" - apollo-link-http-common: "npm:^0.2.16" - tslib: "npm:^1.9.3" - checksum: 10/b417b77acbf464d8246eb79312c6d755f9d398c6c9f560c9e2270260519df639c3f0a8b9454ccc55c7f54eb57490706275ede120ecd7f475ec4627012feaf4b2 - languageName: node - linkType: hard - -"apollo-link-http-common@npm:^0.2.16": - version: 0.2.16 - resolution: "apollo-link-http-common@npm:0.2.16" - dependencies: - apollo-link: "npm:^1.2.14" - ts-invariant: "npm:^0.4.0" - tslib: "npm:^1.9.3" - peerDependencies: - graphql: ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 - checksum: 10/8cf50fc8a10a0f1c6c6ddf4a3f76c4652e07e9dede52da020d3f430199dccf813fb9b953e8725b8621007405d626c270e67b994c60a974e5598021cd4d84e549 - languageName: node - linkType: hard - -"apollo-link-http@npm:^1.5.5": - version: 1.5.17 - resolution: "apollo-link-http@npm:1.5.17" - dependencies: - apollo-link: "npm:^1.2.14" - apollo-link-http-common: "npm:^0.2.16" - tslib: "npm:^1.9.3" - peerDependencies: - graphql: ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 - checksum: 10/b66cc1ce3307a86b96b633b06ada015df094f796d289f1c29ff004075bb9505ac5d0cfb98ae8155daea0ccd0e181f0cbc08f8277fd211bfd0f572d5a1b64b579 - languageName: node - linkType: hard - -"apollo-link@npm:^1.2.14, apollo-link@npm:^1.2.3": - version: 1.2.14 - resolution: "apollo-link@npm:1.2.14" - dependencies: - apollo-utilities: "npm:^1.3.0" - ts-invariant: "npm:^0.4.0" - tslib: "npm:^1.9.3" - zen-observable-ts: "npm:^0.8.21" - peerDependencies: - graphql: ^0.11.3 || ^0.12.3 || ^0.13.0 || ^14.0.0 || ^15.0.0 - checksum: 10/92e6764038761bef8526b87b21b0a820067ad0f462c2a007d686e9a6f35bfc26b5ed232cc58301c660d28f9fdb2765edcd99e8e4bdf0b32226ec28f95ffcd212 - languageName: node - linkType: hard - -"apollo-server-env@npm:^4.2.1": - version: 4.2.1 - resolution: "apollo-server-env@npm:4.2.1" - dependencies: - node-fetch: "npm:^2.6.7" - checksum: 10/9172288c89c2ebb2a02d58542f807896de1ca0ba80c430f09242f2fa9ece40d7ecb8f9527357ba5e1d9997c64c364e7a9716e4f5485c5fb4938f69627bf1cea4 - languageName: node - linkType: hard - -"apollo-server-errors@npm:^2.0.2": - version: 2.5.0 - resolution: "apollo-server-errors@npm:2.5.0" - peerDependencies: - graphql: ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 - checksum: 10/a378d7fa8a318c293910665d0c5a9b6ec86de387f0cd5f3ab9082eba4228e9846291532c82c599dc14913cb68869e8f8428c855d5486fd05a79886e103f16a40 - languageName: node - linkType: hard - -"apollo-utilities@npm:^1.3.0": - version: 1.3.4 - resolution: "apollo-utilities@npm:1.3.4" - dependencies: - "@wry/equality": "npm:^0.1.2" - fast-json-stable-stringify: "npm:^2.0.0" - ts-invariant: "npm:^0.4.0" - tslib: "npm:^1.10.0" - peerDependencies: - graphql: ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 - checksum: 10/6243ef74167996a7ec4ce2f4bd63c126de531a828ae0a2f07e0b47ca2308f6fed3a521603ed85ee3c08e8eb86ce48e9a3d7a205403112113966653fae4401e8b - languageName: node - linkType: hard - -"apollo@npm:^2.34.0": - version: 2.34.0 - resolution: "apollo@npm:2.34.0" - dependencies: - "@apollographql/apollo-tools": "npm:0.5.4" - "@oclif/command": "npm:1.8.16" - "@oclif/config": "npm:1.18.3" - "@oclif/errors": "npm:1.3.5" - "@oclif/plugin-autocomplete": "npm:1.3.0" - "@oclif/plugin-help": "npm:5.1.12" - "@oclif/plugin-not-found": "npm:2.3.1" - "@oclif/plugin-plugins": "npm:2.1.0" - "@oclif/plugin-warn-if-update-available": "npm:2.0.4" - apollo-codegen-core: "npm:0.40.9" - apollo-codegen-flow: "npm:0.38.9" - apollo-codegen-scala: "npm:0.39.9" - apollo-codegen-swift: "npm:0.40.9" - apollo-codegen-typescript: "npm:0.40.9" - apollo-env: "npm:0.10.2" - apollo-graphql: "npm:0.9.7" - apollo-language-server: "npm:1.26.9" - chalk: "npm:4.1.2" - cli-ux: "npm:6.0.9" - env-ci: "npm:7.1.0" - gaze: "npm:1.1.3" - git-parse: "npm:2.1.1" - git-rev-sync: "npm:3.0.2" - git-url-parse: "npm:11.6.0" - glob: "npm:8.0.1" - global-agent: "npm:3.0.0" - graphql: "npm:14.0.2 - 14.2.0 || ^14.3.1 || ^15.0.0" - graphql-tag: "npm:2.12.6" - listr: "npm:0.14.3" - lodash.identity: "npm:3.0.0" - lodash.pickby: "npm:4.6.0" - mkdirp: "npm:1.0.4" - moment: "npm:2.29.3" - strip-ansi: "npm:6.0.1" - table: "npm:6.8.0" - tty: "npm:1.0.1" - vscode-uri: "npm:1.0.6" - bin: - apollo: bin/run - checksum: 10/a9dd28e4e1212c5cfa9cc6615473ef6ff13611cfb1ade83305b79607556aca06fc31e982fed7063ad4156ca1177db009484adc260049111fc7364b491cb7d870 - languageName: node - linkType: hard - -"arg@npm:^4.1.0": - version: 4.1.3 - resolution: "arg@npm:4.1.3" - checksum: 10/969b491082f20cad166649fa4d2073ea9e974a4e5ac36247ca23d2e5a8b3cb12d60e9ff70a8acfe26d76566c71fd351ee5e6a9a6595157eb36f92b1fd64e1599 - languageName: node - linkType: hard - -"argparse@npm:^1.0.7": - version: 1.0.10 - resolution: "argparse@npm:1.0.10" - dependencies: - sprintf-js: "npm:~1.0.2" - checksum: 10/c6a621343a553ff3779390bb5ee9c2263d6643ebcd7843227bdde6cc7adbed796eb5540ca98db19e3fd7b4714e1faa51551f8849b268bb62df27ddb15cbcd91e - languageName: node - linkType: hard - -"array-buffer-byte-length@npm:^1.0.1, array-buffer-byte-length@npm:^1.0.2": - version: 1.0.2 - resolution: "array-buffer-byte-length@npm:1.0.2" - dependencies: - call-bound: "npm:^1.0.3" - is-array-buffer: "npm:^3.0.5" - checksum: 10/0ae3786195c3211b423e5be8dd93357870e6fb66357d81da968c2c39ef43583ef6eece1f9cb1caccdae4806739c65dea832b44b8593414313cd76a89795fca63 - languageName: node - linkType: hard - -"array-union@npm:^2.1.0": - version: 2.1.0 - resolution: "array-union@npm:2.1.0" - checksum: 10/5bee12395cba82da674931df6d0fea23c4aa4660cb3b338ced9f828782a65caa232573e6bf3968f23e0c5eb301764a382cef2f128b170a9dc59de0e36c39f98d - languageName: node - linkType: hard - -"array.prototype.reduce@npm:^1.0.6": - version: 1.0.7 - resolution: "array.prototype.reduce@npm:1.0.7" - dependencies: - call-bind: "npm:^1.0.7" - define-properties: "npm:^1.2.1" - es-abstract: "npm:^1.23.2" - es-array-method-boxes-properly: "npm:^1.0.0" - es-errors: "npm:^1.3.0" - es-object-atoms: "npm:^1.0.0" - is-string: "npm:^1.0.7" - checksum: 10/3a4fa56cf5843d821e97680861c8edfdfe6684a7f7cd1145ed611b5fa611fd62d1b149a438ae24ae884c843876a6539b67fbcacdd3276f89731eee9415dc9012 - languageName: node - linkType: hard - -"arraybuffer.prototype.slice@npm:^1.0.4": - version: 1.0.4 - resolution: "arraybuffer.prototype.slice@npm:1.0.4" - dependencies: - array-buffer-byte-length: "npm:^1.0.1" - call-bind: "npm:^1.0.8" - define-properties: "npm:^1.2.1" - es-abstract: "npm:^1.23.5" - es-errors: "npm:^1.3.0" - get-intrinsic: "npm:^1.2.6" - is-array-buffer: "npm:^3.0.4" - checksum: 10/4821ebdfe7d699f910c7f09bc9fa996f09b96b80bccb4f5dd4b59deae582f6ad6e505ecef6376f8beac1eda06df2dbc89b70e82835d104d6fcabd33c1aed1ae9 - languageName: node - linkType: hard - -"ast-types@npm:0.15.2": - version: 0.15.2 - resolution: "ast-types@npm:0.15.2" - dependencies: - tslib: "npm:^2.0.1" - checksum: 10/81680bd5829cdec33524e9aa3434e23f3919c0c388927068a0ff2e8466f55b0f34eae53e0007b3668742910c289481ab4e1d486a5318f618ae2fc93b5e7e863b - languageName: node - linkType: hard - -"ast-types@npm:^0.14.0": - version: 0.14.2 - resolution: "ast-types@npm:0.14.2" - dependencies: - tslib: "npm:^2.0.1" - checksum: 10/7c74b3090c90aa600b49a7a8cecc99e329f190600bcaa75ad087472a1a5a7ef23795a17ea00a74c2a8e822b336cd4f874e2e1b815a9877b4dba5e401566b0433 - languageName: node - linkType: hard - -"astral-regex@npm:^2.0.0": - version: 2.0.0 - resolution: "astral-regex@npm:2.0.0" - checksum: 10/876231688c66400473ba505731df37ea436e574dd524520294cc3bbc54ea40334865e01fa0d074d74d036ee874ee7e62f486ea38bc421ee8e6a871c06f011766 - languageName: node - linkType: hard - -"async-function@npm:^1.0.0": - version: 1.0.0 - resolution: "async-function@npm:1.0.0" - checksum: 10/1a09379937d846f0ce7614e75071c12826945d4e417db634156bf0e4673c495989302f52186dfa9767a1d9181794554717badd193ca2bbab046ef1da741d8efd - languageName: node - linkType: hard - -"async@npm:^3.2.3": - version: 3.2.6 - resolution: "async@npm:3.2.6" - checksum: 10/cb6e0561a3c01c4b56a799cc8bab6ea5fef45f069ab32500b6e19508db270ef2dffa55e5aed5865c5526e9907b1f8be61b27530823b411ffafb5e1538c86c368 - languageName: node - linkType: hard - -"asynckit@npm:^0.4.0": - version: 0.4.0 - resolution: "asynckit@npm:0.4.0" - checksum: 10/3ce727cbc78f69d6a4722517a58ee926c8c21083633b1d3fdf66fd688f6c127a53a592141bd4866f9b63240a86e9d8e974b13919450bd17fa33c2d22c4558ad8 - languageName: node - linkType: hard - -"at-least-node@npm:^1.0.0": - version: 1.0.0 - resolution: "at-least-node@npm:1.0.0" - checksum: 10/463e2f8e43384f1afb54bc68485c436d7622acec08b6fad269b421cb1d29cebb5af751426793d0961ed243146fe4dc983402f6d5a51b720b277818dbf6f2e49e - languageName: node - linkType: hard - -"available-typed-arrays@npm:^1.0.7": - version: 1.0.7 - resolution: "available-typed-arrays@npm:1.0.7" - dependencies: - possible-typed-array-names: "npm:^1.0.0" - checksum: 10/6c9da3a66caddd83c875010a1ca8ef11eac02ba15fb592dc9418b2b5e7b77b645fa7729380a92d9835c2f05f2ca1b6251f39b993e0feb3f1517c74fa1af02cab - languageName: node - linkType: hard - -"await-to-js@npm:^3.0.0": - version: 3.0.0 - resolution: "await-to-js@npm:3.0.0" - checksum: 10/b0445e4cbf9cf98482537f09b0a708be01b1e4d85465465545a7718b79cbefe2409a8cd0d4441d95503b1fabf29303fd9b540a8c71ed2a4b899446e6b93f9075 - languageName: node - linkType: hard - -"balanced-match@npm:^1.0.0": - version: 1.0.2 - resolution: "balanced-match@npm:1.0.2" - checksum: 10/9706c088a283058a8a99e0bf91b0a2f75497f185980d9ffa8b304de1d9e58ebda7c72c07ebf01dadedaac5b2907b2c6f566f660d62bd336c3468e960403b9d65 - languageName: node - linkType: hard - -"boolean@npm:^3.0.1": - version: 3.2.0 - resolution: "boolean@npm:3.2.0" - checksum: 10/d28a49dcaeef7fe10cf9fdf488214d3859f07350be8f5caa0c73ec621baf20650e5da6523262e5ce9221909519d4261c16d8430a5bf307fee9ef0e170cdb29f3 - languageName: node - linkType: hard - -"brace-expansion@npm:^1.1.7": - version: 1.1.11 - resolution: "brace-expansion@npm:1.1.11" - dependencies: - balanced-match: "npm:^1.0.0" - concat-map: "npm:0.0.1" - checksum: 10/faf34a7bb0c3fcf4b59c7808bc5d2a96a40988addf2e7e09dfbb67a2251800e0d14cd2bfc1aa79174f2f5095c54ff27f46fb1289fe2d77dac755b5eb3434cc07 - languageName: node - linkType: hard - -"brace-expansion@npm:^2.0.1": - version: 2.0.1 - resolution: "brace-expansion@npm:2.0.1" - dependencies: - balanced-match: "npm:^1.0.0" - checksum: 10/a61e7cd2e8a8505e9f0036b3b6108ba5e926b4b55089eeb5550cd04a471fe216c96d4fe7e4c7f995c728c554ae20ddfc4244cad10aef255e72b62930afd233d1 - languageName: node - linkType: hard - -"braces@npm:^3.0.3": - version: 3.0.3 - resolution: "braces@npm:3.0.3" - dependencies: - fill-range: "npm:^7.1.1" - checksum: 10/fad11a0d4697a27162840b02b1fad249c1683cbc510cd5bf1a471f2f8085c046d41094308c577a50a03a579dd99d5a6b3724c4b5e8b14df2c4443844cfcda2c6 - languageName: node - linkType: hard - -"buffer-from@npm:^1.0.0": - version: 1.1.2 - resolution: "buffer-from@npm:1.1.2" - checksum: 10/0448524a562b37d4d7ed9efd91685a5b77a50672c556ea254ac9a6d30e3403a517d8981f10e565db24e8339413b43c97ca2951f10e399c6125a0d8911f5679bb - languageName: node - linkType: hard - -"byline@npm:5.0.0": - version: 5.0.0 - resolution: "byline@npm:5.0.0" - checksum: 10/737ca83e8eda2976728dae62e68bc733aea095fab08db4c6f12d3cee3cf45b6f97dce45d1f6b6ff9c2c947736d10074985b4425b31ce04afa1985a4ef3d334a7 - languageName: node - linkType: hard - -"call-bind-apply-helpers@npm:^1.0.0, call-bind-apply-helpers@npm:^1.0.1, call-bind-apply-helpers@npm:^1.0.2": - version: 1.0.2 - resolution: "call-bind-apply-helpers@npm:1.0.2" - dependencies: - es-errors: "npm:^1.3.0" - function-bind: "npm:^1.1.2" - checksum: 10/00482c1f6aa7cfb30fb1dbeb13873edf81cfac7c29ed67a5957d60635a56b2a4a480f1016ddbdb3395cc37900d46037fb965043a51c5c789ffeab4fc535d18b5 - languageName: node - linkType: hard - -"call-bind@npm:^1.0.0, call-bind@npm:^1.0.7, call-bind@npm:^1.0.8": - version: 1.0.8 - resolution: "call-bind@npm:1.0.8" - dependencies: - call-bind-apply-helpers: "npm:^1.0.0" - es-define-property: "npm:^1.0.0" - get-intrinsic: "npm:^1.2.4" - set-function-length: "npm:^1.2.2" - checksum: 10/659b03c79bbfccf0cde3a79e7d52570724d7290209823e1ca5088f94b52192dc1836b82a324d0144612f816abb2f1734447438e38d9dafe0b3f82c2a1b9e3bce - languageName: node - linkType: hard - -"call-bound@npm:^1.0.2, call-bound@npm:^1.0.3, call-bound@npm:^1.0.4": - version: 1.0.4 - resolution: "call-bound@npm:1.0.4" - dependencies: - call-bind-apply-helpers: "npm:^1.0.2" - get-intrinsic: "npm:^1.3.0" - checksum: 10/ef2b96e126ec0e58a7ff694db43f4d0d44f80e641370c21549ed911fecbdbc2df3ebc9bddad918d6bbdefeafb60bb3337902006d5176d72bcd2da74820991af7 - languageName: node - linkType: hard - -"callsites@npm:^3.0.0": - version: 3.1.0 - resolution: "callsites@npm:3.1.0" - checksum: 10/072d17b6abb459c2ba96598918b55868af677154bec7e73d222ef95a8fdb9bbf7dae96a8421085cdad8cd190d86653b5b6dc55a4484f2e5b2e27d5e0c3fc15b3 - languageName: node - linkType: hard - -"camel-case@npm:^4.1.2": - version: 4.1.2 - resolution: "camel-case@npm:4.1.2" - dependencies: - pascal-case: "npm:^3.1.2" - tslib: "npm:^2.0.3" - checksum: 10/bcbd25cd253b3cbc69be3f535750137dbf2beb70f093bdc575f73f800acc8443d34fd52ab8f0a2413c34f1e8203139ffc88428d8863e4dfe530cfb257a379ad6 - languageName: node - linkType: hard - -"capital-case@npm:^1.0.4": - version: 1.0.4 - resolution: "capital-case@npm:1.0.4" - dependencies: - no-case: "npm:^3.0.4" - tslib: "npm:^2.0.3" - upper-case-first: "npm:^2.0.2" - checksum: 10/41fa8fa87f6d24d0835a2b4a9341a3eaecb64ac29cd7c5391f35d6175a0fa98ab044e7f2602e1ec3afc886231462ed71b5b80c590b8b41af903ec2c15e5c5931 - languageName: node - linkType: hard - -"cardinal@npm:^2.1.1": - version: 2.1.1 - resolution: "cardinal@npm:2.1.1" - dependencies: - ansicolors: "npm:~0.3.2" - redeyed: "npm:~2.1.0" - bin: - cdl: ./bin/cdl.js - checksum: 10/caf0d34739ef7b1d80e1753311f889997b62c4490906819eb5da5bd46e7f5e5caba7a8a96ca401190c7d9c18443a7749e5338630f7f9a1ae98d60cac49b9008e - languageName: node - linkType: hard - -"chalk@npm:4.1.2, chalk@npm:^4.0.2, chalk@npm:^4.1.0, chalk@npm:^4.1.2": - version: 4.1.2 - resolution: "chalk@npm:4.1.2" - dependencies: - ansi-styles: "npm:^4.1.0" - supports-color: "npm:^7.1.0" - checksum: 10/cb3f3e594913d63b1814d7ca7c9bafbf895f75fbf93b92991980610dfd7b48500af4e3a5d4e3a8f337990a96b168d7eb84ee55efdce965e2ee8efc20f8c8f139 - languageName: node - linkType: hard - -"chalk@npm:^1.0.0, chalk@npm:^1.1.3": - version: 1.1.3 - resolution: "chalk@npm:1.1.3" - dependencies: - ansi-styles: "npm:^2.2.1" - escape-string-regexp: "npm:^1.0.2" - has-ansi: "npm:^2.0.0" - strip-ansi: "npm:^3.0.0" - supports-color: "npm:^2.0.0" - checksum: 10/abcf10da02afde04cc615f06c4bdb3ffc70d2bfbf37e0df03bb88b7459a9411dab4d01210745b773abc936031530a20355f1facc4bee1bbf08613d8fdcfb3aeb - languageName: node - linkType: hard - -"chalk@npm:^2.4.1": - version: 2.4.2 - resolution: "chalk@npm:2.4.2" - dependencies: - ansi-styles: "npm:^3.2.1" - escape-string-regexp: "npm:^1.0.5" - supports-color: "npm:^5.3.0" - checksum: 10/3d1d103433166f6bfe82ac75724951b33769675252d8417317363ef9d54699b7c3b2d46671b772b893a8e50c3ece70c4b933c73c01e81bc60ea4df9b55afa303 - languageName: node - linkType: hard - -"change-case@npm:^4.0.0": - version: 4.1.2 - resolution: "change-case@npm:4.1.2" - dependencies: - camel-case: "npm:^4.1.2" - capital-case: "npm:^1.0.4" - constant-case: "npm:^3.0.4" - dot-case: "npm:^3.0.4" - header-case: "npm:^2.0.4" - no-case: "npm:^3.0.4" - param-case: "npm:^3.0.4" - pascal-case: "npm:^3.1.2" - path-case: "npm:^3.0.4" - sentence-case: "npm:^3.0.4" - snake-case: "npm:^3.0.4" - tslib: "npm:^2.0.3" - checksum: 10/e4bc4a093a1f7cce8b33896665cf9e456e3bc3cc0def2ad7691b1994cfca99b3188d0a513b16855b01a6bd20692fcde12a7d4d87a5615c4c515bbbf0e651f116 - languageName: node - linkType: hard - -"clean-stack@npm:^3.0.0, clean-stack@npm:^3.0.1": - version: 3.0.1 - resolution: "clean-stack@npm:3.0.1" - dependencies: - escape-string-regexp: "npm:4.0.0" - checksum: 10/dc18c842d7792dd72d463936b1b0a5b2621f0fc11588ee48b602e1a29b6c010c606d89f3de1f95d15d72de74aea93c0fbac8246593a31d95f8462cac36148e05 - languageName: node - linkType: hard - -"cli-cursor@npm:^2.0.0, cli-cursor@npm:^2.1.0": - version: 2.1.0 - resolution: "cli-cursor@npm:2.1.0" - dependencies: - restore-cursor: "npm:^2.0.0" - checksum: 10/d88e97bfdac01046a3ffe7d49f06757b3126559d7e44aa2122637eb179284dc6cd49fca2fac4f67c19faaf7e6dab716b6fe1dfcd309977407d8c7578ec2d044d - languageName: node - linkType: hard - -"cli-progress@npm:^3.10.0": - version: 3.12.0 - resolution: "cli-progress@npm:3.12.0" - dependencies: - string-width: "npm:^4.2.3" - checksum: 10/a6a549919a7461f5e798b18a4a19f83154bab145d3ec73d7f3463a8db8e311388c545ace1105557760a058cc4999b7f28c9d8d24d9783ee2912befb32544d4b8 - languageName: node - linkType: hard - -"cli-truncate@npm:^0.2.1": - version: 0.2.1 - resolution: "cli-truncate@npm:0.2.1" - dependencies: - slice-ansi: "npm:0.0.4" - string-width: "npm:^1.0.1" - checksum: 10/c2b0de7c08915eab1e660884251411ad31691c5036a876f98e1bf747f1c165dc8345afdba92b7efb3678478c9fc17c9c9c47c76d181e35478aaa1047459f98aa - languageName: node - linkType: hard - -"cli-ux@npm:6.0.9": - version: 6.0.9 - resolution: "cli-ux@npm:6.0.9" - dependencies: - "@oclif/core": "npm:^1.1.1" - "@oclif/linewrap": "npm:^1.0.0" - "@oclif/screen": "npm:^1.0.4 " - ansi-escapes: "npm:^4.3.0" - ansi-styles: "npm:^4.2.0" - cardinal: "npm:^2.1.1" - chalk: "npm:^4.1.0" - clean-stack: "npm:^3.0.0" - cli-progress: "npm:^3.10.0" - extract-stack: "npm:^2.0.0" - fs-extra: "npm:^8.1" - hyperlinker: "npm:^1.0.0" - indent-string: "npm:^4.0.0" - is-wsl: "npm:^2.2.0" - js-yaml: "npm:^3.13.1" - lodash: "npm:^4.17.21" - natural-orderby: "npm:^2.0.1" - object-treeify: "npm:^1.1.4" - password-prompt: "npm:^1.1.2" - semver: "npm:^7.3.2" - string-width: "npm:^4.2.0" - strip-ansi: "npm:^6.0.0" - supports-color: "npm:^8.1.0" - supports-hyperlinks: "npm:^2.1.0" - tslib: "npm:^2.0.0" - checksum: 10/87e44a4f268b59e1c23a7b8962cdc8172161925fc627c7068881fc98eb636fffc1a5ad885b4673378e428b224b0144d5123d5c4e01e34720fff06c47992358bf - languageName: node - linkType: hard - -"code-point-at@npm:^1.0.0": - version: 1.1.0 - resolution: "code-point-at@npm:1.1.0" - checksum: 10/17d5666611f9b16d64fdf48176d9b7fb1c7d1c1607a189f7e600040a11a6616982876af148230336adb7d8fe728a559f743a4e29db3747e3b1a32fa7f4529681 - languageName: node - linkType: hard - -"color-convert@npm:^1.9.0": - version: 1.9.3 - resolution: "color-convert@npm:1.9.3" - dependencies: - color-name: "npm:1.1.3" - checksum: 10/ffa319025045f2973919d155f25e7c00d08836b6b33ea2d205418c59bd63a665d713c52d9737a9e0fe467fb194b40fbef1d849bae80d674568ee220a31ef3d10 - languageName: node - linkType: hard - -"color-convert@npm:^2.0.1": - version: 2.0.1 - resolution: "color-convert@npm:2.0.1" - dependencies: - color-name: "npm:~1.1.4" - checksum: 10/fa00c91b4332b294de06b443923246bccebe9fab1b253f7fe1772d37b06a2269b4039a85e309abe1fe11b267b11c08d1d0473fda3badd6167f57313af2887a64 - languageName: node - linkType: hard - -"color-name@npm:1.1.3": - version: 1.1.3 - resolution: "color-name@npm:1.1.3" - checksum: 10/09c5d3e33d2105850153b14466501f2bfb30324a2f76568a408763a3b7433b0e50e5b4ab1947868e65cb101bb7cb75029553f2c333b6d4b8138a73fcc133d69d - languageName: node - linkType: hard - -"color-name@npm:~1.1.4": - version: 1.1.4 - resolution: "color-name@npm:1.1.4" - checksum: 10/b0445859521eb4021cd0fb0cc1a75cecf67fceecae89b63f62b201cca8d345baf8b952c966862a9d9a2632987d4f6581f0ec8d957dfacece86f0a7919316f610 - languageName: node - linkType: hard - -"combined-stream@npm:^1.0.8": - version: 1.0.8 - resolution: "combined-stream@npm:1.0.8" - dependencies: - delayed-stream: "npm:~1.0.0" - checksum: 10/2e969e637d05d09fa50b02d74c83a1186f6914aae89e6653b62595cc75a221464f884f55f231b8f4df7a49537fba60bdc0427acd2bf324c09a1dbb84837e36e4 - languageName: node - linkType: hard - -"common-tags@npm:^1.5.1": - version: 1.8.2 - resolution: "common-tags@npm:1.8.2" - checksum: 10/c665d0f463ee79dda801471ad8da6cb33ff7332ba45609916a508ad3d77ba07ca9deeb452e83f81f24c2b081e2c1315347f23d239210e63d1c5e1a0c7c019fe2 - languageName: node - linkType: hard - -"concat-map@npm:0.0.1": - version: 0.0.1 - resolution: "concat-map@npm:0.0.1" - checksum: 10/9680699c8e2b3af0ae22592cb764acaf973f292a7b71b8a06720233011853a58e256c89216a10cbe889727532fd77f8bcd49a760cedfde271b8e006c20e079f2 - languageName: node - linkType: hard - -"constant-case@npm:^3.0.4": - version: 3.0.4 - resolution: "constant-case@npm:3.0.4" - dependencies: - no-case: "npm:^3.0.4" - tslib: "npm:^2.0.3" - upper-case: "npm:^2.0.2" - checksum: 10/6c3346d51afc28d9fae922e966c68eb77a19d94858dba230dd92d7b918b37d36db50f0311e9ecf6847e43e934b1c01406a0936973376ab17ec2c471fbcfb2cf3 - languageName: node - linkType: hard - -"content-type@npm:^1.0.4": - version: 1.0.5 - resolution: "content-type@npm:1.0.5" - checksum: 10/585847d98dc7fb8035c02ae2cb76c7a9bd7b25f84c447e5ed55c45c2175e83617c8813871b4ee22f368126af6b2b167df655829007b21aa10302873ea9c62662 - languageName: node - linkType: hard - -"core-js-pure@npm:^3.10.2": - version: 3.41.0 - resolution: "core-js-pure@npm:3.41.0" - checksum: 10/69cc1d966d8a177be3d8ddbb4460c778dbfa5a458f74069b55322428524a54544a787fc15fe905aa84e93e0eab0d6a6501fb7026a885b7a8553c8542b01e79fb - languageName: node - linkType: hard - -"core-js@npm:^3.0.1": - version: 3.41.0 - resolution: "core-js@npm:3.41.0" - checksum: 10/a06ebae2264dd24c8e4b331a68412f7d0730557c41901f80fa910a9398dbef4670482d9ef5a41fef7efd41307c612d3d4051df7640ac4c01ff6feda45f8b92be - languageName: node - linkType: hard - -"cosmiconfig@npm:^7.0.1": - version: 7.1.0 - resolution: "cosmiconfig@npm:7.1.0" - dependencies: - "@types/parse-json": "npm:^4.0.0" - import-fresh: "npm:^3.2.1" - parse-json: "npm:^5.0.0" - path-type: "npm:^4.0.0" - yaml: "npm:^1.10.0" - checksum: 10/03600bb3870c80ed151b7b706b99a1f6d78df8f4bdad9c95485072ea13358ef294b13dd99f9e7bf4cc0b43bcd3599d40df7e648750d21c2f6817ca2cd687e071 - languageName: node - linkType: hard - -"create-require@npm:^1.1.0": - version: 1.1.1 - resolution: "create-require@npm:1.1.1" - checksum: 10/a9a1503d4390d8b59ad86f4607de7870b39cad43d929813599a23714831e81c520bddf61bcdd1f8e30f05fd3a2b71ae8538e946eb2786dc65c2bbc520f692eff - languageName: node - linkType: hard - -"cross-spawn@npm:^7.0.3": - version: 7.0.6 - resolution: "cross-spawn@npm:7.0.6" - dependencies: - path-key: "npm:^3.1.0" - shebang-command: "npm:^2.0.0" - which: "npm:^2.0.1" - checksum: 10/0d52657d7ae36eb130999dffff1168ec348687b48dd38e2ff59992ed916c88d328cf1d07ff4a4a10bc78de5e1c23f04b306d569e42f7a2293915c081e4dfee86 - languageName: node - linkType: hard - -"data-view-buffer@npm:^1.0.2": - version: 1.0.2 - resolution: "data-view-buffer@npm:1.0.2" - dependencies: - call-bound: "npm:^1.0.3" - es-errors: "npm:^1.3.0" - is-data-view: "npm:^1.0.2" - checksum: 10/c10b155a4e93999d3a215d08c23eea95f865e1f510b2e7748fcae1882b776df1afe8c99f483ace7fc0e5a3193ab08da138abebc9829d12003746c5a338c4d644 - languageName: node - linkType: hard - -"data-view-byte-length@npm:^1.0.2": - version: 1.0.2 - resolution: "data-view-byte-length@npm:1.0.2" - dependencies: - call-bound: "npm:^1.0.3" - es-errors: "npm:^1.3.0" - is-data-view: "npm:^1.0.2" - checksum: 10/2a47055fcf1ab3ec41b00b6f738c6461a841391a643c9ed9befec1117c1765b4d492661d97fb7cc899200c328949dca6ff189d2c6537d96d60e8a02dfe3c95f7 - languageName: node - linkType: hard - -"data-view-byte-offset@npm:^1.0.1": - version: 1.0.1 - resolution: "data-view-byte-offset@npm:1.0.1" - dependencies: - call-bound: "npm:^1.0.2" - es-errors: "npm:^1.3.0" - is-data-view: "npm:^1.0.1" - checksum: 10/fa3bdfa0968bea6711ee50375094b39f561bce3f15f9e558df59de9c25f0bdd4cddc002d9c1d70ac7772ebd36854a7e22d1761e7302a934e6f1c2263bcf44aa2 - languageName: node - linkType: hard - -"date-fns@npm:^1.27.2": - version: 1.30.1 - resolution: "date-fns@npm:1.30.1" - checksum: 10/24c0937f4e5704f25627c9d1e92e1fe03cd6165d9f32334b7f923a737a57ef992c287cad0694356071e617fbbfa6bd10dec9192ea9035a3e6d0745b9d1594883 - languageName: node - linkType: hard - -"debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.4": - version: 4.4.0 - resolution: "debug@npm:4.4.0" - dependencies: - ms: "npm:^2.1.3" - peerDependenciesMeta: - supports-color: - optional: true - checksum: 10/1847944c2e3c2c732514b93d11886575625686056cd765336212dc15de2d2b29612b6cd80e1afba767bb8e1803b778caf9973e98169ef1a24a7a7009e1820367 - languageName: node - linkType: hard - -"decode-uri-component@npm:^0.2.0": - version: 0.2.2 - resolution: "decode-uri-component@npm:0.2.2" - checksum: 10/17a0e5fa400bf9ea84432226e252aa7b5e72793e16bf80b907c99b46a799aeacc139ec20ea57121e50c7bd875a1a4365928f884e92abf02e21a5a13790a0f33e - languageName: node - linkType: hard - -"define-data-property@npm:^1.0.1, define-data-property@npm:^1.1.4": - version: 1.1.4 - resolution: "define-data-property@npm:1.1.4" - dependencies: - es-define-property: "npm:^1.0.0" - es-errors: "npm:^1.3.0" - gopd: "npm:^1.0.1" - checksum: 10/abdcb2505d80a53524ba871273e5da75e77e52af9e15b3aa65d8aad82b8a3a424dad7aee2cc0b71470ac7acf501e08defac362e8b6a73cdb4309f028061df4ae - languageName: node - linkType: hard - -"define-properties@npm:^1.1.3, define-properties@npm:^1.2.1": - version: 1.2.1 - resolution: "define-properties@npm:1.2.1" - dependencies: - define-data-property: "npm:^1.0.1" - has-property-descriptors: "npm:^1.0.0" - object-keys: "npm:^1.1.1" - checksum: 10/b4ccd00597dd46cb2d4a379398f5b19fca84a16f3374e2249201992f36b30f6835949a9429669ee6b41b6e837205a163eadd745e472069e70dfc10f03e5fcc12 - languageName: node - linkType: hard - -"delayed-stream@npm:~1.0.0": - version: 1.0.0 - resolution: "delayed-stream@npm:1.0.0" - checksum: 10/46fe6e83e2cb1d85ba50bd52803c68be9bd953282fa7096f51fc29edd5d67ff84ff753c51966061e5ba7cb5e47ef6d36a91924eddb7f3f3483b1c560f77a0020 - languageName: node - linkType: hard - -"detect-node@npm:^2.0.4": - version: 2.1.0 - resolution: "detect-node@npm:2.1.0" - checksum: 10/832184ec458353e41533ac9c622f16c19f7c02d8b10c303dfd3a756f56be93e903616c0bb2d4226183c9351c15fc0b3dba41a17a2308262afabcfa3776e6ae6e - languageName: node - linkType: hard - -"diff@npm:^4.0.1": - version: 4.0.2 - resolution: "diff@npm:4.0.2" - checksum: 10/ec09ec2101934ca5966355a229d77afcad5911c92e2a77413efda5455636c4cf2ce84057e2d7715227a2eeeda04255b849bd3ae3a4dd22eb22e86e76456df069 - languageName: node - linkType: hard - -"dir-glob@npm:^3.0.1": - version: 3.0.1 - resolution: "dir-glob@npm:3.0.1" - dependencies: - path-type: "npm:^4.0.0" - checksum: 10/fa05e18324510d7283f55862f3161c6759a3f2f8dbce491a2fc14c8324c498286c54282c1f0e933cb930da8419b30679389499b919122952a4f8592362ef4615 - languageName: node - linkType: hard - -"dot-case@npm:^3.0.4": - version: 3.0.4 - resolution: "dot-case@npm:3.0.4" - dependencies: - no-case: "npm:^3.0.4" - tslib: "npm:^2.0.3" - checksum: 10/a65e3519414856df0228b9f645332f974f2bf5433370f544a681122eab59e66038fc3349b4be1cdc47152779dac71a5864f1ccda2f745e767c46e9c6543b1169 - languageName: node - linkType: hard - -"dotenv@npm:^16.0.0": - version: 16.4.7 - resolution: "dotenv@npm:16.4.7" - checksum: 10/f13bfe97db88f0df4ec505eeffb8925ec51f2d56a3d0b6d916964d8b4af494e6fb1633ba5d09089b552e77ab2a25de58d70259b2c5ed45ec148221835fc99a0c - languageName: node - linkType: hard - -"dunder-proto@npm:^1.0.0, dunder-proto@npm:^1.0.1": - version: 1.0.1 - resolution: "dunder-proto@npm:1.0.1" - dependencies: - call-bind-apply-helpers: "npm:^1.0.1" - es-errors: "npm:^1.3.0" - gopd: "npm:^1.2.0" - checksum: 10/5add88a3d68d42d6e6130a0cac450b7c2edbe73364bbd2fc334564418569bea97c6943a8fcd70e27130bf32afc236f30982fc4905039b703f23e9e0433c29934 - languageName: node - linkType: hard - -"ejs@npm:^3.1.6": - version: 3.1.10 - resolution: "ejs@npm:3.1.10" - dependencies: - jake: "npm:^10.8.5" - bin: - ejs: bin/cli.js - checksum: 10/a9cb7d7cd13b7b1cd0be5c4788e44dd10d92f7285d2f65b942f33e127230c054f99a42db4d99f766d8dbc6c57e94799593ee66a14efd7c8dd70c4812bf6aa384 - languageName: node - linkType: hard - -"elegant-spinner@npm:^1.0.1": - version: 1.0.1 - resolution: "elegant-spinner@npm:1.0.1" - checksum: 10/d6a773d950c5d403b5f0fa402787e37dde99989ab6c943558fe8491cf7cd0df0e2747a9ff4d391d5a5f20a447cc9e9a63bdc956354ba47bea462f1603a5b04fe - languageName: node - linkType: hard - -"emoji-regex@npm:^8.0.0": - version: 8.0.0 - resolution: "emoji-regex@npm:8.0.0" - checksum: 10/c72d67a6821be15ec11997877c437491c313d924306b8da5d87d2a2bcc2cec9903cb5b04ee1a088460501d8e5b44f10df82fdc93c444101a7610b80c8b6938e1 - languageName: node - linkType: hard - -"env-ci@npm:7.1.0": - version: 7.1.0 - resolution: "env-ci@npm:7.1.0" - dependencies: - execa: "npm:^5.0.0" - fromentries: "npm:^1.3.2" - java-properties: "npm:^1.0.0" - checksum: 10/841e99064161165f180088e36ce2074961c2d095c2d81b1b1c2935d434481ede972bd883fcba843213d216ec4963dc5399ea59f24eb829e3c9602d742e26cc1e - languageName: node - linkType: hard - -"error-ex@npm:^1.3.1": - version: 1.3.2 - resolution: "error-ex@npm:1.3.2" - dependencies: - is-arrayish: "npm:^0.2.1" - checksum: 10/d547740aa29c34e753fb6fed2c5de81802438529c12b3673bd37b6bb1fe49b9b7abdc3c11e6062fe625d8a296b3cf769a80f878865e25e685f787763eede3ffb - languageName: node - linkType: hard - -"es-abstract@npm:^1.23.2, es-abstract@npm:^1.23.5, es-abstract@npm:^1.23.9": - version: 1.23.9 - resolution: "es-abstract@npm:1.23.9" - dependencies: - array-buffer-byte-length: "npm:^1.0.2" - arraybuffer.prototype.slice: "npm:^1.0.4" - available-typed-arrays: "npm:^1.0.7" - call-bind: "npm:^1.0.8" - call-bound: "npm:^1.0.3" - data-view-buffer: "npm:^1.0.2" - data-view-byte-length: "npm:^1.0.2" - data-view-byte-offset: "npm:^1.0.1" - es-define-property: "npm:^1.0.1" - es-errors: "npm:^1.3.0" - es-object-atoms: "npm:^1.0.0" - es-set-tostringtag: "npm:^2.1.0" - es-to-primitive: "npm:^1.3.0" - function.prototype.name: "npm:^1.1.8" - get-intrinsic: "npm:^1.2.7" - get-proto: "npm:^1.0.0" - get-symbol-description: "npm:^1.1.0" - globalthis: "npm:^1.0.4" - gopd: "npm:^1.2.0" - has-property-descriptors: "npm:^1.0.2" - has-proto: "npm:^1.2.0" - has-symbols: "npm:^1.1.0" - hasown: "npm:^2.0.2" - internal-slot: "npm:^1.1.0" - is-array-buffer: "npm:^3.0.5" - is-callable: "npm:^1.2.7" - is-data-view: "npm:^1.0.2" - is-regex: "npm:^1.2.1" - is-shared-array-buffer: "npm:^1.0.4" - is-string: "npm:^1.1.1" - is-typed-array: "npm:^1.1.15" - is-weakref: "npm:^1.1.0" - math-intrinsics: "npm:^1.1.0" - object-inspect: "npm:^1.13.3" - object-keys: "npm:^1.1.1" - object.assign: "npm:^4.1.7" - own-keys: "npm:^1.0.1" - regexp.prototype.flags: "npm:^1.5.3" - safe-array-concat: "npm:^1.1.3" - safe-push-apply: "npm:^1.0.0" - safe-regex-test: "npm:^1.1.0" - set-proto: "npm:^1.0.0" - string.prototype.trim: "npm:^1.2.10" - string.prototype.trimend: "npm:^1.0.9" - string.prototype.trimstart: "npm:^1.0.8" - typed-array-buffer: "npm:^1.0.3" - typed-array-byte-length: "npm:^1.0.3" - typed-array-byte-offset: "npm:^1.0.4" - typed-array-length: "npm:^1.0.7" - unbox-primitive: "npm:^1.1.0" - which-typed-array: "npm:^1.1.18" - checksum: 10/31a321966d760d88fc2ed984104841b42f4f24fc322b246002b9be0af162e03803ee41fcc3cf8be89e07a27ba3033168f877dd983703cb81422ffe5322a27582 - languageName: node - linkType: hard - -"es-array-method-boxes-properly@npm:^1.0.0": - version: 1.0.0 - resolution: "es-array-method-boxes-properly@npm:1.0.0" - checksum: 10/27a8a21acf20f3f51f69dce8e643f151e380bffe569e95dc933b9ded9fcd89a765ee21b5229c93f9206c93f87395c6b75f80be8ac8c08a7ceb8771e1822ff1fb - languageName: node - linkType: hard - -"es-define-property@npm:^1.0.0, es-define-property@npm:^1.0.1": - version: 1.0.1 - resolution: "es-define-property@npm:1.0.1" - checksum: 10/f8dc9e660d90919f11084db0a893128f3592b781ce967e4fccfb8f3106cb83e400a4032c559184ec52ee1dbd4b01e7776c7cd0b3327b1961b1a4a7008920fe78 - languageName: node - linkType: hard - -"es-errors@npm:^1.3.0": - version: 1.3.0 - resolution: "es-errors@npm:1.3.0" - checksum: 10/96e65d640156f91b707517e8cdc454dd7d47c32833aa3e85d79f24f9eb7ea85f39b63e36216ef0114996581969b59fe609a94e30316b08f5f4df1d44134cf8d5 - languageName: node - linkType: hard - -"es-object-atoms@npm:^1.0.0, es-object-atoms@npm:^1.1.1": - version: 1.1.1 - resolution: "es-object-atoms@npm:1.1.1" - dependencies: - es-errors: "npm:^1.3.0" - checksum: 10/54fe77de288451dae51c37bfbfe3ec86732dc3778f98f3eb3bdb4bf48063b2c0b8f9c93542656986149d08aa5be3204286e2276053d19582b76753f1a2728867 - languageName: node - linkType: hard - -"es-set-tostringtag@npm:^2.1.0": - version: 2.1.0 - resolution: "es-set-tostringtag@npm:2.1.0" - dependencies: - es-errors: "npm:^1.3.0" - get-intrinsic: "npm:^1.2.6" - has-tostringtag: "npm:^1.0.2" - hasown: "npm:^2.0.2" - checksum: 10/86814bf8afbcd8966653f731415888019d4bc4aca6b6c354132a7a75bb87566751e320369654a101d23a91c87a85c79b178bcf40332839bd347aff437c4fb65f - languageName: node - linkType: hard - -"es-to-primitive@npm:^1.3.0": - version: 1.3.0 - resolution: "es-to-primitive@npm:1.3.0" - dependencies: - is-callable: "npm:^1.2.7" - is-date-object: "npm:^1.0.5" - is-symbol: "npm:^1.0.4" - checksum: 10/17faf35c221aad59a16286cbf58ef6f080bf3c485dff202c490d074d8e74da07884e29b852c245d894eac84f73c58330ec956dfd6d02c0b449d75eb1012a3f9b - languageName: node - linkType: hard - -"es6-error@npm:^4.1.1": - version: 4.1.1 - resolution: "es6-error@npm:4.1.1" - checksum: 10/48483c25701dc5a6376f39bbe2eaf5da0b505607ec5a98cd3ade472c1939242156660636e2e508b33211e48e88b132d245341595c067bd4a95ac79fa7134da06 - languageName: node - linkType: hard - -"escape-string-regexp@npm:1.0.5, escape-string-regexp@npm:^1.0.2, escape-string-regexp@npm:^1.0.5": - version: 1.0.5 - resolution: "escape-string-regexp@npm:1.0.5" - checksum: 10/6092fda75c63b110c706b6a9bfde8a612ad595b628f0bd2147eea1d3406723020810e591effc7db1da91d80a71a737a313567c5abb3813e8d9c71f4aa595b410 - languageName: node - linkType: hard - -"escape-string-regexp@npm:4.0.0, escape-string-regexp@npm:^4.0.0": - version: 4.0.0 - resolution: "escape-string-regexp@npm:4.0.0" - checksum: 10/98b48897d93060f2322108bf29db0feba7dd774be96cd069458d1453347b25ce8682ecc39859d4bca2203cc0ab19c237bcc71755eff49a0f8d90beadeeba5cc5 - languageName: node - linkType: hard - -"esprima@npm:^4.0.0, esprima@npm:~4.0.0": - version: 4.0.1 - resolution: "esprima@npm:4.0.1" - bin: - esparse: ./bin/esparse.js - esvalidate: ./bin/esvalidate.js - checksum: 10/f1d3c622ad992421362294f7acf866aa9409fbad4eb2e8fa230bd33944ce371d32279667b242d8b8907ec2b6ad7353a717f3c0e60e748873a34a7905174bc0eb - languageName: node - linkType: hard - -"execa@npm:^5.0.0": - version: 5.1.1 - resolution: "execa@npm:5.1.1" - dependencies: - cross-spawn: "npm:^7.0.3" - get-stream: "npm:^6.0.0" - human-signals: "npm:^2.1.0" - is-stream: "npm:^2.0.0" - merge-stream: "npm:^2.0.0" - npm-run-path: "npm:^4.0.1" - onetime: "npm:^5.1.2" - signal-exit: "npm:^3.0.3" - strip-final-newline: "npm:^2.0.0" - checksum: 10/8ada91f2d70f7dff702c861c2c64f21dfdc1525628f3c0454fd6f02fce65f7b958616cbd2b99ca7fa4d474e461a3d363824e91b3eb881705231abbf387470597 - languageName: node - linkType: hard - -"extract-stack@npm:^2.0.0": - version: 2.0.0 - resolution: "extract-stack@npm:2.0.0" - checksum: 10/dfe47560b2f47735e6c8ff5e61aa82b98a267b020d373bb3175146257d088b20460332cb95fbcda62380706774c5b864b743532f5392f40e03162bfef133fe38 - languageName: node - linkType: hard - -"fast-deep-equal@npm:^3.1.3": - version: 3.1.3 - resolution: "fast-deep-equal@npm:3.1.3" - checksum: 10/e21a9d8d84f53493b6aa15efc9cfd53dd5b714a1f23f67fb5dc8f574af80df889b3bce25dc081887c6d25457cce704e636395333abad896ccdec03abaf1f3f9d - languageName: node - linkType: hard - -"fast-glob@npm:^3.2.9": - version: 3.3.3 - resolution: "fast-glob@npm:3.3.3" - dependencies: - "@nodelib/fs.stat": "npm:^2.0.2" - "@nodelib/fs.walk": "npm:^1.2.3" - glob-parent: "npm:^5.1.2" - merge2: "npm:^1.3.0" - micromatch: "npm:^4.0.8" - checksum: 10/dcc6432b269762dd47381d8b8358bf964d8f4f60286ac6aa41c01ade70bda459ff2001b516690b96d5365f68a49242966112b5d5cc9cd82395fa8f9d017c90ad - languageName: node - linkType: hard - -"fast-json-stable-stringify@npm:^2.0.0": - version: 2.1.0 - resolution: "fast-json-stable-stringify@npm:2.1.0" - checksum: 10/2c20055c1fa43c922428f16ca8bb29f2807de63e5c851f665f7ac9790176c01c3b40335257736b299764a8d383388dabc73c8083b8e1bc3d99f0a941444ec60e - languageName: node - linkType: hard - -"fast-levenshtein@npm:^3.0.0": - version: 3.0.0 - resolution: "fast-levenshtein@npm:3.0.0" - dependencies: - fastest-levenshtein: "npm:^1.0.7" - checksum: 10/df98841b262eb345335043ae42f0219f1acf1a88f2e0959ca94c4a46df44e40455d9ee11a3f1c730dee2b1b87dc8b20d4184e71712b30b229df5b40c944ea649 - languageName: node - linkType: hard - -"fast-uri@npm:^3.0.1": - version: 3.0.6 - resolution: "fast-uri@npm:3.0.6" - checksum: 10/43c87cd03926b072a241590e49eca0e2dfe1d347ddffd4b15307613b42b8eacce00a315cf3c7374736b5f343f27e27ec88726260eb03a758336d507d6fbaba0a - languageName: node - linkType: hard - -"fastest-levenshtein@npm:^1.0.7": - version: 1.0.16 - resolution: "fastest-levenshtein@npm:1.0.16" - checksum: 10/ee85d33b5cef592033f70e1c13ae8624055950b4eb832435099cd56aa313d7f251b873bedbc06a517adfaff7b31756d139535991e2406967438e03a1bf1b008e - languageName: node - linkType: hard - -"fastq@npm:^1.6.0": - version: 1.19.1 - resolution: "fastq@npm:1.19.1" - dependencies: - reusify: "npm:^1.0.4" - checksum: 10/75679dc226316341c4f2a6b618571f51eac96779906faecd8921b984e844d6ae42fabb2df69b1071327d398d5716693ea9c9c8941f64ac9e89ec2032ce59d730 - languageName: node - linkType: hard - -"figures@npm:^1.7.0": - version: 1.7.0 - resolution: "figures@npm:1.7.0" - dependencies: - escape-string-regexp: "npm:^1.0.5" - object-assign: "npm:^4.1.0" - checksum: 10/3a815f8a3b488f818e661694112b4546ddff799aa6a07c864c46dadff923af74021f84d42ded402432a98c3208acebf2d096f3a7cc3d1a7b19a2cdc9cbcaea2e - languageName: node - linkType: hard - -"figures@npm:^2.0.0": - version: 2.0.0 - resolution: "figures@npm:2.0.0" - dependencies: - escape-string-regexp: "npm:^1.0.5" - checksum: 10/0e5bba8d2b8847c6844a476113d8d283af8757143d7760cc1a5422cceec5e8dd68c15ba50e0847597bc2c4e3865711657aeef394478c6ddce8aed7e0cd18beca - languageName: node - linkType: hard - -"filelist@npm:^1.0.4": - version: 1.0.4 - resolution: "filelist@npm:1.0.4" - dependencies: - minimatch: "npm:^5.0.1" - checksum: 10/4b436fa944b1508b95cffdfc8176ae6947b92825483639ef1b9a89b27d82f3f8aa22b21eed471993f92709b431670d4e015b39c087d435a61e1bb04564cf51de - languageName: node - linkType: hard - -"fill-range@npm:^7.1.1": - version: 7.1.1 - resolution: "fill-range@npm:7.1.1" - dependencies: - to-regex-range: "npm:^5.0.1" - checksum: 10/a7095cb39e5bc32fada2aa7c7249d3f6b01bd1ce461a61b0adabacccabd9198500c6fb1f68a7c851a657e273fce2233ba869638897f3d7ed2e87a2d89b4436ea - languageName: node - linkType: hard - -"filter-obj@npm:^1.1.0": - version: 1.1.0 - resolution: "filter-obj@npm:1.1.0" - checksum: 10/9d681939eec2b4b129cb4f307b7e93d954a0657421d4e5357d86093b26d3f4f570909ed43717dcfd62428b3cf8cddd9841b35f9d40d12ac62cfabaa677942593 - languageName: node - linkType: hard - -"for-each@npm:^0.3.3, for-each@npm:^0.3.5": - version: 0.3.5 - resolution: "for-each@npm:0.3.5" - dependencies: - is-callable: "npm:^1.2.7" - checksum: 10/330cc2439f85c94f4609de3ee1d32c5693ae15cdd7fe3d112c4fd9efd4ce7143f2c64ef6c2c9e0cfdb0058437f33ef05b5bdae5b98fcc903fb2143fbaf0fea0f - languageName: node - linkType: hard - -"form-data@npm:^4.0.0": - version: 4.0.4 - resolution: "form-data@npm:4.0.4" - dependencies: - asynckit: "npm:^0.4.0" - combined-stream: "npm:^1.0.8" - es-set-tostringtag: "npm:^2.1.0" - hasown: "npm:^2.0.2" - mime-types: "npm:^2.1.12" - checksum: 10/a4b62e21932f48702bc468cc26fb276d186e6b07b557e3dd7cc455872bdbb82db7db066844a64ad3cf40eaf3a753c830538183570462d3649fdfd705601cbcfb - languageName: node - linkType: hard - -"fromentries@npm:^1.3.2": - version: 1.3.2 - resolution: "fromentries@npm:1.3.2" - checksum: 10/10d6e07d289db102c0c1eaf5c3e3fa55ddd6b50033d7de16d99a7cd89f1e1a302dfadb26457031f9bb5d2ed95a179aaf0396092dde5abcae06e8a2f0476826be - languageName: node - linkType: hard - -"fs-extra@npm:^8.1": - version: 8.1.0 - resolution: "fs-extra@npm:8.1.0" - dependencies: - graceful-fs: "npm:^4.2.0" - jsonfile: "npm:^4.0.0" - universalify: "npm:^0.1.0" - checksum: 10/6fb12449f5349be724a138b4a7b45fe6a317d2972054517f5971959c26fbd17c0e145731a11c7324460262baa33e0a799b183ceace98f7a372c95fbb6f20f5de - languageName: node - linkType: hard - -"fs-extra@npm:^9.0, fs-extra@npm:^9.0.1, fs-extra@npm:^9.1.0": - version: 9.1.0 - resolution: "fs-extra@npm:9.1.0" - dependencies: - at-least-node: "npm:^1.0.0" - graceful-fs: "npm:^4.2.0" - jsonfile: "npm:^6.0.1" - universalify: "npm:^2.0.0" - checksum: 10/08600da1b49552ed23dfac598c8fc909c66776dd130fea54fbcad22e330f7fcc13488bb995f6bc9ce5651aa35b65702faf616fe76370ee56f1aade55da982dca - languageName: node - linkType: hard - -"fs.realpath@npm:^1.0.0": - version: 1.0.0 - resolution: "fs.realpath@npm:1.0.0" - checksum: 10/e703107c28e362d8d7b910bbcbfd371e640a3bb45ae157a362b5952c0030c0b6d4981140ec319b347bce7adc025dd7813da1ff908a945ac214d64f5402a51b96 - languageName: node - linkType: hard - -"function-bind@npm:^1.1.2": - version: 1.1.2 - resolution: "function-bind@npm:1.1.2" - checksum: 10/185e20d20f10c8d661d59aac0f3b63b31132d492e1b11fcc2a93cb2c47257ebaee7407c38513efd2b35cafdf972d9beb2ea4593c1e0f3bf8f2744836928d7454 - languageName: node - linkType: hard - -"function.prototype.name@npm:^1.1.6, function.prototype.name@npm:^1.1.8": - version: 1.1.8 - resolution: "function.prototype.name@npm:1.1.8" - dependencies: - call-bind: "npm:^1.0.8" - call-bound: "npm:^1.0.3" - define-properties: "npm:^1.2.1" - functions-have-names: "npm:^1.2.3" - hasown: "npm:^2.0.2" - is-callable: "npm:^1.2.7" - checksum: 10/25b9e5bea936732a6f0c0c08db58cc0d609ac1ed458c6a07ead46b32e7b9bf3fe5887796c3f83d35994efbc4fdde81c08ac64135b2c399b8f2113968d44082bc - languageName: node - linkType: hard - -"functions-have-names@npm:^1.2.3": - version: 1.2.3 - resolution: "functions-have-names@npm:1.2.3" - checksum: 10/0ddfd3ed1066a55984aaecebf5419fbd9344a5c38dd120ffb0739fac4496758dcf371297440528b115e4367fc46e3abc86a2cc0ff44612181b175ae967a11a05 - languageName: node - linkType: hard - -"gaze@npm:1.1.3": - version: 1.1.3 - resolution: "gaze@npm:1.1.3" - dependencies: - globule: "npm:^1.0.0" - checksum: 10/9ff1110aae5c7d96cbd49812883558971c2f9eba00bdd20e326b5644e262956464fa67edcad03f2cb2ae6ca4f26c80cb1fb5b4a610280a77fca51046acc7749c - languageName: node - linkType: hard - -"get-intrinsic@npm:^1.2.4, get-intrinsic@npm:^1.2.5, get-intrinsic@npm:^1.2.6, get-intrinsic@npm:^1.2.7, get-intrinsic@npm:^1.3.0": - version: 1.3.0 - resolution: "get-intrinsic@npm:1.3.0" - dependencies: - call-bind-apply-helpers: "npm:^1.0.2" - es-define-property: "npm:^1.0.1" - es-errors: "npm:^1.3.0" - es-object-atoms: "npm:^1.1.1" - function-bind: "npm:^1.1.2" - get-proto: "npm:^1.0.1" - gopd: "npm:^1.2.0" - has-symbols: "npm:^1.1.0" - hasown: "npm:^2.0.2" - math-intrinsics: "npm:^1.1.0" - checksum: 10/6e9dd920ff054147b6f44cb98104330e87caafae051b6d37b13384a45ba15e71af33c3baeac7cb630a0aaa23142718dcf25b45cfdd86c184c5dcb4e56d953a10 - languageName: node - linkType: hard - -"get-package-type@npm:^0.1.0": - version: 0.1.0 - resolution: "get-package-type@npm:0.1.0" - checksum: 10/bba0811116d11e56d702682ddef7c73ba3481f114590e705fc549f4d868972263896af313c57a25c076e3c0d567e11d919a64ba1b30c879be985fc9d44f96148 - languageName: node - linkType: hard - -"get-proto@npm:^1.0.0, get-proto@npm:^1.0.1": - version: 1.0.1 - resolution: "get-proto@npm:1.0.1" - dependencies: - dunder-proto: "npm:^1.0.1" - es-object-atoms: "npm:^1.0.0" - checksum: 10/4fc96afdb58ced9a67558698b91433e6b037aaa6f1493af77498d7c85b141382cf223c0e5946f334fb328ee85dfe6edd06d218eaf09556f4bc4ec6005d7f5f7b - languageName: node - linkType: hard - -"get-stream@npm:^6.0.0": - version: 6.0.1 - resolution: "get-stream@npm:6.0.1" - checksum: 10/781266d29725f35c59f1d214aedc92b0ae855800a980800e2923b3fbc4e56b3cb6e462c42e09a1cf1a00c64e056a78fa407cbe06c7c92b7e5cd49b4b85c2a497 - languageName: node - linkType: hard - -"get-symbol-description@npm:^1.1.0": - version: 1.1.0 - resolution: "get-symbol-description@npm:1.1.0" - dependencies: - call-bound: "npm:^1.0.3" - es-errors: "npm:^1.3.0" - get-intrinsic: "npm:^1.2.6" - checksum: 10/a353e3a9595a74720b40fb5bae3ba4a4f826e186e83814d93375182384265676f59e49998b9cdfac4a2225ce95a3d32a68f502a2c5619303987f1c183ab80494 - languageName: node - linkType: hard - -"git-parse@npm:2.1.1": - version: 2.1.1 - resolution: "git-parse@npm:2.1.1" - dependencies: - byline: "npm:5.0.0" - util.promisify: "npm:1.1.1" - checksum: 10/2d17577d7eca0bea897cd45c274182c0a32ecfde0e6cf77bdea85b7667c9cefefc829c1df61ed56a79311b6bf22586540a697f69f6bab8628404b471a881d8a6 - languageName: node - linkType: hard - -"git-rev-sync@npm:3.0.2": - version: 3.0.2 - resolution: "git-rev-sync@npm:3.0.2" - dependencies: - escape-string-regexp: "npm:1.0.5" - graceful-fs: "npm:4.1.15" - shelljs: "npm:0.8.5" - checksum: 10/a6c1b8d9417643f40db0e005df9f05b0f6440756ef5ed379f1565cb90ad58ac1192da193838b243ccf8979ca5109a43c9ca7eb016c612af06912868dab307dd3 - languageName: node - linkType: hard - -"git-up@npm:^4.0.0": - version: 4.0.5 - resolution: "git-up@npm:4.0.5" - dependencies: - is-ssh: "npm:^1.3.0" - parse-url: "npm:^6.0.0" - checksum: 10/8c47757dfbe7a7e6ada6464eac031812e051e0a87eb475cf6be1957d41299032fa498af8104c77668566dd5d1a5c7677a42273f4381f59ad23e39020c7566365 - languageName: node - linkType: hard - -"git-url-parse@npm:11.6.0": - version: 11.6.0 - resolution: "git-url-parse@npm:11.6.0" - dependencies: - git-up: "npm:^4.0.0" - checksum: 10/8fac688231fc86273e0a3f33ddeef9f463834af805ec2e40ccbd72b38b52a1def3d2dafcb32b1395095a93e70f115b30030c5fdb55c97fddd3ec35f26d8b6fb2 - languageName: node - linkType: hard - -"glob-parent@npm:^5.1.2": - version: 5.1.2 - resolution: "glob-parent@npm:5.1.2" - dependencies: - is-glob: "npm:^4.0.1" - checksum: 10/32cd106ce8c0d83731966d31517adb766d02c3812de49c30cfe0675c7c0ae6630c11214c54a5ae67aca882cf738d27fd7768f21aa19118b9245950554be07247 - languageName: node - linkType: hard - -"glob@npm:8.0.1": - version: 8.0.1 - resolution: "glob@npm:8.0.1" - dependencies: - fs.realpath: "npm:^1.0.0" - inflight: "npm:^1.0.4" - inherits: "npm:2" - minimatch: "npm:^5.0.1" - once: "npm:^1.3.0" - path-is-absolute: "npm:^1.0.0" - checksum: 10/b69c95a2019ef186ba4dfa7a8c5382b901fd81caf66ab71a474a94f34d46f2b9ce81ba3099d4f3a3689a9a0b2fa74757d0793e110f27f62a35356209c5b65107 - languageName: node - linkType: hard - -"glob@npm:^7.0.0": - version: 7.2.3 - resolution: "glob@npm:7.2.3" - dependencies: - fs.realpath: "npm:^1.0.0" - inflight: "npm:^1.0.4" - inherits: "npm:2" - minimatch: "npm:^3.1.1" - once: "npm:^1.3.0" - path-is-absolute: "npm:^1.0.0" - checksum: 10/59452a9202c81d4508a43b8af7082ca5c76452b9fcc4a9ab17655822e6ce9b21d4f8fbadabe4fe3faef448294cec249af305e2cd824b7e9aaf689240e5e96a7b - languageName: node - linkType: hard - -"glob@npm:^8.0.0": - version: 8.1.0 - resolution: "glob@npm:8.1.0" - dependencies: - fs.realpath: "npm:^1.0.0" - inflight: "npm:^1.0.4" - inherits: "npm:2" - minimatch: "npm:^5.0.1" - once: "npm:^1.3.0" - checksum: 10/9aab1c75eb087c35dbc41d1f742e51d0507aa2b14c910d96fb8287107a10a22f4bbdce26fc0a3da4c69a20f7b26d62f1640b346a4f6e6becfff47f335bb1dc5e - languageName: node - linkType: hard - -"glob@npm:~7.1.1": - version: 7.1.7 - resolution: "glob@npm:7.1.7" - dependencies: - fs.realpath: "npm:^1.0.0" - inflight: "npm:^1.0.4" - inherits: "npm:2" - minimatch: "npm:^3.0.4" - once: "npm:^1.3.0" - path-is-absolute: "npm:^1.0.0" - checksum: 10/ff5aab0386e9cace92b0550d42085b71013c5ea382982dd7fdded998a559635f61413b8ba6fb7294eef289c83b52f4e64136f888300ac8afc4f3e5623182d6c8 - languageName: node - linkType: hard - -"global-agent@npm:3.0.0": - version: 3.0.0 - resolution: "global-agent@npm:3.0.0" - dependencies: - boolean: "npm:^3.0.1" - es6-error: "npm:^4.1.1" - matcher: "npm:^3.0.0" - roarr: "npm:^2.15.3" - semver: "npm:^7.3.2" - serialize-error: "npm:^7.0.1" - checksum: 10/a26d96d1d79af57a8ef957f66cef6f3889a8fa55131f0bbd72b8e1bc340a9b7ed7b627b96eaf5eb14aee08a8b4ad44395090e2cf77146e993f1d2df7abaa0a0d - languageName: node - linkType: hard - -"globalthis@npm:^1.0.1, globalthis@npm:^1.0.4": - version: 1.0.4 - resolution: "globalthis@npm:1.0.4" - dependencies: - define-properties: "npm:^1.2.1" - gopd: "npm:^1.0.1" - checksum: 10/1f1fd078fb2f7296306ef9dd51019491044ccf17a59ed49d375b576ca108ff37e47f3d29aead7add40763574a992f16a5367dd1e2173b8634ef18556ab719ac4 - languageName: node - linkType: hard - -"globby@npm:^11.0.1, globby@npm:^11.1.0": - version: 11.1.0 - resolution: "globby@npm:11.1.0" - dependencies: - array-union: "npm:^2.1.0" - dir-glob: "npm:^3.0.1" - fast-glob: "npm:^3.2.9" - ignore: "npm:^5.2.0" - merge2: "npm:^1.4.1" - slash: "npm:^3.0.0" - checksum: 10/288e95e310227bbe037076ea81b7c2598ccbc3122d87abc6dab39e1eec309aa14f0e366a98cdc45237ffcfcbad3db597778c0068217dcb1950fef6249104e1b1 - languageName: node - linkType: hard - -"globule@npm:^1.0.0": - version: 1.3.4 - resolution: "globule@npm:1.3.4" - dependencies: - glob: "npm:~7.1.1" - lodash: "npm:^4.17.21" - minimatch: "npm:~3.0.2" - checksum: 10/04ac30656f9fc34e7e30a700ef39bfc357629a9214e2e228ee714bc0f1be60c5e4e2a78facafa5588889b02d25f02012d9e8c057704040e19e86b920effe54d5 - languageName: node - linkType: hard - -"gopd@npm:^1.0.1, gopd@npm:^1.2.0": - version: 1.2.0 - resolution: "gopd@npm:1.2.0" - checksum: 10/94e296d69f92dc1c0768fcfeecfb3855582ab59a7c75e969d5f96ce50c3d201fd86d5a2857c22565764d5bb8a816c7b1e58f133ec318cd56274da36c5e3fb1a1 - languageName: node - linkType: hard - -"graceful-fs@npm:4.1.15": - version: 4.1.15 - resolution: "graceful-fs@npm:4.1.15" - checksum: 10/eecc88fc447c0d92257ac2583e1177574de6995a8627778bff56e8d14991275eb26a9d248e04c3c128fd17522cfbce17d2ce1bd03ec15efd42d2b3a7bab577c2 - languageName: node - linkType: hard - -"graceful-fs@npm:^4.1.15, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0": - version: 4.2.11 - resolution: "graceful-fs@npm:4.2.11" - checksum: 10/bf152d0ed1dc159239db1ba1f74fdbc40cb02f626770dcd5815c427ce0688c2635a06ed69af364396da4636d0408fcf7d4afdf7881724c3307e46aff30ca49e2 - languageName: node - linkType: hard - -"graphql-tag@npm:2.12.6, graphql-tag@npm:^2.10.1": - version: 2.12.6 - resolution: "graphql-tag@npm:2.12.6" - dependencies: - tslib: "npm:^2.1.0" - peerDependencies: - graphql: ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 - checksum: 10/23a2bc1d3fbeae86444204e0ac08522e09dc369559ba75768e47421a7321b59f352fb5b2c9a5c37d3cf6de890dca4e5ac47e740c7cc622e728572ecaa649089e - languageName: node - linkType: hard - -"graphql@npm:14.0.2 - 14.2.0 || ^14.3.1 || ^15.0.0": - version: 15.10.1 - resolution: "graphql@npm:15.10.1" - checksum: 10/49177f52c1fbac022866d39bb15040122da4c3ccd12644233b20edfcb1f76aa92aa92f2a82af4668101d8f726112fda111cb11a12feb05635cd689443a7aafb8 - languageName: node - linkType: hard - -"has-ansi@npm:^2.0.0": - version: 2.0.0 - resolution: "has-ansi@npm:2.0.0" - dependencies: - ansi-regex: "npm:^2.0.0" - checksum: 10/1b51daa0214440db171ff359d0a2d17bc20061164c57e76234f614c91dbd2a79ddd68dfc8ee73629366f7be45a6df5f2ea9de83f52e1ca24433f2cc78c35d8ec - languageName: node - linkType: hard - -"has-bigints@npm:^1.0.2": - version: 1.1.0 - resolution: "has-bigints@npm:1.1.0" - checksum: 10/90fb1b24d40d2472bcd1c8bd9dd479037ec240215869bdbff97b2be83acef57d28f7e96bdd003a21bed218d058b49097f4acc8821c05b1629cc5d48dd7bfcccd - languageName: node - linkType: hard - -"has-flag@npm:^3.0.0": - version: 3.0.0 - resolution: "has-flag@npm:3.0.0" - checksum: 10/4a15638b454bf086c8148979aae044dd6e39d63904cd452d970374fa6a87623423da485dfb814e7be882e05c096a7ccf1ebd48e7e7501d0208d8384ff4dea73b - languageName: node - linkType: hard - -"has-flag@npm:^4.0.0": - version: 4.0.0 - resolution: "has-flag@npm:4.0.0" - checksum: 10/261a1357037ead75e338156b1f9452c016a37dcd3283a972a30d9e4a87441ba372c8b81f818cd0fbcd9c0354b4ae7e18b9e1afa1971164aef6d18c2b6095a8ad - languageName: node - linkType: hard - -"has-property-descriptors@npm:^1.0.0, has-property-descriptors@npm:^1.0.2": - version: 1.0.2 - resolution: "has-property-descriptors@npm:1.0.2" - dependencies: - es-define-property: "npm:^1.0.0" - checksum: 10/2d8c9ab8cebb572e3362f7d06139a4592105983d4317e68f7adba320fe6ddfc8874581e0971e899e633fd5f72e262830edce36d5a0bc863dad17ad20572484b2 - languageName: node - linkType: hard - -"has-proto@npm:^1.2.0": - version: 1.2.0 - resolution: "has-proto@npm:1.2.0" - dependencies: - dunder-proto: "npm:^1.0.0" - checksum: 10/7eaed07728eaa28b77fadccabce53f30de467ff186a766872669a833ac2e87d8922b76a22cc58339d7e0277aefe98d6d00762113b27a97cdf65adcf958970935 - languageName: node - linkType: hard - -"has-symbols@npm:^1.0.1, has-symbols@npm:^1.0.3, has-symbols@npm:^1.1.0": - version: 1.1.0 - resolution: "has-symbols@npm:1.1.0" - checksum: 10/959385c98696ebbca51e7534e0dc723ada325efa3475350951363cce216d27373e0259b63edb599f72eb94d6cde8577b4b2375f080b303947e560f85692834fa - languageName: node - linkType: hard - -"has-tostringtag@npm:^1.0.2": - version: 1.0.2 - resolution: "has-tostringtag@npm:1.0.2" - dependencies: - has-symbols: "npm:^1.0.3" - checksum: 10/c74c5f5ceee3c8a5b8bc37719840dc3749f5b0306d818974141dda2471a1a2ca6c8e46b9d6ac222c5345df7a901c9b6f350b1e6d62763fec877e26609a401bfe - languageName: node - linkType: hard - -"hasown@npm:^2.0.2": - version: 2.0.2 - resolution: "hasown@npm:2.0.2" - dependencies: - function-bind: "npm:^1.1.2" - checksum: 10/7898a9c1788b2862cf0f9c345a6bec77ba4a0c0983c7f19d610c382343d4f98fa260686b225dfb1f88393a66679d2ec58ee310c1d6868c081eda7918f32cc70a - languageName: node - linkType: hard - -"header-case@npm:^2.0.4": - version: 2.0.4 - resolution: "header-case@npm:2.0.4" - dependencies: - capital-case: "npm:^1.0.4" - tslib: "npm:^2.0.3" - checksum: 10/571c83eeb25e8130d172218712f807c0b96d62b020981400bccc1503a7cf14b09b8b10498a962d2739eccf231d950e3848ba7d420b58a6acd2f9283439546cd9 - languageName: node - linkType: hard - -"http-call@npm:^5.2.2": - version: 5.3.0 - resolution: "http-call@npm:5.3.0" - dependencies: - content-type: "npm:^1.0.4" - debug: "npm:^4.1.1" - is-retry-allowed: "npm:^1.1.0" - is-stream: "npm:^2.0.0" - parse-json: "npm:^4.0.0" - tunnel-agent: "npm:^0.6.0" - checksum: 10/458c890c95573db831daa2346ff98b1630543c9b2fc3cfc432e1fb6968d6eeb6a5abe87e551f0fc3bce1972215a69fd133b8d25ff8cff2276c2c153d405b3d1f - languageName: node - linkType: hard - -"human-signals@npm:^2.1.0": - version: 2.1.0 - resolution: "human-signals@npm:2.1.0" - checksum: 10/df59be9e0af479036798a881d1f136c4a29e0b518d4abb863afbd11bf30efa3eeb1d0425fc65942dcc05ab3bf40205ea436b0ff389f2cd20b75b8643d539bf86 - languageName: node - linkType: hard - -"hyperlinker@npm:^1.0.0": - version: 1.0.0 - resolution: "hyperlinker@npm:1.0.0" - checksum: 10/fdcf08c72dde534e127cfc40e4c28de5106c58b58f0191d117a8a78802aeeff98dd870a2ee1ac7ee877861b9d0bd7b515a8d0759f1e319ea3162d3c210dbea7c - languageName: node - linkType: hard - -"ignore@npm:^5.2.0": - version: 5.3.2 - resolution: "ignore@npm:5.3.2" - checksum: 10/cceb6a457000f8f6a50e1196429750d782afce5680dd878aa4221bd79972d68b3a55b4b1458fc682be978f4d3c6a249046aa0880637367216444ab7b014cfc98 - languageName: node - linkType: hard - -"import-fresh@npm:^3.2.1": - version: 3.3.1 - resolution: "import-fresh@npm:3.3.1" - dependencies: - parent-module: "npm:^1.0.0" - resolve-from: "npm:^4.0.0" - checksum: 10/a06b19461b4879cc654d46f8a6244eb55eb053437afd4cbb6613cad6be203811849ed3e4ea038783092879487299fda24af932b86bdfff67c9055ba3612b8c87 - languageName: node - linkType: hard - -"indent-string@npm:^3.0.0": - version: 3.2.0 - resolution: "indent-string@npm:3.2.0" - checksum: 10/a0b72603bba6c985d367fda3a25aad16423d2056b22a7e83ee2dd9ce0ce3d03d1e078644b679087aa7edf1cfb457f0d96d9eeadc0b12f38582088cc00e995d2f - languageName: node - linkType: hard - -"indent-string@npm:^4.0.0": - version: 4.0.0 - resolution: "indent-string@npm:4.0.0" - checksum: 10/cd3f5cbc9ca2d624c6a1f53f12e6b341659aba0e2d3254ae2b4464aaea8b4294cdb09616abbc59458f980531f2429784ed6a420d48d245bcad0811980c9efae9 - languageName: node - linkType: hard - -"inflected@npm:^2.0.3": - version: 2.1.0 - resolution: "inflected@npm:2.1.0" - checksum: 10/1a4d88cf12803663e25214df1f66f027b572c961f014e958a5607c1e95476f42c27e641db0b8779ef7b05c6d4d1ea4744c61d04dfb7cddf6c8fc26acdca1199d - languageName: node - linkType: hard - -"inflight@npm:^1.0.4": - version: 1.0.6 - resolution: "inflight@npm:1.0.6" - dependencies: - once: "npm:^1.3.0" - wrappy: "npm:1" - checksum: 10/d2ebd65441a38c8336c223d1b80b921b9fa737e37ea466fd7e253cb000c64ae1f17fa59e68130ef5bda92cfd8d36b83d37dab0eb0a4558bcfec8e8cdfd2dcb67 - languageName: node - linkType: hard - -"inherits@npm:2, inherits@npm:^2.0.1": - version: 2.0.4 - resolution: "inherits@npm:2.0.4" - checksum: 10/cd45e923bee15186c07fa4c89db0aace24824c482fb887b528304694b2aa6ff8a898da8657046a5dcf3e46cd6db6c61629551f9215f208d7c3f157cf9b290521 - languageName: node - linkType: hard - -"internal-slot@npm:^1.1.0": - version: 1.1.0 - resolution: "internal-slot@npm:1.1.0" - dependencies: - es-errors: "npm:^1.3.0" - hasown: "npm:^2.0.2" - side-channel: "npm:^1.1.0" - checksum: 10/1d5219273a3dab61b165eddf358815eefc463207db33c20fcfca54717da02e3f492003757721f972fd0bf21e4b426cab389c5427b99ceea4b8b670dc88ee6d4a - languageName: node - linkType: hard - -"interpret@npm:^1.0.0": - version: 1.4.0 - resolution: "interpret@npm:1.4.0" - checksum: 10/5beec568d3f60543d0f61f2c5969d44dffcb1a372fe5abcdb8013968114d4e4aaac06bc971a4c9f5bd52d150881d8ebad72a8c60686b1361f5f0522f39c0e1a3 - languageName: node - linkType: hard - -"is-array-buffer@npm:^3.0.4, is-array-buffer@npm:^3.0.5": - version: 3.0.5 - resolution: "is-array-buffer@npm:3.0.5" - dependencies: - call-bind: "npm:^1.0.8" - call-bound: "npm:^1.0.3" - get-intrinsic: "npm:^1.2.6" - checksum: 10/ef1095c55b963cd0dcf6f88a113e44a0aeca91e30d767c475e7d746d28d1195b10c5076b94491a7a0cd85020ca6a4923070021d74651d093dc909e9932cf689b - languageName: node - linkType: hard - -"is-arrayish@npm:^0.2.1": - version: 0.2.1 - resolution: "is-arrayish@npm:0.2.1" - checksum: 10/73ced84fa35e59e2c57da2d01e12cd01479f381d7f122ce41dcbb713f09dbfc651315832cd2bf8accba7681a69e4d6f1e03941d94dd10040d415086360e7005e - languageName: node - linkType: hard - -"is-async-function@npm:^2.0.0": - version: 2.1.1 - resolution: "is-async-function@npm:2.1.1" - dependencies: - async-function: "npm:^1.0.0" - call-bound: "npm:^1.0.3" - get-proto: "npm:^1.0.1" - has-tostringtag: "npm:^1.0.2" - safe-regex-test: "npm:^1.1.0" - checksum: 10/7c2ac7efdf671e03265e74a043bcb1c0a32e226bc2a42dfc5ec8644667df668bbe14b91c08e6c1414f392f8cf86cd1d489b3af97756e2c7a49dd1ba63fd40ca6 - languageName: node - linkType: hard - -"is-bigint@npm:^1.1.0": - version: 1.1.0 - resolution: "is-bigint@npm:1.1.0" - dependencies: - has-bigints: "npm:^1.0.2" - checksum: 10/10cf327310d712fe227cfaa32d8b11814c214392b6ac18c827f157e1e85363cf9c8e2a22df526689bd5d25e53b58cc110894787afb54e138e7c504174dba15fd - languageName: node - linkType: hard - -"is-boolean-object@npm:^1.2.1": - version: 1.2.2 - resolution: "is-boolean-object@npm:1.2.2" - dependencies: - call-bound: "npm:^1.0.3" - has-tostringtag: "npm:^1.0.2" - checksum: 10/051fa95fdb99d7fbf653165a7e6b2cba5d2eb62f7ffa81e793a790f3fb5366c91c1b7b6af6820aa2937dd86c73aa3ca9d9ca98f500988457b1c59692c52ba911 - languageName: node - linkType: hard - -"is-callable@npm:^1.2.7": - version: 1.2.7 - resolution: "is-callable@npm:1.2.7" - checksum: 10/48a9297fb92c99e9df48706241a189da362bff3003354aea4048bd5f7b2eb0d823cd16d0a383cece3d76166ba16d85d9659165ac6fcce1ac12e6c649d66dbdb9 - languageName: node - linkType: hard - -"is-core-module@npm:^2.16.0": - version: 2.16.1 - resolution: "is-core-module@npm:2.16.1" - dependencies: - hasown: "npm:^2.0.2" - checksum: 10/452b2c2fb7f889cbbf7e54609ef92cf6c24637c568acc7e63d166812a0fb365ae8a504c333a29add8bdb1686704068caa7f4e4b639b650dde4f00a038b8941fb - languageName: node - linkType: hard - -"is-data-view@npm:^1.0.1, is-data-view@npm:^1.0.2": - version: 1.0.2 - resolution: "is-data-view@npm:1.0.2" - dependencies: - call-bound: "npm:^1.0.2" - get-intrinsic: "npm:^1.2.6" - is-typed-array: "npm:^1.1.13" - checksum: 10/357e9a48fa38f369fd6c4c3b632a3ab2b8adca14997db2e4b3fe94c4cd0a709af48e0fb61b02c64a90c0dd542fd489d49c2d03157b05ae6c07f5e4dec9e730a8 - languageName: node - linkType: hard - -"is-date-object@npm:^1.0.5, is-date-object@npm:^1.1.0": - version: 1.1.0 - resolution: "is-date-object@npm:1.1.0" - dependencies: - call-bound: "npm:^1.0.2" - has-tostringtag: "npm:^1.0.2" - checksum: 10/3a811b2c3176fb31abee1d23d3dc78b6c65fd9c07d591fcb67553cab9e7f272728c3dd077d2d738b53f9a2103255b0a6e8dfc9568a7805c56a78b2563e8d1dec - languageName: node - linkType: hard - -"is-docker@npm:^2.0.0": - version: 2.2.1 - resolution: "is-docker@npm:2.2.1" - bin: - is-docker: cli.js - checksum: 10/3fef7ddbf0be25958e8991ad941901bf5922ab2753c46980b60b05c1bf9c9c2402d35e6dc32e4380b980ef5e1970a5d9d5e5aa2e02d77727c3b6b5e918474c56 - languageName: node - linkType: hard - -"is-extglob@npm:^2.1.1": - version: 2.1.1 - resolution: "is-extglob@npm:2.1.1" - checksum: 10/df033653d06d0eb567461e58a7a8c9f940bd8c22274b94bf7671ab36df5719791aae15eef6d83bbb5e23283967f2f984b8914559d4449efda578c775c4be6f85 - languageName: node - linkType: hard - -"is-finalizationregistry@npm:^1.1.0": - version: 1.1.1 - resolution: "is-finalizationregistry@npm:1.1.1" - dependencies: - call-bound: "npm:^1.0.3" - checksum: 10/0bfb145e9a1ba852ddde423b0926d2169ae5fe9e37882cde9e8f69031281a986308df4d982283e152396e88b86562ed2256cbaa5e6390fb840a4c25ab54b8a80 - languageName: node - linkType: hard - -"is-fullwidth-code-point@npm:^1.0.0": - version: 1.0.0 - resolution: "is-fullwidth-code-point@npm:1.0.0" - dependencies: - number-is-nan: "npm:^1.0.0" - checksum: 10/4d46a7465a66a8aebcc5340d3b63a56602133874af576a9ca42c6f0f4bd787a743605771c5f246db77da96605fefeffb65fc1dbe862dcc7328f4b4d03edf5a57 - languageName: node - linkType: hard - -"is-fullwidth-code-point@npm:^2.0.0": - version: 2.0.0 - resolution: "is-fullwidth-code-point@npm:2.0.0" - checksum: 10/eef9c6e15f68085fec19ff6a978a6f1b8f48018fd1265035552078ee945573594933b09bbd6f562553e2a241561439f1ef5339276eba68d272001343084cfab8 - languageName: node - linkType: hard - -"is-fullwidth-code-point@npm:^3.0.0": - version: 3.0.0 - resolution: "is-fullwidth-code-point@npm:3.0.0" - checksum: 10/44a30c29457c7fb8f00297bce733f0a64cd22eca270f83e58c105e0d015e45c019491a4ab2faef91ab51d4738c670daff901c799f6a700e27f7314029e99e348 - languageName: node - linkType: hard - -"is-generator-function@npm:^1.0.10": - version: 1.1.0 - resolution: "is-generator-function@npm:1.1.0" - dependencies: - call-bound: "npm:^1.0.3" - get-proto: "npm:^1.0.0" - has-tostringtag: "npm:^1.0.2" - safe-regex-test: "npm:^1.1.0" - checksum: 10/5906ff51a856a5fbc6b90a90fce32040b0a6870da905f98818f1350f9acadfc9884f7c3dec833fce04b83dd883937b86a190b6593ede82e8b1af8b6c4ecf7cbd - languageName: node - linkType: hard - -"is-glob@npm:^4.0.1": - version: 4.0.3 - resolution: "is-glob@npm:4.0.3" - dependencies: - is-extglob: "npm:^2.1.1" - checksum: 10/3ed74f2b0cdf4f401f38edb0442ddfde3092d79d7d35c9919c86641efdbcbb32e45aa3c0f70ce5eecc946896cd5a0f26e4188b9f2b881876f7cb6c505b82da11 - languageName: node - linkType: hard - -"is-map@npm:^2.0.3": - version: 2.0.3 - resolution: "is-map@npm:2.0.3" - checksum: 10/8de7b41715b08bcb0e5edb0fb9384b80d2d5bcd10e142188f33247d19ff078abaf8e9b6f858e2302d8d05376a26a55cd23a3c9f8ab93292b02fcd2cc9e4e92bb - languageName: node - linkType: hard - -"is-number-object@npm:^1.1.1": - version: 1.1.1 - resolution: "is-number-object@npm:1.1.1" - dependencies: - call-bound: "npm:^1.0.3" - has-tostringtag: "npm:^1.0.2" - checksum: 10/a5922fb8779ab1ea3b8a9c144522b3d0bea5d9f8f23f7a72470e61e1e4df47714e28e0154ac011998b709cce260c3c9447ad3cd24a96c2f2a0abfdb2cbdc76c8 - languageName: node - linkType: hard - -"is-number@npm:^7.0.0": - version: 7.0.0 - resolution: "is-number@npm:7.0.0" - checksum: 10/6a6c3383f68afa1e05b286af866017c78f1226d43ac8cb064e115ff9ed85eb33f5c4f7216c96a71e4dfea289ef52c5da3aef5bbfade8ffe47a0465d70c0c8e86 - languageName: node - linkType: hard - -"is-observable@npm:^1.1.0": - version: 1.1.0 - resolution: "is-observable@npm:1.1.0" - dependencies: - symbol-observable: "npm:^1.1.0" - checksum: 10/ab3d7e740915e6b53a81d96ce7d581f4dd26dacceb95278b74e7bf3123221073ea02cde810f864cff94ed5c394f18248deefd6a8f2d40137d868130eb5be6f85 - languageName: node - linkType: hard - -"is-promise@npm:^2.1.0": - version: 2.2.2 - resolution: "is-promise@npm:2.2.2" - checksum: 10/18bf7d1c59953e0ad82a1ed963fb3dc0d135c8f299a14f89a17af312fc918373136e56028e8831700e1933519630cc2fd4179a777030330fde20d34e96f40c78 - languageName: node - linkType: hard - -"is-regex@npm:^1.2.1": - version: 1.2.1 - resolution: "is-regex@npm:1.2.1" - dependencies: - call-bound: "npm:^1.0.2" - gopd: "npm:^1.2.0" - has-tostringtag: "npm:^1.0.2" - hasown: "npm:^2.0.2" - checksum: 10/c42b7efc5868a5c9a4d8e6d3e9816e8815c611b09535c00fead18a1138455c5cb5e1887f0023a467ad3f9c419d62ba4dc3d9ba8bafe55053914d6d6454a945d2 - languageName: node - linkType: hard - -"is-retry-allowed@npm:^1.1.0": - version: 1.2.0 - resolution: "is-retry-allowed@npm:1.2.0" - checksum: 10/50d700a89ae31926b1c91b3eb0104dbceeac8790d8b80d02f5c76d9a75c2056f1bb24b5268a8a018dead606bddf116b2262e5ac07401eb8b8783b266ed22558d - languageName: node - linkType: hard - -"is-set@npm:^2.0.3": - version: 2.0.3 - resolution: "is-set@npm:2.0.3" - checksum: 10/5685df33f0a4a6098a98c72d94d67cad81b2bc72f1fb2091f3d9283c4a1c582123cd709145b02a9745f0ce6b41e3e43f1c944496d1d74d4ea43358be61308669 - languageName: node - linkType: hard - -"is-shared-array-buffer@npm:^1.0.4": - version: 1.0.4 - resolution: "is-shared-array-buffer@npm:1.0.4" - dependencies: - call-bound: "npm:^1.0.3" - checksum: 10/0380d7c60cc692856871526ffcd38a8133818a2ee42d47bb8008248a0cd2121d8c8b5f66b6da3cac24bc5784553cacb6faaf678f66bc88c6615b42af2825230e - languageName: node - linkType: hard - -"is-ssh@npm:^1.3.0": - version: 1.4.1 - resolution: "is-ssh@npm:1.4.1" - dependencies: - protocols: "npm:^2.0.1" - checksum: 10/f60910cd83fa94e9874655a672c3849312c12af83c0fe3dbff9945755fe838a73985d8f94e32ebf5626ba4148ee10eef51b7240b0218dbb6e9a43a06899b0529 - languageName: node - linkType: hard - -"is-stream@npm:^1.1.0": - version: 1.1.0 - resolution: "is-stream@npm:1.1.0" - checksum: 10/351aa77c543323c4e111204482808cfad68d2e940515949e31ccd0b010fc13d5fba4b9c230e4887fd24284713040f43e542332fbf172f6b9944b7d62e389c0ec - languageName: node - linkType: hard - -"is-stream@npm:^2.0.0": - version: 2.0.1 - resolution: "is-stream@npm:2.0.1" - checksum: 10/b8e05ccdf96ac330ea83c12450304d4a591f9958c11fd17bed240af8d5ffe08aedafa4c0f4cfccd4d28dc9d4d129daca1023633d5c11601a6cbc77521f6fae66 - languageName: node - linkType: hard - -"is-string@npm:^1.0.7, is-string@npm:^1.1.1": - version: 1.1.1 - resolution: "is-string@npm:1.1.1" - dependencies: - call-bound: "npm:^1.0.3" - has-tostringtag: "npm:^1.0.2" - checksum: 10/5277cb9e225a7cc8a368a72623b44a99f2cfa139659c6b203553540681ad4276bfc078420767aad0e73eef5f0bd07d4abf39a35d37ec216917879d11cebc1f8b - languageName: node - linkType: hard - -"is-symbol@npm:^1.0.4, is-symbol@npm:^1.1.1": - version: 1.1.1 - resolution: "is-symbol@npm:1.1.1" - dependencies: - call-bound: "npm:^1.0.2" - has-symbols: "npm:^1.1.0" - safe-regex-test: "npm:^1.1.0" - checksum: 10/db495c0d8cd0a7a66b4f4ef7fccee3ab5bd954cb63396e8ac4d32efe0e9b12fdfceb851d6c501216a71f4f21e5ff20fc2ee845a3d52d455e021c466ac5eb2db2 - languageName: node - linkType: hard - -"is-typed-array@npm:^1.1.13, is-typed-array@npm:^1.1.14, is-typed-array@npm:^1.1.15": - version: 1.1.15 - resolution: "is-typed-array@npm:1.1.15" - dependencies: - which-typed-array: "npm:^1.1.16" - checksum: 10/e8cf60b9ea85667097a6ad68c209c9722cfe8c8edf04d6218366469e51944c5cc25bae45ffb845c23f811d262e4314d3b0168748eb16711aa34d12724cdf0735 - languageName: node - linkType: hard - -"is-weakmap@npm:^2.0.2": - version: 2.0.2 - resolution: "is-weakmap@npm:2.0.2" - checksum: 10/a7b7e23206c542dcf2fa0abc483142731788771527e90e7e24f658c0833a0d91948a4f7b30d78f7a65255a48512e41a0288b778ba7fc396137515c12e201fd11 - languageName: node - linkType: hard - -"is-weakref@npm:^1.0.2, is-weakref@npm:^1.1.0": - version: 1.1.1 - resolution: "is-weakref@npm:1.1.1" - dependencies: - call-bound: "npm:^1.0.3" - checksum: 10/543506fd8259038b371bb083aac25b16cb4fd8b12fc58053aa3d45ac28dfd001cd5c6dffbba7aeea4213c74732d46b6cb2cfb5b412eed11f2db524f3f97d09a0 - languageName: node - linkType: hard - -"is-weakset@npm:^2.0.3": - version: 2.0.4 - resolution: "is-weakset@npm:2.0.4" - dependencies: - call-bound: "npm:^1.0.3" - get-intrinsic: "npm:^1.2.6" - checksum: 10/1d5e1d0179beeed3661125a6faa2e59bfb48afda06fc70db807f178aa0ebebc3758fb6358d76b3d528090d5ef85148c345dcfbf90839592fe293e3e5e82f2134 - languageName: node - linkType: hard - -"is-wsl@npm:^2.1.1, is-wsl@npm:^2.2.0": - version: 2.2.0 - resolution: "is-wsl@npm:2.2.0" - dependencies: - is-docker: "npm:^2.0.0" - checksum: 10/20849846ae414997d290b75e16868e5261e86ff5047f104027026fd61d8b5a9b0b3ade16239f35e1a067b3c7cc02f70183cb661010ed16f4b6c7c93dad1b19d8 - languageName: node - linkType: hard - -"isarray@npm:^2.0.5": - version: 2.0.5 - resolution: "isarray@npm:2.0.5" - checksum: 10/1d8bc7911e13bb9f105b1b3e0b396c787a9e63046af0b8fe0ab1414488ab06b2b099b87a2d8a9e31d21c9a6fad773c7fc8b257c4880f2d957274479d28ca3414 - languageName: node - linkType: hard - -"isexe@npm:^2.0.0": - version: 2.0.0 - resolution: "isexe@npm:2.0.0" - checksum: 10/7c9f715c03aff08f35e98b1fadae1b9267b38f0615d501824f9743f3aab99ef10e303ce7db3f186763a0b70a19de5791ebfc854ff884d5a8c4d92211f642ec92 - languageName: node - linkType: hard - -"jake@npm:^10.8.5": - version: 10.9.2 - resolution: "jake@npm:10.9.2" - dependencies: - async: "npm:^3.2.3" - chalk: "npm:^4.0.2" - filelist: "npm:^1.0.4" - minimatch: "npm:^3.1.2" - bin: - jake: bin/cli.js - checksum: 10/3be324708f99f031e0aec49ef8fd872eb4583cbe8a29a0c875f554f6ac638ee4ea5aa759bb63723fd54f77ca6d7db851eaa78353301734ed3700db9cb109a0cd - languageName: node - linkType: hard - -"java-properties@npm:^1.0.0": - version: 1.0.2 - resolution: "java-properties@npm:1.0.2" - checksum: 10/d6e8bf8a28a8782afadbcebf2504ab8ea2c75d3675d7eec470920f6c056fd90c8a35a2705cd492a07ec3b2309d3d848ff4cfae098a2cda33a922153eed4bef6a - languageName: node - linkType: hard - -"js-tokens@npm:^4.0.0": - version: 4.0.0 - resolution: "js-tokens@npm:4.0.0" - checksum: 10/af37d0d913fb56aec6dc0074c163cc71cd23c0b8aad5c2350747b6721d37ba118af35abdd8b33c47ec2800de07dedb16a527ca9c530ee004093e04958bd0cbf2 - languageName: node - linkType: hard - -"js-yaml@npm:^3.13.1, js-yaml@npm:^3.14.1": - version: 3.14.1 - resolution: "js-yaml@npm:3.14.1" - dependencies: - argparse: "npm:^1.0.7" - esprima: "npm:^4.0.0" - bin: - js-yaml: bin/js-yaml.js - checksum: 10/9e22d80b4d0105b9899135365f746d47466ed53ef4223c529b3c0f7a39907743fdbd3c4379f94f1106f02755b5e90b2faaf84801a891135544e1ea475d1a1379 - languageName: node - linkType: hard - -"jsesc@npm:^2.5.1": - version: 2.5.2 - resolution: "jsesc@npm:2.5.2" - bin: - jsesc: bin/jsesc - checksum: 10/d2096abdcdec56969764b40ffc91d4a23408aa2f351b4d1c13f736f25476643238c43fdbaf38a191c26b1b78fd856d965f5d4d0dde7b89459cd94025190cdf13 - languageName: node - linkType: hard - -"json-parse-better-errors@npm:^1.0.1": - version: 1.0.2 - resolution: "json-parse-better-errors@npm:1.0.2" - checksum: 10/5553232045359b767b0f2039a6777fede1a8d7dca1a0ffb1f9ef73a7519489ae7f566b2e040f2b4c38edb8e35e37ae07af7f0a52420902f869ee0dbf5dc6c784 - languageName: node - linkType: hard - -"json-parse-even-better-errors@npm:^2.3.0": - version: 2.3.1 - resolution: "json-parse-even-better-errors@npm:2.3.1" - checksum: 10/5f3a99009ed5f2a5a67d06e2f298cc97bc86d462034173308156f15b43a6e850be8511dc204b9b94566305da2947f7d90289657237d210351a39059ff9d666cf - languageName: node - linkType: hard - -"json-schema-traverse@npm:^1.0.0": - version: 1.0.0 - resolution: "json-schema-traverse@npm:1.0.0" - checksum: 10/02f2f466cdb0362558b2f1fd5e15cce82ef55d60cd7f8fa828cf35ba74330f8d767fcae5c5c2adb7851fa811766c694b9405810879bc4e1ddd78a7c0e03658ad - languageName: node - linkType: hard - -"json-stringify-safe@npm:^5.0.1": - version: 5.0.1 - resolution: "json-stringify-safe@npm:5.0.1" - checksum: 10/59169a081e4eeb6f9559ae1f938f656191c000e0512aa6df9f3c8b2437a4ab1823819c6b9fd1818a4e39593ccfd72e9a051fdd3e2d1e340ed913679e888ded8c - languageName: node - linkType: hard - -"jsonfile@npm:^4.0.0": - version: 4.0.0 - resolution: "jsonfile@npm:4.0.0" - dependencies: - graceful-fs: "npm:^4.1.6" - dependenciesMeta: - graceful-fs: - optional: true - checksum: 10/17796f0ab1be8479827d3683433f97ebe0a1c6932c3360fa40348eac36904d69269aab26f8b16da311882d94b42e9208e8b28e490bf926364f3ac9bff134c226 - languageName: node - linkType: hard - -"jsonfile@npm:^6.0.1": - version: 6.1.0 - resolution: "jsonfile@npm:6.1.0" - dependencies: - graceful-fs: "npm:^4.1.6" - universalify: "npm:^2.0.0" - dependenciesMeta: - graceful-fs: - optional: true - checksum: 10/03014769e7dc77d4cf05fa0b534907270b60890085dd5e4d60a382ff09328580651da0b8b4cdf44d91e4c8ae64d91791d965f05707beff000ed494a38b6fec85 - languageName: node - linkType: hard - -"lines-and-columns@npm:^1.1.6": - version: 1.2.4 - resolution: "lines-and-columns@npm:1.2.4" - checksum: 10/0c37f9f7fa212b38912b7145e1cd16a5f3cd34d782441c3e6ca653485d326f58b3caccda66efce1c5812bde4961bbde3374fae4b0d11bf1226152337f3894aa5 - languageName: node - linkType: hard - -"listr-silent-renderer@npm:^1.1.1": - version: 1.1.1 - resolution: "listr-silent-renderer@npm:1.1.1" - checksum: 10/81982612e4d207be2e69c4dcf2a6e0aaa6080e41bfe0b73e8d0b040dcdb79874248b1040558793a2f0fcc9c2252ec8af47379650f59bf2a7656c11cd5a48c948 - languageName: node - linkType: hard - -"listr-update-renderer@npm:^0.5.0": - version: 0.5.0 - resolution: "listr-update-renderer@npm:0.5.0" - dependencies: - chalk: "npm:^1.1.3" - cli-truncate: "npm:^0.2.1" - elegant-spinner: "npm:^1.0.1" - figures: "npm:^1.7.0" - indent-string: "npm:^3.0.0" - log-symbols: "npm:^1.0.2" - log-update: "npm:^2.3.0" - strip-ansi: "npm:^3.0.1" - peerDependencies: - listr: ^0.14.2 - checksum: 10/2dddc763837a9086a684545ee9049fcb102d423b0c840ad929471ab461075ed78d5c79f1e8334cd7a76aa9076e7631c04a38733bb4d88c23ca6082c087335864 - languageName: node - linkType: hard - -"listr-verbose-renderer@npm:^0.5.0": - version: 0.5.0 - resolution: "listr-verbose-renderer@npm:0.5.0" - dependencies: - chalk: "npm:^2.4.1" - cli-cursor: "npm:^2.1.0" - date-fns: "npm:^1.27.2" - figures: "npm:^2.0.0" - checksum: 10/3e504be729f9dd15b40db743e403673b76331774411dbc29d6f48136f6ba8bc1dee645a4e621c1cb781e6e69a58b78cb9aa8c153c7ceccfe4e4ea74d563bca3a - languageName: node - linkType: hard - -"listr@npm:0.14.3": - version: 0.14.3 - resolution: "listr@npm:0.14.3" - dependencies: - "@samverschueren/stream-to-observable": "npm:^0.3.0" - is-observable: "npm:^1.1.0" - is-promise: "npm:^2.1.0" - is-stream: "npm:^1.1.0" - listr-silent-renderer: "npm:^1.1.1" - listr-update-renderer: "npm:^0.5.0" - listr-verbose-renderer: "npm:^0.5.0" - p-map: "npm:^2.0.0" - rxjs: "npm:^6.3.3" - checksum: 10/6d5dc899c62b240bd28a22c26e88cf005696786a28e7239adbe044fd9ebcb5261b1503a555c8ba7f45b10d0eabb5d159c91791daee83d803b4caf64fd8adbdf9 - languageName: node - linkType: hard - -"load-json-file@npm:^5.3.0": - version: 5.3.0 - resolution: "load-json-file@npm:5.3.0" - dependencies: - graceful-fs: "npm:^4.1.15" - parse-json: "npm:^4.0.0" - pify: "npm:^4.0.1" - strip-bom: "npm:^3.0.0" - type-fest: "npm:^0.3.0" - checksum: 10/8bf15599db9471e264d916f98f1f51eb5d1e6a26d0ec3711d17df54d5983ccba1a0a4db2a6490bb27171f1261b72bf237d557f34e87d26e724472b92bdbdd4f7 - languageName: node - linkType: hard - -"lodash.debounce@npm:^4.0.8": - version: 4.0.8 - resolution: "lodash.debounce@npm:4.0.8" - checksum: 10/cd0b2819786e6e80cb9f5cda26b1a8fc073daaf04e48d4cb462fa4663ec9adb3a5387aa22d7129e48eed1afa05b482e2a6b79bfc99b86886364449500cbb00fd - languageName: node - linkType: hard - -"lodash.get@npm:^4": - version: 4.4.2 - resolution: "lodash.get@npm:4.4.2" - checksum: 10/2a4925f6e89bc2c010a77a802d1ba357e17ed1ea03c2ddf6a146429f2856a216663e694a6aa3549a318cbbba3fd8b7decb392db457e6ac0b83dc745ed0a17380 - languageName: node - linkType: hard - -"lodash.identity@npm:3.0.0": - version: 3.0.0 - resolution: "lodash.identity@npm:3.0.0" - checksum: 10/cd0365f99cafba23237e7c7d33123ac670ebbaacb4e9213fdddb7a68f317a261bedea93c9b57500942bbccb3fa2dc4e42a388c6024cc00a18937a922dacd68a1 - languageName: node - linkType: hard - -"lodash.merge@npm:^4.6.1": - version: 4.6.2 - resolution: "lodash.merge@npm:4.6.2" - checksum: 10/d0ea2dd0097e6201be083865d50c3fb54fbfbdb247d9cc5950e086c991f448b7ab0cdab0d57eacccb43473d3f2acd21e134db39f22dac2d6c9ba6bf26978e3d6 - languageName: node - linkType: hard - -"lodash.pickby@npm:4.6.0": - version: 4.6.0 - resolution: "lodash.pickby@npm:4.6.0" - checksum: 10/694ec55e0df6bb683e84606eae87f69302b49c28b50988264b2f2f333bd9f008e91c2092f1c14e770f0398b2fa2a2e8d19efb6c4c54b40a8e7951ed2929fd1ba - languageName: node - linkType: hard - -"lodash.sortby@npm:^4.7.0": - version: 4.7.0 - resolution: "lodash.sortby@npm:4.7.0" - checksum: 10/38df19ae28608af2c50ac342fc1f414508309d53e1d58ed9adfb2c3cd17c3af290058c0a0478028d932c5404df3d53349d19fa364ef6bed6145a6bc21320399e - languageName: node - linkType: hard - -"lodash.truncate@npm:^4.4.2": - version: 4.4.2 - resolution: "lodash.truncate@npm:4.4.2" - checksum: 10/7a495616121449e5d2288c606b1025d42ab9979e8c93ba885e5c5802ffd4f1ebad4428c793ccc12f73e73237e85a9f5b67dd6415757546fbd5a4653ba83e25ac - languageName: node - linkType: hard - -"lodash.xorby@npm:^4.7.0": - version: 4.7.0 - resolution: "lodash.xorby@npm:4.7.0" - checksum: 10/6005e6ee52166d7ff17c00c68fd247729e768400a57b04bd8eb01f8e25c73399c35ee6ac6857438fe0f3697385f975284c62d79a540f838b7bb745c58ac830e0 - languageName: node - linkType: hard - -"lodash@npm:^4.17.21": - version: 4.17.21 - resolution: "lodash@npm:4.17.21" - checksum: 10/c08619c038846ea6ac754abd6dd29d2568aa705feb69339e836dfa8d8b09abbb2f859371e86863eda41848221f9af43714491467b5b0299122431e202bb0c532 - languageName: node - linkType: hard - -"log-symbols@npm:^1.0.2": - version: 1.0.2 - resolution: "log-symbols@npm:1.0.2" - dependencies: - chalk: "npm:^1.0.0" - checksum: 10/5214ade9381db5d40528c171fdfd459b75cad7040eb6a347294ae47fa80cfebba4adbc3aa73a1c9da744cbfa240dd93b38f80df8615717affeea6c4bb6b8dfe7 - languageName: node - linkType: hard - -"log-update@npm:^2.3.0": - version: 2.3.0 - resolution: "log-update@npm:2.3.0" - dependencies: - ansi-escapes: "npm:^3.0.0" - cli-cursor: "npm:^2.0.0" - wrap-ansi: "npm:^3.0.1" - checksum: 10/84fd8e93bfc316eb6ca479a37743f2edcb7563fe5b9161205ce2980f0b3c822717b8f8f1871369697fcb0208521d7b8d00750c594edc3f8a8273dd8b48dd14a3 - languageName: node - linkType: hard - -"lower-case@npm:^2.0.2": - version: 2.0.2 - resolution: "lower-case@npm:2.0.2" - dependencies: - tslib: "npm:^2.0.3" - checksum: 10/83a0a5f159ad7614bee8bf976b96275f3954335a84fad2696927f609ddae902802c4f3312d86668722e668bef41400254807e1d3a7f2e8c3eede79691aa1f010 - languageName: node - linkType: hard - -"lru-cache@npm:7.10.1 - 7.13.1": - version: 7.13.1 - resolution: "lru-cache@npm:7.13.1" - checksum: 10/81ebb3f1fd3e1d3c32762a58c5964364220fc4b413f5180588b74473bd9a075cdafee32421f8ee6105148c8986d183bf40539017dea03abed32f4e1e59bf2654 - languageName: node - linkType: hard - -"make-error@npm:^1, make-error@npm:^1.1.1": - version: 1.3.6 - resolution: "make-error@npm:1.3.6" - checksum: 10/b86e5e0e25f7f777b77fabd8e2cbf15737972869d852a22b7e73c17623928fccb826d8e46b9951501d3f20e51ad74ba8c59ed584f610526a48f8ccf88aaec402 - languageName: node - linkType: hard - -"matcher@npm:^3.0.0": - version: 3.0.0 - resolution: "matcher@npm:3.0.0" - dependencies: - escape-string-regexp: "npm:^4.0.0" - checksum: 10/8bee1a7ab7609c2c21d9c9254b6785fa708eadf289032b556d57a34e98fcd4c537659a004dafee6ce80ab157099e645c199dc52678dff1e7fb0a6684e0da4dbe - languageName: node - linkType: hard - -"math-intrinsics@npm:^1.1.0": - version: 1.1.0 - resolution: "math-intrinsics@npm:1.1.0" - checksum: 10/11df2eda46d092a6035479632e1ec865b8134bdfc4bd9e571a656f4191525404f13a283a515938c3a8de934dbfd9c09674d9da9fa831e6eb7e22b50b197d2edd - languageName: node - linkType: hard - -"merge-stream@npm:^2.0.0": - version: 2.0.0 - resolution: "merge-stream@npm:2.0.0" - checksum: 10/6fa4dcc8d86629705cea944a4b88ef4cb0e07656ebf223fa287443256414283dd25d91c1cd84c77987f2aec5927af1a9db6085757cb43d90eb170ebf4b47f4f4 - languageName: node - linkType: hard - -"merge2@npm:^1.3.0, merge2@npm:^1.4.1": - version: 1.4.1 - resolution: "merge2@npm:1.4.1" - checksum: 10/7268db63ed5169466540b6fb947aec313200bcf6d40c5ab722c22e242f651994619bcd85601602972d3c85bd2cc45a358a4c61937e9f11a061919a1da569b0c2 - languageName: node - linkType: hard - -"micromatch@npm:^4.0.8": - version: 4.0.8 - resolution: "micromatch@npm:4.0.8" - dependencies: - braces: "npm:^3.0.3" - picomatch: "npm:^2.3.1" - checksum: 10/6bf2a01672e7965eb9941d1f02044fad2bd12486b5553dc1116ff24c09a8723157601dc992e74c911d896175918448762df3b3fd0a6b61037dd1a9766ddfbf58 - languageName: node - linkType: hard - -"mime-db@npm:1.52.0": - version: 1.52.0 - resolution: "mime-db@npm:1.52.0" - checksum: 10/54bb60bf39e6f8689f6622784e668a3d7f8bed6b0d886f5c3c446cb3284be28b30bf707ed05d0fe44a036f8469976b2629bbea182684977b084de9da274694d7 - languageName: node - linkType: hard - -"mime-types@npm:^2.1.12": - version: 2.1.35 - resolution: "mime-types@npm:2.1.35" - dependencies: - mime-db: "npm:1.52.0" - checksum: 10/89aa9651b67644035de2784a6e665fc685d79aba61857e02b9c8758da874a754aed4a9aced9265f5ed1171fd934331e5516b84a7f0218031b6fa0270eca1e51a - languageName: node - linkType: hard - -"mimic-fn@npm:^1.0.0": - version: 1.2.0 - resolution: "mimic-fn@npm:1.2.0" - checksum: 10/69c08205156a1f4906d9c46f9b4dc08d18a50176352e77fdeb645cedfe9f20c0b19865d465bd2dec27a5c432347f24dc07fc3695e11159d193f892834233e939 - languageName: node - linkType: hard - -"mimic-fn@npm:^2.1.0": - version: 2.1.0 - resolution: "mimic-fn@npm:2.1.0" - checksum: 10/d2421a3444848ce7f84bd49115ddacff29c15745db73f54041edc906c14b131a38d05298dae3081667627a59b2eb1ca4b436ff2e1b80f69679522410418b478a - languageName: node - linkType: hard - -"minimatch@npm:^3.0.4, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": - version: 3.1.2 - resolution: "minimatch@npm:3.1.2" - dependencies: - brace-expansion: "npm:^1.1.7" - checksum: 10/e0b25b04cd4ec6732830344e5739b13f8690f8a012d73445a4a19fbc623f5dd481ef7a5827fde25954cd6026fede7574cc54dc4643c99d6c6b653d6203f94634 - languageName: node - linkType: hard - -"minimatch@npm:^5.0.0, minimatch@npm:^5.0.1": - version: 5.1.6 - resolution: "minimatch@npm:5.1.6" - dependencies: - brace-expansion: "npm:^2.0.1" - checksum: 10/126b36485b821daf96d33b5c821dac600cc1ab36c87e7a532594f9b1652b1fa89a1eebcaad4dff17c764dce1a7ac1531327f190fed5f97d8f6e5f889c116c429 - languageName: node - linkType: hard - -"minimatch@npm:~3.0.2": - version: 3.0.8 - resolution: "minimatch@npm:3.0.8" - dependencies: - brace-expansion: "npm:^1.1.7" - checksum: 10/6df5373cb1ea79020beb6887ff5576c58cfabcfd32c5a65c2cf58f326e4ee8eae84f129e5fa50b8a4347fa1d1e583f931285c9fb3040d984bdfb5109ef6607ec - languageName: node - linkType: hard - -"mkdirp@npm:1.0.4": - version: 1.0.4 - resolution: "mkdirp@npm:1.0.4" - bin: - mkdirp: bin/cmd.js - checksum: 10/d71b8dcd4b5af2fe13ecf3bd24070263489404fe216488c5ba7e38ece1f54daf219e72a833a3a2dc404331e870e9f44963a33399589490956bff003a3404d3b2 - languageName: node - linkType: hard - -"moment@npm:2.29.3": - version: 2.29.3 - resolution: "moment@npm:2.29.3" - checksum: 10/311a26f7e476fb502f902a45b1c91aabc1b866ff8398bc90909135e6f312562eedd4a2287c28d2f7c3dda63e848882a1b505596dd76929b49b9d0176a5e141eb - languageName: node - linkType: hard - -"ms@npm:^2.1.3": - version: 2.1.3 - resolution: "ms@npm:2.1.3" - checksum: 10/aa92de608021b242401676e35cfa5aa42dd70cbdc082b916da7fb925c542173e36bce97ea3e804923fe92c0ad991434e4a38327e15a1b5b5f945d66df615ae6d - languageName: node - linkType: hard - -"natural-orderby@npm:^2.0.1, natural-orderby@npm:^2.0.3": - version: 2.0.3 - resolution: "natural-orderby@npm:2.0.3" - checksum: 10/b0c982709cab2133e65ab2ca9c44cdbca972b5d72886a75fa3afac159b736a586e582e5de46bd04d0f3d5ab6f9d0447e6a5a8c4392de017ac67e6638b317e4aa - languageName: node - linkType: hard - -"no-case@npm:^3.0.4": - version: 3.0.4 - resolution: "no-case@npm:3.0.4" - dependencies: - lower-case: "npm:^2.0.2" - tslib: "npm:^2.0.3" - checksum: 10/0b2ebc113dfcf737d48dde49cfebf3ad2d82a8c3188e7100c6f375e30eafbef9e9124aadc3becef237b042fd5eb0aad2fd78669c20972d045bbe7fea8ba0be5c - languageName: node - linkType: hard - -"node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.7": - version: 2.7.0 - resolution: "node-fetch@npm:2.7.0" - dependencies: - whatwg-url: "npm:^5.0.0" - peerDependencies: - encoding: ^0.1.0 - peerDependenciesMeta: - encoding: - optional: true - checksum: 10/b24f8a3dc937f388192e59bcf9d0857d7b6940a2496f328381641cb616efccc9866e89ec43f2ec956bbd6c3d3ee05524ce77fe7b29ccd34692b3a16f237d6676 - languageName: node - linkType: hard - -"normalize-url@npm:^6.1.0": - version: 6.1.0 - resolution: "normalize-url@npm:6.1.0" - checksum: 10/5ae699402c9d5ffa330adc348fcd6fc6e6a155ab7c811b96e30b7ecab60ceef821d8f86443869671dda71bbc47f4b9625739c82ad247e883e9aefe875bfb8659 - languageName: node - linkType: hard - -"npm-run-path@npm:^4.0.1": - version: 4.0.1 - resolution: "npm-run-path@npm:4.0.1" - dependencies: - path-key: "npm:^3.0.0" - checksum: 10/5374c0cea4b0bbfdfae62da7bbdf1e1558d338335f4cacf2515c282ff358ff27b2ecb91ffa5330a8b14390ac66a1e146e10700440c1ab868208430f56b5f4d23 - languageName: node - linkType: hard - -"number-is-nan@npm:^1.0.0": - version: 1.0.1 - resolution: "number-is-nan@npm:1.0.1" - checksum: 10/13656bc9aa771b96cef209ffca31c31a03b507ca6862ba7c3f638a283560620d723d52e626d57892c7fff475f4c36ac07f0600f14544692ff595abff214b9ffb - languageName: node - linkType: hard - -"object-assign@npm:^4.1.0": - version: 4.1.1 - resolution: "object-assign@npm:4.1.1" - checksum: 10/fcc6e4ea8c7fe48abfbb552578b1c53e0d194086e2e6bbbf59e0a536381a292f39943c6e9628af05b5528aa5e3318bb30d6b2e53cadaf5b8fe9e12c4b69af23f - languageName: node - linkType: hard - -"object-inspect@npm:^1.13.3": - version: 1.13.4 - resolution: "object-inspect@npm:1.13.4" - checksum: 10/aa13b1190ad3e366f6c83ad8a16ed37a19ed57d267385aa4bfdccda833d7b90465c057ff6c55d035a6b2e52c1a2295582b294217a0a3a1ae7abdd6877ef781fb - languageName: node - linkType: hard - -"object-keys@npm:^1.1.1": - version: 1.1.1 - resolution: "object-keys@npm:1.1.1" - checksum: 10/3d81d02674115973df0b7117628ea4110d56042e5326413e4b4313f0bcdf7dd78d4a3acef2c831463fa3796a66762c49daef306f4a0ea1af44877d7086d73bde - languageName: node - linkType: hard - -"object-treeify@npm:^1.1.33, object-treeify@npm:^1.1.4": - version: 1.1.33 - resolution: "object-treeify@npm:1.1.33" - checksum: 10/1c7865240037d7c2d39e28b96598538af59b545dc49cfc45d8c0a96baa343fc3335cbef26ede8c6dc48073368ec16bf194c276ffdedf32b41f3c3c8ef4d27fef - languageName: node - linkType: hard - -"object.assign@npm:^4.1.7": - version: 4.1.7 - resolution: "object.assign@npm:4.1.7" - dependencies: - call-bind: "npm:^1.0.8" - call-bound: "npm:^1.0.3" - define-properties: "npm:^1.2.1" - es-object-atoms: "npm:^1.0.0" - has-symbols: "npm:^1.1.0" - object-keys: "npm:^1.1.1" - checksum: 10/3fe28cdd779f2a728a9a66bd688679ba231a2b16646cd1e46b528fe7c947494387dda4bc189eff3417f3717ef4f0a8f2439347cf9a9aa3cef722fbfd9f615587 - languageName: node - linkType: hard - -"object.getownpropertydescriptors@npm:^2.1.1": - version: 2.1.8 - resolution: "object.getownpropertydescriptors@npm:2.1.8" - dependencies: - array.prototype.reduce: "npm:^1.0.6" - call-bind: "npm:^1.0.7" - define-properties: "npm:^1.2.1" - es-abstract: "npm:^1.23.2" - es-object-atoms: "npm:^1.0.0" - gopd: "npm:^1.0.1" - safe-array-concat: "npm:^1.1.2" - checksum: 10/8c50f52e0d702d30836f3d2772ba02807ca25a5381be6f9470c6d143ee0bad01bce3fff0fedea2bdbc0c9297e4eb7785ffee5739f6a3a7c60fcd622b42f8a9fb - languageName: node - linkType: hard - -"once@npm:^1.3.0": - version: 1.4.0 - resolution: "once@npm:1.4.0" - dependencies: - wrappy: "npm:1" - checksum: 10/cd0a88501333edd640d95f0d2700fbde6bff20b3d4d9bdc521bdd31af0656b5706570d6c6afe532045a20bb8dc0849f8332d6f2a416e0ba6d3d3b98806c7db68 - languageName: node - linkType: hard - -"onetime@npm:^2.0.0": - version: 2.0.1 - resolution: "onetime@npm:2.0.1" - dependencies: - mimic-fn: "npm:^1.0.0" - checksum: 10/5b4f6079e6b4973244017e157833ab5a7a3de4bd2612d69411e3ee46f61fe8bb57b7c2e243b0b23dbaa5bad7641a15f9100a5c80295ff64c0d87aab5d1576ef9 - languageName: node - linkType: hard - -"onetime@npm:^5.1.2": - version: 5.1.2 - resolution: "onetime@npm:5.1.2" - dependencies: - mimic-fn: "npm:^2.1.0" - checksum: 10/e9fd0695a01cf226652f0385bf16b7a24153dbbb2039f764c8ba6d2306a8506b0e4ce570de6ad99c7a6eb49520743afdb66edd95ee979c1a342554ed49a9aadd - languageName: node - linkType: hard - -"own-keys@npm:^1.0.1": - version: 1.0.1 - resolution: "own-keys@npm:1.0.1" - dependencies: - get-intrinsic: "npm:^1.2.6" - object-keys: "npm:^1.1.1" - safe-push-apply: "npm:^1.0.0" - checksum: 10/ab4bb3b8636908554fc19bf899e225444195092864cb61503a0d048fdaf662b04be2605b636a4ffeaf6e8811f6fcfa8cbb210ec964c0eb1a41eb853e1d5d2f41 - languageName: node - linkType: hard - -"p-map@npm:^2.0.0": - version: 2.1.0 - resolution: "p-map@npm:2.1.0" - checksum: 10/9e3ad3c9f6d75a5b5661bcad78c91f3a63849189737cd75e4f1225bf9ac205194e5c44aac2ef6f09562b1facdb9bd1425584d7ac375bfaa17b3f1a142dab936d - languageName: node - linkType: hard - -"param-case@npm:^3.0.4": - version: 3.0.4 - resolution: "param-case@npm:3.0.4" - dependencies: - dot-case: "npm:^3.0.4" - tslib: "npm:^2.0.3" - checksum: 10/b34227fd0f794e078776eb3aa6247442056cb47761e9cd2c4c881c86d84c64205f6a56ef0d70b41ee7d77da02c3f4ed2f88e3896a8fefe08bdfb4deca037c687 - languageName: node - linkType: hard - -"parent-module@npm:^1.0.0": - version: 1.0.1 - resolution: "parent-module@npm:1.0.1" - dependencies: - callsites: "npm:^3.0.0" - checksum: 10/6ba8b255145cae9470cf5551eb74be2d22281587af787a2626683a6c20fbb464978784661478dd2a3f1dad74d1e802d403e1b03c1a31fab310259eec8ac560ff - languageName: node - linkType: hard - -"parse-json@npm:^4.0.0": - version: 4.0.0 - resolution: "parse-json@npm:4.0.0" - dependencies: - error-ex: "npm:^1.3.1" - json-parse-better-errors: "npm:^1.0.1" - checksum: 10/0fe227d410a61090c247e34fa210552b834613c006c2c64d9a05cfe9e89cf8b4246d1246b1a99524b53b313e9ac024438d0680f67e33eaed7e6f38db64cfe7b5 - languageName: node - linkType: hard - -"parse-json@npm:^5.0.0": - version: 5.2.0 - resolution: "parse-json@npm:5.2.0" - dependencies: - "@babel/code-frame": "npm:^7.0.0" - error-ex: "npm:^1.3.1" - json-parse-even-better-errors: "npm:^2.3.0" - lines-and-columns: "npm:^1.1.6" - checksum: 10/62085b17d64da57f40f6afc2ac1f4d95def18c4323577e1eced571db75d9ab59b297d1d10582920f84b15985cbfc6b6d450ccbf317644cfa176f3ed982ad87e2 - languageName: node - linkType: hard - -"parse-path@npm:^4.0.0": - version: 4.0.4 - resolution: "parse-path@npm:4.0.4" - dependencies: - is-ssh: "npm:^1.3.0" - protocols: "npm:^1.4.0" - qs: "npm:^6.9.4" - query-string: "npm:^6.13.8" - checksum: 10/fe07fe22fc0a19ce40168fc1cd5fd4f74bff7065c805a57c20d8ff96640dc91dc126d6a7e37ec4fd043edda797a72b1a494a574cc35704e92e9d0007cdd8628d - languageName: node - linkType: hard - -"parse-url@npm:^6.0.0": - version: 6.0.5 - resolution: "parse-url@npm:6.0.5" - dependencies: - is-ssh: "npm:^1.3.0" - normalize-url: "npm:^6.1.0" - parse-path: "npm:^4.0.0" - protocols: "npm:^1.4.0" - checksum: 10/f79a6da2d7044b6723fee0db823436266c4ccc28da501987fa00c552b654a126faf5f32c2e69453cab2ae5557f5d44afffaf2eaebef45e6863c14a9c2d3e703b - languageName: node - linkType: hard - -"pascal-case@npm:^3.1.2": - version: 3.1.2 - resolution: "pascal-case@npm:3.1.2" - dependencies: - no-case: "npm:^3.0.4" - tslib: "npm:^2.0.3" - checksum: 10/ba98bfd595fc91ef3d30f4243b1aee2f6ec41c53b4546bfa3039487c367abaa182471dcfc830a1f9e1a0df00c14a370514fa2b3a1aacc68b15a460c31116873e - languageName: node - linkType: hard - -"password-prompt@npm:^1.1.2": - version: 1.1.3 - resolution: "password-prompt@npm:1.1.3" - dependencies: - ansi-escapes: "npm:^4.3.2" - cross-spawn: "npm:^7.0.3" - checksum: 10/1cf7001e66868b2ed7a03e036bc2f1dd45eb6dc8fee7e3e2056370057c484be25e7468fee00a1378e1ee8eca77ba79f48bee5ce15dcb464413987ace63c68b35 - languageName: node - linkType: hard - -"path-case@npm:^3.0.4": - version: 3.0.4 - resolution: "path-case@npm:3.0.4" - dependencies: - dot-case: "npm:^3.0.4" - tslib: "npm:^2.0.3" - checksum: 10/61de0526222629f65038a66f63330dd22d5b54014ded6636283e1d15364da38b3cf29e4433aa3f9d8b0dba407ae2b059c23b0104a34ee789944b1bc1c5c7e06d - languageName: node - linkType: hard - -"path-is-absolute@npm:^1.0.0": - version: 1.0.1 - resolution: "path-is-absolute@npm:1.0.1" - checksum: 10/060840f92cf8effa293bcc1bea81281bd7d363731d214cbe5c227df207c34cd727430f70c6037b5159c8a870b9157cba65e775446b0ab06fd5ecc7e54615a3b8 - languageName: node - linkType: hard - -"path-key@npm:^3.0.0, path-key@npm:^3.1.0": - version: 3.1.1 - resolution: "path-key@npm:3.1.1" - checksum: 10/55cd7a9dd4b343412a8386a743f9c746ef196e57c823d90ca3ab917f90ab9f13dd0ded27252ba49dbdfcab2b091d998bc446f6220cd3cea65db407502a740020 - languageName: node - linkType: hard - -"path-parse@npm:^1.0.7": - version: 1.0.7 - resolution: "path-parse@npm:1.0.7" - checksum: 10/49abf3d81115642938a8700ec580da6e830dde670be21893c62f4e10bd7dd4c3742ddc603fe24f898cba7eb0c6bc1777f8d9ac14185d34540c6d4d80cd9cae8a - languageName: node - linkType: hard - -"path-type@npm:^4.0.0": - version: 4.0.0 - resolution: "path-type@npm:4.0.0" - checksum: 10/5b1e2daa247062061325b8fdbfd1fb56dde0a448fb1455453276ea18c60685bdad23a445dc148cf87bc216be1573357509b7d4060494a6fd768c7efad833ee45 - languageName: node - linkType: hard - -"picocolors@npm:^1.0.0": - version: 1.1.1 - resolution: "picocolors@npm:1.1.1" - checksum: 10/e1cf46bf84886c79055fdfa9dcb3e4711ad259949e3565154b004b260cd356c5d54b31a1437ce9782624bf766272fe6b0154f5f0c744fb7af5d454d2b60db045 - languageName: node - linkType: hard - -"picomatch@npm:^2.3.1": - version: 2.3.1 - resolution: "picomatch@npm:2.3.1" - checksum: 10/60c2595003b05e4535394d1da94850f5372c9427ca4413b71210f437f7b2ca091dbd611c45e8b37d10036fa8eade25c1b8951654f9d3973bfa66a2ff4d3b08bc - languageName: node - linkType: hard - -"pify@npm:^4.0.1": - version: 4.0.1 - resolution: "pify@npm:4.0.1" - checksum: 10/8b97cbf9dc6d4c1320cc238a2db0fc67547f9dc77011729ff353faf34f1936ea1a4d7f3c63b2f4980b253be77bcc72ea1e9e76ee3fd53cce2aafb6a8854d07ec - languageName: node - linkType: hard - -"possible-typed-array-names@npm:^1.0.0": - version: 1.1.0 - resolution: "possible-typed-array-names@npm:1.1.0" - checksum: 10/2f44137b8d3dd35f4a7ba7469eec1cd9cfbb46ec164b93a5bc1f4c3d68599c9910ee3b91da1d28b4560e9cc8414c3cd56fedc07259c67e52cc774476270d3302 - languageName: node - linkType: hard - -"protocols@npm:^1.4.0": - version: 1.4.8 - resolution: "protocols@npm:1.4.8" - checksum: 10/2d555c013df0b05402970f67f7207c9955a92b1d13ffa503c814b5fe2f6dde7ac6a03320e0975c1f5832b0113327865e0b3b28bfcad023c25ddb54b53fab8684 - languageName: node - linkType: hard - -"protocols@npm:^2.0.1": - version: 2.0.2 - resolution: "protocols@npm:2.0.2" - checksum: 10/031cc068eb800468a50eb7c1e1c528bf142fb8314f5df9b9ea3c3f9df1697a19f97b9915b1229cef694d156812393172d9c3051ef7878d26eaa8c6faa5cccec4 - languageName: node - linkType: hard - -"qs@npm:^6.9.4": - version: 6.14.0 - resolution: "qs@npm:6.14.0" - dependencies: - side-channel: "npm:^1.1.0" - checksum: 10/a60e49bbd51c935a8a4759e7505677b122e23bf392d6535b8fc31c1e447acba2c901235ecb192764013cd2781723dc1f61978b5fdd93cc31d7043d31cdc01974 - languageName: node - linkType: hard - -"query-string@npm:^6.13.8": - version: 6.14.1 - resolution: "query-string@npm:6.14.1" - dependencies: - decode-uri-component: "npm:^0.2.0" - filter-obj: "npm:^1.1.0" - split-on-first: "npm:^1.0.0" - strict-uri-encode: "npm:^2.0.0" - checksum: 10/95f5a372f777b4fb5bdae5a2d85961cf3894d466cfc3a0cc799320d5ed633af935c0d96ee5d2b1652c02888e749831409ca5dd5eb388ce1014a9074024a22840 - languageName: node - linkType: hard - -"queue-microtask@npm:^1.2.2": - version: 1.2.3 - resolution: "queue-microtask@npm:1.2.3" - checksum: 10/72900df0616e473e824202113c3df6abae59150dfb73ed13273503127235320e9c8ca4aaaaccfd58cf417c6ca92a6e68ee9a5c3182886ae949a768639b388a7b - languageName: node - linkType: hard - -"recast@npm:^0.21.0": - version: 0.21.5 - resolution: "recast@npm:0.21.5" - dependencies: - ast-types: "npm:0.15.2" - esprima: "npm:~4.0.0" - source-map: "npm:~0.6.1" - tslib: "npm:^2.0.1" - checksum: 10/b41da2bcf7e705511db2f27d17420ace027de8dd167de9f19190d4988a1f80d112f60c095101ac2f145c8657ddde0c5133eb71df20504efaf3fd9d76ad07e15d - languageName: node - linkType: hard - -"rechoir@npm:^0.6.2": - version: 0.6.2 - resolution: "rechoir@npm:0.6.2" - dependencies: - resolve: "npm:^1.1.6" - checksum: 10/fe76bf9c21875ac16e235defedd7cbd34f333c02a92546142b7911a0f7c7059d2e16f441fe6fb9ae203f459c05a31b2bcf26202896d89e390eda7514d5d2702b - languageName: node - linkType: hard - -"redeyed@npm:~2.1.0": - version: 2.1.1 - resolution: "redeyed@npm:2.1.1" - dependencies: - esprima: "npm:~4.0.0" - checksum: 10/86880f97d54bb55bbf1c338e27fe28f18f52afc2f5afa808354a09a3777aa79b4f04e04844350d7fec80aa2d299196bde256b21f586e7e5d9b63494bd4a9db27 - languageName: node - linkType: hard - -"reflect.getprototypeof@npm:^1.0.6, reflect.getprototypeof@npm:^1.0.9": - version: 1.0.10 - resolution: "reflect.getprototypeof@npm:1.0.10" - dependencies: - call-bind: "npm:^1.0.8" - define-properties: "npm:^1.2.1" - es-abstract: "npm:^1.23.9" - es-errors: "npm:^1.3.0" - es-object-atoms: "npm:^1.0.0" - get-intrinsic: "npm:^1.2.7" - get-proto: "npm:^1.0.1" - which-builtin-type: "npm:^1.2.1" - checksum: 10/80a4e2be716f4fe46a89a08ccad0863b47e8ce0f49616cab2d65dab0fbd53c6fdba0f52935fd41d37a2e4e22355c272004f920d63070de849f66eea7aeb4a081 - languageName: node - linkType: hard - -"regexp.prototype.flags@npm:^1.5.3": - version: 1.5.4 - resolution: "regexp.prototype.flags@npm:1.5.4" - dependencies: - call-bind: "npm:^1.0.8" - define-properties: "npm:^1.2.1" - es-errors: "npm:^1.3.0" - get-proto: "npm:^1.0.1" - gopd: "npm:^1.2.0" - set-function-name: "npm:^2.0.2" - checksum: 10/8ab897ca445968e0b96f6237641510f3243e59c180ee2ee8d83889c52ff735dd1bf3657fcd36db053e35e1d823dd53f2565d0b8021ea282c9fe62401c6c3bd6d - languageName: node - linkType: hard - -"require-from-string@npm:^2.0.2": - version: 2.0.2 - resolution: "require-from-string@npm:2.0.2" - checksum: 10/839a3a890102a658f4cb3e7b2aa13a1f80a3a976b512020c3d1efc418491c48a886b6e481ea56afc6c4cb5eef678f23b2a4e70575e7534eccadf5e30ed2e56eb - languageName: node - linkType: hard - -"resolve-from@npm:^4.0.0": - version: 4.0.0 - resolution: "resolve-from@npm:4.0.0" - checksum: 10/91eb76ce83621eea7bbdd9b55121a5c1c4a39e54a9ce04a9ad4517f102f8b5131c2cf07622c738a6683991bf54f2ce178f5a42803ecbd527ddc5105f362cc9e3 - languageName: node - linkType: hard - -"resolve@npm:^1.1.6": - version: 1.22.10 - resolution: "resolve@npm:1.22.10" - dependencies: - is-core-module: "npm:^2.16.0" - path-parse: "npm:^1.0.7" - supports-preserve-symlinks-flag: "npm:^1.0.0" - bin: - resolve: bin/resolve - checksum: 10/0a398b44da5c05e6e421d70108822c327675febb880eebe905587628de401854c61d5df02866ff34fc4cb1173a51c9f0e84a94702738df3611a62e2acdc68181 - languageName: node - linkType: hard - -"resolve@patch:resolve@npm%3A^1.1.6#optional!builtin": - version: 1.22.10 - resolution: "resolve@patch:resolve@npm%3A1.22.10#optional!builtin::version=1.22.10&hash=c3c19d" - dependencies: - is-core-module: "npm:^2.16.0" - path-parse: "npm:^1.0.7" - supports-preserve-symlinks-flag: "npm:^1.0.0" - bin: - resolve: bin/resolve - checksum: 10/d4d878bfe3702d215ea23e75e0e9caf99468e3db76f5ca100d27ebdc527366fee3877e54bce7d47cc72ca8952fc2782a070d238bfa79a550eeb0082384c3b81a - languageName: node - linkType: hard - -"restore-cursor@npm:^2.0.0": - version: 2.0.0 - resolution: "restore-cursor@npm:2.0.0" - dependencies: - onetime: "npm:^2.0.0" - signal-exit: "npm:^3.0.2" - checksum: 10/482e13d02d834b6e5e3aa90304a8b5e840775d6f06916cc92a50038adf9f098dcc72405b567da8a37e137ae40ad3e31896fa3136ae62f7a426c2fbf53d036536 - languageName: node - linkType: hard - -"reusify@npm:^1.0.4": - version: 1.1.0 - resolution: "reusify@npm:1.1.0" - checksum: 10/af47851b547e8a8dc89af144fceee17b80d5beaf5e6f57ed086432d79943434ff67ca526e92275be6f54b6189f6920a24eace75c2657eed32d02c400312b21ec - languageName: node - linkType: hard - -"roarr@npm:^2.15.3": - version: 2.15.4 - resolution: "roarr@npm:2.15.4" - dependencies: - boolean: "npm:^3.0.1" - detect-node: "npm:^2.0.4" - globalthis: "npm:^1.0.1" - json-stringify-safe: "npm:^5.0.1" - semver-compare: "npm:^1.0.0" - sprintf-js: "npm:^1.1.2" - checksum: 10/baaa5ad91468bf1b7f0263c4132a40865c8638a3d0916b44dd0d42980a77fb53085a3792e3edf16fc4eea9e31c719793c88bd45b1623b760763c4dc59df97619 - languageName: node - linkType: hard - -"root-workspace-0b6124@workspace:.": - version: 0.0.0-use.local - resolution: "root-workspace-0b6124@workspace:." - dependencies: - apollo: "npm:^2.34.0" - languageName: unknown - linkType: soft - -"run-parallel@npm:^1.1.9": - version: 1.2.0 - resolution: "run-parallel@npm:1.2.0" - dependencies: - queue-microtask: "npm:^1.2.2" - checksum: 10/cb4f97ad25a75ebc11a8ef4e33bb962f8af8516bb2001082ceabd8902e15b98f4b84b4f8a9b222e5d57fc3bd1379c483886ed4619367a7680dad65316993021d - languageName: node - linkType: hard - -"rxjs@npm:^6.3.3": - version: 6.6.7 - resolution: "rxjs@npm:6.6.7" - dependencies: - tslib: "npm:^1.9.0" - checksum: 10/c8263ebb20da80dd7a91c452b9e96a178331f402344bbb40bc772b56340fcd48d13d1f545a1e3d8e464893008c5e306cc42a1552afe0d562b1a6d4e1e6262b03 - languageName: node - linkType: hard - -"safe-array-concat@npm:^1.1.2, safe-array-concat@npm:^1.1.3": - version: 1.1.3 - resolution: "safe-array-concat@npm:1.1.3" - dependencies: - call-bind: "npm:^1.0.8" - call-bound: "npm:^1.0.2" - get-intrinsic: "npm:^1.2.6" - has-symbols: "npm:^1.1.0" - isarray: "npm:^2.0.5" - checksum: 10/fac4f40f20a3f7da024b54792fcc61059e814566dcbb04586bfefef4d3b942b2408933f25b7b3dd024affd3f2a6bbc916bef04807855e4f192413941369db864 - languageName: node - linkType: hard - -"safe-buffer@npm:^5.0.1": - version: 5.2.1 - resolution: "safe-buffer@npm:5.2.1" - checksum: 10/32872cd0ff68a3ddade7a7617b8f4c2ae8764d8b7d884c651b74457967a9e0e886267d3ecc781220629c44a865167b61c375d2da6c720c840ecd73f45d5d9451 - languageName: node - linkType: hard - -"safe-push-apply@npm:^1.0.0": - version: 1.0.0 - resolution: "safe-push-apply@npm:1.0.0" - dependencies: - es-errors: "npm:^1.3.0" - isarray: "npm:^2.0.5" - checksum: 10/2bd4e53b6694f7134b9cf93631480e7fafc8637165f0ee91d5a4af5e7f33d37de9562d1af5021178dd4217d0230cde8d6530fa28cfa1ebff9a431bf8fff124b4 - languageName: node - linkType: hard - -"safe-regex-test@npm:^1.1.0": - version: 1.1.0 - resolution: "safe-regex-test@npm:1.1.0" - dependencies: - call-bound: "npm:^1.0.2" - es-errors: "npm:^1.3.0" - is-regex: "npm:^1.2.1" - checksum: 10/ebdb61f305bf4756a5b023ad86067df5a11b26898573afe9e52a548a63c3bd594825d9b0e2dde2eb3c94e57e0e04ac9929d4107c394f7b8e56a4613bed46c69a - languageName: node - linkType: hard - -"semver-compare@npm:^1.0.0": - version: 1.0.0 - resolution: "semver-compare@npm:1.0.0" - checksum: 10/75f9c7a7786d1756f64b1429017746721e07bd7691bdad6368f7643885d3a98a27586777e9699456564f4844b407e9f186cc1d588a3f9c0be71310e517e942c3 - languageName: node - linkType: hard - -"semver@npm:^7.3.2, semver@npm:^7.3.7": - version: 7.7.1 - resolution: "semver@npm:7.7.1" - bin: - semver: bin/semver.js - checksum: 10/4cfa1eb91ef3751e20fc52e47a935a0118d56d6f15a837ab814da0c150778ba2ca4f1a4d9068b33070ea4273629e615066664c2cfcd7c272caf7a8a0f6518b2c - languageName: node - linkType: hard - -"sentence-case@npm:^3.0.4": - version: 3.0.4 - resolution: "sentence-case@npm:3.0.4" - dependencies: - no-case: "npm:^3.0.4" - tslib: "npm:^2.0.3" - upper-case-first: "npm:^2.0.2" - checksum: 10/3cfe6c0143e649132365695706702d7f729f484fa7b25f43435876efe7af2478243eefb052bacbcce10babf9319fd6b5b6bc59b94c80a1c819bcbb40651465d5 - languageName: node - linkType: hard - -"serialize-error@npm:^7.0.1": - version: 7.0.1 - resolution: "serialize-error@npm:7.0.1" - dependencies: - type-fest: "npm:^0.13.1" - checksum: 10/e0aba4dca2fc9fe74ae1baf38dbd99190e1945445a241ba646290f2176cdb2032281a76443b02ccf0caf30da5657d510746506368889a593b9835a497fc0732e - languageName: node - linkType: hard - -"set-function-length@npm:^1.2.2": - version: 1.2.2 - resolution: "set-function-length@npm:1.2.2" - dependencies: - define-data-property: "npm:^1.1.4" - es-errors: "npm:^1.3.0" - function-bind: "npm:^1.1.2" - get-intrinsic: "npm:^1.2.4" - gopd: "npm:^1.0.1" - has-property-descriptors: "npm:^1.0.2" - checksum: 10/505d62b8e088468917ca4e3f8f39d0e29f9a563b97dbebf92f4bd2c3172ccfb3c5b8e4566d5fcd00784a00433900e7cb8fbc404e2dbd8c3818ba05bb9d4a8a6d - languageName: node - linkType: hard - -"set-function-name@npm:^2.0.2": - version: 2.0.2 - resolution: "set-function-name@npm:2.0.2" - dependencies: - define-data-property: "npm:^1.1.4" - es-errors: "npm:^1.3.0" - functions-have-names: "npm:^1.2.3" - has-property-descriptors: "npm:^1.0.2" - checksum: 10/c7614154a53ebf8c0428a6c40a3b0b47dac30587c1a19703d1b75f003803f73cdfa6a93474a9ba678fa565ef5fbddc2fae79bca03b7d22ab5fd5163dbe571a74 - languageName: node - linkType: hard - -"set-proto@npm:^1.0.0": - version: 1.0.0 - resolution: "set-proto@npm:1.0.0" - dependencies: - dunder-proto: "npm:^1.0.1" - es-errors: "npm:^1.3.0" - es-object-atoms: "npm:^1.0.0" - checksum: 10/b87f8187bca595ddc3c0721ece4635015fd9d7cb294e6dd2e394ce5186a71bbfa4dc8a35010958c65e43ad83cde09642660e61a952883c24fd6b45ead15f045c - languageName: node - linkType: hard - -"sha.js@npm:^2.4.11": - version: 2.4.11 - resolution: "sha.js@npm:2.4.11" - dependencies: - inherits: "npm:^2.0.1" - safe-buffer: "npm:^5.0.1" - bin: - sha.js: ./bin.js - checksum: 10/d833bfa3e0a67579a6ce6e1bc95571f05246e0a441dd8c76e3057972f2a3e098465687a4369b07e83a0375a88703577f71b5b2e966809e67ebc340dbedb478c7 - languageName: node - linkType: hard - -"shebang-command@npm:^2.0.0": - version: 2.0.0 - resolution: "shebang-command@npm:2.0.0" - dependencies: - shebang-regex: "npm:^3.0.0" - checksum: 10/6b52fe87271c12968f6a054e60f6bde5f0f3d2db483a1e5c3e12d657c488a15474121a1d55cd958f6df026a54374ec38a4a963988c213b7570e1d51575cea7fa - languageName: node - linkType: hard - -"shebang-regex@npm:^3.0.0": - version: 3.0.0 - resolution: "shebang-regex@npm:3.0.0" - checksum: 10/1a2bcae50de99034fcd92ad4212d8e01eedf52c7ec7830eedcf886622804fe36884278f2be8be0ea5fde3fd1c23911643a4e0f726c8685b61871c8908af01222 - languageName: node - linkType: hard - -"shelljs@npm:0.8.5": - version: 0.8.5 - resolution: "shelljs@npm:0.8.5" - dependencies: - glob: "npm:^7.0.0" - interpret: "npm:^1.0.0" - rechoir: "npm:^0.6.2" - bin: - shjs: bin/shjs - checksum: 10/f2178274b97b44332bbe9ddb78161137054f55ecf701c7a99db9552cb5478fe279ad5f5131d8a7c2f0730e01ccf0c629d01094143f0541962ce1a3d0243d23f7 - languageName: node - linkType: hard - -"side-channel-list@npm:^1.0.0": - version: 1.0.0 - resolution: "side-channel-list@npm:1.0.0" - dependencies: - es-errors: "npm:^1.3.0" - object-inspect: "npm:^1.13.3" - checksum: 10/603b928997abd21c5a5f02ae6b9cc36b72e3176ad6827fab0417ead74580cc4fb4d5c7d0a8a2ff4ead34d0f9e35701ed7a41853dac8a6d1a664fcce1a044f86f - languageName: node - linkType: hard - -"side-channel-map@npm:^1.0.1": - version: 1.0.1 - resolution: "side-channel-map@npm:1.0.1" - dependencies: - call-bound: "npm:^1.0.2" - es-errors: "npm:^1.3.0" - get-intrinsic: "npm:^1.2.5" - object-inspect: "npm:^1.13.3" - checksum: 10/5771861f77feefe44f6195ed077a9e4f389acc188f895f570d56445e251b861754b547ea9ef73ecee4e01fdada6568bfe9020d2ec2dfc5571e9fa1bbc4a10615 - languageName: node - linkType: hard - -"side-channel-weakmap@npm:^1.0.2": - version: 1.0.2 - resolution: "side-channel-weakmap@npm:1.0.2" - dependencies: - call-bound: "npm:^1.0.2" - es-errors: "npm:^1.3.0" - get-intrinsic: "npm:^1.2.5" - object-inspect: "npm:^1.13.3" - side-channel-map: "npm:^1.0.1" - checksum: 10/a815c89bc78c5723c714ea1a77c938377ea710af20d4fb886d362b0d1f8ac73a17816a5f6640f354017d7e292a43da9c5e876c22145bac00b76cfb3468001736 - languageName: node - linkType: hard - -"side-channel@npm:^1.1.0": - version: 1.1.0 - resolution: "side-channel@npm:1.1.0" - dependencies: - es-errors: "npm:^1.3.0" - object-inspect: "npm:^1.13.3" - side-channel-list: "npm:^1.0.0" - side-channel-map: "npm:^1.0.1" - side-channel-weakmap: "npm:^1.0.2" - checksum: 10/7d53b9db292c6262f326b6ff3bc1611db84ece36c2c7dc0e937954c13c73185b0406c56589e2bb8d071d6fee468e14c39fb5d203ee39be66b7b8174f179afaba - languageName: node - linkType: hard - -"signal-exit@npm:^3.0.2, signal-exit@npm:^3.0.3": - version: 3.0.7 - resolution: "signal-exit@npm:3.0.7" - checksum: 10/a2f098f247adc367dffc27845853e9959b9e88b01cb301658cfe4194352d8d2bb32e18467c786a7fe15f1d44b233ea35633d076d5e737870b7139949d1ab6318 - languageName: node - linkType: hard - -"slash@npm:^3.0.0": - version: 3.0.0 - resolution: "slash@npm:3.0.0" - checksum: 10/94a93fff615f25a999ad4b83c9d5e257a7280c90a32a7cb8b4a87996e4babf322e469c42b7f649fd5796edd8687652f3fb452a86dc97a816f01113183393f11c - languageName: node - linkType: hard - -"slice-ansi@npm:0.0.4": - version: 0.0.4 - resolution: "slice-ansi@npm:0.0.4" - checksum: 10/481d969c6aa771b27d7baacd6fe321751a0b9eb410274bda10ca81ea641bbfe747e428025d6d8f15bd635fdcfd57e8b2d54681ee6b0ce0c40f78644b144759e3 - languageName: node - linkType: hard - -"slice-ansi@npm:^4.0.0": - version: 4.0.0 - resolution: "slice-ansi@npm:4.0.0" - dependencies: - ansi-styles: "npm:^4.0.0" - astral-regex: "npm:^2.0.0" - is-fullwidth-code-point: "npm:^3.0.0" - checksum: 10/4a82d7f085b0e1b070e004941ada3c40d3818563ac44766cca4ceadd2080427d337554f9f99a13aaeb3b4a94d9964d9466c807b3d7b7541d1ec37ee32d308756 - languageName: node - linkType: hard - -"snake-case@npm:^3.0.4": - version: 3.0.4 - resolution: "snake-case@npm:3.0.4" - dependencies: - dot-case: "npm:^3.0.4" - tslib: "npm:^2.0.3" - checksum: 10/0a7a79900bbb36f8aaa922cf111702a3647ac6165736d5dc96d3ef367efc50465cac70c53cd172c382b022dac72ec91710608e5393de71f76d7142e6fd80e8a3 - languageName: node - linkType: hard - -"source-map-support@npm:^0.5.17": - version: 0.5.21 - resolution: "source-map-support@npm:0.5.21" - dependencies: - buffer-from: "npm:^1.0.0" - source-map: "npm:^0.6.0" - checksum: 10/8317e12d84019b31e34b86d483dd41d6f832f389f7417faf8fc5c75a66a12d9686e47f589a0554a868b8482f037e23df9d040d29387eb16fa14cb85f091ba207 - languageName: node - linkType: hard - -"source-map@npm:^0.6.0, source-map@npm:~0.6.1": - version: 0.6.1 - resolution: "source-map@npm:0.6.1" - checksum: 10/59ef7462f1c29d502b3057e822cdbdae0b0e565302c4dd1a95e11e793d8d9d62006cdc10e0fd99163ca33ff2071360cf50ee13f90440806e7ed57d81cba2f7ff - languageName: node - linkType: hard - -"split-on-first@npm:^1.0.0": - version: 1.1.0 - resolution: "split-on-first@npm:1.1.0" - checksum: 10/16ff85b54ddcf17f9147210a4022529b343edbcbea4ce977c8f30e38408b8d6e0f25f92cd35b86a524d4797f455e29ab89eb8db787f3c10708e0b47ebf528d30 - languageName: node - linkType: hard - -"sprintf-js@npm:^1.1.2": - version: 1.1.3 - resolution: "sprintf-js@npm:1.1.3" - checksum: 10/e7587128c423f7e43cc625fe2f87e6affdf5ca51c1cc468e910d8aaca46bb44a7fbcfa552f787b1d3987f7043aeb4527d1b99559e6621e01b42b3f45e5a24cbb - languageName: node - linkType: hard - -"sprintf-js@npm:~1.0.2": - version: 1.0.3 - resolution: "sprintf-js@npm:1.0.3" - checksum: 10/c34828732ab8509c2741e5fd1af6b767c3daf2c642f267788f933a65b1614943c282e74c4284f4fa749c264b18ee016a0d37a3e5b73aee446da46277d3a85daa - languageName: node - linkType: hard - -"strict-uri-encode@npm:^2.0.0": - version: 2.0.0 - resolution: "strict-uri-encode@npm:2.0.0" - checksum: 10/eaac4cf978b6fbd480f1092cab8b233c9b949bcabfc9b598dd79a758f7243c28765ef7639c876fa72940dac687181b35486ea01ff7df3e65ce3848c64822c581 - languageName: node - linkType: hard - -"string-width@npm:^1.0.1": - version: 1.0.2 - resolution: "string-width@npm:1.0.2" - dependencies: - code-point-at: "npm:^1.0.0" - is-fullwidth-code-point: "npm:^1.0.0" - strip-ansi: "npm:^3.0.0" - checksum: 10/5c79439e95bc3bd7233a332c5f5926ab2ee90b23816ed4faa380ce3b2576d7800b0a5bb15ae88ed28737acc7ea06a518c2eef39142dd727adad0e45c776cd37e - languageName: node - linkType: hard - -"string-width@npm:^2.1.1": - version: 2.1.1 - resolution: "string-width@npm:2.1.1" - dependencies: - is-fullwidth-code-point: "npm:^2.0.0" - strip-ansi: "npm:^4.0.0" - checksum: 10/d6173abe088c615c8dffaf3861dc5d5906ed3dc2d6fd67ff2bd2e2b5dce7fd683c5240699cf0b1b8aa679a3b3bd6b28b5053c824cb89b813d7f6541d8f89064a - languageName: node - linkType: hard - -"string-width@npm:^4.0.0, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3": - version: 4.2.3 - resolution: "string-width@npm:4.2.3" - dependencies: - emoji-regex: "npm:^8.0.0" - is-fullwidth-code-point: "npm:^3.0.0" - strip-ansi: "npm:^6.0.1" - checksum: 10/e52c10dc3fbfcd6c3a15f159f54a90024241d0f149cf8aed2982a2d801d2e64df0bf1dc351cf8e95c3319323f9f220c16e740b06faecd53e2462df1d2b5443fb - languageName: node - linkType: hard - -"string.prototype.trim@npm:^1.2.10": - version: 1.2.10 - resolution: "string.prototype.trim@npm:1.2.10" - dependencies: - call-bind: "npm:^1.0.8" - call-bound: "npm:^1.0.2" - define-data-property: "npm:^1.1.4" - define-properties: "npm:^1.2.1" - es-abstract: "npm:^1.23.5" - es-object-atoms: "npm:^1.0.0" - has-property-descriptors: "npm:^1.0.2" - checksum: 10/47bb63cd2470a64bc5e2da1e570d369c016ccaa85c918c3a8bb4ab5965120f35e66d1f85ea544496fac84b9207a6b722adf007e6c548acd0813e5f8a82f9712a - languageName: node - linkType: hard - -"string.prototype.trimend@npm:^1.0.9": - version: 1.0.9 - resolution: "string.prototype.trimend@npm:1.0.9" - dependencies: - call-bind: "npm:^1.0.8" - call-bound: "npm:^1.0.2" - define-properties: "npm:^1.2.1" - es-object-atoms: "npm:^1.0.0" - checksum: 10/140c73899b6747de9e499c7c2e7a83d549c47a26fa06045b69492be9cfb9e2a95187499a373983a08a115ecff8bc3bd7b0fb09b8ff72fb2172abe766849272ef - languageName: node - linkType: hard - -"string.prototype.trimstart@npm:^1.0.8": - version: 1.0.8 - resolution: "string.prototype.trimstart@npm:1.0.8" - dependencies: - call-bind: "npm:^1.0.7" - define-properties: "npm:^1.2.1" - es-object-atoms: "npm:^1.0.0" - checksum: 10/160167dfbd68e6f7cb9f51a16074eebfce1571656fc31d40c3738ca9e30e35496f2c046fe57b6ad49f65f238a152be8c86fd9a2dd58682b5eba39dad995b3674 - languageName: node - linkType: hard - -"strip-ansi@npm:6.0.1, strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1": - version: 6.0.1 - resolution: "strip-ansi@npm:6.0.1" - dependencies: - ansi-regex: "npm:^5.0.1" - checksum: 10/ae3b5436d34fadeb6096367626ce987057713c566e1e7768818797e00ac5d62023d0f198c4e681eae9e20701721980b26a64a8f5b91238869592a9c6800719a2 - languageName: node - linkType: hard - -"strip-ansi@npm:^3.0.0, strip-ansi@npm:^3.0.1": - version: 3.0.1 - resolution: "strip-ansi@npm:3.0.1" - dependencies: - ansi-regex: "npm:^2.0.0" - checksum: 10/9b974de611ce5075c70629c00fa98c46144043db92ae17748fb780f706f7a789e9989fd10597b7c2053ae8d1513fd707816a91f1879b2f71e6ac0b6a863db465 - languageName: node - linkType: hard - -"strip-ansi@npm:^4.0.0": - version: 4.0.0 - resolution: "strip-ansi@npm:4.0.0" - dependencies: - ansi-regex: "npm:^3.0.0" - checksum: 10/d9186e6c0cf78f25274f6750ee5e4a5725fb91b70fdd79aa5fe648eab092a0ec5b9621b22d69d4534a56319f75d8944efbd84e3afa8d4ad1b9a9491f12c84eca - languageName: node - linkType: hard - -"strip-bom@npm:^3.0.0": - version: 3.0.0 - resolution: "strip-bom@npm:3.0.0" - checksum: 10/8d50ff27b7ebe5ecc78f1fe1e00fcdff7af014e73cf724b46fb81ef889eeb1015fc5184b64e81a2efe002180f3ba431bdd77e300da5c6685d702780fbf0c8d5b - languageName: node - linkType: hard - -"strip-final-newline@npm:^2.0.0": - version: 2.0.0 - resolution: "strip-final-newline@npm:2.0.0" - checksum: 10/69412b5e25731e1938184b5d489c32e340605bb611d6140344abc3421b7f3c6f9984b21dff296dfcf056681b82caa3bb4cc996a965ce37bcfad663e92eae9c64 - languageName: node - linkType: hard - -"supports-color@npm:^2.0.0": - version: 2.0.0 - resolution: "supports-color@npm:2.0.0" - checksum: 10/d2957d19e782a806abc3e8616b6648cc1e70c3ebe94fb1c2d43160686f6d79cd7c9f22c4853bc4a362d89d1c249ab6d429788c5f6c83b3086e6d763024bf4581 - languageName: node - linkType: hard - -"supports-color@npm:^5.3.0": - version: 5.5.0 - resolution: "supports-color@npm:5.5.0" - dependencies: - has-flag: "npm:^3.0.0" - checksum: 10/5f505c6fa3c6e05873b43af096ddeb22159831597649881aeb8572d6fe3b81e798cc10840d0c9735e0026b250368851b7f77b65e84f4e4daa820a4f69947f55b - languageName: node - linkType: hard - -"supports-color@npm:^7.0.0, supports-color@npm:^7.1.0": - version: 7.2.0 - resolution: "supports-color@npm:7.2.0" - dependencies: - has-flag: "npm:^4.0.0" - checksum: 10/c8bb7afd564e3b26b50ca6ee47572c217526a1389fe018d00345856d4a9b08ffbd61fadaf283a87368d94c3dcdb8f5ffe2650a5a65863e21ad2730ca0f05210a - languageName: node - linkType: hard - -"supports-color@npm:^8.1.0, supports-color@npm:^8.1.1": - version: 8.1.1 - resolution: "supports-color@npm:8.1.1" - dependencies: - has-flag: "npm:^4.0.0" - checksum: 10/157b534df88e39c5518c5e78c35580c1eca848d7dbaf31bbe06cdfc048e22c7ff1a9d046ae17b25691128f631a51d9ec373c1b740c12ae4f0de6e292037e4282 - languageName: node - linkType: hard - -"supports-hyperlinks@npm:^2.1.0, supports-hyperlinks@npm:^2.2.0": - version: 2.3.0 - resolution: "supports-hyperlinks@npm:2.3.0" - dependencies: - has-flag: "npm:^4.0.0" - supports-color: "npm:^7.0.0" - checksum: 10/3e7df6e9eaa177d7bfbbe065c91325e9b482f48de0f7c9133603e3ffa8af31cbceac104a0941cd0266a57f8e691de6eb58b79fec237852dc84ed7ad152b116b0 - languageName: node - linkType: hard - -"supports-preserve-symlinks-flag@npm:^1.0.0": - version: 1.0.0 - resolution: "supports-preserve-symlinks-flag@npm:1.0.0" - checksum: 10/a9dc19ae2220c952bd2231d08ddeecb1b0328b61e72071ff4000c8384e145cc07c1c0bdb3b5a1cb06e186a7b2790f1dee793418b332f6ddf320de25d9125be7e - languageName: node - linkType: hard - -"symbol-observable@npm:^1.1.0": - version: 1.2.0 - resolution: "symbol-observable@npm:1.2.0" - checksum: 10/4684327a2fef2453dcd4238b5bd8f69c460a4708fb8c024a824c6a707ca644b2b2a586e36e5197d0d1162ff48e288299a48844a8c46274ffcfd9260e03df7692 - languageName: node - linkType: hard - -"table@npm:6.8.0": - version: 6.8.0 - resolution: "table@npm:6.8.0" - dependencies: - ajv: "npm:^8.0.1" - lodash.truncate: "npm:^4.4.2" - slice-ansi: "npm:^4.0.0" - string-width: "npm:^4.2.3" - strip-ansi: "npm:^6.0.1" - checksum: 10/4c2b8ebd75f36db236529680c70f41951c9c7fda3e65cb5b987164244f23f98670ded99983fdc5d62aa02405a212e90f7446efcf87e3435e472dda26d6581645 - languageName: node - linkType: hard - -"to-fast-properties@npm:^2.0.0": - version: 2.0.0 - resolution: "to-fast-properties@npm:2.0.0" - checksum: 10/be2de62fe58ead94e3e592680052683b1ec986c72d589e7b21e5697f8744cdbf48c266fa72f6c15932894c10187b5f54573a3bcf7da0bfd964d5caf23d436168 - languageName: node - linkType: hard - -"to-regex-range@npm:^5.0.1": - version: 5.0.1 - resolution: "to-regex-range@npm:5.0.1" - dependencies: - is-number: "npm:^7.0.0" - checksum: 10/10dda13571e1f5ad37546827e9b6d4252d2e0bc176c24a101252153ef435d83696e2557fe128c4678e4e78f5f01e83711c703eef9814eb12dab028580d45980a - languageName: node - linkType: hard - -"tr46@npm:~0.0.3": - version: 0.0.3 - resolution: "tr46@npm:0.0.3" - checksum: 10/8f1f5aa6cb232f9e1bdc86f485f916b7aa38caee8a778b378ffec0b70d9307873f253f5cbadbe2955ece2ac5c83d0dc14a77513166ccd0a0c7fe197e21396695 - languageName: node - linkType: hard - -"ts-invariant@npm:^0.4.0": - version: 0.4.4 - resolution: "ts-invariant@npm:0.4.4" - dependencies: - tslib: "npm:^1.9.3" - checksum: 10/dd6f268aa358f9d28dec0b8539667cc9829eaac588438a78a422079a1b3b9a98a96306f862a745b0581173528d11a6aeff861c6e3a988d65a113a82e55a5f409 - languageName: node - linkType: hard - -"ts-node@npm:^9": - version: 9.1.1 - resolution: "ts-node@npm:9.1.1" - dependencies: - arg: "npm:^4.1.0" - create-require: "npm:^1.1.0" - diff: "npm:^4.0.1" - make-error: "npm:^1.1.1" - source-map-support: "npm:^0.5.17" - yn: "npm:3.1.1" - peerDependencies: - typescript: ">=2.7" - bin: - ts-node: dist/bin.js - ts-node-script: dist/bin-script.js - ts-node-transpile-only: dist/bin-transpile.js - ts-script: dist/bin-script-deprecated.js - checksum: 10/7fd8da71dd528f0490daf771a185572b485cb7f6be354c4d675011aee63bdb291f919e68c340cc92863c450e685b7f7300bd81ec158c3881a8c3f0e2f1cc10fe - languageName: node - linkType: hard - -"tslib@npm:^1.10.0, tslib@npm:^1.9.0, tslib@npm:^1.9.3": - version: 1.14.1 - resolution: "tslib@npm:1.14.1" - checksum: 10/7dbf34e6f55c6492637adb81b555af5e3b4f9cc6b998fb440dac82d3b42bdc91560a35a5fb75e20e24a076c651438234da6743d139e4feabf0783f3cdfe1dddb - languageName: node - linkType: hard - -"tslib@npm:^2, tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.3.1, tslib@npm:^2.4.1, tslib@npm:^2.6.1, tslib@npm:^2.6.2": - version: 2.8.1 - resolution: "tslib@npm:2.8.1" - checksum: 10/3e2e043d5c2316461cb54e5c7fe02c30ef6dccb3384717ca22ae5c6b5bc95232a6241df19c622d9c73b809bea33b187f6dbc73030963e29950c2141bc32a79f7 - languageName: node - linkType: hard - -"tty@npm:1.0.1": - version: 1.0.1 - resolution: "tty@npm:1.0.1" - checksum: 10/6fa781841fa99b9ae407c713fc08ae71e15a73d9702952cddda3e6a7aa204cdfdbf85f5b4a54bb414bfd63eb8d4ba027973c4026b193081c762d4dc002d3e0a9 - languageName: node - linkType: hard - -"tunnel-agent@npm:^0.6.0": - version: 0.6.0 - resolution: "tunnel-agent@npm:0.6.0" - dependencies: - safe-buffer: "npm:^5.0.1" - checksum: 10/7f0d9ed5c22404072b2ae8edc45c071772affd2ed14a74f03b4e71b4dd1a14c3714d85aed64abcaaee5fec2efc79002ba81155c708f4df65821b444abb0cfade - languageName: node - linkType: hard - -"type-fest@npm:^0.13.1": - version: 0.13.1 - resolution: "type-fest@npm:0.13.1" - checksum: 10/11e9476dc85bf97a71f6844fb67ba8e64a4c7e445724c0f3bd37eb2ddf4bc97c1dc9337bd880b28bce158de1c0cb275c2d03259815a5bf64986727197126ab56 - languageName: node - linkType: hard - -"type-fest@npm:^0.21.3": - version: 0.21.3 - resolution: "type-fest@npm:0.21.3" - checksum: 10/f4254070d9c3d83a6e573bcb95173008d73474ceadbbf620dd32d273940ca18734dff39c2b2480282df9afe5d1675ebed5499a00d791758748ea81f61a38961f - languageName: node - linkType: hard - -"type-fest@npm:^0.3.0": - version: 0.3.1 - resolution: "type-fest@npm:0.3.1" - checksum: 10/a969e953d87889e089ea8b370b12a0c90410e198287aeba1a5618a325492967be338ebaf85aecfb542d312dedbcf5e12be9291e5e5d3b0b6c990992a224d07ae - languageName: node - linkType: hard - -"typed-array-buffer@npm:^1.0.3": - version: 1.0.3 - resolution: "typed-array-buffer@npm:1.0.3" - dependencies: - call-bound: "npm:^1.0.3" - es-errors: "npm:^1.3.0" - is-typed-array: "npm:^1.1.14" - checksum: 10/3fb91f0735fb413b2bbaaca9fabe7b8fc14a3fa5a5a7546bab8a57e755be0e3788d893195ad9c2b842620592de0e68d4c077d4c2c41f04ec25b8b5bb82fa9a80 - languageName: node - linkType: hard - -"typed-array-byte-length@npm:^1.0.3": - version: 1.0.3 - resolution: "typed-array-byte-length@npm:1.0.3" - dependencies: - call-bind: "npm:^1.0.8" - for-each: "npm:^0.3.3" - gopd: "npm:^1.2.0" - has-proto: "npm:^1.2.0" - is-typed-array: "npm:^1.1.14" - checksum: 10/269dad101dda73e3110117a9b84db86f0b5c07dad3a9418116fd38d580cab7fc628a4fc167e29b6d7c39da2f53374b78e7cb578b3c5ec7a556689d985d193519 - languageName: node - linkType: hard - -"typed-array-byte-offset@npm:^1.0.4": - version: 1.0.4 - resolution: "typed-array-byte-offset@npm:1.0.4" - dependencies: - available-typed-arrays: "npm:^1.0.7" - call-bind: "npm:^1.0.8" - for-each: "npm:^0.3.3" - gopd: "npm:^1.2.0" - has-proto: "npm:^1.2.0" - is-typed-array: "npm:^1.1.15" - reflect.getprototypeof: "npm:^1.0.9" - checksum: 10/c2869aa584cdae24ecfd282f20a0f556b13a49a9d5bca1713370bb3c89dff0ccbc5ceb45cb5b784c98f4579e5e3e2a07e438c3a5b8294583e2bd4abbd5104fb5 - languageName: node - linkType: hard - -"typed-array-length@npm:^1.0.7": - version: 1.0.7 - resolution: "typed-array-length@npm:1.0.7" - dependencies: - call-bind: "npm:^1.0.7" - for-each: "npm:^0.3.3" - gopd: "npm:^1.0.1" - is-typed-array: "npm:^1.1.13" - possible-typed-array-names: "npm:^1.0.0" - reflect.getprototypeof: "npm:^1.0.6" - checksum: 10/d6b2f0e81161682d2726eb92b1dc2b0890890f9930f33f9bcf6fc7272895ce66bc368066d273e6677776de167608adc53fcf81f1be39a146d64b630edbf2081c - languageName: node - linkType: hard - -"unbox-primitive@npm:^1.1.0": - version: 1.1.0 - resolution: "unbox-primitive@npm:1.1.0" - dependencies: - call-bound: "npm:^1.0.3" - has-bigints: "npm:^1.0.2" - has-symbols: "npm:^1.1.0" - which-boxed-primitive: "npm:^1.1.1" - checksum: 10/fadb347020f66b2c8aeacf8b9a79826fa34cc5e5457af4eb0bbc4e79bd87fed0fa795949825df534320f7c13f199259516ad30abc55a6e7b91d8d996ca069e50 - languageName: node - linkType: hard - -"undici-types@npm:~6.20.0": - version: 6.20.0 - resolution: "undici-types@npm:6.20.0" - checksum: 10/583ac7bbf4ff69931d3985f4762cde2690bb607844c16a5e2fbb92ed312fe4fa1b365e953032d469fa28ba8b224e88a595f0b10a449332f83fa77c695e567dbe - languageName: node - linkType: hard - -"universalify@npm:^0.1.0": - version: 0.1.2 - resolution: "universalify@npm:0.1.2" - checksum: 10/40cdc60f6e61070fe658ca36016a8f4ec216b29bf04a55dce14e3710cc84c7448538ef4dad3728d0bfe29975ccd7bfb5f414c45e7b78883567fb31b246f02dff - languageName: node - linkType: hard - -"universalify@npm:^2.0.0": - version: 2.0.1 - resolution: "universalify@npm:2.0.1" - checksum: 10/ecd8469fe0db28e7de9e5289d32bd1b6ba8f7183db34f3bfc4ca53c49891c2d6aa05f3fb3936a81285a905cc509fb641a0c3fc131ec786167eff41236ae32e60 - languageName: node - linkType: hard - -"upper-case-first@npm:^2.0.2": - version: 2.0.2 - resolution: "upper-case-first@npm:2.0.2" - dependencies: - tslib: "npm:^2.0.3" - checksum: 10/4487db4701effe3b54ced4b3e4aa4d9ab06c548f97244d04aafb642eedf96a76d5a03cf5f38f10f415531d5792d1ac6e1b50f2a76984dc6964ad530f12876409 - languageName: node - linkType: hard - -"upper-case@npm:^2.0.2": - version: 2.0.2 - resolution: "upper-case@npm:2.0.2" - dependencies: - tslib: "npm:^2.0.3" - checksum: 10/508723a2b03ab90cf1d6b7e0397513980fab821cbe79c87341d0e96cedefadf0d85f9d71eac24ab23f526a041d585a575cfca120a9f920e44eb4f8a7cf89121c - languageName: node - linkType: hard - -"util.promisify@npm:1.1.1": - version: 1.1.1 - resolution: "util.promisify@npm:1.1.1" - dependencies: - call-bind: "npm:^1.0.0" - define-properties: "npm:^1.1.3" - for-each: "npm:^0.3.3" - has-symbols: "npm:^1.0.1" - object.getownpropertydescriptors: "npm:^2.1.1" - checksum: 10/fa1e4e6ae2636b3ab5feaa78f1324603e1f8fc71c27f755df10df83ccf24e48ec12c0d8f42081658487cd09c43ce9c0e7a1c16c8ae0888d582a74b338a5266d2 - languageName: node - linkType: hard - -"vscode-jsonrpc@npm:6.0.0": - version: 6.0.0 - resolution: "vscode-jsonrpc@npm:6.0.0" - checksum: 10/c9af7ed831912b5df0d046260ff24f99582f1f75aa49f49a39a67da1dc595dd00ddae756ae3ef64c73bc2c0e4d79b37aa13e56e24a30319f8053a229d6589b19 - languageName: node - linkType: hard - -"vscode-languageserver-protocol@npm:3.16.0": - version: 3.16.0 - resolution: "vscode-languageserver-protocol@npm:3.16.0" - dependencies: - vscode-jsonrpc: "npm:6.0.0" - vscode-languageserver-types: "npm:3.16.0" - checksum: 10/6cc184e7bc7e9334361080662a1927cc9fa4b42acf19f1e7b92f9cafa5cb0897cc1586ef1c8eb932bf46404f6eed00283540ed2e4c8247506f9bec3a4aee7131 - languageName: node - linkType: hard - -"vscode-languageserver-textdocument@npm:^1.0.4": - version: 1.0.12 - resolution: "vscode-languageserver-textdocument@npm:1.0.12" - checksum: 10/2bc0fde952d40f35a31179623d1491b0fafdee156aaf58557f40f5d394a25fc84826763cdde55fa6ce2ed9cd35a931355ad6dd7fe5db82e7f21e5d865f0af8c6 - languageName: node - linkType: hard - -"vscode-languageserver-types@npm:3.16.0": - version: 3.16.0 - resolution: "vscode-languageserver-types@npm:3.16.0" - checksum: 10/a276ad08bcf6b7eabff50073a927e3053243d558f6d7a9cba7475de2f2623ec42279e4e03544cce4cfdd5a91f3ed517074f688b42395b3e91d21809dbad43019 - languageName: node - linkType: hard - -"vscode-languageserver@npm:^7.0.0": - version: 7.0.0 - resolution: "vscode-languageserver@npm:7.0.0" - dependencies: - vscode-languageserver-protocol: "npm:3.16.0" - bin: - installServerIntoExtension: bin/installServerIntoExtension - checksum: 10/4ea1536e83ee392d0f0d4971828095a4efcd6b5b1310e7fc95f1d5e0e91328e52b27294718dcd5957d029d9301e8bb7cb181f1127937737a09a9a2ae413997e4 - languageName: node - linkType: hard - -"vscode-uri@npm:1.0.6": - version: 1.0.6 - resolution: "vscode-uri@npm:1.0.6" - checksum: 10/9ee7aa1c0198fafe22b8c7788676ff4f0111d76366e2eec1fdc1caa36b464f5d0fd988a447f6729fe5c460e1c40b5915bd6fbf614500688dc4b5b83f3584e454 - languageName: node - linkType: hard - -"webidl-conversions@npm:^3.0.0": - version: 3.0.1 - resolution: "webidl-conversions@npm:3.0.1" - checksum: 10/b65b9f8d6854572a84a5c69615152b63371395f0c5dcd6729c45789052296df54314db2bc3e977df41705eacb8bc79c247cee139a63fa695192f95816ed528ad - languageName: node - linkType: hard - -"whatwg-url@npm:^5.0.0": - version: 5.0.0 - resolution: "whatwg-url@npm:5.0.0" - dependencies: - tr46: "npm:~0.0.3" - webidl-conversions: "npm:^3.0.0" - checksum: 10/f95adbc1e80820828b45cc671d97da7cd5e4ef9deb426c31bcd5ab00dc7103042291613b3ef3caec0a2335ed09e0d5ed026c940755dbb6d404e2b27f940fdf07 - languageName: node - linkType: hard - -"which-boxed-primitive@npm:^1.1.0, which-boxed-primitive@npm:^1.1.1": - version: 1.1.1 - resolution: "which-boxed-primitive@npm:1.1.1" - dependencies: - is-bigint: "npm:^1.1.0" - is-boolean-object: "npm:^1.2.1" - is-number-object: "npm:^1.1.1" - is-string: "npm:^1.1.1" - is-symbol: "npm:^1.1.1" - checksum: 10/a877c0667bc089518c83ad4d845cf8296b03efe3565c1de1940c646e00a2a1ae9ed8a185bcfa27cbf352de7906f0616d83b9d2f19ca500ee02a551fb5cf40740 - languageName: node - linkType: hard - -"which-builtin-type@npm:^1.2.1": - version: 1.2.1 - resolution: "which-builtin-type@npm:1.2.1" - dependencies: - call-bound: "npm:^1.0.2" - function.prototype.name: "npm:^1.1.6" - has-tostringtag: "npm:^1.0.2" - is-async-function: "npm:^2.0.0" - is-date-object: "npm:^1.1.0" - is-finalizationregistry: "npm:^1.1.0" - is-generator-function: "npm:^1.0.10" - is-regex: "npm:^1.2.1" - is-weakref: "npm:^1.0.2" - isarray: "npm:^2.0.5" - which-boxed-primitive: "npm:^1.1.0" - which-collection: "npm:^1.0.2" - which-typed-array: "npm:^1.1.16" - checksum: 10/22c81c5cb7a896c5171742cd30c90d992ff13fb1ea7693e6cf80af077791613fb3f89aa9b4b7f890bd47b6ce09c6322c409932359580a2a2a54057f7b52d1cbe - languageName: node - linkType: hard - -"which-collection@npm:^1.0.2": - version: 1.0.2 - resolution: "which-collection@npm:1.0.2" - dependencies: - is-map: "npm:^2.0.3" - is-set: "npm:^2.0.3" - is-weakmap: "npm:^2.0.2" - is-weakset: "npm:^2.0.3" - checksum: 10/674bf659b9bcfe4055f08634b48a8588e879161b9fefed57e9ec4ff5601e4d50a05ccd76cf10f698ef5873784e5df3223336d56c7ce88e13bcf52ebe582fc8d7 - languageName: node - linkType: hard - -"which-typed-array@npm:^1.1.16, which-typed-array@npm:^1.1.18": - version: 1.1.19 - resolution: "which-typed-array@npm:1.1.19" - dependencies: - available-typed-arrays: "npm:^1.0.7" - call-bind: "npm:^1.0.8" - call-bound: "npm:^1.0.4" - for-each: "npm:^0.3.5" - get-proto: "npm:^1.0.1" - gopd: "npm:^1.2.0" - has-tostringtag: "npm:^1.0.2" - checksum: 10/12be30fb88567f9863186bee1777f11bea09dd59ed8b3ce4afa7dd5cade75e2f4cc56191a2da165113cc7cf79987ba021dac1e22b5b62aa7e5c56949f2469a68 - languageName: node - linkType: hard - -"which@npm:^2.0.1": - version: 2.0.2 - resolution: "which@npm:2.0.2" - dependencies: - isexe: "npm:^2.0.0" - bin: - node-which: ./bin/node-which - checksum: 10/4782f8a1d6b8fc12c65e968fea49f59752bf6302dc43036c3bf87da718a80710f61a062516e9764c70008b487929a73546125570acea95c5b5dcc8ac3052c70f - languageName: node - linkType: hard - -"widest-line@npm:^3.1.0": - version: 3.1.0 - resolution: "widest-line@npm:3.1.0" - dependencies: - string-width: "npm:^4.0.0" - checksum: 10/03db6c9d0af9329c37d74378ff1d91972b12553c7d72a6f4e8525fe61563fa7adb0b9d6e8d546b7e059688712ea874edd5ded475999abdeedf708de9849310e0 - languageName: node - linkType: hard - -"wrap-ansi@npm:^3.0.1": - version: 3.0.1 - resolution: "wrap-ansi@npm:3.0.1" - dependencies: - string-width: "npm:^2.1.1" - strip-ansi: "npm:^4.0.0" - checksum: 10/bdd4248faa2142051ed5802c216076b25ada29778100483bb6f16a52a115bf7cb7e595bdbe9f1ed551dcd4822f3e2ece80c9febedc2b65acb2cc649705d47bc2 - languageName: node - linkType: hard - -"wrap-ansi@npm:^6.2.0": - version: 6.2.0 - resolution: "wrap-ansi@npm:6.2.0" - dependencies: - ansi-styles: "npm:^4.0.0" - string-width: "npm:^4.1.0" - strip-ansi: "npm:^6.0.0" - checksum: 10/0d64f2d438e0b555e693b95aee7b2689a12c3be5ac458192a1ce28f542a6e9e59ddfecc37520910c2c88eb1f82a5411260566dba5064e8f9895e76e169e76187 - languageName: node - linkType: hard - -"wrap-ansi@npm:^7.0.0": - version: 7.0.0 - resolution: "wrap-ansi@npm:7.0.0" - dependencies: - ansi-styles: "npm:^4.0.0" - string-width: "npm:^4.1.0" - strip-ansi: "npm:^6.0.0" - checksum: 10/cebdaeca3a6880da410f75209e68cd05428580de5ad24535f22696d7d9cab134d1f8498599f344c3cf0fb37c1715807a183778d8c648d6cc0cb5ff2bb4236540 - languageName: node - linkType: hard - -"wrappy@npm:1": - version: 1.0.2 - resolution: "wrappy@npm:1.0.2" - checksum: 10/159da4805f7e84a3d003d8841557196034155008f817172d4e986bd591f74aa82aa7db55929a54222309e01079a65a92a9e6414da5a6aa4b01ee44a511ac3ee5 - languageName: node - linkType: hard - -"yaml@npm:^1.10.0": - version: 1.10.2 - resolution: "yaml@npm:1.10.2" - checksum: 10/e088b37b4d4885b70b50c9fa1b7e54bd2e27f5c87205f9deaffd1fb293ab263d9c964feadb9817a7b129a5bf30a06582cb08750f810568ecc14f3cdbabb79cb3 - languageName: node - linkType: hard - -"yarn@npm:^1.22.17": - version: 1.22.22 - resolution: "yarn@npm:1.22.22" - bin: - yarn: bin/yarn.js - yarnpkg: bin/yarn.js - checksum: 10/98d80230beaa81f186b2256dff5ef9dce2dd6073c94299209f8e562da9018cff4275c95c27c788aaa4a9c3c186ea8a9aee9a5b83570696a4c8a9d1fff2d4da3a - languageName: node - linkType: hard - -"yn@npm:3.1.1": - version: 3.1.1 - resolution: "yn@npm:3.1.1" - checksum: 10/2c487b0e149e746ef48cda9f8bad10fc83693cd69d7f9dcd8be4214e985de33a29c9e24f3c0d6bcf2288427040a8947406ab27f7af67ee9456e6b84854f02dd6 - languageName: node - linkType: hard - -"zen-observable-ts@npm:^0.8.21": - version: 0.8.21 - resolution: "zen-observable-ts@npm:0.8.21" - dependencies: - tslib: "npm:^1.9.3" - zen-observable: "npm:^0.8.0" - checksum: 10/557b614e8c7394ec9fe5658c67a3883c3aa329360659a3f9be5b5288142c8cdcbc3a02c10416414ebdde1607a9e052ace58e3cf19f276391b93b24063c8a649f - languageName: node - linkType: hard - -"zen-observable@npm:^0.8.0": - version: 0.8.15 - resolution: "zen-observable@npm:0.8.15" - checksum: 10/30eac3f4055d33f446b4cd075d3543da347c2c8e68fbc35c3f5a19fb43be67c6ed27ee136bc8f8933efa547be7ce04957809ad00ee7f1b00a964f199ae6fb514 - languageName: node - linkType: hard diff --git a/src/Tgstation.Server.Host.Watchdog/Tgstation.Server.Host.Watchdog.csproj b/src/Tgstation.Server.Host.Watchdog/Tgstation.Server.Host.Watchdog.csproj index 5f31ab855f..6ef7397856 100644 --- a/src/Tgstation.Server.Host.Watchdog/Tgstation.Server.Host.Watchdog.csproj +++ b/src/Tgstation.Server.Host.Watchdog/Tgstation.Server.Host.Watchdog.csproj @@ -10,9 +10,9 @@ - + - + diff --git a/src/Tgstation.Server.Host/.config/dotnet-tools.json b/src/Tgstation.Server.Host/.config/dotnet-tools.json index 7cf60e92f7..5efa53e31f 100644 --- a/src/Tgstation.Server.Host/.config/dotnet-tools.json +++ b/src/Tgstation.Server.Host/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "dotnet-ef": { - "version": "9.0.7", + "version": "9.0.8", "commands": [ "dotnet-ef" ] diff --git a/src/Tgstation.Server.Host/Authority/AdministrationAuthority.cs b/src/Tgstation.Server.Host/Authority/AdministrationAuthority.cs index 76c7fc937b..ca369c70dc 100644 --- a/src/Tgstation.Server.Host/Authority/AdministrationAuthority.cs +++ b/src/Tgstation.Server.Host/Authority/AdministrationAuthority.cs @@ -1,9 +1,12 @@ using System; +using System.IO; using System.Threading; using System.Threading.Tasks; +using System.Web; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Octokit; @@ -11,9 +14,11 @@ using Tgstation.Server.Api.Models; using Tgstation.Server.Api.Models.Response; using Tgstation.Server.Api.Rights; using Tgstation.Server.Host.Authority.Core; +using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.Core; using Tgstation.Server.Host.Database; -using Tgstation.Server.Host.Security; +using Tgstation.Server.Host.IO; +using Tgstation.Server.Host.System; using Tgstation.Server.Host.Transfer; using Tgstation.Server.Host.Utils.GitHub; @@ -57,10 +62,29 @@ namespace Tgstation.Server.Host.Authority /// readonly IMemoryCache cacheService; + /// + /// The for the . + /// + readonly IAssemblyInformationProvider assemblyInformationProvider; + + /// + /// The for the . + /// + readonly IPlatformIdentifier platformIdentifier; + + /// + /// The for the . + /// + readonly IIOManager ioManager; + + /// + /// The for the . + /// + readonly IOptionsSnapshot fileLoggingConfigurationOptions; + /// /// Initializes a new instance of the class. /// - /// The to use. /// The to use. /// The to use. /// The value of . @@ -68,17 +92,23 @@ namespace Tgstation.Server.Host.Authority /// The value of . /// The value of . /// The value of . + /// The value of . + /// The value of . + /// The value of . + /// The value of . public AdministrationAuthority( - IAuthenticationContext authenticationContext, IDatabaseContext databaseContext, ILogger logger, IGitHubServiceFactory gitHubServiceFactory, IServerControl serverControl, IServerUpdateInitiator serverUpdateInitiator, IFileTransferTicketProvider fileTransferService, - IMemoryCache cacheService) + IMemoryCache cacheService, + IAssemblyInformationProvider assemblyInformationProvider, + IPlatformIdentifier platformIdentifier, + IIOManager ioManager, + IOptionsSnapshot fileLoggingConfigurationOptions) : base( - authenticationContext, databaseContext, logger) { @@ -87,157 +117,212 @@ namespace Tgstation.Server.Host.Authority this.serverUpdateInitiator = serverUpdateInitiator ?? throw new ArgumentNullException(nameof(serverUpdateInitiator)); this.fileTransferService = fileTransferService ?? throw new ArgumentNullException(nameof(fileTransferService)); this.cacheService = cacheService ?? throw new ArgumentNullException(nameof(cacheService)); + this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider)); + this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier)); + this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); + this.fileLoggingConfigurationOptions = fileLoggingConfigurationOptions ?? throw new ArgumentNullException(nameof(fileLoggingConfigurationOptions)); } /// - public async ValueTask> GetUpdateInformation(bool forceFresh, CancellationToken cancellationToken) - { - try - { - async Task CacheFactory() + public RequirementsGated> GetUpdateInformation(bool forceFresh, CancellationToken cancellationToken) + => new( + () => Flag(AdministrationRights.ChangeVersion), + async () => { - Version? greatestVersion = null; - Uri? repoUrl = null; - var scopeCancellationToken = CancellationToken.None; // DCT: None available try { - var gitHubService = await gitHubServiceFactory.CreateService(scopeCancellationToken); - var repositoryUrlTask = gitHubService.GetUpdatesRepositoryUrl(scopeCancellationToken); - var releases = await gitHubService.GetTgsReleases(scopeCancellationToken); - - foreach (var kvp in releases) + async Task CacheFactory() { - var version = kvp.Key; - var release = kvp.Value; - if (version.Major > 3 // Forward/backward compatible but not before TGS4 - && (greatestVersion == null || version > greatestVersion)) - greatestVersion = version; + Version? greatestVersion = null; + Uri? repoUrl = null; + var scopeCancellationToken = CancellationToken.None; // DCT: None available + try + { + var gitHubService = await gitHubServiceFactory.CreateService(scopeCancellationToken); + var repositoryUrlTask = gitHubService.GetUpdatesRepositoryUrl(scopeCancellationToken); + var releases = await gitHubService.GetTgsReleases(scopeCancellationToken); + + foreach (var kvp in releases) + { + var version = kvp.Key; + var release = kvp.Value; + if (version.Major > 3 // Forward/backward compatible but not before TGS4 + && (greatestVersion == null || version > greatestVersion)) + greatestVersion = version; + } + + repoUrl = await repositoryUrlTask; + } + catch (NotFoundException e) + { + Logger.LogWarning(e, "Not found exception while retrieving upstream repository info!"); + } + + return new AdministrationResponse + { + LatestVersion = greatestVersion, + TrackedRepositoryUrl = repoUrl, + GeneratedAt = DateTimeOffset.UtcNow, + }; } - repoUrl = await repositoryUrlTask; + var ttl = TimeSpan.FromMinutes(30); + Task task; + if (forceFresh || !cacheService.TryGetValue(ReadCacheKey, out var rawCacheObject)) + { + using var entry = cacheService.CreateEntry(ReadCacheKey); + entry.AbsoluteExpirationRelativeToNow = ttl; + entry.Value = task = CacheFactory(); + } + else + task = (Task)rawCacheObject!; + + var result = await task.WaitAsync(cancellationToken); + return new AuthorityResponse(result); } - catch (NotFoundException e) + catch (RateLimitExceededException e) { - Logger.LogWarning(e, "Not found exception while retrieving upstream repository info!"); + return RateLimit(e); + } + catch (ApiException e) + { + Logger.LogWarning(e, OctokitException); + return new AuthorityResponse( + new ErrorMessageResponse(ErrorCode.RemoteApiError) + { + AdditionalData = e.Message, + }, + HttpFailureResponse.FailedDependency); + } + }); + + /// + public RequirementsGated> TriggerServerVersionChange(Version targetVersion, bool uploadZip, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(targetVersion); + + return new( + () => + { + if (uploadZip) + return Flag(AdministrationRights.UploadVersion); + + return Flag(AdministrationRights.ChangeVersion); + }, + async () => + { + if (targetVersion.Major < 4) + return BadRequest(ErrorCode.CannotChangeServerSuite); + + if (!serverControl.WatchdogPresent) + return new AuthorityResponse( + new ErrorMessageResponse(ErrorCode.MissingHostWatchdog), + HttpFailureResponse.UnprocessableEntity); + + IFileUploadTicket? uploadTicket = uploadZip + ? fileTransferService.CreateUpload(FileUploadStreamKind.None) + : null; + + ServerUpdateResult updateResult; + try + { + try + { + updateResult = await serverUpdateInitiator.InitiateUpdate(uploadTicket, targetVersion, cancellationToken); + } + catch + { + if (uploadZip) + await uploadTicket!.DisposeAsync(); + + throw; + } + } + catch (RateLimitExceededException ex) + { + return RateLimit(ex); + } + catch (ApiException e) + { + Logger.LogWarning(e, OctokitException); + return new AuthorityResponse( + new ErrorMessageResponse(ErrorCode.RemoteApiError) + { + AdditionalData = e.Message, + }, + HttpFailureResponse.FailedDependency); } - return new AdministrationResponse + return updateResult switch { - LatestVersion = greatestVersion, - TrackedRepositoryUrl = repoUrl, - GeneratedAt = DateTimeOffset.UtcNow, + ServerUpdateResult.Started => new AuthorityResponse(new ServerUpdateResponse(targetVersion, uploadTicket?.Ticket.FileTicket), HttpSuccessResponse.Accepted), + ServerUpdateResult.ReleaseMissing => Gone(), + ServerUpdateResult.UpdateInProgress => BadRequest(ErrorCode.ServerUpdateInProgress), + ServerUpdateResult.SwarmIntegrityCheckFailed => new AuthorityResponse( + new ErrorMessageResponse(ErrorCode.SwarmIntegrityCheckFailed), + HttpFailureResponse.FailedDependency), + _ => throw new InvalidOperationException($"Unexpected ServerUpdateResult: {updateResult}"), }; - } - - var ttl = TimeSpan.FromMinutes(30); - Task task; - if (forceFresh || !cacheService.TryGetValue(ReadCacheKey, out var rawCacheObject)) - { - using var entry = cacheService.CreateEntry(ReadCacheKey); - entry.AbsoluteExpirationRelativeToNow = ttl; - entry.Value = task = CacheFactory(); - } - else - task = (Task)rawCacheObject!; - - var result = await task.WaitAsync(cancellationToken); - return new AuthorityResponse(result); - } - catch (RateLimitExceededException e) - { - return RateLimit(e); - } - catch (ApiException e) - { - Logger.LogWarning(e, OctokitException); - return new AuthorityResponse( - new ErrorMessageResponse(ErrorCode.RemoteApiError) - { - AdditionalData = e.Message, - }, - HttpFailureResponse.FailedDependency); - } + }); } /// - public async ValueTask> TriggerServerVersionChange(Version targetVersion, bool uploadZip, CancellationToken cancellationToken) - { - var attemptingUpload = uploadZip == true; - if (attemptingUpload) - { - if (!AuthenticationContext.PermissionSet.AdministrationRights!.Value.HasFlag(AdministrationRights.UploadVersion)) - return Forbid(); - } - else if (!AuthenticationContext.PermissionSet.AdministrationRights!.Value.HasFlag(AdministrationRights.ChangeVersion)) - return Forbid(); - - if (targetVersion.Major < 4) - return BadRequest(ErrorCode.CannotChangeServerSuite); - - if (!serverControl.WatchdogPresent) - return new AuthorityResponse( - new ErrorMessageResponse(ErrorCode.MissingHostWatchdog), - HttpFailureResponse.UnprocessableEntity); - - IFileUploadTicket? uploadTicket = attemptingUpload - ? fileTransferService.CreateUpload(FileUploadStreamKind.None) - : null; - - ServerUpdateResult updateResult; - try - { - try + public RequirementsGated TriggerServerRestart() + => new( + () => Flag(AdministrationRights.RestartHost), + async () => { - updateResult = await serverUpdateInitiator.InitiateUpdate(uploadTicket, targetVersion, cancellationToken); - } - catch - { - if (attemptingUpload) - await uploadTicket!.DisposeAsync(); - - throw; - } - } - catch (RateLimitExceededException ex) - { - return RateLimit(ex); - } - catch (ApiException e) - { - Logger.LogWarning(e, OctokitException); - return new AuthorityResponse( - new ErrorMessageResponse(ErrorCode.RemoteApiError) + if (!serverControl.WatchdogPresent) { - AdditionalData = e.Message, - }, - HttpFailureResponse.FailedDependency); - } + Logger.LogDebug("Restart request failed due to lack of host watchdog!"); + return new AuthorityResponse( + new ErrorMessageResponse(ErrorCode.MissingHostWatchdog), + HttpFailureResponse.UnprocessableEntity); + } - return updateResult switch - { - ServerUpdateResult.Started => new AuthorityResponse(new ServerUpdateResponse(targetVersion, uploadTicket?.Ticket.FileTicket), HttpSuccessResponse.Accepted), - ServerUpdateResult.ReleaseMissing => Gone(), - ServerUpdateResult.UpdateInProgress => BadRequest(ErrorCode.ServerUpdateInProgress), - ServerUpdateResult.SwarmIntegrityCheckFailed => new AuthorityResponse( - new ErrorMessageResponse(ErrorCode.SwarmIntegrityCheckFailed), - HttpFailureResponse.FailedDependency), - _ => throw new InvalidOperationException($"Unexpected ServerUpdateResult: {updateResult}"), - }; - } + await serverControl.Restart(); + return new AuthorityResponse(); + }); /// - public async ValueTask TriggerServerRestart() + public RequirementsGated> GetLog(string path, CancellationToken cancellationToken) { - if (!serverControl.WatchdogPresent) - { - Logger.LogDebug("Restart request failed due to lack of host watchdog!"); - return new AuthorityResponse( - new ErrorMessageResponse(ErrorCode.MissingHostWatchdog), - HttpFailureResponse.UnprocessableEntity); - } + ArgumentNullException.ThrowIfNull(path); + return new( + () => Flag(AdministrationRights.DownloadLogs), + async () => + { + path = HttpUtility.UrlDecode(path); - await serverControl.Restart(); - return new AuthorityResponse(); + // guard against directory navigation + var sanitizedPath = ioManager.GetFileName(path); + if (path != sanitizedPath) + return Forbid(); + + var fullPath = ioManager.ConcatPath( + fileLoggingConfigurationOptions.Value.GetFullLogDirectory(ioManager, assemblyInformationProvider, platformIdentifier), + path); + try + { + var fileTransferTicket = fileTransferService.CreateDownload( + new FileDownloadProvider( + () => null, + null, + fullPath, + true)); + + return new AuthorityResponse(new LogFileResponse + { + Name = path, + LastModified = await ioManager.GetLastModified(fullPath, cancellationToken), + FileTicket = fileTransferTicket.FileTicket, + }); + } + catch (IOException ex) + { + return Conflict(ErrorCode.IOError, ex.ToString()); + } + }); } } } diff --git a/src/Tgstation.Server.Host/Authority/ChatAuthority.cs b/src/Tgstation.Server.Host/Authority/ChatAuthority.cs new file mode 100644 index 0000000000..742f47adc0 --- /dev/null +++ b/src/Tgstation.Server.Host/Authority/ChatAuthority.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; + +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +using Tgstation.Server.Api.Models; +using Tgstation.Server.Api.Models.Internal; +using Tgstation.Server.Api.Rights; +using Tgstation.Server.Host.Authority.Core; +using Tgstation.Server.Host.Components; +using Tgstation.Server.Host.Database; +using Tgstation.Server.Host.Models; + +namespace Tgstation.Server.Host.Authority +{ + /// + sealed class ChatAuthority : ComponentInterfacingAuthorityBase, IChatAuthority + { + /// + /// Initializes a new instance of the class. + /// + /// The for the . + /// The . + /// The . + public ChatAuthority(IInstanceManager instanceManager, IDatabaseContext databaseContext, ILogger logger) + : base(instanceManager, databaseContext, logger) + { + } + + /// + /// Perform some basic validation of a given . + /// + /// The to validate. + /// If the is being created. + /// An to respond with or . + static AuthorityResponse? StandardModelChecks(ChatBot model, bool forCreation) + { + if (model.ReconnectionInterval == 0) + throw new InvalidOperationException("RecconnectionInterval cannot be zero!"); + + if (model.Name != null && String.IsNullOrWhiteSpace(model.Name)) + return BadRequest(ErrorCode.ChatBotWhitespaceName); + + if (model.ConnectionString != null && String.IsNullOrWhiteSpace(model.ConnectionString)) + return BadRequest(ErrorCode.ChatBotWhitespaceConnectionString); + + var defaultMaxChannels = (ulong)Math.Max(ChatBot.DefaultChannelLimit, model.Channels?.Count ?? 0); + if (defaultMaxChannels > UInt16.MaxValue) + return BadRequest(ErrorCode.ChatBotMaxChannels); + + if (forCreation) + model.ChannelLimit ??= (ushort)defaultMaxChannels; + + return null; + } + + /// + public RequirementsGated> Create( + IEnumerable initialChannels, + string name, + string connectionString, + ChatProvider provider, + long instanceId, + uint? reconnectionInterval, + ushort? channelLimit, + bool enabled, + CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(initialChannels); + ArgumentNullException.ThrowIfNull(name); + ArgumentNullException.ThrowIfNull(connectionString); + + return new( + () => Flag(ChatBotRights.Create), + async () => + { + var model = new ChatBot + { + Name = name, + ConnectionString = connectionString, + Enabled = enabled, + InstanceId = instanceId, + Provider = provider, + ReconnectionInterval = reconnectionInterval, + ChannelLimit = channelLimit, + Channels = initialChannels.ToList(), + }; + + var earlyOut = StandardModelChecks(model, true); + if (earlyOut != null) + return earlyOut; + + var query = await DatabaseContext + .Instances + .Where(instance => instance.Id == instanceId) + .Select(instance => new + { + ChatBotLimit = instance.ChatBotLimit!.Value, + TotalChatBots = instance.ChatSettings!.Count, + }) + .FirstOrDefaultAsync(cancellationToken); + + if (query == null) + return Gone(); + + if (query.TotalChatBots >= query.ChatBotLimit) + return Conflict(ErrorCode.ChatBotMax); + + model.Enabled ??= false; + model.ReconnectionInterval ??= 1; + + DatabaseContext.ChatBots.Add(model); + + await DatabaseContext.Save(cancellationToken); + return await WithComponentInstance( + async instance => + { + try + { + // try to create it + await instance.Chat.ChangeSettings(model, cancellationToken); + + if (model.Channels.Count > 0) + await instance.Chat.ChangeChannels(model.Require(x => x.Id), model.Channels, cancellationToken); + } + catch (Exception ex) + { + Logger.LogError(ex, "Failed to complete chat bot {id} initialization after addition, removing...", model.Id); + + // undo the add + DatabaseContext.ChatBots.Remove(model); + + // DCTx2: Operations must always run + await DatabaseContext.Save(default); + await instance.Chat.DeleteConnection(model.Require(x => x.Id), default); + throw; + } + + return new AuthorityResponse(model, HttpSuccessResponse.Created); + }, + instanceId); + }, + instanceId); + } + } +} diff --git a/src/Tgstation.Server.Host/Authority/Core/AuthorityBase.cs b/src/Tgstation.Server.Host/Authority/Core/AuthorityBase.cs index 0ea01a89da..e7701c903b 100644 --- a/src/Tgstation.Server.Host/Authority/Core/AuthorityBase.cs +++ b/src/Tgstation.Server.Host/Authority/Core/AuthorityBase.cs @@ -8,7 +8,7 @@ using Octokit; using Tgstation.Server.Api.Models; using Tgstation.Server.Api.Models.Response; using Tgstation.Server.Host.Database; -using Tgstation.Server.Host.Security; +using Tgstation.Server.Host.Security.RightsEvaluation; namespace Tgstation.Server.Host.Authority.Core { @@ -17,11 +17,6 @@ namespace Tgstation.Server.Host.Authority.Core /// abstract class AuthorityBase : IAuthority { - /// - /// Gets the for the . - /// - protected IAuthenticationContext AuthenticationContext { get; } - /// /// Gets the for the . /// @@ -88,24 +83,57 @@ namespace Tgstation.Server.Host.Authority.Core /// /// The of the . /// The . + /// for the error message. /// A new, errored . - protected static AuthorityResponse Conflict(ErrorCode errorCode) + protected static AuthorityResponse Conflict(ErrorCode errorCode, string? additionalData = null) => new( - new ErrorMessageResponse(errorCode), + new ErrorMessageResponse(errorCode) + { + AdditionalData = additionalData, + }, HttpFailureResponse.Conflict); + /// + /// Helper to quickly construct a . + /// + /// The to evaluate. + /// The single bit flag of the . + /// A new . + protected static FlagRightsConditional Flag(TRights flag) + where TRights : Enum + => new(flag); + + /// + /// Helper to quickly construct an . + /// + /// The to evaluate. + /// The left hand side operand. + /// The right hand side operand. + /// A new . + protected static OrRightsConditional Or(RightsConditional lhs, RightsConditional rhs) + where TRights : Enum + => new(lhs, rhs); + + /// + /// Helper to quickly construct an . + /// + /// The to evaluate. + /// The left hand side operand. + /// The right hand side operand. + /// A new . + protected static AndRightsConditional And(RightsConditional lhs, RightsConditional rhs) + where TRights : Enum + => new(lhs, rhs); + /// /// Initializes a new instance of the class. /// - /// The value of . /// The value of . /// The value of . protected AuthorityBase( - IAuthenticationContext authenticationContext, IDatabaseContext databaseContext, ILogger logger) { - AuthenticationContext = authenticationContext ?? throw new ArgumentNullException(nameof(authenticationContext)); DatabaseContext = databaseContext ?? throw new ArgumentNullException(nameof(databaseContext)); Logger = logger ?? throw new ArgumentNullException(nameof(logger)); } diff --git a/src/Tgstation.Server.Host/Authority/Core/AuthorityInvokerBase{TAuthority}.cs b/src/Tgstation.Server.Host/Authority/Core/AuthorityInvokerBase{TAuthority}.cs index f04293aecc..f6fce612e0 100644 --- a/src/Tgstation.Server.Host/Authority/Core/AuthorityInvokerBase{TAuthority}.cs +++ b/src/Tgstation.Server.Host/Authority/Core/AuthorityInvokerBase{TAuthority}.cs @@ -1,7 +1,8 @@ using System; using System.Linq; +using System.Threading.Tasks; -using Tgstation.Server.Api.Models; +using Microsoft.AspNetCore.Authorization; namespace Tgstation.Server.Host.Authority.Core { @@ -14,35 +15,60 @@ namespace Tgstation.Server.Host.Authority.Core /// protected TAuthority Authority { get; } + /// + /// The authorization service for the . + /// + readonly Security.IAuthorizationService authorizationService; + /// /// Initializes a new instance of the class. /// /// The value of . - public AuthorityInvokerBase(TAuthority authority) + /// The value of . + public AuthorityInvokerBase( + TAuthority authority, + Security.IAuthorizationService authorizationService) { Authority = authority ?? throw new ArgumentNullException(nameof(authority)); + this.authorizationService = authorizationService ?? throw new ArgumentNullException(nameof(authorizationService)); } /// - IQueryable IAuthorityInvoker.InvokeQueryable(Func> authorityInvoker) + async ValueTask?> IAuthorityInvoker.InvokeQueryable(Func>> authorityInvoker) { ArgumentNullException.ThrowIfNull(authorityInvoker); - return authorityInvoker(Authority); + + var requirementsGate = authorityInvoker(Authority); + return await ExecuteIfRequirementsSatisfied(requirementsGate); } - /// - IQueryable IAuthorityInvoker.InvokeTransformableQueryable(Func> authorityInvoker) + /// + /// Unwrap a result, returning if the requirements weren't satisfied. + /// + /// The contained by the . + /// The result. + /// A resulting in the if the requirements were met, if the requirments weren't met. + protected async ValueTask ExecuteIfRequirementsSatisfied(RequirementsGated requirementsGate) + where TResult : class { - ArgumentNullException.ThrowIfNull(authorityInvoker); + var requirements = await requirementsGate.GetRequirements(); + var authorizationResult = await authorizationService.AuthorizeAsync(requirements, requirementsGate.InstanceId); - var queryable = authorityInvoker(Authority); + if (!authorizationResult.Succeeded) + { + OnRequirementsFailure(authorizationResult.Failure); + return null; + } - if (typeof(EntityId).IsAssignableFrom(typeof(TResult))) - queryable = queryable.OrderBy(item => ((EntityId)(object)item).Id!.Value); // order by ID to fix an EFCore warning + return await requirementsGate.Execute(authorizationService); + } - var expression = new TTransformer().Expression; - return queryable - .Select(expression); + /// + /// Called to handle generic behavior when requirements evaluation fails. + /// + /// The . + protected virtual void OnRequirementsFailure(AuthorizationFailure authFailure) + { } } } diff --git a/src/Tgstation.Server.Host/Authority/Core/AuthorityResponse{TResult}.cs b/src/Tgstation.Server.Host/Authority/Core/AuthorityResponse{TResult}.cs index 9c2cfd295c..361337c7a8 100644 --- a/src/Tgstation.Server.Host/Authority/Core/AuthorityResponse{TResult}.cs +++ b/src/Tgstation.Server.Host/Authority/Core/AuthorityResponse{TResult}.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics.CodeAnalysis; +using System.Linq.Expressions; using Microsoft.AspNetCore.Mvc; using Tgstation.Server.Api.Models.Response; @@ -12,6 +13,11 @@ namespace Tgstation.Server.Host.Authority.Core /// The of success response. public sealed class AuthorityResponse : AuthorityResponse { + /// + /// An expression to convert a into an . The resulting MUST NOT be used. + /// + public static Expression>> MappingExpression { get; } + /// [MemberNotNullWhen(true, nameof(IsNoContent))] public override bool Success => base.Success; @@ -26,13 +32,24 @@ namespace Tgstation.Server.Host.Authority.Core /// /// The success . /// - public TResult? Result { get; } + public TResult? Result { get; private init; } /// /// The for generating the s. /// public HttpSuccessResponse? SuccessResponse { get; } + /// + /// Initializes static members of the class. + /// + static AuthorityResponse() + { + MappingExpression = result => new AuthorityResponse + { + Result = result, + }; + } + /// /// Initializes a new instance of the class. /// diff --git a/src/Tgstation.Server.Host/Authority/Core/ComponentInterfacingAuthorityBase.cs b/src/Tgstation.Server.Host/Authority/Core/ComponentInterfacingAuthorityBase.cs new file mode 100644 index 0000000000..75972dcb6a --- /dev/null +++ b/src/Tgstation.Server.Host/Authority/Core/ComponentInterfacingAuthorityBase.cs @@ -0,0 +1,60 @@ +using System; +using System.Threading.Tasks; + +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +using Serilog.Context; + +using Tgstation.Server.Api.Models; +using Tgstation.Server.Host.Components; +using Tgstation.Server.Host.Database; +using Tgstation.Server.Host.Utils; + +namespace Tgstation.Server.Host.Authority.Core +{ + /// + /// for s that need to access s. + /// + abstract class ComponentInterfacingAuthorityBase : AuthorityBase + { + /// + /// The for the . + /// + readonly IInstanceManager instanceManager; + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + /// The . + /// The . + protected ComponentInterfacingAuthorityBase( + IInstanceManager instanceManager, + IDatabaseContext databaseContext, + ILogger logger) + : base(databaseContext, logger) + { + this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager)); + } + + /// + /// Run a given with the relevant . + /// + /// The type of result the returned uses. + /// A accepting the and returning a with the . + /// The of to grab. + /// A resulting in the that should be returned. + /// The context of should be as small as possible so as to avoid race conditions. This function can return a if the requested instance was offline. + protected async ValueTask> WithComponentInstance(Func>> action, long instanceId) + { + using var instanceReference = instanceManager.GetInstanceReference(instanceId); + using (LogContext.PushProperty(SerilogContextHelper.InstanceReferenceContextProperty, instanceReference?.Uid)) + { + if (instanceReference == null) + return Conflict(ErrorCode.InstanceOffline); + return await action(instanceReference); + } + } + } +} diff --git a/src/Tgstation.Server.Host/Authority/Core/GraphQLAuthorityInvoker{TAuthority}.cs b/src/Tgstation.Server.Host/Authority/Core/GraphQLAuthorityInvoker{TAuthority}.cs index a25a04e278..f160d48af6 100644 --- a/src/Tgstation.Server.Host/Authority/Core/GraphQLAuthorityInvoker{TAuthority}.cs +++ b/src/Tgstation.Server.Host/Authority/Core/GraphQLAuthorityInvoker{TAuthority}.cs @@ -1,6 +1,17 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; +using GreenDonut.Data; + +using HotChocolate; + +using Microsoft.AspNetCore.Authorization; + +using Tgstation.Server.Api.Models; +using Tgstation.Server.Common.Extensions; +using Tgstation.Server.Host.Extensions; using Tgstation.Server.Host.GraphQL; namespace Tgstation.Server.Host.Authority.Core @@ -12,14 +23,17 @@ namespace Tgstation.Server.Host.Authority.Core /// /// Throws a for errored s. /// - /// The potentially errored . + /// The being checked. + /// The potentially errored or if requirements evaluation failed. /// If an error should be raised for and failures. - static void ThrowGraphQLErrorIfNecessary(AuthorityResponse authorityResponse, bool errorOnMissing) + /// if an wasn't thrown. + public static TAuthorityResponse ThrowGraphQLErrorIfNecessary(TAuthorityResponse authorityResponse, bool errorOnMissing) + where TAuthorityResponse : AuthorityResponse { if (authorityResponse.Success || ((authorityResponse.FailureResponse.Value == HttpFailureResponse.NotFound || authorityResponse.FailureResponse.Value == HttpFailureResponse.Gone) && !errorOnMissing)) - return; + return authorityResponse; var fallbackString = authorityResponse.FailureResponse.ToString()!; throw new ErrorMessageException(authorityResponse.ErrorMessage, fallbackString); @@ -29,38 +43,43 @@ namespace Tgstation.Server.Host.Authority.Core /// Initializes a new instance of the class. /// /// The . - public GraphQLAuthorityInvoker(TAuthority authority) - : base(authority) + /// the authorization service to use. + public GraphQLAuthorityInvoker(TAuthority authority, Security.IAuthorizationService authorizationService) + : base(authority, authorizationService) { } /// - async ValueTask IGraphQLAuthorityInvoker.Invoke(Func> authorityInvoker) + async ValueTask IGraphQLAuthorityInvoker.Invoke(Func> authorityInvoker) { ArgumentNullException.ThrowIfNull(authorityInvoker); - var authorityResponse = await authorityInvoker(Authority); + var requirementsGate = authorityInvoker(Authority); + var authorityResponse = await ExecuteIfRequirementsSatisfied(requirementsGate); ThrowGraphQLErrorIfNecessary(authorityResponse, true); } /// - async ValueTask IGraphQLAuthorityInvoker.InvokeAllowMissing(Func>> authorityInvoker) + async ValueTask IGraphQLAuthorityInvoker.InvokeAllowMissing(Func>> authorityInvoker) where TApiModel : default { ArgumentNullException.ThrowIfNull(authorityInvoker); - var authorityResponse = await authorityInvoker(Authority); + var requirementsGate = authorityInvoker(Authority); + var authorityResponse = await ExecuteIfRequirementsSatisfied(requirementsGate); ThrowGraphQLErrorIfNecessary(authorityResponse, false); + return authorityResponse.Result; } /// - async ValueTask IGraphQLAuthorityInvoker.InvokeTransformableAllowMissing(Func>> authorityInvoker) + async ValueTask IGraphQLAuthorityInvoker.InvokeTransformableAllowMissing(Func>> authorityInvoker) where TApiModel : default { ArgumentNullException.ThrowIfNull(authorityInvoker); - var authorityResponse = await authorityInvoker(Authority); + var requirementsGate = authorityInvoker(Authority); + var authorityResponse = await ExecuteIfRequirementsSatisfied(requirementsGate); ThrowGraphQLErrorIfNecessary(authorityResponse, false); var result = authorityResponse.Result; if (result == null) @@ -70,11 +89,78 @@ namespace Tgstation.Server.Host.Authority.Core } /// - ValueTask IGraphQLAuthorityInvoker.Invoke(Func>> authorityInvoker) - => ((IGraphQLAuthorityInvoker)this).InvokeAllowMissing(authorityInvoker)!; + async ValueTask> IGraphQLAuthorityInvoker.InvokeTransformableQueryable( + Func>> authorityInvoker, + Func, IQueryable>? preTransformer) + { + ArgumentNullException.ThrowIfNull(authorityInvoker); + + var requirementsGate = authorityInvoker(Authority); + var queryable = await ExecuteIfRequirementsSatisfied(requirementsGate); + + if (preTransformer != null) + queryable = preTransformer(queryable); + + if (typeof(EntityId).IsAssignableFrom(typeof(TResult))) + queryable = queryable.OrderBy(item => ((EntityId)(object)item).Id!.Value); // order by ID to fix an EFCore warning + + var expression = new TTransformer().Expression; + return queryable + .Select(expression); + } /// - ValueTask IGraphQLAuthorityInvoker.InvokeTransformable(Func>> authorityInvoker) - => ((IGraphQLAuthorityInvoker)this).InvokeTransformableAllowMissing(authorityInvoker)!; + async ValueTask>> IGraphQLAuthorityInvoker.ExecuteDataLoader( + Func>> authorityInvoker, + IReadOnlyList ids, + QueryContext>? queryContext) + { + ArgumentNullException.ThrowIfNull(ids); + + var resultsTask = ValueTaskExtensions.WhenAll( + ids.Select( + id => + { + var requirementsGate = authorityInvoker(Authority, id); + return ExecuteIfRequirementsSatisfied(requirementsGate); + }), + ids.Count); + + var unwrappedContext = queryContext?.AuthorityResponseUnwrap(); + + var results = await resultsTask; + var responses = await Projectable.Combine( + queryable => queryable + .Select(new TTransformer().ProjectedExpression) + .With(unwrappedContext), + results); + + return responses; + } + + /// + async ValueTask IGraphQLAuthorityInvoker.Invoke(Func>> authorityInvoker) + => await ((IGraphQLAuthorityInvoker)this).InvokeAllowMissing(authorityInvoker) + ?? throw new InvalidOperationException("Authority invocation should have returned a non-nullable result!"); + + /// + async ValueTask IGraphQLAuthorityInvoker.InvokeTransformable(Func>> authorityInvoker) + => await ((IGraphQLAuthorityInvoker)this).InvokeTransformableAllowMissing(authorityInvoker) + ?? throw new InvalidOperationException("Authority invocation should have returned a non-nullable result!"); + + /// + protected override void OnRequirementsFailure(AuthorizationFailure authFailure) + => throw authFailure.ForbiddenGraphQLException(); + + /// + /// Unwrap a result, throwing a if they weren't met. + /// + /// The contained by the . + /// The result. + /// A resulting in the if the requirements were met. + /// Throw when requirements were not met. + new async ValueTask ExecuteIfRequirementsSatisfied(RequirementsGated requirementsGate) + where TResult : class + => (await base.ExecuteIfRequirementsSatisfied(requirementsGate))!; // base class throws if requirements evaluation fails } } diff --git a/src/Tgstation.Server.Host/Authority/Core/IAuthorityInvoker{TAuthority}.cs b/src/Tgstation.Server.Host/Authority/Core/IAuthorityInvoker{TAuthority}.cs index 9b49bd9dda..98270fcc96 100644 --- a/src/Tgstation.Server.Host/Authority/Core/IAuthorityInvoker{TAuthority}.cs +++ b/src/Tgstation.Server.Host/Authority/Core/IAuthorityInvoker{TAuthority}.cs @@ -1,7 +1,6 @@ using System; using System.Linq; - -using Tgstation.Server.Host.Models; +using System.Threading.Tasks; namespace Tgstation.Server.Host.Authority.Core { @@ -16,21 +15,8 @@ namespace Tgstation.Server.Host.Authority.Core /// Invoke a method and get the result. /// /// The returned . - /// The returning a . - /// A returned. - IQueryable InvokeQueryable(Func> authorityInvoker); - - /// - /// Invoke a method and get the transformed result. - /// - /// The returned by the . - /// The returned . - /// The for converting s to s. - /// The returning a . - /// A returned. - IQueryable InvokeTransformableQueryable(Func> authorityInvoker) - where TResult : IApiTransformable - where TApiModel : notnull - where TTransformer : ITransformer, new(); + /// The authority invocation returning a . + /// A resulting in the returned on success or if the requirements weren't satisfied. + ValueTask?> InvokeQueryable(Func>> authorityInvoker); } } diff --git a/src/Tgstation.Server.Host/Authority/Core/Projectable{TQueried,TResult}.cs b/src/Tgstation.Server.Host/Authority/Core/Projectable{TQueried,TResult}.cs new file mode 100644 index 0000000000..4ca6c0dd2a --- /dev/null +++ b/src/Tgstation.Server.Host/Authority/Core/Projectable{TQueried,TResult}.cs @@ -0,0 +1,220 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.EntityFrameworkCore; + +using Tgstation.Server.Api.Models; + +namespace Tgstation.Server.Host.Authority.Core +{ + /// + /// A projectable based on an underlying . + /// + /// The model queried. + /// The transformed result . + public sealed class Projectable + where TQueried : EntityId + where TResult : notnull + { + /// + /// The underlying . Should only select one entity. + /// + readonly IQueryable query; + + /// + /// The selector for the data required by the . + /// + readonly Expression, ProjectedPair>> selector; + + /// + /// Mapper for transforming the into an . Called with the output of as . + /// + readonly Func?, AuthorityResponse> resultMapper; + + /// + /// for the operation. + /// + readonly CancellationToken cancellationToken; + + /// + /// Combine a set of s into the resulting s. + /// + /// The projection used to each of the . + /// The s to combine. + /// A of the resulting s keyed by their . + public static async ValueTask>> Combine(Func, IQueryable>> projection, params Projectable[] inputs) + { + ArgumentNullException.ThrowIfNull(projection); + ArgumentNullException.ThrowIfNull(inputs); + + if (inputs.Length == 0) + return new Dictionary>(); + + var firstProjectable = inputs[0]; + var workingSet = projection(firstProjectable.query); + foreach (var projectable in inputs.Skip(1)) + { + if (firstProjectable.resultMapper != projectable.resultMapper) + throw new InvalidOperationException($"Different implementations of {nameof(resultMapper)} in combined {firstProjectable.GetType().Name}."); + if (firstProjectable.selector != projectable.selector) + throw new InvalidOperationException($"Different implementations of {nameof(selector)} in combined {firstProjectable.GetType().Name}."); + + workingSet = workingSet.Union(projection(projectable.query)); + } + + using var cts = CancellationTokenSource.CreateLinkedTokenSource(inputs.Select(projectable => projectable.cancellationToken).ToArray()); + var finalQueryable = workingSet + .Select(CombinedProjectionExpression(firstProjectable.selector)); + + return await finalQueryable + .ToDictionaryAsync( + result => result.Id, + result => firstProjectable.resultMapper(result.Projected)); + } + + /// + /// Create a . + /// + /// The underlying . + /// A mapper for taking a new (Who's will be ) with its set amd converting it into an . + /// The for the operations on . + /// A new . + public static Projectable Create( + IQueryable query, + Func?, AuthorityResponse> resultMapper, + CancellationToken cancellationToken) + => Create( + query, + projected => new ProjectedPair + { + Queried = null, + Result = projected.Result, + }, + resultMapper, + cancellationToken); + + /// + /// Create a . + /// + /// A selected out of to be used in . + /// The underlying . + /// Selects a value to be set as the value for later use in . + /// A mapper for taking a new (Who's will be the result of on the original .) with its set amd converting it into an . + /// The for the operations on . + /// A new . + public static Projectable Create( + IQueryable query, + Expression, ProjectedPair>> selector, + Func?, AuthorityResponse> resultMapper, + CancellationToken cancellationToken) + { + Expression, ProjectedPair>> makeProjectedGeneric = projected => new ProjectedPair + { + Queried = projected.Queried, + Result = projected.Result, + }; + + var parameter = Expression.Parameter(typeof(ProjectedPair), "innerProjectedParam"); + return new( + query, + Expression.Lambda, ProjectedPair>>( + Expression.Invoke( + makeProjectedGeneric, + Expression.Invoke( + selector, + parameter)), + parameter), + projected => resultMapper( + projected != null + ? new ProjectedPair + { + Queried = (TSelection)projected.Queried!, + Result = projected.Result, + } + : null), + cancellationToken); + } + + /// + /// Create an to transform a into a . + /// + /// The selector used for to get business data for the . + /// A new transforming a into a . + private static Expression, CombinedProjection>> CombinedProjectionExpression(Expression, ProjectedPair>> selector) + { + Expression, long>> idSelector = projected => projected.Queried.Id!.Value; + + var parameter = Expression.Parameter(typeof(ProjectedPair), "projectedParamForCombined"); + var selection = Expression.Invoke(selector, parameter); + var id = Expression.Invoke(idSelector, parameter); + + var ourType = typeof(CombinedProjection); + + var memberInitExpr = Expression.MemberInit( + Expression.New(ourType), + Expression.Bind( + ourType.GetProperty(nameof(CombinedProjection.Id))!, + id), + Expression.Bind( + ourType.GetProperty(nameof(CombinedProjection.Projected))!, + selection)); + + var finalExpr = Expression.Lambda, CombinedProjection>>(memberInitExpr, parameter); + + return finalExpr; + } + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + /// The value of . + /// The value of . + /// The value of . + private Projectable( + IQueryable query, + Expression, ProjectedPair>> selector, + Func?, AuthorityResponse> resultMapper, + CancellationToken cancellationToken) + { + this.query = query ?? throw new ArgumentNullException(nameof(query)); + this.selector = selector ?? throw new ArgumentNullException(nameof(selector)); + this.resultMapper = resultMapper ?? throw new ArgumentNullException(nameof(resultMapper)); + this.cancellationToken = cancellationToken; + } + + /// + /// Resolve the . + /// + /// The mapping from to . + /// The resolved . + public async ValueTask> Resolve(Func, IQueryable>> projection) + { + ArgumentNullException.ThrowIfNull(projection); + var selection = await projection(query) + .Select(selector) + .FirstOrDefaultAsync(cancellationToken); + return resultMapper(selection); + } + + /// + /// DTO for selecting the from alongside the selection . + /// + private class CombinedProjection + { + /// + /// The of . + /// + public required long Id { get; init; } + + /// + /// The selection/ . + /// + public required ProjectedPair Projected { get; init; } + } + } +} diff --git a/src/Tgstation.Server.Host/Authority/Core/ProjectedPair{TQueried,TResult}.cs b/src/Tgstation.Server.Host/Authority/Core/ProjectedPair{TQueried,TResult}.cs new file mode 100644 index 0000000000..9f80fe2f8a --- /dev/null +++ b/src/Tgstation.Server.Host/Authority/Core/ProjectedPair{TQueried,TResult}.cs @@ -0,0 +1,20 @@ +namespace Tgstation.Server.Host.Authority.Core +{ + /// + /// DTO for moving database projected s through the system. + /// + /// The originally queried . + /// The output DTO . + public sealed class ProjectedPair + { + /// + /// The originally queried . + /// + public required TQueried Queried { get; init; } + + /// + /// The output DTO . + /// + public required TResult Result { get; init; } + } +} diff --git a/src/Tgstation.Server.Host/Authority/Core/RequirementsGated{TResult}.cs b/src/Tgstation.Server.Host/Authority/Core/RequirementsGated{TResult}.cs new file mode 100644 index 0000000000..7522e1a653 --- /dev/null +++ b/src/Tgstation.Server.Host/Authority/Core/RequirementsGated{TResult}.cs @@ -0,0 +1,169 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +using Microsoft.AspNetCore.Authorization; + +using Tgstation.Server.Host.Security; + +namespace Tgstation.Server.Host.Authority.Core +{ + /// + /// Evaluates a set of s to be checked before executing a response. + /// + /// The of object the response generates. + public sealed class RequirementsGated + { + /// + /// of the relevant instance. + /// + public long? InstanceId { get; } + + /// + /// The retrieval function. is included automatically. + /// + readonly Func>> getRequirements; + + /// + /// The response generation function. + /// + readonly Func> getResponse; + + /// + /// If the should not be added. + /// + readonly bool doNotAddUserSessionValidRequirement; + + /// + /// Convert a given into a . + /// + /// The to convert. + /// A new based on . + public static RequirementsGated FromResult(TResult result) + => new( + () => (IAuthorizationRequirement?)null, + () => ValueTask.FromResult(result)); + + /// + /// Initializes a new instance of the class. + /// + /// The value of . Resulting in a value is eqivalent to returning an empty of s. + /// The value of . + public RequirementsGated( + Func> getRequirement, + Func> getResponse) + { + ArgumentNullException.ThrowIfNull(getRequirement); + ArgumentNullException.ThrowIfNull(getResponse); + getRequirements = async () => + { + var requirement = await getRequirement(); + if (requirement == null) + return Enumerable.Empty(); + + return new List + { + requirement, + }; + }; + this.getResponse = _ => getResponse(); + } + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + /// The value of . + public RequirementsGated( + Func> getRequirements, + Func> getResponse) + { + ArgumentNullException.ThrowIfNull(getRequirements); + ArgumentNullException.ThrowIfNull(getResponse); + this.getRequirements = () => ValueTask.FromResult(getRequirements()); + this.getResponse = _ => getResponse(); + } + + /// + /// Initializes a new instance of the class. + /// + /// The value of . Resulting in a value is eqivalent to returning an empty of s. + /// The value of . + /// The value of . + /// The value of . + public RequirementsGated( + Func getRequirement, + Func> getResponse, + long? instanceId = null, + bool doNotAddUserSessionValidRequirement = false) + { + ArgumentNullException.ThrowIfNull(getRequirement); + ArgumentNullException.ThrowIfNull(getResponse); + getRequirements = () => + { + var requirement = getRequirement(); + if (requirement == null) + return ValueTask.FromResult(Enumerable.Empty()); + + return ValueTask.FromResult>( + new List + { + requirement, + }); + }; + + this.getResponse = _ => getResponse(); + + this.doNotAddUserSessionValidRequirement = doNotAddUserSessionValidRequirement; + InstanceId = instanceId; + } + + /// + /// Initializes a new instance of the class. + /// + /// The value of . Resulting in a value is eqivalent to returning an empty of s. + /// The value of . + public RequirementsGated( + Func getRequirement, + Func> getResponse) + { + ArgumentNullException.ThrowIfNull(getRequirement); + getRequirements = () => + { + var requirement = getRequirement(); + if (requirement == null) + return ValueTask.FromResult(Enumerable.Empty()); + + return ValueTask.FromResult>( + new List + { + requirement, + }); + }; + + this.getResponse = getResponse ?? throw new ArgumentNullException(nameof(getResponse)); + } + + /// + /// Evaluates the s of the request. + /// + /// A resulting in the s for the request. + public async ValueTask> GetRequirements() + { + var requirements = await getRequirements(); + if (!doNotAddUserSessionValidRequirement) + requirements = UserSessionValidRequirement.InstanceAsEnumerable.Concat(requirements); + + return requirements; + } + + /// + /// Executes the request. + /// + /// The authorization service to use. + /// A resulting in the request . + public ValueTask Execute(Security.IAuthorizationService authorizationService) + => getResponse(authorizationService); + } +} diff --git a/src/Tgstation.Server.Host/Authority/Core/RestAuthorityInvoker{TAuthority}.cs b/src/Tgstation.Server.Host/Authority/Core/RestAuthorityInvoker{TAuthority}.cs index 52532c2f0e..f0ac777801 100644 --- a/src/Tgstation.Server.Host/Authority/Core/RestAuthorityInvoker{TAuthority}.cs +++ b/src/Tgstation.Server.Host/Authority/Core/RestAuthorityInvoker{TAuthority}.cs @@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Mvc; using Tgstation.Server.Host.Controllers; using Tgstation.Server.Host.Extensions; +using Tgstation.Server.Host.Security; namespace Tgstation.Server.Host.Authority.Core { @@ -22,7 +23,10 @@ namespace Tgstation.Server.Host.Authority.Core /// An for the . /// The result returned in the . /// The REST API result model built from . - static IActionResult CreateSuccessfulActionResult(ApiController controller, Func resultTransformer, AuthorityResponse authorityResponse) + static IActionResult CreateSuccessfulActionResult( + ApiController controller, + Func resultTransformer, + AuthorityResponse authorityResponse) where TApiModel : notnull { if (authorityResponse.IsNoContent!.Value) @@ -44,9 +48,14 @@ namespace Tgstation.Server.Host.Authority.Core /// /// The to use. /// The . - /// An if the is not successful, otherwise. - static IActionResult? CreateErroredActionResult(ApiController controller, AuthorityResponse authorityResponse) + /// An if the is not successful, otherwise. If is returned, is not . + static IActionResult? CreateErroredActionResult( + ApiController controller, + AuthorityResponse? authorityResponse) { + if (authorityResponse == null) + return controller.Forbid(); + if (authorityResponse.Success) return null; @@ -74,47 +83,51 @@ namespace Tgstation.Server.Host.Authority.Core /// Initializes a new instance of the class. /// /// The . - public RestAuthorityInvoker(TAuthority authority) - : base(authority) + /// The to use. + public RestAuthorityInvoker(TAuthority authority, IAuthorizationService authorizationService) + : base(authority, authorizationService) { } /// - async ValueTask IRestAuthorityInvoker.Invoke(ApiController controller, Func> authorityInvoker) + async ValueTask IRestAuthorityInvoker.Invoke(ApiController controller, Func> authorityInvoker) { ArgumentNullException.ThrowIfNull(controller); ArgumentNullException.ThrowIfNull(authorityInvoker); - var authorityResponse = await authorityInvoker(Authority); + var requirementsGate = authorityInvoker(Authority); + var authorityResponse = await ExecuteIfRequirementsSatisfied(requirementsGate); return CreateErroredActionResult(controller, authorityResponse) ?? controller.NoContent(); } /// - async ValueTask IRestAuthorityInvoker.Invoke(ApiController controller, Func>> authorityInvoker) + async ValueTask IRestAuthorityInvoker.Invoke(ApiController controller, Func>> authorityInvoker) { ArgumentNullException.ThrowIfNull(controller); ArgumentNullException.ThrowIfNull(authorityInvoker); - var authorityResponse = await authorityInvoker(Authority); + var requirementsGate = authorityInvoker(Authority); + var authorityResponse = await ExecuteIfRequirementsSatisfied(requirementsGate); var erroredResult = CreateErroredActionResult(controller, authorityResponse); if (erroredResult != null) return erroredResult; - return CreateSuccessfulActionResult(controller, result => result, authorityResponse); + return CreateSuccessfulActionResult(controller, result => result, authorityResponse!); } /// - async ValueTask IRestAuthorityInvoker.InvokeTransformable(ApiController controller, Func>> authorityInvoker) + async ValueTask IRestAuthorityInvoker.InvokeTransformable(ApiController controller, Func>> authorityInvoker) { ArgumentNullException.ThrowIfNull(controller); ArgumentNullException.ThrowIfNull(authorityInvoker); - var authorityResponse = await authorityInvoker(Authority); + var requirementsGate = authorityInvoker(Authority); + var authorityResponse = await ExecuteIfRequirementsSatisfied(requirementsGate); var erroredResult = CreateErroredActionResult(controller, authorityResponse); if (erroredResult != null) return erroredResult; - return CreateSuccessfulActionResult(controller, result => result.ToApi(), authorityResponse); + return CreateSuccessfulActionResult(controller, result => result.ToApi(), authorityResponse!); } } } diff --git a/src/Tgstation.Server.Host/Authority/IAdministrationAuthority.cs b/src/Tgstation.Server.Host/Authority/IAdministrationAuthority.cs index 7fb28925b5..6853c89ca8 100644 --- a/src/Tgstation.Server.Host/Authority/IAdministrationAuthority.cs +++ b/src/Tgstation.Server.Host/Authority/IAdministrationAuthority.cs @@ -1,11 +1,8 @@ using System; using System.Threading; -using System.Threading.Tasks; using Tgstation.Server.Api.Models.Response; -using Tgstation.Server.Api.Rights; using Tgstation.Server.Host.Authority.Core; -using Tgstation.Server.Host.Security; namespace Tgstation.Server.Host.Authority { @@ -19,9 +16,8 @@ namespace Tgstation.Server.Host.Authority /// /// Bypass the caching that the authority performs for this request, forcing it to contact GitHub. /// The for the operation. - /// A resulting in the . - [TgsAuthorize(AdministrationRights.ChangeVersion)] - ValueTask> GetUpdateInformation(bool forceFresh, CancellationToken cancellationToken); + /// A . + RequirementsGated> GetUpdateInformation(bool forceFresh, CancellationToken cancellationToken); /// /// Triggers a restart of tgstation-server without terminating running game instances, setting its version to a given . @@ -29,15 +25,21 @@ namespace Tgstation.Server.Host.Authority /// The TGS will switch to upon reboot. /// If a will be returned and the call must provide an uploaded zip file containing the update data to the file transfer service. /// The for the operation. - /// A resulting in the . - [TgsAuthorize(AdministrationRights.ChangeVersion | AdministrationRights.UploadVersion)] - ValueTask> TriggerServerVersionChange(Version targetVersion, bool uploadZip, CancellationToken cancellationToken); + /// A . + RequirementsGated> TriggerServerVersionChange(Version targetVersion, bool uploadZip, CancellationToken cancellationToken); /// /// Triggers a restart of tgstation-server without terminating running game instances. /// - /// A resulting in the . - [TgsAuthorize(AdministrationRights.RestartHost)] - ValueTask TriggerServerRestart(); + /// A . + RequirementsGated TriggerServerRestart(); + + /// + /// Get a ticket for downloading a log file at a given . + /// + /// The relative path to the log file in the directory. + /// The for the operation. + /// A . + RequirementsGated> GetLog(string path, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Authority/IChatAuthority.cs b/src/Tgstation.Server.Host/Authority/IChatAuthority.cs new file mode 100644 index 0000000000..c45765bcb2 --- /dev/null +++ b/src/Tgstation.Server.Host/Authority/IChatAuthority.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using System.Threading; + +using Tgstation.Server.Api.Models; +using Tgstation.Server.Host.Authority.Core; +using Tgstation.Server.Host.Models; + +namespace Tgstation.Server.Host.Authority +{ + /// + /// for manipulating chat bots. + /// + public interface IChatAuthority : IAuthority + { + /// + /// Create a new . + /// + /// The initial s for the chat bot. Must have been previously validated to be compatible with the . + /// The name of the chat bot. + /// The connection string for the chat bot. + /// The to use. + /// The ID of the instance the chat bot belongs to. + /// The interval in minutes that TGS attempts to reconnect the chat provider if it disconnects while it is enabled. + /// The maximum number of channels that can be created for the chat bot. + /// If the chat bot is enabled. + /// The for operations. + /// A for the created . + RequirementsGated> Create( + IEnumerable initialChannels, + string name, + string connectionString, + ChatProvider provider, + long instanceId, + uint? reconnectionInterval, + ushort? channelLimit, + bool enabled, + CancellationToken cancellationToken); + } +} diff --git a/src/Tgstation.Server.Host/Authority/IGraphQLAuthorityInvoker{TAuthority}.cs b/src/Tgstation.Server.Host/Authority/IGraphQLAuthorityInvoker{TAuthority}.cs index edface219a..12d044844a 100644 --- a/src/Tgstation.Server.Host/Authority/IGraphQLAuthorityInvoker{TAuthority}.cs +++ b/src/Tgstation.Server.Host/Authority/IGraphQLAuthorityInvoker{TAuthority}.cs @@ -1,7 +1,13 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; +using GreenDonut.Data; + +using Tgstation.Server.Api.Models; using Tgstation.Server.Host.Authority.Core; +using Tgstation.Server.Host.GraphQL.Types; using Tgstation.Server.Host.Models; namespace Tgstation.Server.Host.Authority @@ -10,24 +16,25 @@ namespace Tgstation.Server.Host.Authority /// Invokes s from GraphQL endpoints. /// /// The invoked. + /// We take the approach that fields should be non-nullable if that is the case under ideal circumstances. Authorization issues should throw. public interface IGraphQLAuthorityInvoker : IAuthorityInvoker where TAuthority : IAuthority { /// /// Invoke a method with no success result. /// - /// The returning a resulting in the . + /// The resulting in the . /// A representing the running operation. - ValueTask Invoke(Func> authorityInvoker); + ValueTask Invoke(Func> authorityInvoker); /// /// Invoke a method and get the result. /// /// The . /// The resulting of the return value. - /// The returning a resulting in the . + /// The resulting in the . /// A resulting in the generated for the resulting . - ValueTask InvokeAllowMissing(Func>> authorityInvoker) + ValueTask InvokeAllowMissing(Func>> authorityInvoker) where TResult : TApiModel where TApiModel : notnull; @@ -37,9 +44,9 @@ namespace Tgstation.Server.Host.Authority /// The . /// The resulting of the return value. /// The for converting s to s. - /// The returning a resulting in the . + /// The resulting in the . /// A resulting in the generated for the resulting . - ValueTask InvokeTransformableAllowMissing(Func>> authorityInvoker) + ValueTask InvokeTransformableAllowMissing(Func>> authorityInvoker) where TResult : notnull, IApiTransformable where TApiModel : notnull where TTransformer : ITransformer, new(); @@ -49,9 +56,9 @@ namespace Tgstation.Server.Host.Authority /// /// The . /// The resulting of the return value. - /// The returning a resulting in the . + /// The resulting in the . /// A resulting in the generated for the resulting . - ValueTask Invoke(Func>> authorityInvoker) + ValueTask Invoke(Func>> authorityInvoker) where TResult : TApiModel where TApiModel : notnull; @@ -61,11 +68,45 @@ namespace Tgstation.Server.Host.Authority /// The . /// The resulting of the return value. /// The for converting s to s. - /// The returning a resulting in the . + /// The resulting in the . /// A resulting in the generated for the resulting . - ValueTask InvokeTransformable(Func>> authorityInvoker) + ValueTask InvokeTransformable(Func>> authorityInvoker) where TResult : notnull, IApiTransformable where TApiModel : notnull where TTransformer : ITransformer, new(); + + /// + /// Invoke a method and get the transformed result. + /// + /// The returned by the . + /// The returned . + /// The for converting s to s. + /// The returning a . + /// Optional transformer for the run once it has been acquired. + /// A resulting in the returned on success or if the requirements weren't satisfied. + ValueTask> InvokeTransformableQueryable( + Func>> authorityInvoker, + Func, IQueryable>? preTransformer = null) + where TResult : IApiTransformable + where TApiModel : notnull + where TTransformer : ITransformer, new(); + + /// + /// Execute a batch loaded call on a given . + /// + /// The queried result of the . + /// The returned . + /// The for converting s to s. + /// The returning a from to . + /// The list of s to batch load. + /// The active mapped to an of the . MUST have been wrapped with . This will be unwrapped so only a of will be used. + /// A resulting in a of loaded s to s for the loaded s. + ValueTask>> ExecuteDataLoader( + Func>> authorityInvoker, + IReadOnlyList ids, + QueryContext>? queryContext) + where TResult : EntityId, IApiTransformable + where TApiModel : Entity + where TTransformer : ITransformer, new(); } } diff --git a/src/Tgstation.Server.Host/Authority/ILoginAuthority.cs b/src/Tgstation.Server.Host/Authority/ILoginAuthority.cs index 0558bbfaa0..111d83271f 100644 --- a/src/Tgstation.Server.Host/Authority/ILoginAuthority.cs +++ b/src/Tgstation.Server.Host/Authority/ILoginAuthority.cs @@ -16,13 +16,13 @@ namespace Tgstation.Server.Host.Authority /// /// The for the operation. /// A resulting in a . - ValueTask> AttemptLogin(CancellationToken cancellationToken); + RequirementsGated> AttemptLogin(CancellationToken cancellationToken); /// /// Attempt to login to an OAuth service with the current OAuth credentials. /// /// The for the operation. /// A resulting in an . - ValueTask> AttemptOAuthGatewayLogin(CancellationToken cancellationToken); + RequirementsGated> AttemptOAuthGatewayLogin(CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Authority/IPermissionSetAuthority.cs b/src/Tgstation.Server.Host/Authority/IPermissionSetAuthority.cs index bb10fed383..8e4a370087 100644 --- a/src/Tgstation.Server.Host/Authority/IPermissionSetAuthority.cs +++ b/src/Tgstation.Server.Host/Authority/IPermissionSetAuthority.cs @@ -1,10 +1,8 @@ using System.Threading; using System.Threading.Tasks; -using Tgstation.Server.Api.Rights; using Tgstation.Server.Host.Authority.Core; using Tgstation.Server.Host.Models; -using Tgstation.Server.Host.Security; namespace Tgstation.Server.Host.Authority { @@ -20,7 +18,6 @@ namespace Tgstation.Server.Host.Authority /// The of . /// The for the operation. /// A resulting in a . - [TgsAuthorize(AdministrationRights.ReadUsers)] - ValueTask> GetId(long id, PermissionSetLookupType lookupType, CancellationToken cancellationToken); + RequirementsGated> GetId(long id, PermissionSetLookupType lookupType, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Authority/IRestAuthorityInvoker{TAuthority}.cs b/src/Tgstation.Server.Host/Authority/IRestAuthorityInvoker{TAuthority}.cs index 2cb05a2b9a..0e3b6f3b14 100644 --- a/src/Tgstation.Server.Host/Authority/IRestAuthorityInvoker{TAuthority}.cs +++ b/src/Tgstation.Server.Host/Authority/IRestAuthorityInvoker{TAuthority}.cs @@ -20,9 +20,9 @@ namespace Tgstation.Server.Host.Authority /// Invoke a method with no success result. /// /// The invoking the . - /// The returning a resulting in the . + /// The resulting in the . /// A resulting in the generated for the resulting . - ValueTask Invoke(ApiController controller, Func> authorityInvoker); + ValueTask Invoke(ApiController controller, Func> authorityInvoker); /// /// Invoke a method and get the result. @@ -30,9 +30,9 @@ namespace Tgstation.Server.Host.Authority /// The . /// The resulting of the . /// The invoking the . - /// The returning a resulting in the . + /// The resulting in the . /// A resulting in the generated for the resulting . - ValueTask Invoke(ApiController controller, Func>> authorityInvoker) + ValueTask Invoke(ApiController controller, Func>> authorityInvoker) where TResult : TApiModel where TApiModel : notnull; @@ -42,9 +42,9 @@ namespace Tgstation.Server.Host.Authority /// The . /// The returned REST . /// The invoking the . - /// The returning a resulting in the . + /// The resulting in the . /// A resulting in the generated for the resulting . - ValueTask InvokeTransformable(ApiController controller, Func>> authorityInvoker) + ValueTask InvokeTransformable(ApiController controller, Func>> authorityInvoker) where TResult : notnull, ILegacyApiTransformable where TApiModel : notnull; } diff --git a/src/Tgstation.Server.Host/Authority/IUserAuthority.cs b/src/Tgstation.Server.Host/Authority/IUserAuthority.cs index f60275aceb..26c7ee0846 100644 --- a/src/Tgstation.Server.Host/Authority/IUserAuthority.cs +++ b/src/Tgstation.Server.Host/Authority/IUserAuthority.cs @@ -1,13 +1,10 @@ using System.Linq; using System.Threading; -using System.Threading.Tasks; using Tgstation.Server.Api.Models; using Tgstation.Server.Api.Models.Request; -using Tgstation.Server.Api.Rights; using Tgstation.Server.Host.Authority.Core; using Tgstation.Server.Host.Models; -using Tgstation.Server.Host.Security; namespace Tgstation.Server.Host.Authority { @@ -20,9 +17,8 @@ namespace Tgstation.Server.Host.Authority /// Gets the currently authenticated user. /// /// The for the operation. - /// A resulting in a . - [TgsAuthorize] - ValueTask> Read(CancellationToken cancellationToken); + /// A . + RequirementsGated> Read(CancellationToken cancellationToken); /// /// Gets the with a given . @@ -31,33 +27,42 @@ namespace Tgstation.Server.Host.Authority /// If related entities should be loaded. /// If the may be returned. /// The for the operation. - /// A resulting in a . - [TgsAuthorize(AdministrationRights.ReadUsers)] - ValueTask> GetId(long id, bool includeJoins, bool allowSystemUser, CancellationToken cancellationToken); + /// A . + RequirementsGated> GetId(long id, bool includeJoins, bool allowSystemUser, CancellationToken cancellationToken); + + /// + /// Gets the with a given . + /// + /// The result type after projection. + /// The of the . + /// If the may be returned. + /// The for the operation. + /// A for . + RequirementsGated> GetId(long id, bool allowSystemUser, CancellationToken cancellationToken) + where TResult : class; /// /// Gets the s for the with a given . /// /// The of the . /// The for the operation. - /// A resulting in an of . - ValueTask> OAuthConnections(long userId, CancellationToken cancellationToken); + /// A of . + RequirementsGated> OAuthConnections(long userId, CancellationToken cancellationToken); /// /// Gets the s for the with a given . /// /// The of the . /// The for the operation. - /// A resulting in an of . - ValueTask> OidcConnections(long userId, CancellationToken cancellationToken); + /// A of . + RequirementsGated> OidcConnections(long userId, CancellationToken cancellationToken); /// /// Gets all registered s. /// /// If related entities should be loaded. - /// A of s. - [TgsAuthorize(AdministrationRights.ReadUsers)] - IQueryable Queryable(bool includeJoins); + /// A of s. + RequirementsGated> Queryable(bool includeJoins); /// /// Creates a . @@ -65,9 +70,8 @@ namespace Tgstation.Server.Host.Authority /// The . /// If a zero-length indicates and OAuth only user. /// The for the operation. - /// A resulting in am for the created . - [TgsAuthorize(AdministrationRights.WriteUsers)] - ValueTask> Create( + /// A for the created . + RequirementsGated> Create( UserCreateRequest createRequest, bool? needZeroLengthPasswordWithOAuthConnections, CancellationToken cancellationToken); @@ -77,8 +81,7 @@ namespace Tgstation.Server.Host.Authority /// /// The . /// The for the operation. - /// A resulting in am for the created . - [TgsAuthorize(AdministrationRights.WriteUsers | AdministrationRights.EditOwnPassword | AdministrationRights.EditOwnServiceConnections)] - ValueTask> Update(UserUpdateRequest updateRequest, CancellationToken cancellationToken); + /// A for the created . + RequirementsGated> Update(UserUpdateRequest updateRequest, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Authority/IUserGroupAuthority.cs b/src/Tgstation.Server.Host/Authority/IUserGroupAuthority.cs index 28113b95f5..c63c417ab1 100644 --- a/src/Tgstation.Server.Host/Authority/IUserGroupAuthority.cs +++ b/src/Tgstation.Server.Host/Authority/IUserGroupAuthority.cs @@ -17,8 +17,9 @@ namespace Tgstation.Server.Host.Authority /// /// Gets the current . /// + /// The for the operation. /// A resulting in a . - ValueTask> Read(); + RequirementsGated> Read(CancellationToken cancellationToken); /// /// Gets the with a given . @@ -26,17 +27,17 @@ namespace Tgstation.Server.Host.Authority /// The of the . /// If related entities should be loaded. /// The for the operation. - /// A resulting in a . + /// A . [TgsAuthorize(AdministrationRights.ReadUsers)] - ValueTask> GetId(long id, bool includeJoins, CancellationToken cancellationToken); + RequirementsGated> GetId(long id, bool includeJoins, CancellationToken cancellationToken); /// /// Gets all registered s. /// /// If related entities should be loaded. - /// A of s. + /// A of s. [TgsAuthorize(AdministrationRights.ReadUsers)] - IQueryable Queryable(bool includeJoins); + RequirementsGated> Queryable(bool includeJoins); /// /// Create a . @@ -44,9 +45,9 @@ namespace Tgstation.Server.Host.Authority /// The created 's . /// The created 's . /// The for the operation. - /// A resulting in a . + /// A . [TgsAuthorize(AdministrationRights.WriteUsers)] - ValueTask> Create(string name, PermissionSet? permissionSet, CancellationToken cancellationToken); + RequirementsGated> Create(string name, PermissionSet? permissionSet, CancellationToken cancellationToken); /// /// Updates a . @@ -55,17 +56,17 @@ namespace Tgstation.Server.Host.Authority /// The optional new for the . /// The optional new for the . /// The for the operation. - /// A resulting in a . + /// A resulting in a . [TgsAuthorize(AdministrationRights.WriteUsers)] - ValueTask> Update(long id, string? newName, PermissionSet? newPermissionSet, CancellationToken cancellationToken); + RequirementsGated> Update(long id, string? newName, PermissionSet? newPermissionSet, CancellationToken cancellationToken); /// /// Deletes an empty . /// /// The of the to delete. /// The for the operation. - /// A representing the running operation. + /// A representing the running operation. [TgsAuthorize(AdministrationRights.WriteUsers)] - ValueTask DeleteEmpty(long id, CancellationToken cancellationToken); + RequirementsGated DeleteEmpty(long id, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Authority/LoginAuthority.cs b/src/Tgstation.Server.Host/Authority/LoginAuthority.cs index ab8af50f91..f0286767e4 100644 --- a/src/Tgstation.Server.Host/Authority/LoginAuthority.cs +++ b/src/Tgstation.Server.Host/Authority/LoginAuthority.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -61,9 +62,9 @@ namespace Tgstation.Server.Host.Authority readonly ISessionInvalidationTracker sessionInvalidationTracker; /// - /// The for the . + /// The containing the for the . /// - readonly SecurityConfiguration securityConfiguration; + readonly IOptionsSnapshot securityConfigurationOptions; /// /// Generate an for a given . @@ -103,7 +104,6 @@ namespace Tgstation.Server.Host.Authority /// /// Initializes a new instance of the class. /// - /// The to use. /// The to use. /// The to use. /// The value of . @@ -113,9 +113,8 @@ namespace Tgstation.Server.Host.Authority /// The value of . /// The value of . /// The value of . - /// The containing the value of . + /// The value of . public LoginAuthority( - IAuthenticationContext authenticationContext, IDatabaseContext databaseContext, ILogger logger, IApiHeadersProvider apiHeadersProvider, @@ -125,9 +124,8 @@ namespace Tgstation.Server.Host.Authority ICryptographySuite cryptographySuite, IIdentityCache identityCache, ISessionInvalidationTracker sessionInvalidationTracker, - IOptions securityConfigurationOptions) + IOptionsSnapshot securityConfigurationOptions) : base( - authenticationContext, databaseContext, logger) { @@ -138,14 +136,52 @@ namespace Tgstation.Server.Host.Authority this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite)); this.identityCache = identityCache ?? throw new ArgumentNullException(nameof(identityCache)); this.sessionInvalidationTracker = sessionInvalidationTracker ?? throw new ArgumentNullException(nameof(sessionInvalidationTracker)); - securityConfiguration = securityConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(securityConfigurationOptions)); + this.securityConfigurationOptions = securityConfigurationOptions ?? throw new ArgumentNullException(nameof(securityConfigurationOptions)); } /// - public async ValueTask> AttemptLogin(CancellationToken cancellationToken) + public RequirementsGated> AttemptLogin(CancellationToken cancellationToken) + => new( + () => null, + () => AttemptLoginImpl(cancellationToken), + doNotAddUserSessionValidRequirement: true); + + /// + public RequirementsGated> AttemptOAuthGatewayLogin(CancellationToken cancellationToken) + => new( + () => (IAuthorizationRequirement?)null, + async () => + { + var headers = apiHeadersProvider.ApiHeaders; + if (headers == null) + return GenerateHeadersExceptionResponse(apiHeadersProvider.HeadersException!); + + var oAuthProvider = headers.OAuthProvider; + if (!oAuthProvider.HasValue) + return BadRequest(ErrorCode.BadHeaders); + + var (errorResponse, oAuthResult) = await TryOAuthenticate(headers, oAuthProvider.Value, false, cancellationToken); + if (errorResponse != null) + return errorResponse; + + Logger.LogDebug("Generated {provider} OAuth AccessCode", oAuthProvider.Value); + + return new( + new OAuthGatewayLoginResult + { + AccessCode = oAuthResult!.Value.AccessCode, + }); + }); + + /// + /// Login process. + /// + /// The for the operation. + /// A resulting in the for the . + private async ValueTask> AttemptLoginImpl(CancellationToken cancellationToken) { // password and oauth logins disabled - if (securityConfiguration.OidcStrictMode) + if (securityConfigurationOptions.Value.OidcStrictMode) return Unauthorized(); var headers = apiHeadersProvider.ApiHeaders; @@ -172,7 +208,7 @@ namespace Tgstation.Server.Host.Authority using (systemIdentity) { // Get the user from the database - IQueryable query = DatabaseContext.Users.AsQueryable(); + IQueryable query = DatabaseContext.Users; if (oAuthLogin) { var oAuthProvider = headers.OAuthProvider!.Value; @@ -278,30 +314,6 @@ namespace Tgstation.Server.Host.Authority } } - /// - public async ValueTask> AttemptOAuthGatewayLogin(CancellationToken cancellationToken) - { - var headers = apiHeadersProvider.ApiHeaders; - if (headers == null) - return GenerateHeadersExceptionResponse(apiHeadersProvider.HeadersException!); - - var oAuthProvider = headers.OAuthProvider; - if (!oAuthProvider.HasValue) - return BadRequest(ErrorCode.BadHeaders); - - var (errorResponse, oAuthResult) = await TryOAuthenticate(headers, oAuthProvider.Value, false, cancellationToken); - if (errorResponse != null) - return errorResponse; - - Logger.LogDebug("Generated {provider} OAuth AccessCode", oAuthProvider.Value); - - return new AuthorityResponse( - new OAuthGatewayLoginResult - { - AccessCode = oAuthResult!.Value.AccessCode, - }); - } - /// /// Add a given to the . /// diff --git a/src/Tgstation.Server.Host/Authority/PermissionSetAuthority.cs b/src/Tgstation.Server.Host/Authority/PermissionSetAuthority.cs index 12e63a9ce6..22fdea4114 100644 --- a/src/Tgstation.Server.Host/Authority/PermissionSetAuthority.cs +++ b/src/Tgstation.Server.Host/Authority/PermissionSetAuthority.cs @@ -12,6 +12,7 @@ using Microsoft.Extensions.Logging; using Tgstation.Server.Api.Rights; using Tgstation.Server.Host.Authority.Core; using Tgstation.Server.Host.Database; +using Tgstation.Server.Host.Extensions; using Tgstation.Server.Host.Models; using Tgstation.Server.Host.Security; @@ -25,6 +26,11 @@ namespace Tgstation.Server.Host.Authority /// readonly IPermissionSetsDataLoader permissionSetsDataLoader; + /// + /// The for the . + /// + readonly IClaimsPrincipalAccessor claimsPrincipalAccessor; + /// /// Implements . /// @@ -84,34 +90,58 @@ namespace Tgstation.Server.Host.Authority /// /// Initializes a new instance of the class. /// - /// The to use. /// The to use. /// The to use. /// The value of . + /// The value of . public PermissionSetAuthority( - IAuthenticationContext authenticationContext, IDatabaseContext databaseContext, ILogger logger, - IPermissionSetsDataLoader permissionSetsDataLoader) + IPermissionSetsDataLoader permissionSetsDataLoader, + IClaimsPrincipalAccessor claimsPrincipalAccessor) : base( - authenticationContext, databaseContext, logger) { this.permissionSetsDataLoader = permissionSetsDataLoader ?? throw new ArgumentNullException(nameof(permissionSetsDataLoader)); + this.claimsPrincipalAccessor = claimsPrincipalAccessor ?? throw new ArgumentNullException(nameof(claimsPrincipalAccessor)); } /// - public async ValueTask> GetId(long id, PermissionSetLookupType lookupType, CancellationToken cancellationToken) + public RequirementsGated> GetId(long id, PermissionSetLookupType lookupType, CancellationToken cancellationToken) { - if (id != AuthenticationContext.PermissionSet.Id && !((AdministrationRights)AuthenticationContext.GetRight(RightsType.Administration)).HasFlag(AdministrationRights.ReadUsers)) - return Forbid(); + var permissionSetTask = permissionSetsDataLoader.LoadAsync((Id: id, LookupType: lookupType), cancellationToken); + return new( + async () => + { + var userId = claimsPrincipalAccessor.User.GetTgsUserId(); - var permissionSet = await permissionSetsDataLoader.LoadAsync((Id: id, LookupType: lookupType), cancellationToken); - if (permissionSet == null) - return NotFound(); + var groupIdQuery = DatabaseContext + .Users + .Where(user => user.Id == userId) + .Select(user => user.GroupId); - return new AuthorityResponse(permissionSet); + var permissionSetId = await DatabaseContext + .PermissionSets + .Where(permissionSet => permissionSet.UserId == userId + || groupIdQuery.Contains(permissionSet.GroupId)) + .Select(permissionSet => permissionSet.Id!.Value) + .FirstAsync(cancellationToken); + + if (permissionSetId == id) + return null; + + return Flag(AdministrationRights.ReadUsers); + }, + async () => + { + var permissionSet = await permissionSetTask; + + if (permissionSet == null) + return NotFound(); + + return new AuthorityResponse(permissionSet); + }); } } } diff --git a/src/Tgstation.Server.Host/Authority/UserAuthority.cs b/src/Tgstation.Server.Host/Authority/UserAuthority.cs index e9577dbcfd..1022e79e28 100644 --- a/src/Tgstation.Server.Host/Authority/UserAuthority.cs +++ b/src/Tgstation.Server.Host/Authority/UserAuthority.cs @@ -9,6 +9,7 @@ using GreenDonut; using HotChocolate.Subscriptions; +using Microsoft.AspNetCore.Authorization; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -22,9 +23,11 @@ using Tgstation.Server.Common.Extensions; using Tgstation.Server.Host.Authority.Core; using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.Database; +using Tgstation.Server.Host.Extensions; using Tgstation.Server.Host.Models; using Tgstation.Server.Host.Models.Transformers; using Tgstation.Server.Host.Security; +using Tgstation.Server.Host.Security.RightsEvaluation; namespace Tgstation.Server.Host.Authority { @@ -71,15 +74,20 @@ namespace Tgstation.Server.Host.Authority /// readonly ITopicEventSender topicEventSender; + /// + /// The for the . + /// + readonly IClaimsPrincipalAccessor claimsPrincipalAccessor; + /// /// The of for the . /// readonly IOptionsSnapshot generalConfigurationOptions; /// - /// The of for the . + /// The of for the . /// - readonly IOptions securityConfigurationOptions; + readonly IOptionsSnapshot securityConfigurationOptions; /// /// Implements the . @@ -99,7 +107,6 @@ namespace Tgstation.Server.Host.Authority return databaseContext .Users - .AsQueryable() .Where(x => ids.Contains(x.Id!.Value)) .ToDictionaryAsync(user => user.Id!.Value, cancellationToken); } @@ -122,13 +129,16 @@ namespace Tgstation.Server.Host.Authority var list = await databaseContext .OAuthConnections - .AsQueryable() .Where(x => userIds.Contains(x.User!.Id!.Value)) .ToListAsync(cancellationToken); return list.ToLookup( oAuthConnection => oAuthConnection.UserId, - x => new GraphQL.Types.OAuth.OAuthConnection(x.ExternalUserId!, x.Provider)); + x => new GraphQL.Types.OAuth.OAuthConnection + { + ExternalUserId = x.ExternalUserId!, + Provider = x.Provider, + }); } /// @@ -149,13 +159,16 @@ namespace Tgstation.Server.Host.Authority var list = await databaseContext .OidcConnections - .AsQueryable() .Where(x => userIds.Contains(x.User!.Id!.Value)) .ToListAsync(cancellationToken); return list.ToLookup( oidcConnection => oidcConnection.UserId, - x => new GraphQL.Types.OAuth.OidcConnection(x.ExternalUserId!, x.SchemeKey!)); + x => new GraphQL.Types.OAuth.OidcConnection + { + ExternalUserId = x.ExternalUserId!, + SchemeKey = x.SchemeKey!, + }); } /// @@ -164,22 +177,21 @@ namespace Tgstation.Server.Host.Authority /// The to check. /// If this is a new . /// if is valid, an errored otherwise. - static AuthorityResponse? CheckValidName(UserUpdateRequest model, bool newUser) + static AuthorityResponse? CheckValidName(UserUpdateRequest model, bool newUser) { var userInvalidWithNullName = newUser && model.Name == null && model.SystemIdentifier == null; if (userInvalidWithNullName || (model.Name != null && String.IsNullOrWhiteSpace(model.Name))) - return BadRequest(ErrorCode.UserMissingName); + return BadRequest(ErrorCode.UserMissingName); model.Name = model.Name?.Trim(); if (model.Name != null && model.Name.Contains(':', StringComparison.InvariantCulture)) - return BadRequest(ErrorCode.UserColonInName); + return BadRequest(ErrorCode.UserColonInName); return null; } /// /// Initializes a new instance of the class. /// - /// The to use. /// The to use. /// The to use. /// The value of . @@ -190,10 +202,10 @@ namespace Tgstation.Server.Host.Authority /// The value of . /// The value of . /// The value of . + /// The value of . /// The value of . /// The value of . public UserAuthority( - IAuthenticationContext authenticationContext, IDatabaseContext databaseContext, ILogger logger, IUsersDataLoader usersDataLoader, @@ -204,10 +216,10 @@ namespace Tgstation.Server.Host.Authority ICryptographySuite cryptographySuite, ISessionInvalidationTracker sessionInvalidationTracker, ITopicEventSender topicEventSender, + IClaimsPrincipalAccessor claimsPrincipalAccessor, IOptionsSnapshot generalConfigurationOptions, - IOptions securityConfigurationOptions) + IOptionsSnapshot securityConfigurationOptions) : base( - authenticationContext, databaseContext, logger) { @@ -219,6 +231,7 @@ namespace Tgstation.Server.Host.Authority this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite)); this.sessionInvalidationTracker = sessionInvalidationTracker ?? throw new ArgumentNullException(nameof(sessionInvalidationTracker)); this.topicEventSender = topicEventSender ?? throw new ArgumentNullException(nameof(topicEventSender)); + this.claimsPrincipalAccessor = claimsPrincipalAccessor ?? throw new ArgumentNullException(nameof(claimsPrincipalAccessor)); this.generalConfigurationOptions = generalConfigurationOptions ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); this.securityConfigurationOptions = securityConfigurationOptions ?? throw new ArgumentNullException(nameof(securityConfigurationOptions)); } @@ -233,11 +246,11 @@ namespace Tgstation.Server.Host.Authority static bool BadCreateRequestChecks( UserCreateRequest createRequest, bool? needZeroLengthPasswordWithOAuthConnections, - [NotNullWhen(true)] out AuthorityResponse? failResponse) + [NotNullWhen(true)] out AuthorityResponse? failResponse) { if (createRequest.OAuthConnections?.Any(x => x == null) == true) { - failResponse = BadRequest(ErrorCode.ModelValidationFailure); + failResponse = BadRequest(ErrorCode.ModelValidationFailure); return true; } @@ -247,7 +260,7 @@ namespace Tgstation.Server.Host.Authority if ((hasNonNullPassword && hasNonNullSystemIdentifier) || (!hasNonNullPassword && !hasNonNullSystemIdentifier && !hasOAuthConnections)) { - failResponse = BadRequest(ErrorCode.UserMismatchPasswordSid); + failResponse = BadRequest(ErrorCode.UserMismatchPasswordSid); return true; } @@ -261,20 +274,20 @@ namespace Tgstation.Server.Host.Authority if (createRequest.OAuthConnections.Count == 0) { - failResponse = BadRequest(ErrorCode.ModelValidationFailure); + failResponse = BadRequest(ErrorCode.ModelValidationFailure); return true; } } else if (hasZeroLengthPassword) { - failResponse = BadRequest(ErrorCode.ModelValidationFailure); + failResponse = BadRequest(ErrorCode.ModelValidationFailure); return true; } } if (createRequest.Group != null && createRequest.PermissionSet != null) { - failResponse = BadRequest(ErrorCode.UserGroupAndPermissionSet); + failResponse = BadRequest(ErrorCode.UserGroupAndPermissionSet); return true; } @@ -284,7 +297,7 @@ namespace Tgstation.Server.Host.Authority if (!(createRequest.Name == null ^ createRequest.SystemIdentifier == null)) { - failResponse = BadRequest(ErrorCode.UserMismatchNameSid); + failResponse = BadRequest(ErrorCode.UserMismatchNameSid); return true; } @@ -293,15 +306,384 @@ namespace Tgstation.Server.Host.Authority } /// - public ValueTask> Read(CancellationToken cancellationToken) - => ValueTask.FromResult(new AuthorityResponse(AuthenticationContext.User)); + public RequirementsGated> Read(CancellationToken cancellationToken) + => new( + () => Enumerable.Empty(), + () => GetIdImpl(claimsPrincipalAccessor.User.RequireTgsUserId(), true, false, cancellationToken)); /// - public async ValueTask> GetId(long id, bool includeJoins, bool allowSystemUser, CancellationToken cancellationToken) - { - if (id != AuthenticationContext.User.Id && !((AdministrationRights)AuthenticationContext.GetRight(RightsType.Administration)).HasFlag(AdministrationRights.ReadUsers)) - return Forbid(); + public RequirementsGated> GetId(long id, bool includeJoins, bool allowSystemUser, CancellationToken cancellationToken) + => new( + () => + { + if (id != claimsPrincipalAccessor.User.GetTgsUserId()) + return Enumerable.Empty(); + return new List + { + Flag(AdministrationRights.ReadUsers), + }; + }, + () => GetIdImpl(id, includeJoins, allowSystemUser, cancellationToken)); + + /// + public RequirementsGated> Queryable(bool includeJoins) + => new( + () => Flag(AdministrationRights.ReadUsers), + () => ValueTask.FromResult(Queryable(includeJoins, false))); + + /// + public RequirementsGated> OAuthConnections(long userId, CancellationToken cancellationToken) + => new( + () => claimsPrincipalAccessor.User.GetTgsUserId() != userId + ? Flag(AdministrationRights.ReadUsers) + : null, + async () => new AuthorityResponse( + await oAuthConnectionsDataLoader.LoadRequiredAsync(userId, cancellationToken))); + + /// + public RequirementsGated> OidcConnections(long userId, CancellationToken cancellationToken) + => new( + () => claimsPrincipalAccessor.User.GetTgsUserId() != userId + ? Flag(AdministrationRights.ReadUsers) + : null, + async () => new AuthorityResponse( + await oidcConnectionsDataLoader.LoadRequiredAsync(userId, cancellationToken))); + + /// +#pragma warning disable CA1506 // TODO: Decomplexify + public RequirementsGated> Create( + UserCreateRequest createRequest, + bool? needZeroLengthPasswordWithOAuthConnections, + CancellationToken cancellationToken) +#pragma warning restore CA1506 + => new( + () => Flag(AdministrationRights.WriteUsers), + async authorizationService => + { + ArgumentNullException.ThrowIfNull(createRequest); + + if (BadCreateRequestChecks(createRequest, needZeroLengthPasswordWithOAuthConnections, out var failResponse)) + return failResponse; + + var totalUsers = await DatabaseContext + .Users + .CountAsync(cancellationToken); + if (totalUsers >= generalConfigurationOptions.Value.UserLimit) + return Conflict(ErrorCode.UserLimitReached); + + var dbUser = await CreateNewUserFromModel( + createRequest, + cancellationToken); + if (dbUser == null) + return Gone(); + + if (createRequest.SystemIdentifier != null) + try + { + using var sysIdentity = await systemIdentityFactory.CreateSystemIdentity(dbUser, cancellationToken); + if (sysIdentity == null) + return Gone(); + dbUser.Name = sysIdentity.Username; + dbUser.SystemIdentifier = sysIdentity.Uid; + } + catch (NotImplementedException ex) + { + Logger.LogTrace(ex, "System identities not implemented!"); + return new AuthorityResponse( + new ErrorMessageResponse(ErrorCode.RequiresPosixSystemIdentity), + HttpFailureResponse.NotImplemented); + } + else + { + var hasZeroLengthPassword = createRequest.Password?.Length == 0; + var hasOAuthConnections = (createRequest.OAuthConnections?.Count > 0) == true; + + // special case allow PasswordHash to be null by setting Password to "" if OAuthConnections are set + if (!(needZeroLengthPasswordWithOAuthConnections != false && hasZeroLengthPassword && hasOAuthConnections)) + { + var result = TrySetPassword(dbUser, createRequest.Password!, true); + if (result != null) + return result; + } + } + + dbUser.CanonicalName = User.CanonicalizeName(dbUser.Name!); + + DatabaseContext.Users.Add(dbUser); + + await DatabaseContext.Save(cancellationToken); + + Logger.LogInformation("Created new user {name} ({id})", dbUser.Name, dbUser.Id); + + var responseTask = UpdatedUserResponse(authorizationService, dbUser, HttpSuccessResponse.Created); + + await SendUserUpdatedTopics(dbUser); + + return await responseTask; + }); + + /// +#pragma warning disable CA1502 +#pragma warning disable CA1506 // TODO: Decomplexify + public RequirementsGated> Update(UserUpdateRequest model, CancellationToken cancellationToken) +#pragma warning restore CA1502 +#pragma warning restore CA1506 + => new( + () => + { + RightsConditional? conditional = null; + + // Ensure they are only trying to edit things they have perms for (system identity change will trigger a bad request) + if (model.OidcConnections != null || model.OAuthConnections != null) + conditional = Flag(AdministrationRights.EditOwnServiceConnections); + + if (model.Password != null && model.Id == claimsPrincipalAccessor.User.GetTgsUserId()) + { + var newFlag = Flag(AdministrationRights.EditOwnPassword); + if (conditional != null) + conditional = And(conditional, newFlag); + else + conditional = newFlag; + } + + if (conditional != null) + conditional = Or(conditional, Flag(AdministrationRights.WriteUsers)); + else if (model.Enabled.HasValue + || model.Group != null + || model.Name != null + || model.PermissionSet != null) + conditional = Flag(AdministrationRights.WriteUsers); + + return conditional; + }, + async authorizationService => + { + ArgumentNullException.ThrowIfNull(model); + + if (!model.Id.HasValue || model.OAuthConnections?.Any(x => x == null) == true) + return BadRequest(ErrorCode.ModelValidationFailure); + + if (model.Group != null && model.PermissionSet != null) + return BadRequest(ErrorCode.UserGroupAndPermissionSet); + + var userQuery = DatabaseContext + .Users + .Where(x => x.Id == model.Id) + .Include(x => x.CreatedBy) + .Include(x => x.OAuthConnections) + .Include(x => x.OidcConnections) + .Include(x => x.Group!) + .ThenInclude(x => x.PermissionSet) + .Include(x => x.PermissionSet) + .FirstOrDefaultAsync(cancellationToken); + + var originalUser = await userQuery; + + if (originalUser == default) + return NotFound(); + + if (originalUser.CanonicalName == User.CanonicalizeName(User.TgsSystemUserName)) + return Forbid(); + + var originalUserHasSid = originalUser.SystemIdentifier != null; + var invalidateSessions = false; + if (originalUserHasSid && originalUser.PasswordHash != null) + { + // cleanup from https://github.com/tgstation/tgstation-server/issues/1528 + Logger.LogDebug("System user ID {userId}'s PasswordHash is polluted, updating database.", originalUser.Id); + originalUser.PasswordHash = null; + + invalidateSessions = true; + } + + if (model.SystemIdentifier != null && model.SystemIdentifier != originalUser.SystemIdentifier) + return BadRequest(ErrorCode.UserSidChange); + + if (model.Password != null) + { + if (originalUserHasSid) + return BadRequest(ErrorCode.UserMismatchPasswordSid); + + var result = TrySetPassword(originalUser, model.Password, false); + if (result != null) + return result; + + invalidateSessions = true; + } + + if (model.Name != null && User.CanonicalizeName(model.Name) != originalUser.CanonicalName) + return BadRequest(ErrorCode.UserNameChange); + + if (model.OAuthConnections != null + && (model.OAuthConnections.Count != originalUser.OAuthConnections!.Count + || !model.OAuthConnections.All(x => originalUser.OAuthConnections.Any(y => y.Provider == x.Provider && y.ExternalUserId == x.ExternalUserId)))) + { + if (securityConfigurationOptions.Value.OidcStrictMode) + return BadRequest(ErrorCode.BadUserEditDueToOidcStrictMode); + + if (originalUser.CanonicalName == User.CanonicalizeName(DefaultCredentials.AdminUserName)) + return BadRequest(ErrorCode.AdminUserCannotHaveServiceConnection); + + if (model.OAuthConnections.Count == 0 && originalUser.PasswordHash == null && originalUser.SystemIdentifier == null) + return BadRequest(ErrorCode.CannotRemoveLastAuthenticationOption); + + DatabaseContext.OAuthConnections.RemoveRange(originalUser.OAuthConnections); + originalUser.OAuthConnections.Clear(); + + foreach (var updatedConnection in model.OAuthConnections) + originalUser.OAuthConnections.Add(new Models.OAuthConnection + { + Provider = updatedConnection.Provider, + ExternalUserId = updatedConnection.ExternalUserId, + }); + } + + if (model.OidcConnections != null + && (model.OidcConnections.Count != originalUser.OidcConnections!.Count + || !model.OidcConnections.All(x => originalUser.OidcConnections.Any(y => y.SchemeKey == x.SchemeKey && y.ExternalUserId == x.ExternalUserId)))) + { + if (securityConfigurationOptions.Value.OidcStrictMode) + return BadRequest(ErrorCode.BadUserEditDueToOidcStrictMode); + + if (originalUser.CanonicalName == User.CanonicalizeName(DefaultCredentials.AdminUserName)) + return BadRequest(ErrorCode.AdminUserCannotHaveServiceConnection); + + if (model.OidcConnections.Count == 0 && originalUser.PasswordHash == null && originalUser.SystemIdentifier == null) + return BadRequest(ErrorCode.CannotRemoveLastAuthenticationOption); + + DatabaseContext.OidcConnections.RemoveRange(originalUser.OidcConnections); + originalUser.OidcConnections.Clear(); + foreach (var updatedConnection in model.OidcConnections) + originalUser.OidcConnections.Add(new Models.OidcConnection + { + SchemeKey = updatedConnection.SchemeKey, + ExternalUserId = updatedConnection.ExternalUserId, + }); + } + + if (model.Group != null) + { + if (securityConfigurationOptions.Value.OidcStrictMode) + return BadRequest(ErrorCode.BadUserEditDueToOidcStrictMode); + + originalUser.Group = await DatabaseContext + .Groups + .Where(x => x.Id == model.Group.Id) + .Include(x => x.PermissionSet) + .FirstOrDefaultAsync(cancellationToken); + + if (originalUser.Group == default) + return Gone(); + + DatabaseContext.Groups.Attach(originalUser.Group); + if (originalUser.PermissionSet != null) + { + Logger.LogInformation("Deleting permission set {permissionSetId}...", originalUser.PermissionSet.Id); + DatabaseContext.PermissionSets.Remove(originalUser.PermissionSet); + originalUser.PermissionSet = null; + } + } + else if (model.PermissionSet != null) + { + if (securityConfigurationOptions.Value.OidcStrictMode) + return BadRequest(ErrorCode.BadUserEditDueToOidcStrictMode); + + if (originalUser.PermissionSet == null) + { + Logger.LogTrace("Creating new permission set..."); + originalUser.PermissionSet = new Models.PermissionSet(); + } + + originalUser.PermissionSet.AdministrationRights = model.PermissionSet.AdministrationRights ?? AdministrationRights.None; + originalUser.PermissionSet.InstanceManagerRights = model.PermissionSet.InstanceManagerRights ?? InstanceManagerRights.None; + + originalUser.Group = null; + originalUser.GroupId = null; + } + + var fail = CheckValidName(model, false); + if (fail != null) + return fail; + + originalUser.Name = model.Name ?? originalUser.Name; + + if (model.Enabled.HasValue) + { + if (securityConfigurationOptions.Value.OidcStrictMode) + return BadRequest(ErrorCode.BadUserEditDueToOidcStrictMode); + + invalidateSessions = originalUser.Require(x => x.Enabled) && !model.Enabled.Value; + originalUser.Enabled = model.Enabled.Value; + } + + if (invalidateSessions) + sessionInvalidationTracker.UserModifiedInvalidateSessions(originalUser); + + await DatabaseContext.Save(cancellationToken); + + Logger.LogInformation("Updated user {userName} ({userId})", originalUser.Name, originalUser.Id); + + var responseTask = UpdatedUserResponse(authorizationService, originalUser, HttpSuccessResponse.Ok); + + ValueTask sessionInvalidationTask; + if (invalidateSessions) + sessionInvalidationTask = permissionsUpdateNotifyee.UserDisabled(originalUser, cancellationToken); + else + sessionInvalidationTask = ValueTask.CompletedTask; + + await ValueTaskExtensions.WhenAll(SendUserUpdatedTopics(originalUser), sessionInvalidationTask); + + return await responseTask; + }); + + /// + public RequirementsGated> GetId(long id, bool allowSystemUser, CancellationToken cancellationToken) + where TResult : class + => new( + () => + { + if (id != claimsPrincipalAccessor.User.GetTgsUserId()) + return Enumerable.Empty(); + + return new List + { + Flag(AdministrationRights.ReadUsers), + }; + }, + () => ValueTask.FromResult( + Projectable.Create( + Queryable(true, allowSystemUser) + .Where(user => user.Id == id) + .TagWith("User by ID"), + projected => new ProjectedPair + { + Queried = projected.Queried.CanonicalName!, + Result = projected.Result, + }, + projected => + { + if (projected == default) + return NotFound(); + + string canonicalName = projected.Queried; + if (!allowSystemUser && canonicalName == User.CanonicalizeName(User.TgsSystemUserName)) + return Forbid(); + + return new AuthorityResponse(projected.Result); + }, + cancellationToken))); + + /// + /// Implementation of retrieving a by ID. + /// + /// The of the user to retrieve. + /// If related entities should be loaded. + /// If the may be returned. + /// The for the operation. + /// A . + async ValueTask> GetIdImpl(long id, bool includeJoins, bool allowSystemUser, CancellationToken cancellationToken) + { User? user; if (includeJoins) { @@ -323,286 +705,29 @@ namespace Tgstation.Server.Host.Authority return new AuthorityResponse(user); } - /// - public IQueryable Queryable(bool includeJoins) - => Queryable(includeJoins, false); - - /// - public async ValueTask> OAuthConnections(long userId, CancellationToken cancellationToken) - => new AuthorityResponse( - await oAuthConnectionsDataLoader.LoadRequiredAsync(userId, cancellationToken)); - - /// - public async ValueTask> OidcConnections(long userId, CancellationToken cancellationToken) - => new AuthorityResponse( - await oidcConnectionsDataLoader.LoadRequiredAsync(userId, cancellationToken)); - - /// - public async ValueTask> Create( - UserCreateRequest createRequest, - bool? needZeroLengthPasswordWithOAuthConnections, - CancellationToken cancellationToken) + /// + /// Create the for an . + /// + /// The authorization service to use. + /// The for the result. + /// The to use. + /// A resulting in the . + async ValueTask> UpdatedUserResponse( + Security.IAuthorizationService authorizationService, + User user, + HttpSuccessResponse successResponse) { - ArgumentNullException.ThrowIfNull(createRequest); - - if (BadCreateRequestChecks(createRequest, needZeroLengthPasswordWithOAuthConnections, out var failResponse)) - return failResponse; - - var totalUsers = await DatabaseContext - .Users - .AsQueryable() - .CountAsync(cancellationToken); - if (totalUsers >= generalConfigurationOptions.Value.UserLimit) - return Conflict(ErrorCode.UserLimitReached); - - var dbUser = await CreateNewUserFromModel(createRequest, cancellationToken); - if (dbUser == null) - return Gone(); - - if (createRequest.SystemIdentifier != null) - try - { - using var sysIdentity = await systemIdentityFactory.CreateSystemIdentity(dbUser, cancellationToken); - if (sysIdentity == null) - return Gone(); - dbUser.Name = sysIdentity.Username; - dbUser.SystemIdentifier = sysIdentity.Uid; - } - catch (NotImplementedException ex) - { - Logger.LogTrace(ex, "System identities not implemented!"); - return new AuthorityResponse( - new ErrorMessageResponse(ErrorCode.RequiresPosixSystemIdentity), - HttpFailureResponse.NotImplemented); - } - else - { - var hasZeroLengthPassword = createRequest.Password?.Length == 0; - var hasOAuthConnections = (createRequest.OAuthConnections?.Count > 0) == true; - - // special case allow PasswordHash to be null by setting Password to "" if OAuthConnections are set - if (!(needZeroLengthPasswordWithOAuthConnections != false && hasZeroLengthPassword && hasOAuthConnections)) - { - var result = TrySetPassword(dbUser, createRequest.Password!, true); - if (result != null) - return result; - } - } - - dbUser.CanonicalName = User.CanonicalizeName(dbUser.Name!); - - DatabaseContext.Users.Add(dbUser); - - await DatabaseContext.Save(cancellationToken); - - Logger.LogInformation("Created new user {name} ({id})", dbUser.Name, dbUser.Id); - - await SendUserUpdatedTopics(dbUser); - - return new AuthorityResponse(dbUser, HttpSuccessResponse.Created); - } - - /// -#pragma warning disable CA1502 -#pragma warning disable CA1506 // TODO: Decomplexify - public async ValueTask> Update(UserUpdateRequest model, CancellationToken cancellationToken) -#pragma warning restore CA1502 -#pragma warning restore CA1506 - { - ArgumentNullException.ThrowIfNull(model); - - if (!model.Id.HasValue || model.OAuthConnections?.Any(x => x == null) == true) - return BadRequest(ErrorCode.ModelValidationFailure); - - if (model.Group != null && model.PermissionSet != null) - return BadRequest(ErrorCode.UserGroupAndPermissionSet); - - var callerAdministrationRights = (AdministrationRights)AuthenticationContext.GetRight(RightsType.Administration); - var canEditAllUsers = callerAdministrationRights.HasFlag(AdministrationRights.WriteUsers); - var passwordEdit = canEditAllUsers || callerAdministrationRights.HasFlag(AdministrationRights.EditOwnPassword); - var oAuthEdit = canEditAllUsers || callerAdministrationRights.HasFlag(AdministrationRights.EditOwnServiceConnections); - - var originalUser = !canEditAllUsers - ? AuthenticationContext.User - : await DatabaseContext - .Users - .AsQueryable() - .Where(x => x.Id == model.Id) - .Include(x => x.CreatedBy) - .Include(x => x.OAuthConnections) - .Include(x => x.OidcConnections) - .Include(x => x.Group!) - .ThenInclude(x => x.PermissionSet) - .Include(x => x.PermissionSet) - .FirstOrDefaultAsync(cancellationToken); - - if (originalUser == default) - return NotFound(); - - if (originalUser.CanonicalName == User.CanonicalizeName(User.TgsSystemUserName)) - return Forbid(); - - // Ensure they are only trying to edit things they have perms for (system identity change will trigger a bad request) - if ((!canEditAllUsers - && (model.Id != originalUser.Id - || model.Enabled.HasValue - || model.Group != null - || model.PermissionSet != null - || model.Name != null)) - || (!passwordEdit && model.Password != null) - || (!oAuthEdit && model.OAuthConnections != null)) - return Forbid(); - - var originalUserHasSid = originalUser.SystemIdentifier != null; - var invalidateSessions = false; - if (originalUserHasSid && originalUser.PasswordHash != null) - { - // cleanup from https://github.com/tgstation/tgstation-server/issues/1528 - Logger.LogDebug("System user ID {userId}'s PasswordHash is polluted, updating database.", originalUser.Id); - originalUser.PasswordHash = null; - - invalidateSessions = true; - } - - if (model.SystemIdentifier != null && model.SystemIdentifier != originalUser.SystemIdentifier) - return BadRequest(ErrorCode.UserSidChange); - - if (model.Password != null) - { - if (originalUserHasSid) - return BadRequest(ErrorCode.UserMismatchPasswordSid); - - var result = TrySetPassword(originalUser, model.Password, false); - if (result != null) - return result; - - invalidateSessions = true; - } - - if (model.Name != null && User.CanonicalizeName(model.Name) != originalUser.CanonicalName) - return BadRequest(ErrorCode.UserNameChange); - - if (model.OAuthConnections != null - && (model.OAuthConnections.Count != originalUser.OAuthConnections!.Count - || !model.OAuthConnections.All(x => originalUser.OAuthConnections.Any(y => y.Provider == x.Provider && y.ExternalUserId == x.ExternalUserId)))) - { - if (securityConfigurationOptions.Value.OidcStrictMode) - return BadRequest(ErrorCode.BadUserEditDueToOidcStrictMode); - - if (originalUser.CanonicalName == User.CanonicalizeName(DefaultCredentials.AdminUserName)) - return BadRequest(ErrorCode.AdminUserCannotHaveServiceConnection); - - if (model.OAuthConnections.Count == 0 && originalUser.PasswordHash == null && originalUser.SystemIdentifier == null) - return BadRequest(ErrorCode.CannotRemoveLastAuthenticationOption); - - DatabaseContext.OAuthConnections.RemoveRange(originalUser.OAuthConnections); - originalUser.OAuthConnections.Clear(); - - foreach (var updatedConnection in model.OAuthConnections) - originalUser.OAuthConnections.Add(new Models.OAuthConnection - { - Provider = updatedConnection.Provider, - ExternalUserId = updatedConnection.ExternalUserId, - }); - } - - if (model.OidcConnections != null - && (model.OidcConnections.Count != originalUser.OidcConnections!.Count - || !model.OidcConnections.All(x => originalUser.OidcConnections.Any(y => y.SchemeKey == x.SchemeKey && y.ExternalUserId == x.ExternalUserId)))) - { - if (securityConfigurationOptions.Value.OidcStrictMode) - return BadRequest(ErrorCode.BadUserEditDueToOidcStrictMode); - - if (originalUser.CanonicalName == User.CanonicalizeName(DefaultCredentials.AdminUserName)) - return BadRequest(ErrorCode.AdminUserCannotHaveServiceConnection); - - if (model.OidcConnections.Count == 0 && originalUser.PasswordHash == null && originalUser.SystemIdentifier == null) - return BadRequest(ErrorCode.CannotRemoveLastAuthenticationOption); - - DatabaseContext.OidcConnections.RemoveRange(originalUser.OidcConnections); - originalUser.OidcConnections.Clear(); - foreach (var updatedConnection in model.OidcConnections) - originalUser.OidcConnections.Add(new Models.OidcConnection - { - SchemeKey = updatedConnection.SchemeKey, - ExternalUserId = updatedConnection.ExternalUserId, - }); - } - - if (model.Group != null) - { - if (securityConfigurationOptions.Value.OidcStrictMode) - return BadRequest(ErrorCode.BadUserEditDueToOidcStrictMode); - - originalUser.Group = await DatabaseContext - .Groups - .AsQueryable() - .Where(x => x.Id == model.Group.Id) - .Include(x => x.PermissionSet) - .FirstOrDefaultAsync(cancellationToken); - - if (originalUser.Group == default) - return Gone(); - - DatabaseContext.Groups.Attach(originalUser.Group); - if (originalUser.PermissionSet != null) - { - Logger.LogInformation("Deleting permission set {permissionSetId}...", originalUser.PermissionSet.Id); - DatabaseContext.PermissionSets.Remove(originalUser.PermissionSet); - originalUser.PermissionSet = null; - } - } - else if (model.PermissionSet != null) - { - if (securityConfigurationOptions.Value.OidcStrictMode) - return BadRequest(ErrorCode.BadUserEditDueToOidcStrictMode); - - if (originalUser.PermissionSet == null) - { - Logger.LogTrace("Creating new permission set..."); - originalUser.PermissionSet = new Models.PermissionSet(); - } - - originalUser.PermissionSet.AdministrationRights = model.PermissionSet.AdministrationRights ?? AdministrationRights.None; - originalUser.PermissionSet.InstanceManagerRights = model.PermissionSet.InstanceManagerRights ?? InstanceManagerRights.None; - - originalUser.Group = null; - originalUser.GroupId = null; - } - - var fail = CheckValidName(model, false); - if (fail != null) - return fail; - - originalUser.Name = model.Name ?? originalUser.Name; - - if (model.Enabled.HasValue) - { - if (securityConfigurationOptions.Value.OidcStrictMode) - return BadRequest(ErrorCode.BadUserEditDueToOidcStrictMode); - - invalidateSessions = originalUser.Require(x => x.Enabled) && !model.Enabled.Value; - originalUser.Enabled = model.Enabled.Value; - } - - if (invalidateSessions) - sessionInvalidationTracker.UserModifiedInvalidateSessions(originalUser); - - await DatabaseContext.Save(cancellationToken); - - Logger.LogInformation("Updated user {userName} ({userId})", originalUser.Name, originalUser.Id); - - if (invalidateSessions) - await permissionsUpdateNotifyee.UserDisabled(originalUser, cancellationToken); - - await SendUserUpdatedTopics(originalUser); - // return id only if not a self update and cannot read users - var canReadBack = AuthenticationContext.User.Id == originalUser.Id - || callerAdministrationRights.HasFlag(AdministrationRights.ReadUsers); - return canReadBack - ? new AuthorityResponse(originalUser) - : new AuthorityResponse(); + var userId = user.Require(u => u.Id); + var canReadBack = claimsPrincipalAccessor.User.GetTgsUserId() == userId + || (await authorizationService.AuthorizeAsync( + [Flag(AdministrationRights.ReadUsers)])).Succeeded; + + return new AuthorityResponse( + canReadBack + ? new UpdatedUser(user) + : new UpdatedUser(userId), + successResponse); } /// @@ -628,9 +753,8 @@ namespace Tgstation.Server.Host.Authority IQueryable Queryable(bool includeJoins, bool allowSystemUser) { var tgsUserCanonicalName = User.CanonicalizeName(User.TgsSystemUserName); - var queryable = DatabaseContext - .Users - .AsQueryable(); + IQueryable queryable = DatabaseContext + .Users; if (!allowSystemUser) queryable = queryable @@ -661,7 +785,6 @@ namespace Tgstation.Server.Host.Authority if (model.Group != null) group = await DatabaseContext .Groups - .AsQueryable() .Where(x => x.Id == model.Group.Id) .Include(x => x.PermissionSet) .FirstOrDefaultAsync(cancellationToken); @@ -672,10 +795,23 @@ namespace Tgstation.Server.Host.Authority InstanceManagerRights = model.PermissionSet?.InstanceManagerRights ?? InstanceManagerRights.None, }; + /* + var currentUser = new User + { + Id = claimsPrincipalAccessor.User.GetTgsUserId(), + }; + */ + + // Temporary workaround while we work to remove authentication context + var currentUser = DatabaseContext.Users.Local.First( + user => user.Id == claimsPrincipalAccessor.User.GetTgsUserId()); + + DatabaseContext.Users.Attach(currentUser); + return new User { CreatedAt = DateTimeOffset.UtcNow, - CreatedBy = AuthenticationContext.User, + CreatedBy = currentUser, Enabled = model.Enabled ?? false, PermissionSet = permissionSet, Group = group, @@ -709,11 +845,11 @@ namespace Tgstation.Server.Host.Authority /// The new password. /// If this is for a new . /// on success, an errored if is too short. - AuthorityResponse? TrySetPassword(User dbUser, string newPassword, bool newUser) + AuthorityResponse? TrySetPassword(User dbUser, string newPassword, bool newUser) { newPassword ??= String.Empty; if (newPassword.Length < generalConfigurationOptions.Value.MinimumPasswordLength) - return new AuthorityResponse( + return new AuthorityResponse( new ErrorMessageResponse(ErrorCode.UserPasswordLength) { AdditionalData = $"Required password length: {generalConfigurationOptions.Value.MinimumPasswordLength}", diff --git a/src/Tgstation.Server.Host/Authority/UserGroupAuthority.cs b/src/Tgstation.Server.Host/Authority/UserGroupAuthority.cs index 4b9fcdb401..47ca690d5e 100644 --- a/src/Tgstation.Server.Host/Authority/UserGroupAuthority.cs +++ b/src/Tgstation.Server.Host/Authority/UserGroupAuthority.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using GreenDonut; +using Microsoft.AspNetCore.Authorization; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -16,6 +17,7 @@ using Tgstation.Server.Api.Rights; using Tgstation.Server.Host.Authority.Core; using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.Database; +using Tgstation.Server.Host.Extensions; using Tgstation.Server.Host.Models; using Tgstation.Server.Host.Security; @@ -29,6 +31,11 @@ namespace Tgstation.Server.Host.Authority /// readonly IUserGroupsDataLoader userGroupsDataLoader; + /// + /// The for the . + /// + readonly IClaimsPrincipalAccessor claimsPrincipalAccessor; + /// /// The of the . /// @@ -59,62 +66,179 @@ namespace Tgstation.Server.Host.Authority /// /// Initializes a new instance of the class. /// - /// The to use. /// The to use. /// The to use. + /// The value of . /// The value of . /// The value of . public UserGroupAuthority( - IAuthenticationContext authenticationContext, IDatabaseContext databaseContext, ILogger logger, IUserGroupsDataLoader userGroupsDataLoader, + IClaimsPrincipalAccessor claimsPrincipalAccessor, IOptionsSnapshot generalConfigurationOptions) : base( - authenticationContext, databaseContext, logger) { this.userGroupsDataLoader = userGroupsDataLoader ?? throw new ArgumentNullException(nameof(userGroupsDataLoader)); + this.claimsPrincipalAccessor = claimsPrincipalAccessor ?? throw new ArgumentNullException(nameof(claimsPrincipalAccessor)); this.generalConfigurationOptions = generalConfigurationOptions ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); } /// - public async ValueTask> GetId(long id, bool includeJoins, CancellationToken cancellationToken) + public RequirementsGated> GetId(long id, bool includeJoins, CancellationToken cancellationToken) + => new( + () => + { + if (id != claimsPrincipalAccessor.User.GetTgsUserId()) + return Flag(AdministrationRights.ReadUsers); + + return null; + }, + async () => + { + UserGroup? userGroup; + if (includeJoins) + userGroup = await QueryableImpl(true) + .Where(x => x.Id == id) + .FirstOrDefaultAsync(cancellationToken); + else + userGroup = await userGroupsDataLoader.LoadAsync(id, cancellationToken); + + if (userGroup == null) + return Gone(); + + return new AuthorityResponse(userGroup); + }); + + /// + public RequirementsGated> Read(CancellationToken cancellationToken) + => new( + () => (IAuthorizationRequirement?)null, + async () => + { + var userId = claimsPrincipalAccessor.User.GetTgsUserId(); + var group = await DatabaseContext + .Users + .Where(user => user.Id == userId) + .Select(user => user.Group) + .FirstOrDefaultAsync(cancellationToken); + + if (group == null) + return Gone(); + + return new AuthorityResponse(group); + }); + + /// + public RequirementsGated> Queryable(bool includeJoins) + => new( + () => Flag(AdministrationRights.ReadUsers), + () => ValueTask.FromResult(QueryableImpl(includeJoins))); + + /// + public RequirementsGated> Create(string name, Models.PermissionSet? permissionSet, CancellationToken cancellationToken) { - if (id != AuthenticationContext.User.GroupId && !((AdministrationRights)AuthenticationContext.GetRight(RightsType.Administration)).HasFlag(AdministrationRights.ReadUsers)) - return Forbid(); + ArgumentNullException.ThrowIfNull(name); + return new( + () => Flag(AdministrationRights.WriteUsers), + async () => + { + var totalGroups = await DatabaseContext + .Groups + .CountAsync(cancellationToken); + if (totalGroups >= generalConfigurationOptions.Value.UserGroupLimit) + return Conflict(ErrorCode.UserGroupLimitReached); - UserGroup? userGroup; - if (includeJoins) - userGroup = await Queryable(true) - .Where(x => x.Id == id) - .FirstOrDefaultAsync(cancellationToken); - else - userGroup = await userGroupsDataLoader.LoadAsync(id, cancellationToken); + var modelPermissionSet = new Models.PermissionSet + { + AdministrationRights = permissionSet?.AdministrationRights ?? AdministrationRights.None, + InstanceManagerRights = permissionSet?.InstanceManagerRights ?? InstanceManagerRights.None, + }; - if (userGroup == null) - return Gone(); + var dbGroup = new UserGroup + { + Name = name, + PermissionSet = modelPermissionSet, + }; - return new AuthorityResponse(userGroup); + DatabaseContext.Groups.Add(dbGroup); + await DatabaseContext.Save(cancellationToken); + Logger.LogInformation("Created new user group {groupName} ({groupId})", dbGroup.Name, dbGroup.Id); + + return new AuthorityResponse( + dbGroup, + HttpSuccessResponse.Created); + }); } /// - public ValueTask> Read() - { - var group = AuthenticationContext.User!.Group; - if (group == null) - return ValueTask.FromResult(Gone()); + public RequirementsGated> Update(long id, string? newName, Models.PermissionSet? newPermissionSet, CancellationToken cancellationToken) + => new( + () => Flag(AdministrationRights.WriteUsers), + async () => + { + var currentGroup = await DatabaseContext + .Groups + .Where(x => x.Id == id) + .Include(x => x.PermissionSet) + .FirstOrDefaultAsync(cancellationToken); - return ValueTask.FromResult(new AuthorityResponse(group)); - } + if (currentGroup == default) + return Gone(); + + if (newPermissionSet != null) + { + currentGroup.PermissionSet!.AdministrationRights = newPermissionSet.AdministrationRights ?? currentGroup.PermissionSet.AdministrationRights; + currentGroup.PermissionSet.InstanceManagerRights = newPermissionSet.InstanceManagerRights ?? currentGroup.PermissionSet.InstanceManagerRights; + } + + currentGroup.Name = newName ?? currentGroup.Name; + + await DatabaseContext.Save(cancellationToken); + + return new AuthorityResponse(currentGroup); + }); /// - public IQueryable Queryable(bool includeJoins) + public RequirementsGated DeleteEmpty(long id, CancellationToken cancellationToken) + => new( + () => Flag(AdministrationRights.WriteUsers), + async () => + { + var numDeleted = await DatabaseContext + .Groups + .Where(x => x.Id == id && x.Users!.Count == 0) + .ExecuteDeleteAsync(cancellationToken); + + if (numDeleted > 0) + return new(); + + // find out how we failed + var groupExists = await DatabaseContext + .Groups + .Where(x => x.Id == id) + .AnyAsync(cancellationToken); + + return new( + groupExists + ? new ErrorMessageResponse(ErrorCode.UserGroupNotEmpty) + : new ErrorMessageResponse(), + groupExists + ? HttpFailureResponse.Conflict + : HttpFailureResponse.Gone); + }); + + /// + /// Get the s. + /// + /// If and should be included. + /// An of s. + IQueryable QueryableImpl(bool includeJoins) { - var queryable = DatabaseContext - .Groups - .AsQueryable(); + IQueryable queryable = DatabaseContext + .Groups; if (includeJoins) queryable = queryable @@ -123,92 +247,5 @@ namespace Tgstation.Server.Host.Authority return queryable; } - - /// - public async ValueTask> Create(string name, Models.PermissionSet? permissionSet, CancellationToken cancellationToken) - { - ArgumentNullException.ThrowIfNull(name); - - var totalGroups = await DatabaseContext - .Groups - .AsQueryable() - .CountAsync(cancellationToken); - if (totalGroups >= generalConfigurationOptions.Value.UserGroupLimit) - return Conflict(ErrorCode.UserGroupLimitReached); - - var modelPermissionSet = new Models.PermissionSet - { - AdministrationRights = permissionSet?.AdministrationRights ?? AdministrationRights.None, - InstanceManagerRights = permissionSet?.InstanceManagerRights ?? InstanceManagerRights.None, - }; - - var dbGroup = new UserGroup - { - Name = name, - PermissionSet = modelPermissionSet, - }; - - DatabaseContext.Groups.Add(dbGroup); - await DatabaseContext.Save(cancellationToken); - Logger.LogInformation("Created new user group {groupName} ({groupId})", dbGroup.Name, dbGroup.Id); - - return new AuthorityResponse( - dbGroup, - HttpSuccessResponse.Created); - } - - /// - public async ValueTask> Update(long id, string? newName, Models.PermissionSet? newPermissionSet, CancellationToken cancellationToken) - { - var currentGroup = await DatabaseContext - .Groups - .AsQueryable() - .Where(x => x.Id == id) - .Include(x => x.PermissionSet) - .FirstOrDefaultAsync(cancellationToken); - - if (currentGroup == default) - return Gone(); - - if (newPermissionSet != null) - { - currentGroup.PermissionSet!.AdministrationRights = newPermissionSet.AdministrationRights ?? currentGroup.PermissionSet.AdministrationRights; - currentGroup.PermissionSet.InstanceManagerRights = newPermissionSet.InstanceManagerRights ?? currentGroup.PermissionSet.InstanceManagerRights; - } - - currentGroup.Name = newName ?? currentGroup.Name; - - await DatabaseContext.Save(cancellationToken); - - return new AuthorityResponse(currentGroup); - } - - /// - public async ValueTask DeleteEmpty(long id, CancellationToken cancellationToken) - { - var numDeleted = await DatabaseContext - .Groups - .AsQueryable() - .Where(x => x.Id == id && x.Users!.Count == 0) - .ExecuteDeleteAsync(cancellationToken); - - if (numDeleted > 0) - return new(); - - // find out how we failed - var groupExists = await DatabaseContext - .Groups - .AsQueryable() - .Where(x => x.Id == id) - .AnyAsync(cancellationToken); - - return new( - groupExists - ? new ErrorMessageResponse(ErrorCode.UserGroupNotEmpty) - : new ErrorMessageResponse(), - groupExists - ? HttpFailureResponse.Conflict - : HttpFailureResponse.Gone); - } } } diff --git a/src/Tgstation.Server.Host/Components/Chat/Commands/PullRequestsCommand.cs b/src/Tgstation.Server.Host/Components/Chat/Commands/PullRequestsCommand.cs index 5cc52d2b83..573473c2f6 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Commands/PullRequestsCommand.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Commands/PullRequestsCommand.cs @@ -116,7 +116,6 @@ namespace Tgstation.Server.Host.Components.Chat.Commands await databaseContextFactory.UseContext( async db => results = await db .RevisionInformations - .AsQueryable() .Where(x => x.Instance!.Id == instance.Id && x.CommitSha == head) .SelectMany(x => x.ActiveTestMerges!) .Select(x => x.TestMerge) diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs index f76ee34517..63f8c166f5 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Remora.Discord.API.Abstractions.Gateway.Commands; using Remora.Discord.API.Abstractions.Gateway.Events; @@ -72,9 +73,9 @@ namespace Tgstation.Server.Host.Components.Chat.Providers readonly IAssemblyInformationProvider assemblyInformationProvider; /// - /// The for the . + /// The for the . /// - readonly GeneralConfiguration generalConfiguration; + readonly IOptionsMonitor generalConfigurationOptions; /// /// The containing Discord services. @@ -141,18 +142,18 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /// The for the . /// The value of . /// The for the . - /// The value of . + /// The value of . public DiscordProvider( IJobManager jobManager, IAsyncDelayer asyncDelayer, ILogger logger, IAssemblyInformationProvider assemblyInformationProvider, - ChatBot chatBot, - GeneralConfiguration generalConfiguration) + IOptionsMonitor generalConfigurationOptions, + ChatBot chatBot) : base(jobManager, asyncDelayer, logger, chatBot) { this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider)); - this.generalConfiguration = generalConfiguration ?? throw new ArgumentNullException(nameof(generalConfiguration)); + this.generalConfigurationOptions = generalConfigurationOptions ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); mappedChannels = new List(); connectDisconnectLock = new object(); @@ -924,7 +925,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers true), EngineType.OpenDream => new EmbedField( "OpenDream Version", - $"[{engineVersion.SourceSHA![..7]}]({generalConfiguration.OpenDreamGitUrl}/commit/{engineVersion.SourceSHA})", + $"[{engineVersion.SourceSHA![..7]}]({generalConfigurationOptions.CurrentValue.OpenDreamGitUrl}/commit/{engineVersion.SourceSHA})", true), _ => throw new InvalidOperationException($"Invaild EngineType: {engineVersion.Engine.Value}"), }; diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs index 560390201d..0a947c85e2 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using Meebey.SmartIrc4net; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Newtonsoft.Json; @@ -92,7 +93,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /// /// The for the . /// - readonly FileLoggingConfiguration loggingConfiguration; + readonly IOptionsMonitor loggingConfigurationOptions; /// /// The client. @@ -117,18 +118,18 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /// The for the . /// The to get the from. /// The for the . - /// The for the . + /// The value of . public IrcProvider( IJobManager jobManager, IAsyncDelayer asyncDelayer, ILogger logger, - IAssemblyInformationProvider assemblyInformationProvider, Models.ChatBot chatBot, - FileLoggingConfiguration loggingConfiguration) + IAssemblyInformationProvider assemblyInformationProvider, + IOptionsMonitor loggingConfigurationOptions) : base(jobManager, asyncDelayer, logger, chatBot) { ArgumentNullException.ThrowIfNull(assemblyInformationProvider); - ArgumentNullException.ThrowIfNull(loggingConfiguration); + ArgumentNullException.ThrowIfNull(loggingConfigurationOptions); var builder = chatBot.CreateConnectionStringBuilder(); if (builder == null || !builder.Valid || builder is not IrcConnectionStringBuilder ircBuilder) @@ -143,7 +144,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers passwordType = ircBuilder.PasswordType; assemblyInfo = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider)); - this.loggingConfiguration = loggingConfiguration ?? throw new ArgumentNullException(nameof(loggingConfiguration)); + this.loggingConfigurationOptions = loggingConfigurationOptions ?? throw new ArgumentNullException(nameof(loggingConfigurationOptions)); client = InstantiateClient(); @@ -758,7 +759,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers newClient.OnChannelMessage += Client_OnChannelMessage; newClient.OnQueryMessage += Client_OnQueryMessage; - if (loggingConfiguration.ProviderNetworkDebug) + if (loggingConfigurationOptions.CurrentValue.ProviderNetworkDebug) { newClient.OnReadLine += (sender, e) => Logger.LogTrace("READ: {line}", e.Line); newClient.OnWriteLine += (sender, e) => Logger.LogTrace("WRITE: {line}", e.Line); diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/ProviderFactory.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/ProviderFactory.cs index 41113d196f..f8b3be51d0 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/ProviderFactory.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/ProviderFactory.cs @@ -36,14 +36,14 @@ namespace Tgstation.Server.Host.Components.Chat.Providers readonly ILoggerFactory loggerFactory; /// - /// The for the . + /// The for the . /// - readonly GeneralConfiguration generalConfiguration; + readonly IOptionsMonitor generalConfigurationOptions; /// - /// The for the . + /// The for the . /// - readonly FileLoggingConfiguration loggingConfiguration; + readonly IOptionsMonitor loggingConfigurationOptions; /// /// Initializes a new instance of the class. @@ -52,22 +52,22 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /// The value of . /// The value of . /// The value of . - /// The containing the value of . - /// The containing the value of . + /// The value of . + /// The value of . public ProviderFactory( IJobManager jobManager, IAssemblyInformationProvider assemblyInformationProvider, IAsyncDelayer asyncDelayer, ILoggerFactory loggerFactory, - IOptions generalConfigurationOptions, - IOptions loggingConfigurationOptions) + IOptionsMonitor generalConfigurationOptions, + IOptionsMonitor loggingConfigurationOptions) { this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager)); this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer)); this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider)); - generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); - loggingConfiguration = loggingConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(loggingConfigurationOptions)); + this.generalConfigurationOptions = generalConfigurationOptions ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); + this.loggingConfigurationOptions = loggingConfigurationOptions ?? throw new ArgumentNullException(nameof(loggingConfigurationOptions)); } /// @@ -80,16 +80,16 @@ namespace Tgstation.Server.Host.Components.Chat.Providers jobManager, asyncDelayer, loggerFactory.CreateLogger(), - assemblyInformationProvider, settings, - loggingConfiguration), + assemblyInformationProvider, + loggingConfigurationOptions), ChatProvider.Discord => new DiscordProvider( jobManager, asyncDelayer, loggerFactory.CreateLogger(), assemblyInformationProvider, - settings, - generalConfiguration), + generalConfigurationOptions, + settings), _ => throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid ChatProvider: {0}", settings.Provider)), }; } diff --git a/src/Tgstation.Server.Host/Components/Deployment/DeploymentLockManager.cs b/src/Tgstation.Server.Host/Components/Deployment/DeploymentLockManager.cs index 46efbfdbc9..568fca8022 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/DeploymentLockManager.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/DeploymentLockManager.cs @@ -63,7 +63,7 @@ namespace Tgstation.Server.Host.Components.Deployment /// /// The to get a description of. /// A verbose description of . - static string GetFullLockDescriptor(DmbLock dmbLock) => $"{dmbLock.LockID} {dmbLock.Descriptor} (Created at {dmbLock.LockTime}){(dmbLock.KeptAlive ? " (RELEASED)" : String.Empty)}"; + static string GetFullLockDescriptor(DmbLock dmbLock) => $"{dmbLock.LockID} {dmbLock.EngineVersion} {dmbLock.Descriptor} (Created at {dmbLock.LockTime}){(dmbLock.KeptAlive ? " (RELEASED)" : String.Empty)}"; /// /// Initializes a new instance of the class. diff --git a/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs b/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs index a9cd217f18..b32aaeaea4 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs @@ -81,7 +81,7 @@ namespace Tgstation.Server.Host.Components.Deployment readonly CancellationTokenSource cleanupCts; /// - /// The for . + /// The for . /// readonly CancellationTokenSource lockLogCts; @@ -207,7 +207,6 @@ namespace Tgstation.Server.Host.Components.Deployment async (db) => cj = await db .CompileJobs - .AsQueryable() .Where(x => x.Job.Instance!.Id == metadata.Id) .OrderByDescending(x => x.Job.StoppedAt) .FirstOrDefaultAsync(cancellationToken)); @@ -224,7 +223,7 @@ namespace Tgstation.Server.Host.Components.Deployment } // we dont do CleanUnusedCompileJobs here because the watchdog may have plans for them yet - cleanupTask = Task.WhenAll(cleanupTask, LogLockStates()); + cleanupTask = Task.WhenAll(cleanupTask, LogLockStatesLoop()); } /// @@ -277,7 +276,6 @@ namespace Tgstation.Server.Host.Components.Deployment { jobUidsToNotErase = (await db .CompileJobs - .AsQueryable() .Where( x => x.Job.Instance!.Id == metadata.Id && jobIdsToSkip.Contains(x.Id!.Value)) @@ -331,6 +329,18 @@ namespace Tgstation.Server.Host.Components.Deployment return provider.CompileJob; } + /// + public void LogLockStates() + { + var builder = new StringBuilder(); + + lock (jobLockManagers) + foreach (var lockManager in jobLockManagers.Values) + lockManager.LogLockStats(builder); + + logger.LogTrace("Periodic deployment log states report:{newLine}{report}", Environment.NewLine, builder); + } + /// /// Gets a and potentially the for a given . /// @@ -352,7 +362,6 @@ namespace Tgstation.Server.Host.Components.Deployment await databaseContextFactory.UseContext( async db => compileJob = await db .CompileJobs - .AsQueryable() .Where(x => x!.Id == compileJobId) .Include(x => x.Job!) .ThenInclude(x => x.StartedBy) @@ -509,7 +518,7 @@ namespace Tgstation.Server.Host.Components.Deployment async ValueTask DeleteCompileJobContent(string directory, CancellationToken cancellationToken) { // Then call the cleanup event, waiting here first - await eventConsumer.HandleEvent(EventType.DeploymentCleanup, new List { ioManager.ResolvePath(directory) }, true, cancellationToken); + await eventConsumer.HandleEvent(EventType.DeploymentCleanup, new List { ioManager.ResolvePath(directory) }, false, true, cancellationToken); await ioManager.DeleteDirectory(directory, cancellationToken); } @@ -517,7 +526,7 @@ namespace Tgstation.Server.Host.Components.Deployment /// Lock all s states. /// /// A representing the running operation. - async Task LogLockStates() + async Task LogLockStatesLoop() { logger.LogTrace("Entering lock logging loop"); CancellationToken cancellationToken = lockLogCts.Token; @@ -525,14 +534,7 @@ namespace Tgstation.Server.Host.Components.Deployment while (!cancellationToken.IsCancellationRequested) try { - var builder = new StringBuilder(); - - lock (jobLockManagers) - foreach (var lockManager in jobLockManagers.Values) - lockManager.LogLockStats(builder); - - logger.LogTrace("Periodic deployment log states report:{newLine}{report}", Environment.NewLine, builder); - + LogLockStates(); await asyncDelayer.Delay(TimeSpan.FromMinutes(10), cancellationToken); } catch (OperationCanceledException ex) diff --git a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs index 6f8b05c9d2..cf801061cf 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Prometheus; @@ -30,7 +31,9 @@ using Tgstation.Server.Host.Utils; namespace Tgstation.Server.Host.Components.Deployment { /// +#pragma warning disable CA1506 // TODO: Decomplexify sealed class DreamMaker : IDreamMaker +#pragma warning restore CA1506 { /// /// Extension for .dmes. @@ -52,11 +55,6 @@ namespace Tgstation.Server.Host.Components.Deployment /// readonly StaticFiles.IConfiguration configuration; - /// - /// The for . - /// - readonly ISessionControllerFactory sessionControllerFactory; - /// /// The for . /// @@ -92,16 +90,16 @@ namespace Tgstation.Server.Host.Components.Deployment /// readonly IAsyncDelayer asyncDelayer; + /// + /// The of for . + /// + readonly IOptionsMonitor sessionConfigurationOptions; + /// /// The for . /// readonly ILogger logger; - /// - /// The for . - /// - readonly SessionConfiguration sessionConfiguration; - /// /// The belongs to. /// @@ -127,6 +125,11 @@ namespace Tgstation.Server.Host.Components.Deployment /// readonly object deploymentLock; + /// + /// The for . + /// + ISessionControllerFactory? sessionControllerFactory; + /// /// The active callback from . /// @@ -158,7 +161,6 @@ namespace Tgstation.Server.Host.Components.Deployment /// The value of . /// The value of . /// The value of . - /// The value of . /// The value of . /// The value of . /// The value of . @@ -167,14 +169,13 @@ namespace Tgstation.Server.Host.Components.Deployment /// The value of . /// The value of . /// The to use. + /// The value of . /// The value of . - /// The value of . /// The value of . public DreamMaker( IEngineManager engineManager, IIOManager ioManager, StaticFiles.IConfiguration configuration, - ISessionControllerFactory sessionControllerFactory, IEventConsumer eventConsumer, IChatManager chatManager, IProcessExecutor processExecutor, @@ -183,14 +184,13 @@ namespace Tgstation.Server.Host.Components.Deployment IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory, IAsyncDelayer asyncDelayer, IMetricFactory metricFactory, + IOptionsMonitor sessionConfigurationOptions, ILogger logger, - SessionConfiguration sessionConfiguration, Api.Models.Instance metadata) { this.engineManager = engineManager ?? throw new ArgumentNullException(nameof(engineManager)); this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); this.configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); - this.sessionControllerFactory = sessionControllerFactory ?? throw new ArgumentNullException(nameof(sessionControllerFactory)); this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer)); this.chatManager = chatManager ?? throw new ArgumentNullException(nameof(chatManager)); this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor)); @@ -199,8 +199,8 @@ namespace Tgstation.Server.Host.Components.Deployment this.remoteDeploymentManagerFactory = remoteDeploymentManagerFactory ?? throw new ArgumentNullException(nameof(remoteDeploymentManagerFactory)); this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer)); ArgumentNullException.ThrowIfNull(metricFactory); + this.sessionConfigurationOptions = sessionConfigurationOptions ?? throw new ArgumentNullException(nameof(sessionConfigurationOptions)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); - this.sessionConfiguration = sessionConfiguration ?? throw new ArgumentNullException(nameof(sessionConfiguration)); this.metadata = metadata ?? throw new ArgumentNullException(nameof(metadata)); successfulDeployments = metricFactory.CreateCounter("tgs_successful_deployments", "The number of deployments that have completed successfully"); @@ -253,7 +253,6 @@ namespace Tgstation.Server.Host.Components.Deployment ddSettings = await databaseContext .DreamDaemonSettings - .AsQueryable() .Where(x => x.InstanceId == metadata.Id) .Select(x => new Models.DreamDaemonSettings { @@ -266,7 +265,6 @@ namespace Tgstation.Server.Host.Components.Deployment dreamMakerSettings = await databaseContext .DreamMakerSettings - .AsQueryable() .Where(x => x.InstanceId == metadata.Id) .FirstAsync(cancellationToken); if (dreamMakerSettings == default) @@ -274,7 +272,6 @@ namespace Tgstation.Server.Host.Components.Deployment repositorySettings = await databaseContext .RepositorySettings - .AsQueryable() .Where(x => x.InstanceId == metadata.Id) .Select(x => new Models.RepositorySettings { @@ -302,7 +299,6 @@ namespace Tgstation.Server.Host.Components.Deployment repoName = repo.RemoteRepositoryName; revInfo = await databaseContext .RevisionInformations - .AsQueryable() .Where(x => x.CommitSha == repoSha && x.InstanceId == metadata.Id) .Include(x => x.ActiveTestMerges!) .ThenInclude(x => x.TestMerge!) @@ -412,7 +408,7 @@ namespace Tgstation.Server.Host.Components.Deployment repoName, cancellationToken); - var eventTask = eventConsumer.HandleEvent(EventType.DeploymentComplete, Enumerable.Empty(), false, cancellationToken); + var eventTask = eventConsumer.HandleEvent(EventType.DeploymentComplete, Enumerable.Empty(), false, false, cancellationToken); try { @@ -446,6 +442,17 @@ namespace Tgstation.Server.Host.Components.Deployment } #pragma warning restore CA1506 + /// + /// Set the for the . Must be called exactly once after construction before use. + /// + /// The value of . + public void SetSessionControllerFactory(ISessionControllerFactory sessionControllerFactory) + { + ArgumentNullException.ThrowIfNull(sessionControllerFactory); + if (Interlocked.CompareExchange(ref this.sessionControllerFactory, sessionControllerFactory, null) != null) + throw new InvalidOperationException($"{nameof(SetSessionControllerFactory)} called multiple times!"); + } + /// /// Calculate the average length of a deployment using a given . /// @@ -456,7 +463,6 @@ namespace Tgstation.Server.Host.Components.Deployment { var previousCompileJobs = await databaseContext .CompileJobs - .AsQueryable() .Where(x => x.Job.Instance!.Id == metadata.Id) .OrderByDescending(x => x.Job.StoppedAt) .Take(10) @@ -568,7 +574,7 @@ namespace Tgstation.Server.Host.Components.Deployment { // DCT: Cancellation token is for job, delaying here is fine progressReporter.StageName = "Running CompileCancelled event"; - await eventConsumer.HandleEvent(EventType.CompileCancelled, Enumerable.Empty(), true, CancellationToken.None); + await eventConsumer.HandleEvent(EventType.CompileCancelled, Enumerable.Empty(), false, true, CancellationToken.None); throw; } finally @@ -627,6 +633,7 @@ namespace Tgstation.Server.Host.Components.Deployment engineLock.Version.ToString(), repoReference, }, + false, true, cancellationToken); @@ -669,6 +676,7 @@ namespace Tgstation.Server.Host.Components.Deployment repoOrigin.ToString(), engineLock.Version.ToString(), }, + false, true, cancellationToken); @@ -710,6 +718,7 @@ namespace Tgstation.Server.Host.Components.Deployment compileSuceeded ? "1" : "0", engineVersion.ToString(), }, + false, true, cancellationToken); throw; @@ -723,6 +732,7 @@ namespace Tgstation.Server.Host.Components.Deployment resolvedOutputDirectory, engineVersion.ToString(), }, + false, true, cancellationToken); @@ -852,6 +862,9 @@ namespace Tgstation.Server.Host.Components.Deployment job.MinimumSecurityLevel = securityLevel; // needed for the TempDmbProvider + if (sessionControllerFactory == null) + throw new InvalidOperationException($"{nameof(SetSessionControllerFactory)} was not called!"); + ApiValidationStatus validationStatus; await using (var provider = new TemporaryDmbProvider( ioManager.ResolvePath(job.DirectoryName!.Value.ToString()), @@ -927,7 +940,7 @@ namespace Tgstation.Server.Host.Components.Deployment readStandardHandles: true, noShellExecute: true); - if (sessionConfiguration.LowPriorityDeploymentProcesses) + if (sessionConfigurationOptions.CurrentValue.LowPriorityDeploymentProcesses) dm.AdjustPriority(false); int exitCode; @@ -1018,7 +1031,7 @@ namespace Tgstation.Server.Host.Components.Deployment { async ValueTask CleanDir() { - if (sessionConfiguration.DelayCleaningFailedDeployments) + if (sessionConfigurationOptions.CurrentValue.DelayCleaningFailedDeployments) { logger.LogDebug("Not cleaning up errored deployment directory {guid} due to config.", job.DirectoryName); return; @@ -1029,7 +1042,7 @@ namespace Tgstation.Server.Host.Components.Deployment try { // DCT: None available - await eventConsumer.HandleEvent(EventType.DeploymentCleanup, new List { jobPath }, true, CancellationToken.None); + await eventConsumer.HandleEvent(EventType.DeploymentCleanup, new List { jobPath }, false, true, CancellationToken.None); await ioManager.DeleteDirectory(jobPath, CancellationToken.None); } catch (Exception e) diff --git a/src/Tgstation.Server.Host/Components/Deployment/IDmbFactory.cs b/src/Tgstation.Server.Host/Components/Deployment/IDmbFactory.cs index c737b8b358..7c8046a222 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/IDmbFactory.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/IDmbFactory.cs @@ -49,5 +49,10 @@ namespace Tgstation.Server.Host.Components.Deployment /// The for the operation. /// A representing the running operation. ValueTask CleanUnusedCompileJobs(CancellationToken cancellationToken); + + /// + /// Log the states of all active s. + /// + void LogLockStates(); } } diff --git a/src/Tgstation.Server.Host/Components/Deployment/Remote/GitHubRemoteDeploymentManager.cs b/src/Tgstation.Server.Host/Components/Deployment/Remote/GitHubRemoteDeploymentManager.cs index 790b9ea1d3..a023910f6f 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/Remote/GitHubRemoteDeploymentManager.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/Remote/GitHubRemoteDeploymentManager.cs @@ -70,7 +70,6 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote async databaseContext => repositorySettings = await databaseContext .RepositorySettings - .AsQueryable() .Where(x => x.InstanceId == Metadata.Id) .FirstAsync(cancellationToken)); @@ -376,7 +375,6 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote async databaseContext => gitHubAccessToken = await databaseContext .RepositorySettings - .AsQueryable() .Where(x => x.InstanceId == Metadata.Id) .Select(x => x.AccessToken) .FirstAsync(cancellationToken)); diff --git a/src/Tgstation.Server.Host/Components/Engine/ByondInstallation.cs b/src/Tgstation.Server.Host/Components/Engine/ByondInstallation.cs index 8ee095babc..524151784f 100644 --- a/src/Tgstation.Server.Host/Components/Engine/ByondInstallation.cs +++ b/src/Tgstation.Server.Host/Components/Engine/ByondInstallation.cs @@ -113,19 +113,24 @@ namespace Tgstation.Server.Host.Components.Engine /// public override string FormatServerArguments( IDmbProvider dmbProvider, - IReadOnlyDictionary parameters, + IReadOnlyDictionary? parameters, DreamDaemonLaunchParameters launchParameters, + string accessIdentifier, string? logFilePath) { ArgumentNullException.ThrowIfNull(dmbProvider); - ArgumentNullException.ThrowIfNull(parameters); ArgumentNullException.ThrowIfNull(launchParameters); + ArgumentNullException.ThrowIfNull(accessIdentifier); - var parametersString = EncodeParameters(parameters, launchParameters); + var encodedParameters = EncodeParameters(parameters, launchParameters); + var parametersString = !String.IsNullOrEmpty(encodedParameters) + ? $" -params \"{encodedParameters}\"" + : String.Empty; + // important to run on all ports to allow port changing var arguments = String.Format( CultureInfo.InvariantCulture, - "\"{0}\" -port {1} -ports 1-65535 {2}-close -verbose -{3} -{4}{5}{6}{7} -params \"{8}\"", + "\"{0}\" -port {1} -ports 1-65535 {2}-close -verbose -{3} -{4}{5}{6}{7}{8}", dmbProvider.DmbName, launchParameters.Port!.Value, launchParameters.AllowWebClient!.Value diff --git a/src/Tgstation.Server.Host/Components/Engine/ByondInstallerBase.cs b/src/Tgstation.Server.Host/Components/Engine/ByondInstallerBase.cs index 41f657f10f..668a487692 100644 --- a/src/Tgstation.Server.Host/Components/Engine/ByondInstallerBase.cs +++ b/src/Tgstation.Server.Host/Components/Engine/ByondInstallerBase.cs @@ -141,34 +141,38 @@ namespace Tgstation.Server.Host.Components.Engine } /// - public override ValueTask CreateInstallation(EngineVersion version, string path, Task installationTask, CancellationToken cancellationToken) + public sealed override ValueTask GetInstallation(EngineVersion version, string path, Task installationTask, CancellationToken cancellationToken) { CheckVersionValidity(version); var installationIOManager = IOManager.CreateResolverForSubdirectory(path); var supportsMapThreads = version.Version >= MapThreadsVersion; + var dreamDaemonName = GetDreamDaemonName( + version.Version!, + out var supportsCli); + var dreamDaemonPath = installationIOManager.ResolvePath( + installationIOManager.ConcatPath( + ByondBinPath, + dreamDaemonName)); + var dreamMakerPath = installationIOManager.ResolvePath( + installationIOManager.ConcatPath( + ByondBinPath, + DreamMakerName)); + return ValueTask.FromResult( new ByondInstallation( installationIOManager, installationTask, version, - installationIOManager.ResolvePath( - installationIOManager.ConcatPath( - ByondBinPath, - GetDreamDaemonName( - version.Version!, - out var supportsCli))), - installationIOManager.ResolvePath( - installationIOManager.ConcatPath( - ByondBinPath, - DreamMakerName)), + dreamDaemonPath, + dreamMakerPath, supportsCli, supportsMapThreads)); } /// - public override async Task CleanCache(CancellationToken cancellationToken) + public sealed override async Task CleanCache(CancellationToken cancellationToken) { try { @@ -214,7 +218,7 @@ namespace Tgstation.Server.Host.Components.Engine } /// - public override async ValueTask DownloadVersion(EngineVersion version, JobProgressReporter progressReporter, CancellationToken cancellationToken) + public sealed override async ValueTask DownloadVersion(EngineVersion version, JobProgressReporter progressReporter, CancellationToken cancellationToken) { CheckVersionValidity(version); diff --git a/src/Tgstation.Server.Host/Components/Engine/DelegatingEngineInstaller.cs b/src/Tgstation.Server.Host/Components/Engine/DelegatingEngineInstaller.cs index 5b3cc20c36..6d690bb50b 100644 --- a/src/Tgstation.Server.Host/Components/Engine/DelegatingEngineInstaller.cs +++ b/src/Tgstation.Server.Host/Components/Engine/DelegatingEngineInstaller.cs @@ -33,15 +33,15 @@ namespace Tgstation.Server.Host.Components.Engine => Task.WhenAll(delegatedInstallers.Values.Select(installer => installer.CleanCache(cancellationToken))); /// - public ValueTask CreateInstallation(EngineVersion version, string path, Task installationTask, CancellationToken cancellationToken) - => DelegateCall(version, installer => installer.CreateInstallation(version, path, installationTask, cancellationToken)); + public ValueTask GetInstallation(EngineVersion version, string path, Task installationTask, CancellationToken cancellationToken) + => DelegateCall(version, installer => installer.GetInstallation(version, path, installationTask, cancellationToken)); /// public ValueTask DownloadVersion(EngineVersion version, JobProgressReporter jobProgressReporter, CancellationToken cancellationToken) => DelegateCall(version, installer => installer.DownloadVersion(version, jobProgressReporter, cancellationToken)); /// - public ValueTask Install(EngineVersion version, string path, bool deploymentPipelineProcesses, CancellationToken cancellationToken) + public ValueTask Install(EngineVersion version, string path, bool deploymentPipelineProcesses, CancellationToken cancellationToken) => DelegateCall(version, installer => installer.Install(version, path, deploymentPipelineProcesses, cancellationToken)); /// diff --git a/src/Tgstation.Server.Host/Components/Engine/EngineExecutableLock.cs b/src/Tgstation.Server.Host/Components/Engine/EngineExecutableLock.cs index 3589d6c4af..1c6e69d95b 100644 --- a/src/Tgstation.Server.Host/Components/Engine/EngineExecutableLock.cs +++ b/src/Tgstation.Server.Host/Components/Engine/EngineExecutableLock.cs @@ -45,13 +45,15 @@ namespace Tgstation.Server.Host.Components.Engine /// public string FormatServerArguments( IDmbProvider dmbProvider, - IReadOnlyDictionary parameters, + IReadOnlyDictionary? parameters, DreamDaemonLaunchParameters launchParameters, + string accessIdentifier, string? logFilePath) => Instance.FormatServerArguments( dmbProvider, parameters, launchParameters, + accessIdentifier, logFilePath); /// diff --git a/src/Tgstation.Server.Host/Components/Engine/EngineInstallationBase.cs b/src/Tgstation.Server.Host/Components/Engine/EngineInstallationBase.cs index 928f8e3006..e499ea9dfd 100644 --- a/src/Tgstation.Server.Host/Components/Engine/EngineInstallationBase.cs +++ b/src/Tgstation.Server.Host/Components/Engine/EngineInstallationBase.cs @@ -57,13 +57,15 @@ namespace Tgstation.Server.Host.Components.Engine /// The active . /// The formatted parameters . protected static string EncodeParameters( - IReadOnlyDictionary parameters, + IReadOnlyDictionary? parameters, DreamDaemonLaunchParameters launchParameters) { - var parametersString = String.Join('&', parameters.Select(kvp => $"{HttpUtility.UrlEncode(kvp.Key)}={HttpUtility.UrlEncode(kvp.Value)}")); + var parametersString = parameters != null + ? $"{String.Join('&', parameters.Select(kvp => $"{HttpUtility.UrlEncode(kvp.Key)}={HttpUtility.UrlEncode(kvp.Value)}"))}&" + : String.Empty; if (!String.IsNullOrEmpty(launchParameters.AdditionalParameters)) - parametersString = $"{parametersString}&{launchParameters.AdditionalParameters}"; + parametersString += launchParameters.AdditionalParameters; return parametersString; } @@ -83,8 +85,9 @@ namespace Tgstation.Server.Host.Components.Engine /// public abstract string FormatServerArguments( IDmbProvider dmbProvider, - IReadOnlyDictionary parameters, + IReadOnlyDictionary? parameters, DreamDaemonLaunchParameters launchParameters, + string accessIdentifier, string? logFilePath); /// diff --git a/src/Tgstation.Server.Host/Components/Engine/EngineInstallerBase.cs b/src/Tgstation.Server.Host/Components/Engine/EngineInstallerBase.cs index 30329edd34..f7fbf7d072 100644 --- a/src/Tgstation.Server.Host/Components/Engine/EngineInstallerBase.cs +++ b/src/Tgstation.Server.Host/Components/Engine/EngineInstallerBase.cs @@ -40,13 +40,21 @@ namespace Tgstation.Server.Host.Components.Engine } /// - public abstract ValueTask CreateInstallation(EngineVersion version, string path, Task installationTask, CancellationToken cancellationToken); + public abstract ValueTask GetInstallation(EngineVersion version, string path, Task installationTask, CancellationToken cancellationToken); /// public abstract Task CleanCache(CancellationToken cancellationToken); /// - public abstract ValueTask Install(EngineVersion version, string path, bool deploymentPipelineProcesses, CancellationToken cancellationToken); + public async ValueTask Install(EngineVersion version, string path, bool deploymentPipelineProcesses, CancellationToken cancellationToken) + { + CheckVersionValidity(version); + ArgumentNullException.ThrowIfNull(path); + + await InstallImpl(version, path, deploymentPipelineProcesses, cancellationToken); + + return await GetInstallation(version, path, Task.CompletedTask, cancellationToken); + } /// public abstract ValueTask UpgradeInstallation(EngineVersion version, string path, CancellationToken cancellationToken); @@ -67,5 +75,15 @@ namespace Tgstation.Server.Host.Components.Engine if (version.Engine!.Value != TargetEngineType) throw new InvalidOperationException($"Non-{TargetEngineType} engine specified: {version.Engine.Value}"); } + + /// + /// Does actions necessary to get an extracted installation working. + /// + /// The being installed. + /// The path to the installation. + /// If the operation should consider processes it launches to be part of the deployment pipeline. + /// The for the operation. + /// A representing the running operation. + protected abstract ValueTask InstallImpl(EngineVersion version, string path, bool deploymentPipelineProcesses, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Components/Engine/EngineManager.cs b/src/Tgstation.Server.Host/Components/Engine/EngineManager.cs index eeadebc71f..23534c3b4c 100644 --- a/src/Tgstation.Server.Host/Components/Engine/EngineManager.cs +++ b/src/Tgstation.Server.Host/Components/Engine/EngineManager.cs @@ -11,6 +11,7 @@ using Microsoft.Extensions.Logging; using Tgstation.Server.Api.Models; using Tgstation.Server.Common.Extensions; +using Tgstation.Server.Host.Components.Deployment; using Tgstation.Server.Host.Components.Events; using Tgstation.Server.Host.IO; using Tgstation.Server.Host.Jobs; @@ -59,6 +60,11 @@ namespace Tgstation.Server.Host.Components.Engine /// readonly IEventConsumer eventConsumer; + /// + /// The for the . + /// + readonly IDmbFactory dmbFactory; + /// /// The for the . /// @@ -100,12 +106,19 @@ namespace Tgstation.Server.Host.Components.Engine /// The value of . /// The value of . /// The value of . + /// The value of . /// The value of . - public EngineManager(IIOManager ioManager, IEngineInstaller engineInstaller, IEventConsumer eventConsumer, ILogger logger) + public EngineManager( + IIOManager ioManager, + IEngineInstaller engineInstaller, + IEventConsumer eventConsumer, + IDmbFactory dmbFactory, + ILogger logger) { this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); this.engineInstaller = engineInstaller ?? throw new ArgumentNullException(nameof(engineInstaller)); this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer)); + this.dmbFactory = dmbFactory ?? throw new ArgumentNullException(nameof(dmbFactory)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); installedVersions = new Dictionary>(); @@ -149,6 +162,7 @@ namespace Tgstation.Server.Host.Components.Engine stringVersion, }, false, + false, cancellationToken); ActiveVersion = version; @@ -228,6 +242,9 @@ namespace Tgstation.Server.Host.Components.Engine activeVersionUpdate = activeVersionChanged.Task; logger.LogTrace("Waiting for container.OnZeroReferences or switch of active version..."); + if (!containerTask.IsCompleted) + dmbFactory.LogLockStates(); + await Task.WhenAny( containerTask, activeVersionUpdate) @@ -337,7 +354,7 @@ namespace Tgstation.Server.Host.Components.Engine try { - var installation = await engineInstaller.CreateInstallation(version, path, Task.CompletedTask, cancellationToken); + var installation = await engineInstaller.GetInstallation(version, path, Task.CompletedTask, cancellationToken); AddInstallationContainer(installation); logger.LogDebug("Added detected BYOND version {versionKey}...", version); } @@ -428,7 +445,7 @@ namespace Tgstation.Server.Host.Components.Engine } } - var potentialInstallation = await engineInstaller.CreateInstallation( + var potentialInstallation = await engineInstaller.GetInstallation( version, ioManager.ResolvePath(version.ToString()), ourTcs.Task, @@ -488,10 +505,10 @@ namespace Tgstation.Server.Host.Components.Engine progressReporter.StageName = "Running event"; var versionString = version.ToString(); - await eventConsumer.HandleEvent(EventType.EngineInstallStart, new List { versionString }, deploymentPipelineProcesses, cancellationToken); + await eventConsumer.HandleEvent(EventType.EngineInstallStart, new List { versionString }, false, deploymentPipelineProcesses, cancellationToken); installPath = await InstallVersionFiles(progressReporter, version, customVersionStream, deploymentPipelineProcesses, cancellationToken); - await eventConsumer.HandleEvent(EventType.EngineInstallComplete, new List { versionString }, deploymentPipelineProcesses, cancellationToken); + await eventConsumer.HandleEvent(EventType.EngineInstallComplete, new List { versionString }, false, deploymentPipelineProcesses, cancellationToken); ourTcs.SetResult(); } @@ -510,7 +527,7 @@ namespace Tgstation.Server.Host.Components.Engine } } else if (ex is not OperationCanceledException) - await eventConsumer.HandleEvent(EventType.EngineInstallFail, new List { ex.Message }, deploymentPipelineProcesses, cancellationToken); + await eventConsumer.HandleEvent(EventType.EngineInstallFail, new List { ex.Message }, false, deploymentPipelineProcesses, cancellationToken); lock (installedVersions) installedVersions.Remove(version); @@ -598,7 +615,21 @@ namespace Tgstation.Server.Host.Components.Engine remainingReporter.StageName = "Running installation actions"; - await engineInstaller.Install(version, installFullPath, deploymentPipelineProcesses, cancellationToken); + var installation = await engineInstaller.Install(version, installFullPath, deploymentPipelineProcesses, cancellationToken); + + // some minor validation + var serverInstallTask = ioManager.FileExists(installation.ServerExePath, cancellationToken); + if (!await ioManager.FileExists(installation.CompilerExePath, cancellationToken)) + { + logger.LogError("Compiler executable does not exist after engine installation!"); + throw new JobException(ErrorCode.EngineDownloadFail); + } + + if (!await serverInstallTask) + { + logger.LogError("Server executable does not exist after engine installation!"); + throw new JobException(ErrorCode.EngineDownloadFail); + } remainingReporter.ReportProgress(0.9); remainingReporter.StageName = "Writing version file"; diff --git a/src/Tgstation.Server.Host/Components/Engine/IEngineInstallation.cs b/src/Tgstation.Server.Host/Components/Engine/IEngineInstallation.cs index 1d467a2881..d5cb2136ae 100644 --- a/src/Tgstation.Server.Host/Components/Engine/IEngineInstallation.cs +++ b/src/Tgstation.Server.Host/Components/Engine/IEngineInstallation.cs @@ -60,14 +60,16 @@ namespace Tgstation.Server.Host.Components.Engine /// Return the command line arguments for launching with given . /// /// The . - /// The map of parameter s as a . MUST include . Should NOT include the of . + /// The optional map of parameter s as a . MUST include . Should NOT include the of . /// The . + /// The secure used to authenticate communication with the game server. /// The full path to the log file, if any. /// The formatted arguments . string FormatServerArguments( IDmbProvider dmbProvider, - IReadOnlyDictionary parameters, + IReadOnlyDictionary? parameters, DreamDaemonLaunchParameters launchParameters, + string accessIdentifier, string? logFilePath); /// @@ -83,7 +85,7 @@ namespace Tgstation.Server.Host.Components.Engine /// /// The to write to. /// The to be terminated. - /// The of the session. + /// The secure used to authenticate communication with the game server. /// The port the server is running on. /// The for the operation. /// A representing the running operation. diff --git a/src/Tgstation.Server.Host/Components/Engine/IEngineInstallationData.cs b/src/Tgstation.Server.Host/Components/Engine/IEngineInstallationData.cs index b3d8458973..0194c10d52 100644 --- a/src/Tgstation.Server.Host/Components/Engine/IEngineInstallationData.cs +++ b/src/Tgstation.Server.Host/Components/Engine/IEngineInstallationData.cs @@ -14,7 +14,7 @@ namespace Tgstation.Server.Host.Components.Engine /// /// The full path to extract to. /// The for the operation. - /// A representing the running operation. - Task ExtractToPath(string path, CancellationToken cancellationToken); + /// A representing the running operation. + ValueTask ExtractToPath(string path, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Components/Engine/IEngineInstaller.cs b/src/Tgstation.Server.Host/Components/Engine/IEngineInstaller.cs index 1e943f9759..50e54dad60 100644 --- a/src/Tgstation.Server.Host/Components/Engine/IEngineInstaller.cs +++ b/src/Tgstation.Server.Host/Components/Engine/IEngineInstaller.cs @@ -18,8 +18,8 @@ namespace Tgstation.Server.Host.Components.Engine /// The path to the installation. /// The representing the installation process for the installation. /// The for the operation. - /// A resulting in the . - ValueTask CreateInstallation(EngineVersion version, string path, Task installationTask, CancellationToken cancellationToken); + /// A resulting in a new for the given . + ValueTask GetInstallation(EngineVersion version, string path, Task installationTask, CancellationToken cancellationToken); /// /// Download a given engine . @@ -37,8 +37,8 @@ namespace Tgstation.Server.Host.Components.Engine /// The path to the installation. /// If the operation should consider processes it launches to be part of the deployment pipeline. /// The for the operation. - /// A representing the running operation. - ValueTask Install(EngineVersion version, string path, bool deploymentPipelineProcesses, CancellationToken cancellationToken); + /// A resulting in the new . + ValueTask Install(EngineVersion version, string path, bool deploymentPipelineProcesses, CancellationToken cancellationToken); /// /// Does actions necessary to get upgrade a version installed by a previous version of TGS. diff --git a/src/Tgstation.Server.Host/Components/Engine/OpenDreamInstallation.cs b/src/Tgstation.Server.Host/Components/Engine/OpenDreamInstallation.cs index b8f5e7f877..686b0994d2 100644 --- a/src/Tgstation.Server.Host/Components/Engine/OpenDreamInstallation.cs +++ b/src/Tgstation.Server.Host/Components/Engine/OpenDreamInstallation.cs @@ -12,9 +12,7 @@ using Microsoft.Extensions.Logging; using Tgstation.Server.Api.Models; using Tgstation.Server.Api.Models.Internal; -using Tgstation.Server.Common.Http; using Tgstation.Server.Host.Components.Deployment; -using Tgstation.Server.Host.Components.Interop; using Tgstation.Server.Host.IO; using Tgstation.Server.Host.System; using Tgstation.Server.Host.Utils; @@ -56,9 +54,9 @@ namespace Tgstation.Server.Host.Components.Engine readonly IAsyncDelayer asyncDelayer; /// - /// The for the . + /// The for the . /// - readonly IAbstractHttpClientFactory httpClientFactory; + readonly IHttpClientFactory httpClientFactory; /// /// Path to the Robust.Server.dll. @@ -84,7 +82,7 @@ namespace Tgstation.Server.Host.Components.Engine public OpenDreamInstallation( IIOManager installationIOManager, IAsyncDelayer asyncDelayer, - IAbstractHttpClientFactory httpClientFactory, + IHttpClientFactory httpClientFactory, string dotnetPath, string serverDllPath, string compilerDllPath, @@ -110,20 +108,21 @@ namespace Tgstation.Server.Host.Components.Engine /// public override string FormatServerArguments( IDmbProvider dmbProvider, - IReadOnlyDictionary parameters, + IReadOnlyDictionary? parameters, DreamDaemonLaunchParameters launchParameters, + string accessIdentifier, string? logFilePath) { ArgumentNullException.ThrowIfNull(dmbProvider); - ArgumentNullException.ThrowIfNull(parameters); ArgumentNullException.ThrowIfNull(launchParameters); + ArgumentNullException.ThrowIfNull(accessIdentifier); - if (!parameters.TryGetValue(DMApiConstants.ParamAccessIdentifier, out var accessIdentifier)) - throw new ArgumentException($"parameters must have \"{DMApiConstants.ParamAccessIdentifier}\" set!", nameof(parameters)); + var encodedParameters = EncodeParameters(parameters, launchParameters); + var parametersString = !String.IsNullOrEmpty(encodedParameters) + ? $" --cvar opendream.world_params=\"{encodedParameters}\"" + : String.Empty; - var parametersString = EncodeParameters(parameters, launchParameters); - - var arguments = $"{serverDllPath} --cvar {(logFilePath != null ? $"log.path=\"{InstallationIOManager.GetDirectoryName(logFilePath)}\" --cvar log.format=\"{InstallationIOManager.GetFileName(logFilePath)}\"" : "log.enabled=false")} --cvar watchdog.token={accessIdentifier} --cvar log.runtimelog=false --cvar net.port={launchParameters.Port!.Value} --cvar opendream.topic_port={launchParameters.OpenDreamTopicPort!.Value} --cvar opendream.world_params=\"{parametersString}\" --cvar opendream.json_path=\"./{dmbProvider.DmbName}\""; + var arguments = $"{serverDllPath} --cvar {(logFilePath != null ? $"log.path=\"{InstallationIOManager.GetDirectoryName(logFilePath)}\" --cvar log.format=\"{InstallationIOManager.GetFileName(logFilePath)}\"" : "log.enabled=false")} --cvar watchdog.token={accessIdentifier} --cvar log.runtimelog=false --cvar net.port={launchParameters.Port!.Value} --cvar opendream.topic_port={launchParameters.OpenDreamTopicPort!.Value}{parametersString} --cvar opendream.json_path=\"./{dmbProvider.DmbName}\""; return arguments; } diff --git a/src/Tgstation.Server.Host/Components/Engine/OpenDreamInstaller.cs b/src/Tgstation.Server.Host/Components/Engine/OpenDreamInstaller.cs index 87ad2cd721..071fcbaab4 100644 --- a/src/Tgstation.Server.Host/Components/Engine/OpenDreamInstaller.cs +++ b/src/Tgstation.Server.Host/Components/Engine/OpenDreamInstaller.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; @@ -8,7 +9,6 @@ using Microsoft.Extensions.Options; using Tgstation.Server.Api.Models; using Tgstation.Server.Common.Extensions; -using Tgstation.Server.Common.Http; using Tgstation.Server.Host.Components.Repository; using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.IO; @@ -52,14 +52,14 @@ namespace Tgstation.Server.Host.Components.Engine protected IProcessExecutor ProcessExecutor { get; } /// - /// The for the . + /// The for the . /// - protected GeneralConfiguration GeneralConfiguration { get; } + protected IOptionsMonitor GeneralConfigurationOptions { get; } /// /// The for the . /// - protected SessionConfiguration SessionConfiguration { get; } + protected IOptionsMonitor SessionConfigurationOptions { get; } /// /// The for the . @@ -77,9 +77,9 @@ namespace Tgstation.Server.Host.Components.Engine readonly IAsyncDelayer asyncDelayer; /// - /// The for the . + /// The for the . /// - readonly IAbstractHttpClientFactory httpClientFactory; + readonly IHttpClientFactory httpClientFactory; /// /// Initializes a new instance of the class. @@ -91,8 +91,8 @@ namespace Tgstation.Server.Host.Components.Engine /// The value of . /// The value of . /// The value of . - /// The containing value of . - /// The containing value of . + /// The value of . + /// The value of . public OpenDreamInstaller( IIOManager ioManager, ILogger logger, @@ -100,9 +100,9 @@ namespace Tgstation.Server.Host.Components.Engine IProcessExecutor processExecutor, IRepositoryManager repositoryManager, IAsyncDelayer asyncDelayer, - IAbstractHttpClientFactory httpClientFactory, - IOptions generalConfigurationOptions, - IOptions sessionConfigurationOptions) + IHttpClientFactory httpClientFactory, + IOptionsMonitor generalConfigurationOptions, + IOptionsMonitor sessionConfigurationOptions) : base(ioManager, logger) { this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier)); @@ -110,15 +110,15 @@ namespace Tgstation.Server.Host.Components.Engine this.repositoryManager = repositoryManager ?? throw new ArgumentNullException(nameof(repositoryManager)); this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer)); this.httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory)); - GeneralConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); - SessionConfiguration = sessionConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(sessionConfigurationOptions)); + GeneralConfigurationOptions = generalConfigurationOptions ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); + SessionConfigurationOptions = sessionConfigurationOptions ?? throw new ArgumentNullException(nameof(sessionConfigurationOptions)); } /// - public override Task CleanCache(CancellationToken cancellationToken) => Task.CompletedTask; + public sealed override Task CleanCache(CancellationToken cancellationToken) => Task.CompletedTask; /// - public override async ValueTask CreateInstallation(EngineVersion version, string path, Task installationTask, CancellationToken cancellationToken) + public sealed override async ValueTask GetInstallation(EngineVersion version, string path, Task installationTask, CancellationToken cancellationToken) { CheckVersionValidity(version); GetExecutablePaths(path, out var serverExePath, out var compilerExePath); @@ -137,7 +137,7 @@ namespace Tgstation.Server.Host.Components.Engine } /// - public override async ValueTask DownloadVersion(EngineVersion version, JobProgressReporter jobProgressReporter, CancellationToken cancellationToken) + public sealed override async ValueTask DownloadVersion(EngineVersion version, JobProgressReporter jobProgressReporter, CancellationToken cancellationToken) { CheckVersionValidity(version); ArgumentNullException.ThrowIfNull(jobProgressReporter); @@ -147,10 +147,11 @@ namespace Tgstation.Server.Host.Components.Engine var progressSection1 = jobProgressReporter.CreateSection("Updating OpenDream git repository", 0.5f); IRepository? repo; + var generalConfig = GeneralConfigurationOptions.CurrentValue; try { repo = await repositoryManager.CloneRepository( - GeneralConfiguration.OpenDreamGitUrl, + generalConfig.OpenDreamGitUrl, null, null, null, @@ -187,7 +188,7 @@ namespace Tgstation.Server.Host.Components.Engine using (var progressSection2 = jobProgressReporter.CreateSection("Checking out OpenDream version", 0.5f)) { var committish = version.SourceSHA - ?? $"{GeneralConfiguration.OpenDreamGitTagPrefix}{version.Version!.Semver()}"; + ?? $"{generalConfig.OpenDreamGitTagPrefix}{version.Version!.Semver()}"; await repo.CheckoutObject( committish, @@ -216,10 +217,26 @@ namespace Tgstation.Server.Host.Components.Engine } /// - public override async ValueTask Install(EngineVersion version, string installPath, bool deploymentPipelineProcesses, CancellationToken cancellationToken) + public override ValueTask UpgradeInstallation(EngineVersion version, string path, CancellationToken cancellationToken) { CheckVersionValidity(version); - ArgumentNullException.ThrowIfNull(installPath); + ArgumentNullException.ThrowIfNull(path); + return ValueTask.CompletedTask; + } + + /// + public override ValueTask TrustDmbPath(EngineVersion engineVersion, string fullDmbPath, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(engineVersion); + ArgumentNullException.ThrowIfNull(fullDmbPath); + + Logger.LogTrace("TrustDmbPath is a no-op: {path}", fullDmbPath); + return ValueTask.CompletedTask; + } + + /// + protected override async ValueTask InstallImpl(EngineVersion version, string installPath, bool deploymentPipelineProcesses, CancellationToken cancellationToken) + { var sourcePath = IOManager.ConcatPath(installPath, InstallationSourceSubDirectory); if (!await IOManager.DirectoryExists(sourcePath, cancellationToken)) @@ -263,6 +280,7 @@ namespace Tgstation.Server.Host.Components.Engine async shortenedPath => { var shortenedDeployPath = IOManager.ConcatPath(shortenedPath, DeployDir); + var generalConfig = GeneralConfigurationOptions.CurrentValue; await using var buildProcess = await ProcessExecutor.LaunchProcess( dotnetPath, shortenedPath, @@ -270,17 +288,17 @@ namespace Tgstation.Server.Host.Components.Engine cancellationToken, null, null, - !GeneralConfiguration.OpenDreamSuppressInstallOutput, - !GeneralConfiguration.OpenDreamSuppressInstallOutput); + !generalConfig.OpenDreamSuppressInstallOutput, + !generalConfig.OpenDreamSuppressInstallOutput); - if (deploymentPipelineProcesses && SessionConfiguration.LowPriorityDeploymentProcesses) + if (deploymentPipelineProcesses && SessionConfigurationOptions.CurrentValue.LowPriorityDeploymentProcesses) buildProcess.AdjustPriority(false); using (cancellationToken.Register(() => buildProcess.Terminate())) buildExitCode = await buildProcess.Lifetime; string? output; - if (!GeneralConfiguration.OpenDreamSuppressInstallOutput) + if (!GeneralConfigurationOptions.CurrentValue.OpenDreamSuppressInstallOutput) { var buildOutputTask = buildProcess.GetCombinedOutput(cancellationToken); if (!buildOutputTask.IsCompleted) @@ -335,24 +353,6 @@ namespace Tgstation.Server.Host.Components.Engine await IOManager.DeleteDirectory(sourcePath, cancellationToken); } - /// - public override ValueTask UpgradeInstallation(EngineVersion version, string path, CancellationToken cancellationToken) - { - CheckVersionValidity(version); - ArgumentNullException.ThrowIfNull(path); - return ValueTask.CompletedTask; - } - - /// - public override ValueTask TrustDmbPath(EngineVersion engineVersion, string fullDmbPath, CancellationToken cancellationToken) - { - ArgumentNullException.ThrowIfNull(engineVersion); - ArgumentNullException.ThrowIfNull(fullDmbPath); - - Logger.LogTrace("TrustDmbPath is a no-op: {path}", fullDmbPath); - return ValueTask.CompletedTask; - } - /// /// Perform an operation on a very long path. /// diff --git a/src/Tgstation.Server.Host/Components/Engine/PosixByondInstaller.cs b/src/Tgstation.Server.Host/Components/Engine/PosixByondInstaller.cs index 7e21f6a378..ee98a5acd9 100644 --- a/src/Tgstation.Server.Host/Components/Engine/PosixByondInstaller.cs +++ b/src/Tgstation.Server.Host/Components/Engine/PosixByondInstaller.cs @@ -75,11 +75,27 @@ namespace Tgstation.Server.Host.Components.Engine } /// - public override ValueTask Install(EngineVersion version, string path, bool deploymentPipelineProcesses, CancellationToken cancellationToken) + public override ValueTask UpgradeInstallation(EngineVersion version, string path, CancellationToken cancellationToken) { CheckVersionValidity(version); ArgumentNullException.ThrowIfNull(path); + return ValueTask.CompletedTask; + } + + /// + public override ValueTask TrustDmbPath(EngineVersion version, string fullDmbPath, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(version); + ArgumentNullException.ThrowIfNull(fullDmbPath); + + Logger.LogTrace("No need to trust .dmb path \"{path}\" on POSIX", fullDmbPath); + return ValueTask.CompletedTask; + } + + /// + protected override ValueTask InstallImpl(EngineVersion version, string path, bool deploymentPipelineProcesses, CancellationToken cancellationToken) + { // write the scripts for running the ting // need to add $ORIGIN to LD_LIBRARY_PATH const string StandardScript = "#!/bin/sh\nexport LD_LIBRARY_PATH=\"\\$ORIGIN:$LD_LIBRARY_PATH\"\nBASEDIR=$(dirname \"$0\")\nexec \"$BASEDIR/{0}\" \"$@\"\n"; @@ -114,25 +130,6 @@ namespace Tgstation.Server.Host.Components.Engine return task; } - /// - public override ValueTask UpgradeInstallation(EngineVersion version, string path, CancellationToken cancellationToken) - { - CheckVersionValidity(version); - ArgumentNullException.ThrowIfNull(path); - - return ValueTask.CompletedTask; - } - - /// - public override ValueTask TrustDmbPath(EngineVersion version, string fullDmbPath, CancellationToken cancellationToken) - { - ArgumentNullException.ThrowIfNull(version); - ArgumentNullException.ThrowIfNull(fullDmbPath); - - Logger.LogTrace("No need to trust .dmb path \"{path}\" on POSIX", fullDmbPath); - return ValueTask.CompletedTask; - } - /// protected override string GetDreamDaemonName(Version byondVersion, out bool supportsCli) { diff --git a/src/Tgstation.Server.Host/Components/Engine/RepositoryEngineInstallationData.cs b/src/Tgstation.Server.Host/Components/Engine/RepositoryEngineInstallationData.cs index 88fbb77911..8b89075d4e 100644 --- a/src/Tgstation.Server.Host/Components/Engine/RepositoryEngineInstallationData.cs +++ b/src/Tgstation.Server.Host/Components/Engine/RepositoryEngineInstallationData.cs @@ -48,12 +48,11 @@ namespace Tgstation.Server.Host.Components.Engine } /// - public Task ExtractToPath(string path, CancellationToken cancellationToken) + public ValueTask ExtractToPath(string path, CancellationToken cancellationToken) => repository.CopyTo( ioManager.ConcatPath( path, targetSubDirectory), - cancellationToken) - .AsTask(); + cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Components/Engine/WindowsByondInstaller.cs b/src/Tgstation.Server.Host/Components/Engine/WindowsByondInstaller.cs index 786d8df147..c7f1dfb155 100644 --- a/src/Tgstation.Server.Host/Components/Engine/WindowsByondInstaller.cs +++ b/src/Tgstation.Server.Host/Components/Engine/WindowsByondInstaller.cs @@ -79,7 +79,7 @@ namespace Tgstation.Server.Host.Components.Engine /// /// The for the . /// - readonly SessionConfiguration sessionConfiguration; + readonly IOptionsMonitor sessionConfigurationOptions; /// /// The for the . @@ -96,7 +96,7 @@ namespace Tgstation.Server.Host.Components.Engine /// /// The value of . /// The containing the . - /// The containing the value of . + /// The value of . /// The for the . /// The for the . /// The for the . @@ -105,12 +105,12 @@ namespace Tgstation.Server.Host.Components.Engine IIOManager ioManager, IFileDownloader fileDownloader, IOptionsMonitor generalConfigurationOptions, - IOptions sessionConfigurationOptions, + IOptionsMonitor sessionConfigurationOptions, ILogger logger) : base(ioManager, logger, fileDownloader, generalConfigurationOptions) { this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor)); - sessionConfiguration = sessionConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(sessionConfigurationOptions)); + this.sessionConfigurationOptions = sessionConfigurationOptions ?? throw new ArgumentNullException(nameof(sessionConfigurationOptions)); var useServiceSpecialTactics = Environment.Is64BitProcess && Environment.UserName == $"{Environment.MachineName}$"; @@ -130,29 +130,6 @@ namespace Tgstation.Server.Host.Components.Engine /// public void Dispose() => semaphore.Dispose(); - /// - public override ValueTask Install(EngineVersion version, string path, bool deploymentPipelineProcesses, CancellationToken cancellationToken) - { - CheckVersionValidity(version); - ArgumentNullException.ThrowIfNull(path); - - var noPromptTrustedTask = SetNoPromptTrusted(path, cancellationToken); - var installDirectXTask = InstallDirectX(path, cancellationToken); - var tasks = new List(3) - { - noPromptTrustedTask, - installDirectXTask, - }; - - if (!GeneralConfigurationOptions.CurrentValue.SkipAddingByondFirewallException) - { - var firewallTask = AddDreamDaemonToFirewall(version, path, deploymentPipelineProcesses, cancellationToken); - tasks.Add(firewallTask); - } - - return ValueTaskExtensions.WhenAll(tasks); - } - /// public override async ValueTask UpgradeInstallation(EngineVersion version, string path, CancellationToken cancellationToken) { @@ -215,10 +192,30 @@ namespace Tgstation.Server.Host.Components.Engine } } + /// + protected override ValueTask InstallImpl(EngineVersion version, string path, bool deploymentPipelineProcesses, CancellationToken cancellationToken) + { + var noPromptTrustedTask = SetNoPromptTrusted(path, cancellationToken); + var installDirectXTask = InstallDirectX(path, cancellationToken); + var tasks = new List(3) + { + noPromptTrustedTask, + installDirectXTask, + }; + + if (!GeneralConfigurationOptions.CurrentValue.SkipAddingByondFirewallException) + { + var firewallTask = AddDreamDaemonToFirewall(version, path, deploymentPipelineProcesses, cancellationToken); + tasks.Add(firewallTask); + } + + return ValueTaskExtensions.WhenAll(tasks); + } + /// protected override string GetDreamDaemonName(Version byondVersion, out bool supportsCli) { - supportsCli = byondVersion >= DDExeVersion && !sessionConfiguration.ForceUseDreamDaemonExe; + supportsCli = byondVersion >= DDExeVersion && !sessionConfigurationOptions.CurrentValue.ForceUseDreamDaemonExe; return supportsCli ? "dd.exe" : "dreamdaemon.exe"; } @@ -330,7 +327,7 @@ namespace Tgstation.Server.Host.Components.Engine Logger, ruleName, dreamDaemonPath, - deploymentPipelineProcesses && sessionConfiguration.LowPriorityDeploymentProcesses, + deploymentPipelineProcesses && sessionConfigurationOptions.CurrentValue.LowPriorityDeploymentProcesses, cancellationToken); } catch (Exception ex) diff --git a/src/Tgstation.Server.Host/Components/Engine/WindowsOpenDreamInstaller.cs b/src/Tgstation.Server.Host/Components/Engine/WindowsOpenDreamInstaller.cs index 1cc8da52c5..9ee684d1cf 100644 --- a/src/Tgstation.Server.Host/Components/Engine/WindowsOpenDreamInstaller.cs +++ b/src/Tgstation.Server.Host/Components/Engine/WindowsOpenDreamInstaller.cs @@ -1,4 +1,5 @@ using System; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; @@ -7,7 +8,6 @@ using Microsoft.Extensions.Options; using Tgstation.Server.Api.Models; using Tgstation.Server.Common.Extensions; -using Tgstation.Server.Common.Http; using Tgstation.Server.Host.Components.Repository; using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.IO; @@ -36,9 +36,9 @@ namespace Tgstation.Server.Host.Components.Engine /// The for the . /// The for the . /// The for the . - /// The for the . - /// The of for the . - /// The of for the . + /// The for the . + /// The of for the . + /// The of for the . /// The value of . public WindowsOpenDreamInstaller( IIOManager ioManager, @@ -47,9 +47,9 @@ namespace Tgstation.Server.Host.Components.Engine IProcessExecutor processExecutor, IRepositoryManager repositoryManager, IAsyncDelayer asyncDelayer, - IAbstractHttpClientFactory httpClientFactory, - IOptions generalConfigurationOptions, - IOptions sessionConfigurationOptions, + IHttpClientFactory httpClientFactory, + IOptionsMonitor generalConfigurationOptions, + IOptionsMonitor sessionConfigurationOptions, IFilesystemLinkFactory linkFactory) : base( ioManager, @@ -66,9 +66,9 @@ namespace Tgstation.Server.Host.Components.Engine } /// - public override ValueTask Install(EngineVersion version, string installPath, bool deploymentPipelineProcesses, CancellationToken cancellationToken) + protected override ValueTask InstallImpl(EngineVersion version, string installPath, bool deploymentPipelineProcesses, CancellationToken cancellationToken) { - var installTask = base.Install( + var installTask = base.InstallImpl( version, installPath, deploymentPipelineProcesses, @@ -108,7 +108,7 @@ namespace Tgstation.Server.Host.Components.Engine /// A representing the running operation. async ValueTask AddServerFirewallException(EngineVersion version, string path, bool deploymentPipelineProcesses, CancellationToken cancellationToken) { - if (GeneralConfiguration.SkipAddingByondFirewallException) + if (GeneralConfigurationOptions.CurrentValue.SkipAddingByondFirewallException) return; GetExecutablePaths(path, out var serverExePath, out _); @@ -126,7 +126,7 @@ namespace Tgstation.Server.Host.Components.Engine Logger, ruleName, serverExePath, - deploymentPipelineProcesses && SessionConfiguration.LowPriorityDeploymentProcesses, + deploymentPipelineProcesses && SessionConfigurationOptions.CurrentValue.LowPriorityDeploymentProcesses, cancellationToken); } catch (Exception ex) diff --git a/src/Tgstation.Server.Host/Components/Engine/ZipStreamEngineInstallationData.cs b/src/Tgstation.Server.Host/Components/Engine/ZipStreamEngineInstallationData.cs index 116689f8de..195c2b3b04 100644 --- a/src/Tgstation.Server.Host/Components/Engine/ZipStreamEngineInstallationData.cs +++ b/src/Tgstation.Server.Host/Components/Engine/ZipStreamEngineInstallationData.cs @@ -37,7 +37,7 @@ namespace Tgstation.Server.Host.Components.Engine public ValueTask DisposeAsync() => zipStream.DisposeAsync(); /// - public Task ExtractToPath(string path, CancellationToken cancellationToken) + public ValueTask ExtractToPath(string path, CancellationToken cancellationToken) => ioManager.ZipToDirectory(path, zipStream, cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Components/Events/EventConsumer.cs b/src/Tgstation.Server.Host/Components/Events/EventConsumer.cs index fa98e56607..95febc9b15 100644 --- a/src/Tgstation.Server.Host/Components/Events/EventConsumer.cs +++ b/src/Tgstation.Server.Host/Components/Events/EventConsumer.cs @@ -35,15 +35,15 @@ namespace Tgstation.Server.Host.Components.Events => configuration.HandleCustomEvent(eventName, parameters, cancellationToken); /// - public async ValueTask HandleEvent(EventType eventType, IEnumerable parameters, bool deploymentPipeline, CancellationToken cancellationToken) + public async ValueTask HandleEvent(EventType eventType, IEnumerable parameters, bool sensitiveParameters, bool deploymentPipeline, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(parameters); if (watchdog == null) throw new InvalidOperationException("EventConsumer used without watchdog set!"); - var scriptTask = configuration.HandleEvent(eventType, parameters, deploymentPipeline, cancellationToken); - await watchdog.HandleEvent(eventType, parameters, deploymentPipeline, cancellationToken); + var scriptTask = configuration.HandleEvent(eventType, parameters, sensitiveParameters, deploymentPipeline, cancellationToken); + await watchdog.HandleEvent(eventType, parameters, sensitiveParameters, deploymentPipeline, cancellationToken); await scriptTask; } diff --git a/src/Tgstation.Server.Host/Components/Events/EventType.cs b/src/Tgstation.Server.Host/Components/Events/EventType.cs index 1dbfdb2b6f..fd20a911f9 100644 --- a/src/Tgstation.Server.Host/Components/Events/EventType.cs +++ b/src/Tgstation.Server.Host/Components/Events/EventType.cs @@ -18,7 +18,7 @@ RepoCheckout, /// - /// No parameters. + /// Parameters: (Optional) Repository access username, (Optional) repository access password. /// [EventScript("RepoFetch")] RepoFetch, diff --git a/src/Tgstation.Server.Host/Components/Events/IEventConsumer.cs b/src/Tgstation.Server.Host/Components/Events/IEventConsumer.cs index a12547e8c2..ef8fc76411 100644 --- a/src/Tgstation.Server.Host/Components/Events/IEventConsumer.cs +++ b/src/Tgstation.Server.Host/Components/Events/IEventConsumer.cs @@ -14,10 +14,11 @@ namespace Tgstation.Server.Host.Components.Events /// /// The . /// An of parameters for . + /// If parameters are considered sensitive and should not be logged. /// If this event is part of the deployment pipeline. /// The for the operation. /// A representing the running operation. - ValueTask HandleEvent(EventType eventType, IEnumerable parameters, bool deploymentPipeline, CancellationToken cancellationToken); + ValueTask HandleEvent(EventType eventType, IEnumerable parameters, bool sensitiveParameters, bool deploymentPipeline, CancellationToken cancellationToken); /// /// Handles a given custom event. diff --git a/src/Tgstation.Server.Host/Components/Events/NoopEventConsumer.cs b/src/Tgstation.Server.Host/Components/Events/NoopEventConsumer.cs index 880a45c9ef..41de8862d3 100644 --- a/src/Tgstation.Server.Host/Components/Events/NoopEventConsumer.cs +++ b/src/Tgstation.Server.Host/Components/Events/NoopEventConsumer.cs @@ -10,7 +10,7 @@ namespace Tgstation.Server.Host.Components.Events sealed class NoopEventConsumer : IEventConsumer { /// - public ValueTask HandleEvent(EventType eventType, IEnumerable parameters, bool deploymentPipeline, CancellationToken cancellationToken) + public ValueTask HandleEvent(EventType eventType, IEnumerable parameters, bool sensitiveParameters, bool deploymentPipeline, CancellationToken cancellationToken) => ValueTask.CompletedTask; /// diff --git a/src/Tgstation.Server.Host/Components/IInstanceManager.cs b/src/Tgstation.Server.Host/Components/IInstanceManager.cs index 516b2a8f30..3184fe9b1f 100644 --- a/src/Tgstation.Server.Host/Components/IInstanceManager.cs +++ b/src/Tgstation.Server.Host/Components/IInstanceManager.cs @@ -15,10 +15,10 @@ namespace Tgstation.Server.Host.Components Task Ready { get; } /// - /// Get the associated with given . + /// Get the associated with given . /// - /// The of the desired . - /// The associated with the given if it is online, otherwise. - IInstanceReference? GetInstanceReference(Api.Models.Instance metadata); + /// The of the desired . + /// The associated with the given if it is online, otherwise. + IInstanceReference? GetInstanceReference(long instanceId); } } diff --git a/src/Tgstation.Server.Host/Components/Instance.cs b/src/Tgstation.Server.Host/Components/Instance.cs index 37454b7dbc..a03c0606ad 100644 --- a/src/Tgstation.Server.Host/Components/Instance.cs +++ b/src/Tgstation.Server.Host/Components/Instance.cs @@ -389,7 +389,6 @@ namespace Tgstation.Server.Host.Components // assume 5 steps with synchronize var repositorySettingsTask = databaseContext .RepositorySettings - .AsQueryable() .Where(x => x.InstanceId == metadata.Id) .FirstAsync(cancellationToken); @@ -433,7 +432,6 @@ namespace Tgstation.Server.Host.Components logger.LogTrace("Loading revision info for commit {sha}...", startSha[..7]); currentRevInfo = await databaseContext .RevisionInformations - .AsQueryable() .Where(x => x.CommitSha == startSha && x.InstanceId == metadata.Id) .Include(x => x.ActiveTestMerges!) .ThenInclude(x => x.TestMerge) @@ -550,8 +548,8 @@ namespace Tgstation.Server.Host.Components var currentHead = repo.Head; - currentRevInfo = await databaseContext.RevisionInformations - .AsQueryable() + currentRevInfo = await databaseContext + .RevisionInformations .Where(x => x.CommitSha == currentHead && x.InstanceId == metadata.Id) .FirstOrDefaultAsync(cancellationToken); @@ -656,7 +654,7 @@ namespace Tgstation.Server.Host.Components async ValueTask AutoUpdateAction(CancellationToken cancellationToken) { logger.LogInformation("Beginning auto update..."); - await eventConsumer.HandleEvent(EventType.InstanceAutoUpdateStart, Enumerable.Empty(), true, cancellationToken); + await eventConsumer.HandleEvent(EventType.InstanceAutoUpdateStart, Enumerable.Empty(), false, true, cancellationToken); var repositoryUpdateJob = Job.Create(Api.Models.JobCode.RepositoryAutoUpdate, null, metadata, RepositoryRights.CancelPendingChanges); await jobManager.RegisterOperation( diff --git a/src/Tgstation.Server.Host/Components/InstanceFactory.cs b/src/Tgstation.Server.Host/Components/InstanceFactory.cs index 04994acec5..f9ad842d1a 100644 --- a/src/Tgstation.Server.Host/Components/InstanceFactory.cs +++ b/src/Tgstation.Server.Host/Components/InstanceFactory.cs @@ -150,14 +150,14 @@ namespace Tgstation.Server.Host.Components readonly IMetricFactory metricFactory; /// - /// The for the . + /// The of for the . /// - readonly GeneralConfiguration generalConfiguration; + readonly IOptionsMonitor generalConfigurationOptions; /// - /// The for the . + /// The of for the . /// - readonly SessionConfiguration sessionConfiguration; + readonly IOptionsMonitor sessionConfigurationOptions; /// /// Create the pointing to the "Game" directory of a given . @@ -193,8 +193,8 @@ namespace Tgstation.Server.Host.Components /// The value of . /// The value of . /// The value of . - /// The containing the value of . - /// The containing the value of . + /// The value of . + /// The value of . public InstanceFactory( IIOManager ioManager, IDatabaseContextFactory databaseContextFactory, @@ -219,8 +219,8 @@ namespace Tgstation.Server.Host.Components IAsyncDelayer asyncDelayer, IDotnetDumpService dotnetDumpService, IMetricFactory metricFactory, - IOptions generalConfigurationOptions, - IOptions sessionConfigurationOptions) + IOptionsMonitor generalConfigurationOptions, + IOptionsMonitor sessionConfigurationOptions) { this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory)); @@ -245,8 +245,8 @@ namespace Tgstation.Server.Host.Components this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer)); this.dotnetDumpService = dotnetDumpService ?? throw new ArgumentNullException(nameof(dotnetDumpService)); this.metricFactory = metricFactory ?? throw new ArgumentNullException(nameof(metricFactory)); - generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); - sessionConfiguration = sessionConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(sessionConfigurationOptions)); + this.generalConfigurationOptions = generalConfigurationOptions ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); + this.sessionConfigurationOptions = sessionConfigurationOptions ?? throw new ArgumentNullException(nameof(sessionConfigurationOptions)); } #pragma warning restore CA1502 @@ -291,20 +291,14 @@ namespace Tgstation.Server.Host.Components postWriteHandler, platformIdentifier, fileTransferService, + generalConfigurationOptions, + sessionConfigurationOptions, loggerFactory.CreateLogger(), - metadata, - generalConfiguration, - sessionConfiguration); + metadata); var eventConsumer = new EventConsumer(configuration); var repoManager = repositoryManagerFactory.CreateRepositoryManager(repoIoManager, eventConsumer); try { - var engineManager = new EngineManager( - byondIOManager, - engineInstaller, - eventConsumer, - loggerFactory.CreateLogger()); - var dmbFactory = new DmbFactory( databaseContextFactory, gameIoManager, @@ -315,63 +309,30 @@ namespace Tgstation.Server.Host.Components metadata); try { - var commandFactory = new CommandFactory(assemblyInformationProvider, engineManager, repoManager, databaseContextFactory, dmbFactory, metadata); - - var chatManager = chatFactory.CreateChatManager(commandFactory, metadata.ChatSettings); + var engineManager = new EngineManager( + byondIOManager, + engineInstaller, + eventConsumer, + dmbFactory, + loggerFactory.CreateLogger()); try { - var reattachInfoHandler = new SessionPersistor( - databaseContextFactory, - dmbFactory, - processExecutor, - loggerFactory.CreateLogger(), - metadata); + var commandFactory = new CommandFactory(assemblyInformationProvider, engineManager, repoManager, databaseContextFactory, dmbFactory, metadata); - var sessionControllerFactory = new SessionControllerFactory( - processExecutor, - engineManager, - topicClientFactory, - cryptographySuite, - assemblyInformationProvider, - gameIoManager, - diagnosticsIOManager, - chatManager, - networkPromptReaper, - platformIdentifier, - bridgeRegistrar, - serverPortProvider, - eventConsumer, - asyncDelayer, - dotnetDumpService, - metricFactory, - loggerFactory, - loggerFactory.CreateLogger(), - sessionConfiguration, - metadata); - - var watchdog = watchdogFactory.CreateWatchdog( - chatManager, - dmbFactory, - reattachInfoHandler, - sessionControllerFactory, - gameIoManager, - diagnosticsIOManager, - configuration, // watchdog doesn't need itself as an event consumer - remoteDeploymentManagerFactory, - metricFactory, - metadata, - metadata.DreamDaemonSettings!); + var chatManager = chatFactory.CreateChatManager(commandFactory, metadata.ChatSettings); try { - eventConsumer.SetWatchdog(watchdog); - commandFactory.SetWatchdog(watchdog); + var reattachInfoHandler = new SessionPersistor( + databaseContextFactory, + dmbFactory, + processExecutor, + loggerFactory.CreateLogger(), + metadata); - Instance? instance = null; var dreamMaker = new DreamMaker( engineManager, gameIoManager, configuration, - sessionControllerFactory, eventConsumer, chatManager, processExecutor, @@ -380,36 +341,86 @@ namespace Tgstation.Server.Host.Components remoteDeploymentManagerFactory, asyncDelayer, metricFactory, + sessionConfigurationOptions, loggerFactory.CreateLogger(), - sessionConfiguration, metadata); - instance = new Instance( - metadata, - repoManager, + var sessionControllerFactory = new SessionControllerFactory( + processExecutor, engineManager, - dreamMaker, - watchdog, + topicClientFactory, + cryptographySuite, + assemblyInformationProvider, + gameIoManager, + diagnosticsIOManager, chatManager, - configuration, - dmbFactory, - jobManager, + networkPromptReaper, + platformIdentifier, + bridgeRegistrar, + serverPortProvider, eventConsumer, - remoteDeploymentManagerFactory, + jobManager, + dreamMaker, asyncDelayer, - loggerFactory.CreateLogger()); + dotnetDumpService, + metricFactory, + loggerFactory, + sessionConfigurationOptions, + loggerFactory.CreateLogger(), + metadata); - return instance; + dreamMaker.SetSessionControllerFactory(sessionControllerFactory); + + var watchdog = watchdogFactory.CreateWatchdog( + chatManager, + dmbFactory, + reattachInfoHandler, + sessionControllerFactory, + gameIoManager, + diagnosticsIOManager, + configuration, // watchdog doesn't need itself as an event consumer + remoteDeploymentManagerFactory, + metricFactory, + metadata, + metadata.DreamDaemonSettings!); + try + { + eventConsumer.SetWatchdog(watchdog); + commandFactory.SetWatchdog(watchdog); + + Instance? instance = null; + instance = new Instance( + metadata, + repoManager, + engineManager, + dreamMaker, + watchdog, + chatManager, + configuration, + dmbFactory, + jobManager, + eventConsumer, + remoteDeploymentManagerFactory, + asyncDelayer, + loggerFactory.CreateLogger()); + + return instance; + } + catch + { + await watchdog.DisposeAsync(); + throw; + } } catch { - await watchdog.DisposeAsync(); + await chatManager.DisposeAsync(); throw; } } catch { - await chatManager.DisposeAsync(); + engineManager.Dispose(); throw; } } diff --git a/src/Tgstation.Server.Host/Components/InstanceManager.cs b/src/Tgstation.Server.Host/Components/InstanceManager.cs index e1fa9b8edd..a361a7314a 100644 --- a/src/Tgstation.Server.Host/Components/InstanceManager.cs +++ b/src/Tgstation.Server.Host/Components/InstanceManager.cs @@ -102,6 +102,21 @@ namespace Tgstation.Server.Host.Components /// readonly IPlatformIdentifier platformIdentifier; + /// + /// The of for the . + /// + readonly IOptions generalConfigurationOptions; + + /// + /// The of for the . + /// + readonly IOptions swarmConfigurationOptions; + + /// + /// The of for the . + /// + readonly IOptions internalConfigurationOptions; + /// /// The for the . /// @@ -122,21 +137,6 @@ namespace Tgstation.Server.Host.Components /// readonly SemaphoreSlim instanceStateChangeSemaphore; - /// - /// The for the . - /// - readonly GeneralConfiguration generalConfiguration; - - /// - /// The for the . - /// - readonly SwarmConfiguration swarmConfiguration; - - /// - /// The for the . - /// - readonly InternalConfiguration internalConfiguration; - /// /// The for . /// @@ -189,9 +189,9 @@ namespace Tgstation.Server.Host.Components /// The value of . /// The used to create metrics. /// The to use. - /// The containing the value of . - /// The containing the value of . - /// The containing the value of . + /// The value of . + /// The value of . + /// The value of . /// The value of . public InstanceManager( IInstanceFactory instanceFactory, @@ -227,9 +227,9 @@ namespace Tgstation.Server.Host.Components this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier)); ArgumentNullException.ThrowIfNull(metricFactory); ArgumentNullException.ThrowIfNull(collectorRegistry); - generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); - swarmConfiguration = swarmConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(swarmConfigurationOptions)); - internalConfiguration = internalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(internalConfigurationOptions)); + this.generalConfigurationOptions = generalConfigurationOptions ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); + this.swarmConfigurationOptions = swarmConfigurationOptions ?? throw new ArgumentNullException(nameof(swarmConfigurationOptions)); + this.internalConfigurationOptions = internalConfigurationOptions ?? throw new ArgumentNullException(nameof(internalConfigurationOptions)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); originalConsoleTitle = console.Title; @@ -272,13 +272,11 @@ namespace Tgstation.Server.Host.Components } /// - public IInstanceReference? GetInstanceReference(Api.Models.Instance metadata) + public IInstanceReference? GetInstanceReference(long instanceId) { - ArgumentNullException.ThrowIfNull(metadata); - lock (instances) { - if (!instances.TryGetValue(metadata.Require(x => x.Id), out var instance)) + if (!instances.TryGetValue(instanceId, out var instance)) return null; return instance.AddReference(); @@ -291,7 +289,7 @@ namespace Tgstation.Server.Host.Components ArgumentNullException.ThrowIfNull(oldPath); using var lockContext = await SemaphoreSlimContext.Lock(instanceStateChangeSemaphore, cancellationToken); - using var instanceReferenceCheck = GetInstanceReference(instance); + using var instanceReferenceCheck = this.GetInstanceReference(instance); if (instanceReferenceCheck != null) throw new InvalidOperationException("Cannot move an online instance!"); var newPath = instance.Path!; @@ -389,7 +387,6 @@ namespace Tgstation.Server.Host.Components { var jobs = await db .Jobs - .AsQueryable() .Where(x => x.Instance!.Id == metadata.Id && !x.StoppedAt.HasValue) .Select(x => new Job(x.Id!.Value)) .ToListAsync(cancellationToken); @@ -640,8 +637,7 @@ namespace Tgstation.Server.Host.Components async ValueTask EnumerateInstances(IDatabaseContext databaseContext) => dbInstances = await databaseContext .Instances - .AsQueryable() - .Where(x => x.Online!.Value && x.SwarmIdentifer == swarmConfiguration.Identifier) + .Where(x => x.Online!.Value && x.SwarmIdentifer == swarmConfigurationOptions.Value.Identifier) .Include(x => x.RepositorySettings) .Include(x => x.ChatSettings) .ThenInclude(x => x.Channels) @@ -702,14 +698,14 @@ namespace Tgstation.Server.Host.Components { logger.LogDebug("Running as user: {username}", Environment.UserName); - generalConfiguration.CheckCompatibility(logger, ioManager); + generalConfigurationOptions.Value.CheckCompatibility(logger, ioManager); using (var systemIdentity = systemIdentityFactory.GetCurrent()) { if (!systemIdentity.CanCreateSymlinks) throw new InvalidOperationException($"The user running {Constants.CanonicalPackageName} cannot create symlinks! Please try running as an administrative user!"); - if (!platformIdentifier.IsWindows && systemIdentity.IsSuperUser && !internalConfiguration.UsingDocker) + if (!platformIdentifier.IsWindows && systemIdentity.IsSuperUser && !internalConfigurationOptions.Value.UsingDocker) { logger.LogWarning("TGS is being run as the root account. This is not recommended."); } diff --git a/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeCommandType.cs b/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeCommandType.cs index 287e9aceda..dc748cb44d 100644 --- a/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeCommandType.cs +++ b/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeCommandType.cs @@ -44,5 +44,10 @@ /// DreamDaemon requesting a custom event to be triggered. /// Event, + + /// + /// DreamDaemon requesting TGS trigger a deployment. + /// + Deploy, } } diff --git a/src/Tgstation.Server.Host/Components/Repository/Repository.cs b/src/Tgstation.Server.Host/Components/Repository/Repository.cs index 636321b5c7..a62a50418b 100644 --- a/src/Tgstation.Server.Host/Components/Repository/Repository.cs +++ b/src/Tgstation.Server.Host/Components/Repository/Repository.cs @@ -9,6 +9,7 @@ using LibGit2Sharp; using LibGit2Sharp.Handlers; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Tgstation.Server.Api.Models; using Tgstation.Server.Host.Components.Events; @@ -115,16 +116,16 @@ namespace Tgstation.Server.Host.Components.Repository /// readonly ILibGit2RepositoryFactory submoduleFactory; + /// + /// The of for the . + /// + readonly IOptionsMonitor generalConfigurationOptions; + /// /// The for the . /// readonly ILogger logger; - /// - /// The for the . - /// - readonly GeneralConfiguration generalConfiguration; - /// /// Initializes a new instance of the class. /// @@ -136,8 +137,8 @@ namespace Tgstation.Server.Host.Components.Repository /// The value of . /// The to provide the value of . /// The value of . + /// The value of . /// The value of . - /// The value of . /// The action for the . public Repository( LibGit2Sharp.IRepository libGitRepo, @@ -148,8 +149,8 @@ namespace Tgstation.Server.Host.Components.Repository IPostWriteHandler postWriteHandler, IGitRemoteFeaturesFactory gitRemoteFeaturesFactory, ILibGit2RepositoryFactory submoduleFactory, + IOptionsMonitor generalConfigurationOptions, ILogger logger, - GeneralConfiguration generalConfiguration, Action disposeAction) : base(disposeAction) { @@ -161,9 +162,8 @@ namespace Tgstation.Server.Host.Components.Repository this.postWriteHandler = postWriteHandler ?? throw new ArgumentNullException(nameof(postWriteHandler)); ArgumentNullException.ThrowIfNull(gitRemoteFeaturesFactory); this.submoduleFactory = submoduleFactory ?? throw new ArgumentNullException(nameof(submoduleFactory)); - + this.generalConfigurationOptions = generalConfigurationOptions ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); - this.generalConfiguration = generalConfiguration ?? throw new ArgumentNullException(nameof(generalConfiguration)); gitRemoteFeatures = gitRemoteFeaturesFactory.CreateGitRemoteFeatures(this); } @@ -321,6 +321,7 @@ namespace Tgstation.Server.Host.Components.Repository EventType.RepoMergeConflict, arguments, false, + false, cancellationToken); return new TestMergeResult { @@ -362,6 +363,7 @@ namespace Tgstation.Server.Host.Components.Repository testMergeParameters.Comment, }, false, + false, cancellationToken); return new TestMergeResult @@ -384,7 +386,7 @@ namespace Tgstation.Server.Host.Components.Repository ArgumentNullException.ThrowIfNull(committish); logger.LogDebug("Checkout object: {committish}...", committish); - await eventConsumer.HandleEvent(EventType.RepoCheckout, new List { committish, moveCurrentReference.ToString() }, false, cancellationToken); + await eventConsumer.HandleEvent(EventType.RepoCheckout, new List { committish, moveCurrentReference.ToString() }, false, false, cancellationToken); await Task.Factory.StartNew( () => { @@ -421,7 +423,16 @@ namespace Tgstation.Server.Host.Components.Repository CancellationToken cancellationToken) { logger.LogDebug("Fetch origin..."); - await eventConsumer.HandleEvent(EventType.RepoFetch, Enumerable.Empty(), deploymentPipeline, cancellationToken); + + var parameters = new List(); + if (username != null) + { + parameters.Add(username); + if (password != null) + parameters.Add(password); + } + + await eventConsumer.HandleEvent(EventType.RepoFetch, parameters, true, deploymentPipeline, cancellationToken); await Task.Factory.StartNew( () => { @@ -476,7 +487,7 @@ namespace Tgstation.Server.Host.Components.Repository throw new JobException(ErrorCode.RepoReferenceRequired); logger.LogTrace("Reset to origin..."); var trackedBranch = libGitRepo.Head.TrackedBranch; - await eventConsumer.HandleEvent(EventType.RepoResetOrigin, new List { trackedBranch.FriendlyName, trackedBranch.Tip.Sha }, deploymentPipeline, cancellationToken); + await eventConsumer.HandleEvent(EventType.RepoResetOrigin, new List { trackedBranch.FriendlyName, trackedBranch.Tip.Sha }, false, deploymentPipeline, cancellationToken); using (var progressReporter2 = progressReporter.CreateSection(null, updateSubmodules ? 2.0 / 3 : 1.0)) await ResetToSha( @@ -539,7 +550,7 @@ namespace Tgstation.Server.Host.Components.Repository }, ioManager.ResolvePath(), path, - generalConfiguration.GetCopyDirectoryTaskThrottle(), + generalConfigurationOptions.CurrentValue.GetCopyDirectoryTaskThrottle(), cancellationToken); } @@ -629,6 +640,7 @@ namespace Tgstation.Server.Host.Components.Repository oldHead.FriendlyName ?? UnknownReference, trackedBranch.FriendlyName, }, + false, deploymentPipeline, cancellationToken); return null; @@ -686,6 +698,7 @@ namespace Tgstation.Server.Host.Components.Repository { ioManager.ResolvePath(), }, + false, deploymentPipeline, cancellationToken); } @@ -1192,6 +1205,7 @@ namespace Tgstation.Server.Host.Components.Repository await eventConsumer.HandleEvent( EventType.RepoSubmoduleUpdate, new List { submodule.Name }, + false, deploymentPipeline, cancellationToken); diff --git a/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs b/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs index 74a6bc6751..ddf05a7856 100644 --- a/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs +++ b/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using LibGit2Sharp; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Tgstation.Server.Api.Models; using Tgstation.Server.Host.Components.Events; @@ -55,6 +56,11 @@ namespace Tgstation.Server.Host.Components.Repository /// readonly IGitRemoteFeaturesFactory gitRemoteFeaturesFactory; + /// + /// The of for the . + /// + readonly IOptionsMonitor generalConfigurationOptions; + /// /// The created s. /// @@ -65,11 +71,6 @@ namespace Tgstation.Server.Host.Components.Repository /// readonly ILogger logger; - /// - /// The for the . - /// - readonly GeneralConfiguration generalConfiguration; - /// /// Used for controlling single access to the . /// @@ -85,8 +86,8 @@ namespace Tgstation.Server.Host.Components.Repository /// The value of . /// The value of . /// The value of . + /// The value of . /// The value of . - /// The value of . public RepositoryManager( ILibGit2RepositoryFactory repositoryFactory, ILibGit2Commands commands, @@ -94,9 +95,9 @@ namespace Tgstation.Server.Host.Components.Repository IEventConsumer eventConsumer, IPostWriteHandler postWriteHandler, IGitRemoteFeaturesFactory gitRemoteFeaturesFactory, + IOptionsMonitor generalConfigurationOptions, ILogger repositoryLogger, - ILogger logger, - GeneralConfiguration generalConfiguration) + ILogger logger) { this.repositoryFactory = repositoryFactory ?? throw new ArgumentNullException(nameof(repositoryFactory)); this.commands = commands ?? throw new ArgumentNullException(nameof(commands)); @@ -105,8 +106,8 @@ namespace Tgstation.Server.Host.Components.Repository this.postWriteHandler = postWriteHandler ?? throw new ArgumentNullException(nameof(postWriteHandler)); this.gitRemoteFeaturesFactory = gitRemoteFeaturesFactory ?? throw new ArgumentNullException(nameof(gitRemoteFeaturesFactory)); this.repositoryLogger = repositoryLogger ?? throw new ArgumentNullException(nameof(repositoryLogger)); + this.generalConfigurationOptions = generalConfigurationOptions ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); - this.generalConfiguration = generalConfiguration ?? throw new ArgumentNullException(nameof(generalConfiguration)); semaphore = new SemaphoreSlim(1); } @@ -232,8 +233,8 @@ namespace Tgstation.Server.Host.Components.Repository postWriteHandler, gitRemoteFeaturesFactory, repositoryFactory, + generalConfigurationOptions, repositoryLogger, - generalConfiguration, () => { logger.LogTrace("Releasing semaphore due to Repository disposal..."); diff --git a/src/Tgstation.Server.Host/Components/Repository/RepositoryUpdateService.cs b/src/Tgstation.Server.Host/Components/Repository/RepositoryUpdateService.cs index cb15e878f1..de3feb5d08 100644 --- a/src/Tgstation.Server.Host/Components/Repository/RepositoryUpdateService.cs +++ b/src/Tgstation.Server.Host/Components/Repository/RepositoryUpdateService.cs @@ -90,7 +90,10 @@ namespace Tgstation.Server.Host.Components.Repository .ThenInclude(x => x.TestMerge) .ThenInclude(x => x.MergedBy); - var revisionInfo = await ApplyQuery(databaseContext.RevisionInformations).FirstOrDefaultAsync(cancellationToken); + var revisionInfo = await ApplyQuery( + databaseContext + .RevisionInformations) + .FirstOrDefaultAsync(cancellationToken); // If the DB doesn't have it, check the local set if (revisionInfo == default) @@ -400,8 +403,8 @@ namespace Tgstation.Server.Host.Components.Repository await databaseContextFactory.UseContext( async databaseContext => - dbPull = await databaseContext.RevisionInformations - .AsQueryable() + dbPull = await databaseContext + .RevisionInformations .Where(x => x.InstanceId == instanceId && x.OriginCommitSha == lastRevisionInfo.OriginCommitSha && x.ActiveTestMerges!.Count <= newTestMergeModels.Count diff --git a/src/Tgstation.Server.Host/Components/Repository/RepostoryManagerFactory.cs b/src/Tgstation.Server.Host/Components/Repository/RepostoryManagerFactory.cs index cba66ff69a..c48ba8702e 100644 --- a/src/Tgstation.Server.Host/Components/Repository/RepostoryManagerFactory.cs +++ b/src/Tgstation.Server.Host/Components/Repository/RepostoryManagerFactory.cs @@ -34,16 +34,16 @@ namespace Tgstation.Server.Host.Components.Repository /// readonly IGitRemoteFeaturesFactory gitRemoteFeaturesFactory; + /// + /// The of for the . + /// + readonly IOptionsMonitor generalConfigurationOptions; + /// /// The for the . /// readonly ILoggerFactory loggerFactory; - /// - /// The for the . - /// - readonly GeneralConfiguration generalConfiguration; - /// /// Initializes a new instance of the class. /// @@ -52,21 +52,21 @@ namespace Tgstation.Server.Host.Components.Repository /// The value of . /// The value of . /// The value of . - /// The containing the value of . + /// The value of . public RepostoryManagerFactory( ILibGit2RepositoryFactory repositoryFactory, ILibGit2Commands repositoryCommands, IPostWriteHandler postWriteHandler, IGitRemoteFeaturesFactory gitRemoteFeaturesFactory, ILoggerFactory loggerFactory, - IOptions generalConfigurationOptions) + IOptionsMonitor generalConfigurationOptions) { this.repositoryFactory = repositoryFactory ?? throw new ArgumentNullException(nameof(repositoryFactory)); this.repositoryCommands = repositoryCommands ?? throw new ArgumentNullException(nameof(repositoryCommands)); this.postWriteHandler = postWriteHandler ?? throw new ArgumentNullException(nameof(postWriteHandler)); this.gitRemoteFeaturesFactory = gitRemoteFeaturesFactory ?? throw new ArgumentNullException(nameof(gitRemoteFeaturesFactory)); this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); - generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); + this.generalConfigurationOptions = generalConfigurationOptions ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); } /// @@ -78,9 +78,9 @@ namespace Tgstation.Server.Host.Components.Repository eventConsumer, postWriteHandler, gitRemoteFeaturesFactory, + generalConfigurationOptions, loggerFactory.CreateLogger(), - loggerFactory.CreateLogger(), - generalConfiguration); + loggerFactory.CreateLogger()); /// public Task StartAsync(CancellationToken cancellationToken) diff --git a/src/Tgstation.Server.Host/Components/Session/ISessionController.cs b/src/Tgstation.Server.Host/Components/Session/ISessionController.cs index bba0dd4152..3b9bd04caa 100644 --- a/src/Tgstation.Server.Host/Components/Session/ISessionController.cs +++ b/src/Tgstation.Server.Host/Components/Session/ISessionController.cs @@ -89,6 +89,11 @@ namespace Tgstation.Server.Host.Components.Session /// string DumpFileExtension { get; } + /// + /// The number of times a startup bridge request has been received. if is . + /// + long? StartupBridgeRequestsReceived { get; } + /// /// Releases the without terminating it. Also calls . /// diff --git a/src/Tgstation.Server.Host/Components/Session/SessionController.cs b/src/Tgstation.Server.Host/Components/Session/SessionController.cs index 3992acc6ef..fa64c7ed43 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionController.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionController.cs @@ -14,7 +14,7 @@ using Newtonsoft.Json; using Serilog.Context; using Tgstation.Server.Api.Models; -using Tgstation.Server.Api.Models.Internal; +using Tgstation.Server.Api.Rights; using Tgstation.Server.Common.Extensions; using Tgstation.Server.Host.Components.Chat; using Tgstation.Server.Host.Components.Chat.Commands; @@ -25,13 +25,17 @@ using Tgstation.Server.Host.Components.Interop; using Tgstation.Server.Host.Components.Interop.Bridge; using Tgstation.Server.Host.Components.Interop.Topic; using Tgstation.Server.Host.Extensions; +using Tgstation.Server.Host.Jobs; +using Tgstation.Server.Host.Models; using Tgstation.Server.Host.System; using Tgstation.Server.Host.Utils; namespace Tgstation.Server.Host.Components.Session { +#pragma warning disable CA1506 /// sealed class SessionController : Chunker, ISessionController, IBridgeHandler, IChannelSink +#pragma warning restore CA1506 { /// /// If calls to should be trace logged. @@ -65,7 +69,7 @@ namespace Tgstation.Server.Host.Components.Session public Version? DMApiVersion { get; private set; } /// - public bool TerminationWasIntentional => terminationWasIntentional || (Lifetime.IsCompleted && Lifetime.Result == 0); + public bool TerminationWasIntentional => terminationWasIntentionalForced || (Lifetime.IsCompleted && Lifetime.Result == 0); /// public Task LaunchResult { get; } @@ -79,6 +83,9 @@ namespace Tgstation.Server.Host.Components.Session /// public Task OnReboot => rebootTcs.Task; + /// + public long? StartupBridgeRequestsReceived { get; private set; } + /// public Task RebootGate { @@ -172,6 +179,16 @@ namespace Tgstation.Server.Host.Components.Session /// readonly IEventConsumer eventConsumer; + /// + /// The for the . + /// + readonly IJobManager jobManager; + + /// + /// The for the . + /// + readonly IDreamMaker dreamMaker; + /// /// The that completes when DD makes it's first bridge request. /// @@ -250,7 +267,7 @@ namespace Tgstation.Server.Host.Components.Session /// /// Backing field for overriding . /// - bool terminationWasIntentional; + bool terminationWasIntentionalForced; /// /// Initializes a new instance of the class. @@ -267,6 +284,8 @@ namespace Tgstation.Server.Host.Components.Session /// The value of . /// The value of . /// The value of . + /// The value of . + /// The value of . /// The value of . /// The returning a to be run after the ends. /// The optional time to wait before failing the . @@ -285,6 +304,8 @@ namespace Tgstation.Server.Host.Components.Session IAsyncDelayer asyncDelayer, IDotnetDumpService dotnetDumpService, IEventConsumer eventConsumer, + IJobManager jobManager, + IDreamMaker dreamMaker, ILogger logger, Func postLifetimeCallback, uint? startupTimeout, @@ -306,6 +327,8 @@ namespace Tgstation.Server.Host.Components.Session this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer)); this.dotnetDumpService = dotnetDumpService ?? throw new ArgumentNullException(nameof(dotnetDumpService)); this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer)); + this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager)); + this.dreamMaker = dreamMaker ?? throw new ArgumentNullException(nameof(dreamMaker)); apiValidationSession = apiValidate; @@ -329,6 +352,11 @@ namespace Tgstation.Server.Host.Components.Session TopicSendSemaphore = new FifoSemaphore(); synchronizationLock = new object(); + if (DMApiAvailable) + { + StartupBridgeRequestsReceived = 0; + } + if (apiValidationSession || DMApiAvailable) { bridgeRegistration = bridgeRegistrar.RegisterHandler(this); @@ -421,6 +449,12 @@ namespace Tgstation.Server.Host.Components.Session using (LogContext.PushProperty(SerilogContextHelper.InstanceIdContextProperty, metadata.Id)) { + if (!DMApiAvailable && !apiValidationSession) + { + Logger.LogWarning("Ignoring bridge request from session without confirmed DMAPI!"); + return null; + } + Logger.LogTrace("Handling bridge request..."); try @@ -636,6 +670,7 @@ namespace Tgstation.Server.Host.Components.Session /// The for the operation. /// A resulting in the for the request or if the request could not be dispatched. #pragma warning disable CA1502 // TODO: Decomplexify +#pragma warning disable CA1506 async ValueTask ProcessBridgeCommand(BridgeParameters parameters, CancellationToken cancellationToken) { var response = new BridgeResponse(); @@ -675,13 +710,14 @@ namespace Tgstation.Server.Host.Components.Session case BridgeCommandType.Kill: Logger.LogInformation("Bridge requested process termination!"); chatTrackingContext.Active = false; - terminationWasIntentional = true; + terminationWasIntentionalForced = true; process.Terminate(); break; case BridgeCommandType.DeprecatedPortUpdate: return BridgeError("Port switching is no longer supported!"); case BridgeCommandType.Startup: apiValidationStatus = ApiValidationStatus.BadValidationRequest; + ++StartupBridgeRequestsReceived; if (apiValidationSession) { @@ -753,8 +789,9 @@ namespace Tgstation.Server.Host.Components.Session try { chatTrackingContext.Active = false; + var rebootGate = RebootGate; // Important to read this before setting the TCS or it could change Interlocked.Exchange(ref rebootTcs, new TaskCompletionSource()).SetResult(); - await RebootGate.WaitAsync(cancellationToken); + await rebootGate.WaitAsync(cancellationToken); } finally { @@ -766,6 +803,19 @@ namespace Tgstation.Server.Host.Components.Session return await ProcessChunk(ProcessBridgeCommand, BridgeError, parameters.Chunk, cancellationToken); case BridgeCommandType.Event: return TriggerCustomEvent(parameters.EventInvocation); + case BridgeCommandType.Deploy: + var job = Job.Create(JobCode.AutomaticDeployment, null, metadata, DreamMakerRights.CancelCompile); + await jobManager.RegisterOperation( + job, + (core, databaseContextFactory, job, progressReporter, jobCancellationToken) => + dreamMaker.DeploymentProcess( + job, + databaseContextFactory, + progressReporter, + jobCancellationToken), + cancellationToken); + + break; case null: return BridgeError("Missing commandType!"); default: @@ -774,6 +824,7 @@ namespace Tgstation.Server.Host.Components.Session return response; } +#pragma warning restore CA1506 #pragma warning restore CA1502 /// diff --git a/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs b/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs index 5c602ca1e5..a117632d1c 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs @@ -8,6 +8,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Prometheus; @@ -104,6 +105,16 @@ namespace Tgstation.Server.Host.Components.Session /// readonly IEventConsumer eventConsumer; + /// + /// The for the . + /// + readonly IJobManager jobManager; + + /// + /// The for the . + /// + readonly IDreamMaker dreamMaker; + /// /// The for the . /// @@ -119,16 +130,16 @@ namespace Tgstation.Server.Host.Components.Session /// readonly ILoggerFactory loggerFactory; + /// + /// The of for the . + /// + readonly IOptionsMonitor sessionConfigurationOptions; + /// /// The for the . /// readonly ILogger logger; - /// - /// The for the . - /// - readonly SessionConfiguration sessionConfiguration; - /// /// The number of sessions launched. /// @@ -195,12 +206,14 @@ namespace Tgstation.Server.Host.Components.Session /// The value of . /// The value of . /// The value of . + /// The value of . + /// The value of . /// The value of . /// The value of . /// The used to create metrics. + /// The value of . /// The value of . /// The value of . - /// The value of . public SessionControllerFactory( IProcessExecutor processExecutor, IEngineManager engineManager, @@ -215,12 +228,14 @@ namespace Tgstation.Server.Host.Components.Session IBridgeRegistrar bridgeRegistrar, IServerPortProvider serverPortProvider, IEventConsumer eventConsumer, + IJobManager jobManager, + IDreamMaker dreamMaker, IAsyncDelayer asyncDelayer, IDotnetDumpService dotnetDumpService, IMetricFactory metricFactory, ILoggerFactory loggerFactory, + IOptionsMonitor sessionConfigurationOptions, ILogger logger, - SessionConfiguration sessionConfiguration, Api.Models.Instance instance) { this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor)); @@ -236,12 +251,14 @@ namespace Tgstation.Server.Host.Components.Session this.bridgeRegistrar = bridgeRegistrar ?? throw new ArgumentNullException(nameof(bridgeRegistrar)); this.serverPortProvider = serverPortProvider ?? throw new ArgumentNullException(nameof(serverPortProvider)); this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer)); + this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager)); + this.dreamMaker = dreamMaker ?? throw new ArgumentNullException(nameof(dreamMaker)); this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer)); this.dotnetDumpService = dotnetDumpService ?? throw new ArgumentNullException(nameof(dotnetDumpService)); ArgumentNullException.ThrowIfNull(metricFactory); + this.sessionConfigurationOptions = sessionConfigurationOptions ?? throw new ArgumentNullException(nameof(sessionConfigurationOptions)); this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); - this.sessionConfiguration = sessionConfiguration ?? throw new ArgumentNullException(nameof(sessionConfiguration)); this.instance = instance ?? throw new ArgumentNullException(nameof(instance)); sessionsLaunched = metricFactory.CreateCounter("tgs_sessions_launched", "The number of game server processes created"); @@ -249,7 +266,7 @@ namespace Tgstation.Server.Host.Components.Session } /// - #pragma warning disable CA1506 // TODO: Decomplexify +#pragma warning disable CA1506 // TODO: Decomplexify public async ValueTask LaunchNew( IDmbProvider dmbProvider, IEngineExecutableLock? currentByondLock, @@ -375,6 +392,8 @@ namespace Tgstation.Server.Host.Components.Session asyncDelayer, dotnetDumpService, eventConsumer, + jobManager, + dreamMaker, loggerFactory.CreateLogger(), () => LogDDOutput( process, @@ -473,6 +492,8 @@ namespace Tgstation.Server.Host.Components.Session asyncDelayer, dotnetDumpService, eventConsumer, + jobManager, + dreamMaker, loggerFactory.CreateLogger(), () => ValueTask.CompletedTask, null, @@ -526,17 +547,23 @@ namespace Tgstation.Server.Host.Components.Session bool apiValidate, CancellationToken cancellationToken) { - // important to run on all ports to allow port changing - var environment = await engineLock.LoadEnv(logger, false, cancellationToken); - var arguments = engineLock.FormatServerArguments( - dmbProvider, - new Dictionary + var serverMayHaveDMApi = apiValidate || dmbProvider.CompileJob.DMApiVersion != null; + + var serverArguments = serverMayHaveDMApi + ? new Dictionary { { DMApiConstants.ParamApiVersion, DMApiConstants.InteropVersion.Semver().ToString() }, { DMApiConstants.ParamServerPort, serverPortProvider.HttpApiPort.ToString(CultureInfo.InvariantCulture) }, { DMApiConstants.ParamAccessIdentifier, accessIdentifier }, - }, + } + : null; + + var environment = await engineLock.LoadEnv(logger, false, cancellationToken); + var arguments = engineLock.FormatServerArguments( + dmbProvider, + serverArguments, launchParameters, + accessIdentifier, !engineLock.HasStandardOutput || engineLock.PreferFileLogging ? logFilePath : null); @@ -547,6 +574,7 @@ namespace Tgstation.Server.Host.Components.Session EventType.DreamDaemonPreLaunch, Enumerable.Empty(), false, + false, cancellationToken); var process = await processExecutor.LaunchProcess( @@ -561,6 +589,7 @@ namespace Tgstation.Server.Host.Components.Session try { + var sessionConfiguration = sessionConfigurationOptions.CurrentValue; if (!apiValidate) { if (sessionConfiguration.HighPriorityLiveDreamDaemon) @@ -580,6 +609,7 @@ namespace Tgstation.Server.Host.Components.Session process.Id.ToString(CultureInfo.InvariantCulture), }, false, + false, cancellationToken); return process; @@ -612,11 +642,11 @@ namespace Tgstation.Server.Host.Components.Session if (cliSupported) ddOutput = (await process.GetCombinedOutput(cancellationToken))!; - if (ddOutput == null) + if (String.IsNullOrWhiteSpace(ddOutput) && outputFilePath != null) try { var dreamDaemonLogBytes = await gameIOManager.ReadAllBytes( - outputFilePath!, + outputFilePath, cancellationToken); ddOutput = Encoding.UTF8.GetString(dreamDaemonLogBytes); @@ -627,7 +657,7 @@ namespace Tgstation.Server.Host.Components.Session try { logger.LogTrace("Deleting temporary log file {path}...", outputFilePath); - await gameIOManager.DeleteFile(outputFilePath!, cancellationToken); + await gameIOManager.DeleteFile(outputFilePath, cancellationToken); } catch (Exception ex) { diff --git a/src/Tgstation.Server.Host/Components/Session/SessionPersistor.cs b/src/Tgstation.Server.Host/Components/Session/SessionPersistor.cs index edfc6a5145..d82d4937b1 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionPersistor.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionPersistor.cs @@ -158,7 +158,6 @@ namespace Tgstation.Server.Host.Components.Session { var dbReattachInfos = await db .ReattachInformations - .AsQueryable() .Where(x => x.CompileJob!.Job.Instance!.Id == metadata.Id) .Include(x => x.CompileJob) .Include(x => x.InitialCompileJob) @@ -169,7 +168,6 @@ namespace Tgstation.Server.Host.Components.Session var timeoutMilliseconds = await db .Instances - .AsQueryable() .Where(x => x.Id == metadata.Id) .Select(x => x.DreamDaemonSettings!.TopicRequestTimeout) .FirstOrDefaultAsync(cancellationToken); @@ -217,7 +215,6 @@ namespace Tgstation.Server.Host.Components.Session logger.LogTrace("Deleting ReattachInformation {id}...", result.Id); await db .ReattachInformations - .AsQueryable() .Where(x => x.Id == result.Id) .ExecuteDeleteAsync(cancellationToken); }); @@ -264,7 +261,6 @@ namespace Tgstation.Server.Host.Components.Session { var baseQuery = databaseContext .ReattachInformations - .AsQueryable() .Where(x => x.CompileJob!.Job.Instance!.Id == metadata.Id); if (instant) diff --git a/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs b/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs index 6dbfc3cc9c..28f6c1fee5 100644 --- a/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs +++ b/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs @@ -9,6 +9,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Tgstation.Server.Api.Models; using Tgstation.Server.Api.Models.Response; @@ -119,6 +120,16 @@ namespace Tgstation.Server.Host.Components.StaticFiles /// > readonly IFileTransferTicketProvider fileTransferService; + /// + /// The of for . + /// + readonly IOptionsMonitor generalConfigurationOptions; + + /// + /// The of for . + /// + readonly IOptionsMonitor sessionConfigurationOptions; + /// /// The for . /// @@ -129,16 +140,6 @@ namespace Tgstation.Server.Host.Components.StaticFiles /// readonly Models.Instance metadata; - /// - /// The for . - /// - readonly GeneralConfiguration generalConfiguration; - - /// - /// The for . - /// - readonly SessionConfiguration sessionConfiguration; - /// /// The for . Also used as a . /// @@ -166,8 +167,8 @@ namespace Tgstation.Server.Host.Components.StaticFiles /// The value of . /// The value of . /// The value of . - /// The value of . - /// The value of . + /// The value of . + /// The value of . public Configuration( IIOManager ioManager, ISynchronousIOManager synchronousIOManager, @@ -176,10 +177,10 @@ namespace Tgstation.Server.Host.Components.StaticFiles IPostWriteHandler postWriteHandler, IPlatformIdentifier platformIdentifier, IFileTransferTicketProvider fileTransferService, + IOptionsMonitor generalConfigurationOptions, + IOptionsMonitor sessionConfigurationOptions, ILogger logger, - Models.Instance metadata, - GeneralConfiguration generalConfiguration, - SessionConfiguration sessionConfiguration) + Models.Instance metadata) { this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); this.synchronousIOManager = synchronousIOManager ?? throw new ArgumentNullException(nameof(synchronousIOManager)); @@ -188,10 +189,10 @@ namespace Tgstation.Server.Host.Components.StaticFiles this.postWriteHandler = postWriteHandler ?? throw new ArgumentNullException(nameof(postWriteHandler)); this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier)); this.fileTransferService = fileTransferService ?? throw new ArgumentNullException(nameof(fileTransferService)); + this.generalConfigurationOptions = generalConfigurationOptions ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); + this.sessionConfigurationOptions = sessionConfigurationOptions ?? throw new ArgumentNullException(nameof(sessionConfigurationOptions)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); this.metadata = metadata ?? throw new ArgumentNullException(nameof(metadata)); - this.generalConfiguration = generalConfiguration ?? throw new ArgumentNullException(nameof(generalConfiguration)); - this.sessionConfiguration = sessionConfiguration ?? throw new ArgumentNullException(nameof(sessionConfiguration)); semaphore = new SemaphoreSlim(1, 1); stoppingCts = new CancellationTokenSource(); @@ -223,7 +224,7 @@ namespace Tgstation.Server.Host.Components.StaticFiles null, CodeModificationsSubdirectory, destination, - generalConfiguration.GetCopyDirectoryTaskThrottle(), + generalConfigurationOptions.CurrentValue.GetCopyDirectoryTaskThrottle(), cancellationToken); await Task.WhenAll(dmeExistsTask, headFileExistsTask, tailFileExistsTask, copyTask.AsTask()); @@ -652,7 +653,7 @@ namespace Tgstation.Server.Host.Components.StaticFiles } /// - public ValueTask HandleEvent(EventType eventType, IEnumerable parameters, bool deploymentPipeline, CancellationToken cancellationToken) + public ValueTask HandleEvent(EventType eventType, IEnumerable parameters, bool sensitiveParameters, bool deploymentPipeline, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(parameters); @@ -662,7 +663,7 @@ namespace Tgstation.Server.Host.Components.StaticFiles return ValueTask.CompletedTask; } - return ExecuteEventScripts(parameters, deploymentPipeline, cancellationToken, scriptNames); + return ExecuteEventScripts(parameters, sensitiveParameters, deploymentPipeline, cancellationToken, scriptNames); } /// @@ -683,7 +684,7 @@ namespace Tgstation.Server.Host.Components.StaticFiles } #pragma warning disable CA2012 // Use ValueTasks correctly - return ExecuteEventScripts(parameters, false, cancellationToken, scriptName); + return ExecuteEventScripts(parameters, false, false, cancellationToken, scriptName); #pragma warning restore CA2012 // Use ValueTasks correctly } @@ -784,18 +785,20 @@ namespace Tgstation.Server.Host.Components.StaticFiles /// Execute a set of given . /// /// An of parameters for the . + /// If parameters are considered sensitive and should not be logged. /// If this event is part of the deployment pipeline. /// The for the operation. /// The names of the scripts to execute. /// A representing the running operation. - async ValueTask ExecuteEventScripts(IEnumerable parameters, bool deploymentPipeline, CancellationToken cancellationToken, params string[] scriptNames) + async ValueTask ExecuteEventScripts(IEnumerable parameters, bool sensitiveParameters, bool deploymentPipeline, CancellationToken cancellationToken, params string[] scriptNames) { await EnsureDirectories(cancellationToken); // always execute in serial using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken, logger)) { - var directories = generalConfiguration.AdditionalEventScriptsDirectories?.ToList() ?? new List(); + var sessionConfiguration = sessionConfigurationOptions.CurrentValue; + var directories = generalConfigurationOptions.CurrentValue.AdditionalEventScriptsDirectories?.ToList() ?? new List(); directories.Add(EventScriptsSubdirectory); var allScripts = new List(); @@ -857,7 +860,8 @@ namespace Tgstation.Server.Host.Components.StaticFiles { "TGS_INSTANCE_ROOT", metadata.Path! }, }, readStandardHandles: true, - noShellExecute: true)) + noShellExecute: true, + doNotLogArguments: sensitiveParameters)) using (cancellationToken.Register(() => script.Terminate())) { if (sessionConfiguration.LowPriorityDeploymentProcesses && deploymentPipeline) diff --git a/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs index bacb2cb280..1798d388b0 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs @@ -19,6 +19,11 @@ namespace Tgstation.Server.Host.Components.Watchdog /// long? SessionId { get; } + /// + /// A incrementing ID for representing current iteration of servers world (i.e. after calling /world/proc/Reboot). Only unique within the current . Only tracked in game sessions with the DMAPI enabled. + /// + long? WorldIteration { get; } + /// /// When the current server executions was started. /// diff --git a/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdog.cs index b97517d0f7..6b99878466 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdog.cs @@ -4,6 +4,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Prometheus; @@ -28,9 +29,9 @@ namespace Tgstation.Server.Host.Components.Watchdog sealed class PosixWatchdog : AdvancedWatchdog { /// - /// The for the . + /// The of for the . /// - readonly GeneralConfiguration generalConfiguration; + readonly IOptionsMonitor generalConfigurationOptions; /// /// Initializes a new instance of the class. @@ -48,10 +49,10 @@ namespace Tgstation.Server.Host.Components.Watchdog /// The for the . /// The pointing to the game directory for the .. /// The for the . + /// The value of . /// The for the . /// The for the . /// The for the . - /// The value of . /// The autostart value for the . public PosixWatchdog( IChatManager chat, @@ -67,10 +68,10 @@ namespace Tgstation.Server.Host.Components.Watchdog IMetricFactory metricFactory, IIOManager gameIOManager, IFilesystemLinkFactory linkFactory, + IOptionsMonitor generalConfigurationOptions, ILogger logger, DreamDaemonLaunchParameters initialLaunchParameters, Api.Models.Instance instance, - GeneralConfiguration generalConfiguration, bool autoStart) : base( chat, @@ -91,7 +92,7 @@ namespace Tgstation.Server.Host.Components.Watchdog instance, autoStart) { - this.generalConfiguration = generalConfiguration ?? throw new ArgumentNullException(nameof(generalConfiguration)); + this.generalConfigurationOptions = generalConfigurationOptions ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); } /// @@ -100,6 +101,12 @@ namespace Tgstation.Server.Host.Components.Watchdog /// protected override SwappableDmbProvider CreateSwappableDmbProvider(IDmbProvider dmbProvider) - => new HardLinkDmbProvider(dmbProvider, GameIOManager, LinkFactory, Logger, generalConfiguration, ActiveLaunchParameters.SecurityLevel!.Value); + => new HardLinkDmbProvider( + dmbProvider, + GameIOManager, + LinkFactory, + Logger, + generalConfigurationOptions.CurrentValue, + ActiveLaunchParameters.SecurityLevel!.Value); } } diff --git a/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdogFactory.cs b/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdogFactory.cs index 23539d34ac..19f7344089 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdogFactory.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdogFactory.cs @@ -34,14 +34,14 @@ namespace Tgstation.Server.Host.Components.Watchdog /// The for the . /// The for the . /// The for the . - /// The for for the . + /// The of for the . public PosixWatchdogFactory( IServerControl serverControl, ILoggerFactory loggerFactory, IJobManager jobManager, IAsyncDelayer asyncDelayer, IFilesystemLinkFactory linkFactory, - IOptions generalConfigurationOptions) + IOptionsMonitor generalConfigurationOptions) : base( serverControl, loggerFactory, @@ -79,10 +79,10 @@ namespace Tgstation.Server.Host.Components.Watchdog metricFactory, gameIOManager, LinkFactory, + GeneralConfigurationOptions, LoggerFactory.CreateLogger(), settings, instance, - GeneralConfiguration, settings.AutoStart ?? throw new ArgumentNullException(nameof(settings))); } } diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs index 805957535e..dfe2e45609 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs @@ -39,6 +39,9 @@ namespace Tgstation.Server.Host.Components.Watchdog /// public long? SessionId => GetActiveController()?.ReattachInformation.Id; + /// + public long? WorldIteration => GetActiveController()?.StartupBridgeRequestsReceived; + /// public uint? ClientCount { get; private set; } @@ -538,10 +541,16 @@ namespace Tgstation.Server.Host.Components.Watchdog } /// - async ValueTask IEventConsumer.HandleEvent(EventType eventType, IEnumerable parameters, bool deploymentPipeline, CancellationToken cancellationToken) + async ValueTask IEventConsumer.HandleEvent(EventType eventType, IEnumerable parameters, bool sensitiveParameters, bool deploymentPipeline, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(parameters); + if (sensitiveParameters) + { + Logger.LogDebug("Not sending sensitive event parameters"); + parameters = Enumerable.Empty(); + } + // Method explicitly implemented to prevent accidental calls when this.eventConsumer should be used. var activeServer = GetActiveController(); @@ -777,6 +786,7 @@ namespace Tgstation.Server.Host.Components.Watchdog GameIOManager.ResolvePath(newCompileJob.DirectoryName!.Value.ToString()), }, false, + false, cancellationToken); try @@ -803,8 +813,8 @@ namespace Tgstation.Server.Host.Components.Watchdog { try { - var sessionEventTask = relayToSession ? ((IEventConsumer)this).HandleEvent(eventType, parameters, false, cancellationToken) : ValueTask.CompletedTask; - var eventConsumerTask = eventConsumer.HandleEvent(eventType, parameters, false, cancellationToken); + var sessionEventTask = relayToSession ? ((IEventConsumer)this).HandleEvent(eventType, parameters, false, false, cancellationToken) : ValueTask.CompletedTask; + var eventConsumerTask = eventConsumer.HandleEvent(eventType, parameters, false, false, cancellationToken); await ValueTaskExtensions.WhenAll( eventConsumerTask, sessionEventTask); diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogFactory.cs b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogFactory.cs index d052001900..c2fd1c981e 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogFactory.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogFactory.cs @@ -43,9 +43,9 @@ namespace Tgstation.Server.Host.Components.Watchdog protected IAsyncDelayer AsyncDelayer { get; } /// - /// The for the . + /// The of for the . /// - protected GeneralConfiguration GeneralConfiguration { get; } + protected IOptionsMonitor GeneralConfigurationOptions { get; } /// /// Initializes a new instance of the class. @@ -54,19 +54,19 @@ namespace Tgstation.Server.Host.Components.Watchdog /// The value of . /// The value of . /// The value of . - /// The containing the value of . + /// The containing the value of . public WatchdogFactory( IServerControl serverControl, ILoggerFactory loggerFactory, IJobManager jobManager, IAsyncDelayer asyncDelayer, - IOptions generalConfigurationOptions) + IOptionsMonitor generalConfigurationOptions) { ServerControl = serverControl ?? throw new ArgumentNullException(nameof(serverControl)); LoggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); JobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager)); AsyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer)); - GeneralConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); + GeneralConfigurationOptions = generalConfigurationOptions ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); } /// diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdogFactory.cs b/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdogFactory.cs index 5639219021..7f41d39ef7 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdogFactory.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdogFactory.cs @@ -37,14 +37,14 @@ namespace Tgstation.Server.Host.Components.Watchdog /// The for the . /// The for the . /// The value of . - /// The for for the . + /// The for for the . public WindowsWatchdogFactory( IServerControl serverControl, ILoggerFactory loggerFactory, IJobManager jobManager, IAsyncDelayer asyncDelayer, IFilesystemLinkFactory symlinkFactory, - IOptions generalConfigurationOptions) + IOptionsMonitor generalConfigurationOptions) : base( serverControl, loggerFactory, diff --git a/src/Tgstation.Server.Host/Configuration/TelemetryConfiguration.cs b/src/Tgstation.Server.Host/Configuration/TelemetryConfiguration.cs deleted file mode 100644 index 7ec0361759..0000000000 --- a/src/Tgstation.Server.Host/Configuration/TelemetryConfiguration.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace Tgstation.Server.Host.Configuration -{ - /// - /// Configuration options for telemetry. - /// - public sealed class TelemetryConfiguration - { - /// - /// The key for the the resides in. - /// - public const string Section = "Telemetry"; - - /// - /// The default value of . - /// - private const long DefaultVersionReportingRepositoryId = 841149827; // https://github.com/tgstation/tgstation-server-deployments - - /// - /// If version reporting telemetry is disabled. - /// - public bool DisableVersionReporting { get; set; } - - /// - /// The friendly name used on GitHub deployments for version reporting. If only the server will be shown. - /// - public string? ServerFriendlyName { get; set; } - - /// - /// The GitHub repository ID used for version reporting. - /// - public long? VersionReportingRepositoryId { get; set; } = DefaultVersionReportingRepositoryId; - } -} diff --git a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs index 52004ad968..1b9b20f198 100644 --- a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs +++ b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs @@ -3,8 +3,8 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; -using System.Web; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -21,7 +21,6 @@ using Tgstation.Server.Host.Database; using Tgstation.Server.Host.IO; using Tgstation.Server.Host.Security; using Tgstation.Server.Host.System; -using Tgstation.Server.Host.Transfer; using Tgstation.Server.Host.Utils; namespace Tgstation.Server.Host.Controllers @@ -29,6 +28,7 @@ namespace Tgstation.Server.Host.Controllers /// /// for TGS administration purposes. /// + [Authorize] [Route(Routes.Administration)] public sealed class AdministrationController : ApiController { @@ -53,14 +53,9 @@ namespace Tgstation.Server.Host.Controllers readonly IPlatformIdentifier platformIdentifier; /// - /// The for the . + /// The for the . /// - readonly IFileTransferTicketProvider fileTransferService; - - /// - /// The for the . - /// - readonly FileLoggingConfiguration fileLoggingConfiguration; + readonly IOptions fileLoggingConfigurationOptions; /// /// Initializes a new instance of the class. @@ -73,8 +68,7 @@ namespace Tgstation.Server.Host.Controllers /// The value of . /// The value of . /// The value of . - /// The value of . - /// The containing value of . + /// The containing value of . public AdministrationController( IDatabaseContext databaseContext, IAuthenticationContext authenticationContext, @@ -84,7 +78,6 @@ namespace Tgstation.Server.Host.Controllers IAssemblyInformationProvider assemblyInformationProvider, IIOManager ioManager, IPlatformIdentifier platformIdentifier, - IFileTransferTicketProvider fileTransferService, IOptions fileLoggingConfigurationOptions) : base( databaseContext, @@ -97,8 +90,7 @@ namespace Tgstation.Server.Host.Controllers this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider)); this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier)); - this.fileTransferService = fileTransferService ?? throw new ArgumentNullException(nameof(fileTransferService)); - fileLoggingConfiguration = fileLoggingConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(fileLoggingConfigurationOptions)); + this.fileLoggingConfigurationOptions = fileLoggingConfigurationOptions ?? throw new ArgumentNullException(nameof(fileLoggingConfigurationOptions)); } /// @@ -111,7 +103,6 @@ namespace Tgstation.Server.Host.Controllers /// The GitHub API rate limit was hit. See response header Retry-After. /// A GitHub API error occurred. See error message for details. [HttpGet] - [TgsRestAuthorize(nameof(IAdministrationAuthority.GetUpdateInformation))] [ProducesResponseType(typeof(AdministrationResponse), 200)] [ProducesResponseType(typeof(ErrorMessageResponse), 424)] [ProducesResponseType(typeof(ErrorMessageResponse), 429)] @@ -132,7 +123,6 @@ namespace Tgstation.Server.Host.Controllers /// A GitHub rate limit was encountered or the swarm integrity check failed. /// A GitHub API error occurred. [HttpPost] - [TgsRestAuthorize(nameof(IAdministrationAuthority.TriggerServerVersionChange))] [ProducesResponseType(typeof(ServerUpdateResponse), 202)] [ProducesResponseType(typeof(ErrorMessageResponse), 410)] [ProducesResponseType(typeof(ErrorMessageResponse), 422)] @@ -160,7 +150,6 @@ namespace Tgstation.Server.Host.Controllers /// Restart begun successfully. /// Restart operations are unavailable due to the launch configuration of TGS. [HttpDelete] - [TgsRestAuthorize(nameof(IAdministrationAuthority.TriggerServerRestart))] [ProducesResponseType(204)] [ProducesResponseType(typeof(ErrorMessageResponse), 422)] public ValueTask Delete() @@ -187,7 +176,7 @@ namespace Tgstation.Server.Host.Controllers => Paginated( async () => { - var path = fileLoggingConfiguration.GetFullLogDirectory(ioManager, assemblyInformationProvider, platformIdentifier); + var path = fileLoggingConfigurationOptions.Value.GetFullLogDirectory(ioManager, assemblyInformationProvider, platformIdentifier); try { var files = await ioManager.GetFiles(path, cancellationToken); @@ -236,43 +225,9 @@ namespace Tgstation.Server.Host.Controllers [TgsAuthorize(AdministrationRights.DownloadLogs)] [ProducesResponseType(typeof(LogFileResponse), 200)] [ProducesResponseType(typeof(ErrorMessageResponse), 409)] - public async ValueTask GetLog(string path, CancellationToken cancellationToken) - { - ArgumentNullException.ThrowIfNull(path); - - path = HttpUtility.UrlDecode(path); - - // guard against directory navigation - var sanitizedPath = ioManager.GetFileName(path); - if (path != sanitizedPath) - return Forbid(); - - var fullPath = ioManager.ConcatPath( - fileLoggingConfiguration.GetFullLogDirectory(ioManager, assemblyInformationProvider, platformIdentifier), - path); - try - { - var fileTransferTicket = fileTransferService.CreateDownload( - new FileDownloadProvider( - () => null, - null, - fullPath, - true)); - - return Ok(new LogFileResponse - { - Name = path, - LastModified = await ioManager.GetLastModified(fullPath, cancellationToken), - FileTicket = fileTransferTicket.FileTicket, - }); - } - catch (IOException ex) - { - return Conflict(new ErrorMessageResponse(ErrorCode.IOError) - { - AdditionalData = ex.ToString(), - }); - } - } + public ValueTask GetLog(string path, CancellationToken cancellationToken) + => administrationAuthority.Invoke( + this, + authority => authority.GetLog(path, cancellationToken)); } } diff --git a/src/Tgstation.Server.Host/Controllers/ApiController.cs b/src/Tgstation.Server.Host/Controllers/ApiController.cs index 9b2b058010..771972a0f7 100644 --- a/src/Tgstation.Server.Host/Controllers/ApiController.cs +++ b/src/Tgstation.Server.Host/Controllers/ApiController.cs @@ -249,7 +249,7 @@ namespace Tgstation.Server.Host.Controllers /// The for the operation. /// A resulting in the for the operation. protected ValueTask Paginated( - Func>> queryGenerator, + Func?>> queryGenerator, Func? resultTransformer, int? pageQuery, int? pageSizeQuery, @@ -272,7 +272,7 @@ namespace Tgstation.Server.Host.Controllers /// The for the operation. /// A resulting in the for the operation. protected ValueTask Paginated( - Func>> queryGenerator, + Func?>> queryGenerator, Func? resultTransformer, int? pageQuery, int? pageSizeQuery, @@ -290,14 +290,14 @@ namespace Tgstation.Server.Host.Controllers /// /// The of model being generated. If different from , must implement for . /// The of model being returned. - /// A resulting in a resulting in the generated . + /// A resulting in a resulting in the generated or if an authorization requirment failed. /// A to transform the s after being queried. /// The requested page from the query. /// The requested page size from the query. /// The for the operation. /// A resulting in the for the operation. async ValueTask PaginatedImpl( - Func>> queryGenerator, + Func?>> queryGenerator, Func? resultTransformer, int? pageQuery, int? pageSizeQuery, @@ -318,6 +318,9 @@ namespace Tgstation.Server.Host.Controllers var page = pageQuery ?? 1; var paginationResult = await queryGenerator(); + if (paginationResult == null) + return Forbid(); + if (!paginationResult.Valid) return paginationResult.EarlyOut; diff --git a/src/Tgstation.Server.Host/Controllers/ApiRootController.cs b/src/Tgstation.Server.Host/Controllers/ApiRootController.cs index ab7000e9e1..148f5bbc7b 100644 --- a/src/Tgstation.Server.Host/Controllers/ApiRootController.cs +++ b/src/Tgstation.Server.Host/Controllers/ApiRootController.cs @@ -3,7 +3,6 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -66,14 +65,14 @@ namespace Tgstation.Server.Host.Controllers readonly IRestAuthorityInvoker loginAuthority; /// - /// The for the . + /// The of for the . /// - readonly GeneralConfiguration generalConfiguration; + readonly IOptionsSnapshot generalConfigurationOptions; /// - /// The for the . + /// The of for the . /// - readonly SecurityConfiguration securityConfiguration; + readonly IOptionsSnapshot securityConfigurationOptions; /// /// Initializes a new instance of the class. @@ -85,8 +84,8 @@ namespace Tgstation.Server.Host.Controllers /// The value of . /// The value of . /// The value of . - /// The containing the value of . - /// The containing the value of . + /// The value of . + /// The value of . /// The for the . /// The for the . /// The value of . @@ -98,7 +97,7 @@ namespace Tgstation.Server.Host.Controllers IPlatformIdentifier platformIdentifier, ISwarmService swarmService, IServerControl serverControl, - IOptions generalConfigurationOptions, + IOptionsSnapshot generalConfigurationOptions, IOptionsSnapshot securityConfigurationOptions, ILogger logger, IApiHeadersProvider apiHeadersProvider, @@ -115,8 +114,8 @@ namespace Tgstation.Server.Host.Controllers this.oAuthProviders = oAuthProviders ?? throw new ArgumentNullException(nameof(oAuthProviders)); this.swarmService = swarmService ?? throw new ArgumentNullException(nameof(swarmService)); this.serverControl = serverControl ?? throw new ArgumentNullException(nameof(serverControl)); - generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); - securityConfiguration = securityConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(securityConfigurationOptions)); + this.generalConfigurationOptions = generalConfigurationOptions ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); + this.securityConfigurationOptions = securityConfigurationOptions ?? throw new ArgumentNullException(nameof(securityConfigurationOptions)); this.loginAuthority = loginAuthority ?? throw new ArgumentNullException(nameof(loginAuthority)); } @@ -128,7 +127,6 @@ namespace Tgstation.Server.Host.Controllers /// /// retrieved successfully. [HttpGet] - [AllowAnonymous] [ProducesResponseType(typeof(ServerInformationResponse), 200)] public IActionResult ServerInfo() { @@ -159,20 +157,20 @@ namespace Tgstation.Server.Host.Controllers Version = assemblyInformationProvider.Version, ApiVersion = ApiHeaders.Version, DMApiVersion = DMApiConstants.InteropVersion, - MinimumPasswordLength = generalConfiguration.MinimumPasswordLength, - InstanceLimit = generalConfiguration.InstanceLimit, - UserLimit = generalConfiguration.UserLimit, - UserGroupLimit = generalConfiguration.UserGroupLimit, - ValidInstancePaths = generalConfiguration.ValidInstancePaths, + MinimumPasswordLength = generalConfigurationOptions.Value.MinimumPasswordLength, + InstanceLimit = generalConfigurationOptions.Value.InstanceLimit, + UserLimit = generalConfigurationOptions.Value.UserLimit, + UserGroupLimit = generalConfigurationOptions.Value.UserGroupLimit, + ValidInstancePaths = generalConfigurationOptions.Value.ValidInstancePaths, WindowsHost = platformIdentifier.IsWindows, SwarmServers = swarmService .GetSwarmServers() ?.Select(swarmServerInfo => new SwarmServerResponse(swarmServerInfo)) .ToList(), OAuthProviderInfos = oAuthProviders.ProviderInfos(), - OidcProviderInfos = securityConfiguration.OidcProviderInfos().ToList(), + OidcProviderInfos = securityConfigurationOptions.Value.OidcProviderInfos().ToList(), UpdateInProgress = serverControl.UpdateInProgress, - OidcStrictMode = securityConfiguration.OidcStrictMode, + OidcStrictMode = securityConfigurationOptions.Value.OidcStrictMode, }); } diff --git a/src/Tgstation.Server.Host/Controllers/BridgeController.cs b/src/Tgstation.Server.Host/Controllers/BridgeController.cs index 10f9ae94af..49ace73644 100644 --- a/src/Tgstation.Server.Host/Controllers/BridgeController.cs +++ b/src/Tgstation.Server.Host/Controllers/BridgeController.cs @@ -136,7 +136,7 @@ namespace Tgstation.Server.Host.Controllers var response = await bridgeDispatcher.ProcessBridgeRequest(request, cancellationToken); if (response == null) - Forbid(); + return Unauthorized(); var responseJson = JsonConvert.SerializeObject(response, DMApiConstants.SerializerSettings); diff --git a/src/Tgstation.Server.Host/Controllers/ChatController.cs b/src/Tgstation.Server.Host/Controllers/ChatController.cs index fd6cf80e1d..def3c14f1e 100644 --- a/src/Tgstation.Server.Host/Controllers/ChatController.cs +++ b/src/Tgstation.Server.Host/Controllers/ChatController.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Linq.Expressions; -using System.Net; using System.Reflection; using System.Threading; using System.Threading.Tasks; @@ -18,6 +17,7 @@ using Tgstation.Server.Api.Models.Internal; using Tgstation.Server.Api.Models.Request; using Tgstation.Server.Api.Models.Response; using Tgstation.Server.Api.Rights; +using Tgstation.Server.Host.Authority; using Tgstation.Server.Host.Components; using Tgstation.Server.Host.Controllers.Results; using Tgstation.Server.Host.Database; @@ -35,15 +35,22 @@ namespace Tgstation.Server.Host.Controllers #pragma warning disable CA1506 // TODO: Decomplexify public sealed class ChatController : InstanceRequiredController { + /// + /// The for the . + /// + readonly IRestAuthorityInvoker chatAuthority; + /// /// Initializes a new instance of the class. /// + /// The value of . /// The for the . /// The for the . /// The for the . /// The for the . /// The for the . public ChatController( + IRestAuthorityInvoker chatAuthority, IDatabaseContext databaseContext, IAuthenticationContext authenticationContext, ILogger logger, @@ -56,6 +63,7 @@ namespace Tgstation.Server.Host.Controllers instanceManager, apiHeaders) { + this.chatAuthority = chatAuthority ?? throw new ArgumentNullException(nameof(chatAuthority)); } /// @@ -80,7 +88,7 @@ namespace Tgstation.Server.Host.Controllers switch (chatProvider) { case ChatProvider.Discord: - result.DiscordChannelId = ulong.Parse(api.ChannelData, CultureInfo.InvariantCulture); + result.DiscordChannelId = UInt64.Parse(api.ChannelData, CultureInfo.InvariantCulture); break; case ChatProvider.Irc: result.IrcChannel = api.ChannelData; @@ -101,70 +109,37 @@ namespace Tgstation.Server.Host.Controllers /// A resulting in the for the operation. /// Created successfully. [HttpPut] - [TgsAuthorize(ChatBotRights.Create)] [ProducesResponseType(typeof(ChatBotResponse), 201)] public async ValueTask Create([FromBody] ChatBotCreateRequest model, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(model); - var earlyOut = StandardModelChecks(model, true); - if (earlyOut != null) - return earlyOut; + if (!model.Provider.HasValue) + return BadRequest(new ErrorMessageResponse(ErrorCode.ChatBotProviderMissing)); - var countOfExistingBotsInInstance = await DatabaseContext - .ChatBots - .AsQueryable() - .Where(x => x.InstanceId == Instance.Id) - .CountAsync(cancellationToken); + if (String.IsNullOrWhiteSpace(model.Name)) + return BadRequest(new ErrorMessageResponse(ErrorCode.ChatBotWhitespaceName)); - if (countOfExistingBotsInInstance >= Instance.ChatBotLimit!.Value) - return Conflict(new ErrorMessageResponse(ErrorCode.ChatBotMax)); + if (String.IsNullOrWhiteSpace(model.ConnectionString)) + return BadRequest(new ErrorMessageResponse(ErrorCode.ChatBotWhitespaceConnectionString)); - model.Enabled ??= false; - model.ReconnectionInterval ??= 1; + if (!model.ValidateProviderChannelTypes()) + return BadRequest(new ErrorMessageResponse(ErrorCode.ChatBotWrongChannelType)); - // try to update das db first var newChannels = model.Channels?.Select(x => ConvertApiChatChannel(x, model.Provider!.Value)).ToList() ?? new List(); // important that this isn't null - var dbModel = new ChatBot(newChannels) - { - Name = model.Name, - ConnectionString = model.ConnectionString, - Enabled = model.Enabled, - InstanceId = Instance.Id!.Value, - Provider = model.Provider, - ReconnectionInterval = model.ReconnectionInterval, - ChannelLimit = model.ChannelLimit, - }; - DatabaseContext.ChatBots.Add(dbModel); - - await DatabaseContext.Save(cancellationToken); - return await WithComponentInstanceNullable( - async instance => - { - try - { - // try to create it - await instance.Chat.ChangeSettings(dbModel, cancellationToken); - - if (dbModel.Channels.Count > 0) - await instance.Chat.ChangeChannels(dbModel.Id!.Value, dbModel.Channels, cancellationToken); - } - catch - { - // undo the add - DatabaseContext.ChatBots.Remove(dbModel); - - // DCTx2: Operations must always run - await DatabaseContext.Save(default); - await instance.Chat.DeleteConnection(dbModel.Id!.Value, default); - throw; - } - - return null; - }) - - ?? this.StatusCode(HttpStatusCode.Created, dbModel.ToApi()); + return await chatAuthority.InvokeTransformable( + this, + authority => authority.Create( + newChannels, + model.Name, + model.ConnectionString, + model.Provider.Value, + Instance.Require(x => x.Id), + model.ReconnectionInterval, + model.ChannelLimit, + model.Enabled ?? false, + cancellationToken)); } /// @@ -185,7 +160,6 @@ namespace Tgstation.Server.Host.Controllers instance.Chat.DeleteConnection(id, cancellationToken), DatabaseContext .ChatBots - .AsQueryable() .Where(x => x.Id == id) .ExecuteDeleteAsync(cancellationToken)); return null; @@ -208,11 +182,10 @@ namespace Tgstation.Server.Host.Controllers { var connectionStrings = (AuthenticationContext.GetRight(RightsType.ChatBots) & (ulong)ChatBotRights.ReadConnectionString) != 0; return Paginated( - () => ValueTask.FromResult( + () => ValueTask.FromResult?>( new PaginatableResult( DatabaseContext .ChatBots - .AsQueryable() .Where(x => x.InstanceId == Instance.Id) .Include(x => x.Channels) .OrderBy(x => x.Id))), @@ -242,8 +215,8 @@ namespace Tgstation.Server.Host.Controllers [ProducesResponseType(typeof(ErrorMessageResponse), 410)] public async ValueTask GetId(long id, CancellationToken cancellationToken) { - var query = DatabaseContext.ChatBots - .AsQueryable() + var query = DatabaseContext + .ChatBots .Where(x => x.Id == id && x.InstanceId == Instance.Id) .Include(x => x.Channels); @@ -285,7 +258,6 @@ namespace Tgstation.Server.Host.Controllers var query = DatabaseContext .ChatBots - .AsQueryable() .Where(x => x.InstanceId == Instance.Id && x.Id == model.Id) .Include(x => x.Channels); diff --git a/src/Tgstation.Server.Host/Controllers/ComponentInterfacingController.cs b/src/Tgstation.Server.Host/Controllers/ComponentInterfacingController.cs index f04e620d06..89f13fd033 100644 --- a/src/Tgstation.Server.Host/Controllers/ComponentInterfacingController.cs +++ b/src/Tgstation.Server.Host/Controllers/ComponentInterfacingController.cs @@ -10,6 +10,7 @@ using Tgstation.Server.Api.Models; using Tgstation.Server.Api.Models.Response; using Tgstation.Server.Host.Components; using Tgstation.Server.Host.Database; +using Tgstation.Server.Host.Extensions; using Tgstation.Server.Host.Models; using Tgstation.Server.Host.Security; using Tgstation.Server.Host.Utils; diff --git a/src/Tgstation.Server.Host/Controllers/ControlPanelController.cs b/src/Tgstation.Server.Host/Controllers/ControlPanelController.cs index ec763dd955..2a89fcd33f 100644 --- a/src/Tgstation.Server.Host/Controllers/ControlPanelController.cs +++ b/src/Tgstation.Server.Host/Controllers/ControlPanelController.cs @@ -45,29 +45,29 @@ namespace Tgstation.Server.Host.Controllers /// readonly IWebHostEnvironment hostEnvironment; + /// + /// The for the . + /// + readonly IOptionsSnapshot controlPanelConfigurationOptions; + /// /// The for the . /// readonly ILogger logger; - /// - /// The for the . - /// - readonly ControlPanelConfiguration controlPanelConfiguration; - /// /// Initializes a new instance of the class. /// /// The value of . - /// The containing the value of . + /// The value of . /// The value of . public ControlPanelController( IWebHostEnvironment hostEnvironment, - IOptions controlPanelConfigurationOptions, + IOptionsSnapshot controlPanelConfigurationOptions, ILogger logger) { this.hostEnvironment = hostEnvironment ?? throw new ArgumentNullException(nameof(hostEnvironment)); - controlPanelConfiguration = controlPanelConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(controlPanelConfigurationOptions)); + this.controlPanelConfigurationOptions = controlPanelConfigurationOptions ?? throw new ArgumentNullException(nameof(controlPanelConfigurationOptions)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); } @@ -79,13 +79,13 @@ namespace Tgstation.Server.Host.Controllers [HttpGet] public IActionResult GetChannelJson() { - if (!controlPanelConfiguration.Enable) + if (!controlPanelConfigurationOptions.Value.Enable) { logger.LogDebug("Not serving channel.json as control panel is disabled."); return NotFound(); } - var controlPanelChannel = controlPanelConfiguration.Channel; + var controlPanelChannel = controlPanelConfigurationOptions.Value.Channel; logger.LogTrace("Generating channel.json for channel \"{channel}\"...", controlPanelChannel); if (controlPanelChannel == "local") @@ -102,7 +102,7 @@ namespace Tgstation.Server.Host.Controllers { FormatVersion = 1, Channel = controlPanelChannel, - controlPanelConfiguration.PublicPath, + controlPanelConfigurationOptions.Value.PublicPath, }); } @@ -132,7 +132,7 @@ namespace Tgstation.Server.Host.Controllers [HttpGet] public IActionResult Get([FromRoute] string appRoute) { - if (!controlPanelConfiguration.Enable) + if (!controlPanelConfigurationOptions.Value.Enable) { logger.LogDebug("Not serving static files as control panel is disabled."); return NotFound(); diff --git a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs index e6760f0f17..4bf09a03ff 100644 --- a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs +++ b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs @@ -165,7 +165,6 @@ namespace Tgstation.Server.Host.Controllers // alias for changing DD settings var current = await DatabaseContext .Instances - .AsQueryable() .Where(x => x.Id == Instance.Id) .Select(x => x.DreamDaemonSettings) .FirstOrDefaultAsync(cancellationToken); @@ -330,7 +329,6 @@ namespace Tgstation.Server.Host.Controllers { settings = await DatabaseContext .Instances - .AsQueryable() .Where(x => x.Id == Instance.Id) .Select(x => x.DreamDaemonSettings!) .FirstOrDefaultAsync(cancellationToken); @@ -366,6 +364,7 @@ namespace Tgstation.Server.Host.Controllers firstIteration = false; result.Status = dd.Status; result.SessionId = dd.SessionId; + result.WorldIteration = dd.WorldIteration; result.LaunchTime = dd.LaunchTime; result.ClientCount = dd.ClientCount; } diff --git a/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs b/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs index 7f8755fdd2..4ab9d54f9d 100644 --- a/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs +++ b/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs @@ -81,7 +81,6 @@ namespace Tgstation.Server.Host.Controllers { var dreamMakerSettings = await DatabaseContext .DreamMakerSettings - .AsQueryable() .Where(x => x.InstanceId == Instance.Id) .FirstOrDefaultAsync(cancellationToken); @@ -126,7 +125,7 @@ namespace Tgstation.Server.Host.Controllers [ProducesResponseType(typeof(PaginatedResponse), 200)] public ValueTask List([FromQuery] int? page, [FromQuery] int? pageSize, CancellationToken cancellationToken) => Paginated( - () => ValueTask.FromResult( + () => ValueTask.FromResult?>( new PaginatableResult( BaseCompileJobsQuery() .OrderByDescending(x => x.Job.StoppedAt))), @@ -186,7 +185,6 @@ namespace Tgstation.Server.Host.Controllers var hostModel = await DatabaseContext .DreamMakerSettings - .AsQueryable() .Where(x => x.InstanceId == Instance.Id) .FirstOrDefaultAsync(cancellationToken); if (hostModel == null) @@ -295,7 +293,6 @@ namespace Tgstation.Server.Host.Controllers /// An of with all the inclusions. IQueryable BaseCompileJobsQuery() => DatabaseContext .CompileJobs - .AsQueryable() .Include(x => x.Job!) .ThenInclude(x => x.StartedBy) .Include(x => x.Job!) diff --git a/src/Tgstation.Server.Host/Controllers/EngineController.cs b/src/Tgstation.Server.Host/Controllers/EngineController.cs index 83a113e3b4..89a1ba8abe 100644 --- a/src/Tgstation.Server.Host/Controllers/EngineController.cs +++ b/src/Tgstation.Server.Host/Controllers/EngineController.cs @@ -110,7 +110,7 @@ namespace Tgstation.Server.Host.Controllers public ValueTask List([FromQuery] int? page, [FromQuery] int? pageSize, CancellationToken cancellationToken) => WithComponentInstance( instance => Paginated( - () => ValueTask.FromResult( + () => ValueTask.FromResult?>( new PaginatableResult( instance .EngineManager diff --git a/src/Tgstation.Server.Host/Controllers/InstanceController.cs b/src/Tgstation.Server.Host/Controllers/InstanceController.cs index 8f3f53dca2..3c880c02e0 100644 --- a/src/Tgstation.Server.Host/Controllers/InstanceController.cs +++ b/src/Tgstation.Server.Host/Controllers/InstanceController.cs @@ -67,21 +67,21 @@ namespace Tgstation.Server.Host.Controllers /// readonly IPortAllocator portAllocator; + /// + /// The of for the . + /// + readonly IOptions swarmConfigurationOptions; + + /// + /// The of for the . + /// + readonly IOptionsSnapshot generalConfigurationOptions; + /// /// The for the . /// readonly IPermissionsUpdateNotifyee permissionsUpdateNotifyee; - /// - /// The for the . - /// - readonly GeneralConfiguration generalConfiguration; - - /// - /// The for the . - /// - readonly SwarmConfiguration swarmConfiguration; - /// /// Initializes a new instance of the class. /// @@ -94,8 +94,8 @@ namespace Tgstation.Server.Host.Controllers /// The value of . /// The value of . /// The value of . - /// The containing the value of . - /// The containing the value of . + /// The value of . + /// The value of . /// The for the . public InstanceController( IDatabaseContext databaseContext, @@ -107,8 +107,8 @@ namespace Tgstation.Server.Host.Controllers IPlatformIdentifier platformIdentifier, IPortAllocator portAllocator, IPermissionsUpdateNotifyee permissionsUpdateNotifyee, - IOptions generalConfigurationOptions, IOptions swarmConfigurationOptions, + IOptionsSnapshot generalConfigurationOptions, IApiHeadersProvider apiHeaders) : base( databaseContext, @@ -124,8 +124,8 @@ namespace Tgstation.Server.Host.Controllers this.portAllocator = portAllocator ?? throw new ArgumentNullException(nameof(portAllocator)); this.permissionsUpdateNotifyee = permissionsUpdateNotifyee ?? throw new ArgumentNullException(nameof(permissionsUpdateNotifyee)); - generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); - swarmConfiguration = swarmConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(swarmConfigurationOptions)); + this.swarmConfigurationOptions = swarmConfigurationOptions ?? throw new ArgumentNullException(nameof(swarmConfigurationOptions)); + this.generalConfigurationOptions = generalConfigurationOptions ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); } /// @@ -161,14 +161,14 @@ namespace Tgstation.Server.Host.Controllers // Validate it's not a child of any other instance var instancePaths = await DatabaseContext .Instances - .AsQueryable() - .Where(x => x.SwarmIdentifer == swarmConfiguration.Identifier) + .Where(x => x.SwarmIdentifer == swarmConfigurationOptions.Value.Identifier) .Select(x => new Models.Instance { Path = x.Path, }) .ToListAsync(cancellationToken); + var generalConfiguration = generalConfigurationOptions.Value; if ((instancePaths.Count + 1) >= generalConfiguration.InstanceLimit) return Conflict(new ErrorMessageResponse(ErrorCode.InstanceLimitReached)); @@ -278,8 +278,7 @@ namespace Tgstation.Server.Host.Controllers { var originalModel = await DatabaseContext .Instances - .AsQueryable() - .Where(x => x.Id == id && x.SwarmIdentifer == swarmConfiguration.Identifier) + .Where(x => x.Id == id && x.SwarmIdentifer == swarmConfigurationOptions.Value.Identifier) .FirstOrDefaultAsync(cancellationToken); if (originalModel == default) return this.Gone(); @@ -306,17 +305,14 @@ namespace Tgstation.Server.Host.Controllers // there's a bug where removing the root instance doesn't work sometimes await DatabaseContext .CompileJobs - .AsQueryable() .Where(x => x.Job!.Instance!.Id == id) .ExecuteDeleteAsync(cancellationToken); await DatabaseContext .RevInfoTestMerges - .AsQueryable() .Where(x => x.RevisionInformation.InstanceId == id) .ExecuteDeleteAsync(cancellationToken); await DatabaseContext .RevisionInformations - .AsQueryable() .Where(x => x.InstanceId == id) .ExecuteDeleteAsync(cancellationToken); @@ -361,8 +357,7 @@ namespace Tgstation.Server.Host.Controllers IQueryable InstanceQuery() => DatabaseContext .Instances - .AsQueryable() - .Where(x => x.Id == model.Id && x.SwarmIdentifer == swarmConfiguration.Identifier); + .Where(x => x.Id == model.Id && x.SwarmIdentifer == swarmConfigurationOptions.Value.Identifier); var moveJob = await InstanceQuery() .SelectMany(x => x.Jobs) @@ -460,7 +455,6 @@ namespace Tgstation.Server.Host.Controllers { var countOfExistingChatBots = await DatabaseContext .ChatBots - .AsQueryable() .Where(x => x.InstanceId == originalModel.Id) .CountAsync(cancellationToken); @@ -585,8 +579,7 @@ namespace Tgstation.Server.Host.Controllers { var query = DatabaseContext .Instances - .AsQueryable() - .Where(x => x.SwarmIdentifer == swarmConfiguration.Identifier); + .Where(x => x.SwarmIdentifer == swarmConfigurationOptions.Value.Identifier); if (!AuthenticationContext.PermissionSet.InstanceManagerRights!.Value.HasFlag(InstanceManagerRights.List)) query = query .Where(x => x.InstancePermissionSets.Any(y => y.PermissionSetId == AuthenticationContext.PermissionSet.Id)) @@ -612,7 +605,7 @@ namespace Tgstation.Server.Host.Controllers var needsUpdate = false; var result = await Paginated( - () => ValueTask.FromResult( + () => ValueTask.FromResult?>( new PaginatableResult( GetBaseQuery() .OrderBy(x => x.Id))), @@ -651,8 +644,7 @@ namespace Tgstation.Server.Host.Controllers { var query = DatabaseContext .Instances - .AsQueryable() - .Where(x => x.Id == id && x.SwarmIdentifer == swarmConfiguration.Identifier); + .Where(x => x.Id == id && x.SwarmIdentifer == swarmConfigurationOptions.Value.Identifier); if (cantList) query = query.Include(x => x.InstancePermissionSets); @@ -706,8 +698,7 @@ namespace Tgstation.Server.Host.Controllers { IQueryable BaseQuery() => DatabaseContext .Instances - .AsQueryable() - .Where(x => x.Id == id && x.SwarmIdentifer == swarmConfiguration.Identifier); + .Where(x => x.Id == id && x.SwarmIdentifer == swarmConfigurationOptions.Value.Identifier); // ensure the current user has write privilege on the instance var usersInstancePermissionSet = await BaseQuery() @@ -779,7 +770,7 @@ namespace Tgstation.Server.Host.Controllers StartupTimeout = 60, HealthCheckSeconds = 60, DumpOnHealthCheckRestart = false, - TopicRequestTimeout = generalConfiguration.ByondTopicTimeout, + TopicRequestTimeout = generalConfigurationOptions.Value.ByondTopicTimeout, AdditionalParameters = String.Empty, StartProfiler = false, LogOutput = false, @@ -818,7 +809,7 @@ namespace Tgstation.Server.Host.Controllers { InstanceAdminPermissionSet(null), }, - SwarmIdentifer = swarmConfiguration.Identifier, + SwarmIdentifer = swarmConfigurationOptions.Value.Identifier, }; } @@ -871,7 +862,6 @@ namespace Tgstation.Server.Host.Controllers { instanceResponse.Accessible = await DatabaseContext .InstancePermissionSets - .AsQueryable() .Where(x => x.InstanceId == instanceResponse.Id && x.PermissionSetId == AuthenticationContext.PermissionSet.Id) .AnyAsync(cancellationToken); } diff --git a/src/Tgstation.Server.Host/Controllers/InstancePermissionSetController.cs b/src/Tgstation.Server.Host/Controllers/InstancePermissionSetController.cs index 5b4ac2a9c4..6d6b99339c 100644 --- a/src/Tgstation.Server.Host/Controllers/InstancePermissionSetController.cs +++ b/src/Tgstation.Server.Host/Controllers/InstancePermissionSetController.cs @@ -78,7 +78,6 @@ namespace Tgstation.Server.Host.Controllers var existingPermissionSet = await DatabaseContext .PermissionSets - .AsQueryable() .Where(x => x.Id == model.PermissionSetId) .Select(x => new Models.PermissionSet { @@ -94,7 +93,6 @@ namespace Tgstation.Server.Host.Controllers { var userCanonicalName = await DatabaseContext .Users - .AsQueryable() .Where(x => x.Id == existingPermissionSet.UserId.Value) .Select(x => x.CanonicalName) .FirstAsync(cancellationToken); @@ -146,7 +144,6 @@ namespace Tgstation.Server.Host.Controllers var originalPermissionSet = await DatabaseContext .Instances - .AsQueryable() .Where(x => x.Id == Instance.Id) .SelectMany(x => x.InstancePermissionSets) .Where(x => x.PermissionSetId == model.PermissionSetId) @@ -197,11 +194,10 @@ namespace Tgstation.Server.Host.Controllers [ProducesResponseType(typeof(PaginatedResponse), 200)] public ValueTask List([FromQuery] int? page, [FromQuery] int? pageSize, CancellationToken cancellationToken) => Paginated( - () => ValueTask.FromResult( + () => ValueTask.FromResult?>( new PaginatableResult( DatabaseContext .Instances - .AsQueryable() .Where(x => x.Id == Instance.Id) .SelectMany(x => x.InstancePermissionSets) .OrderBy(x => x.PermissionSetId))), @@ -227,7 +223,6 @@ namespace Tgstation.Server.Host.Controllers // this functions as userId var permissionSet = await DatabaseContext .Instances - .AsQueryable() .Where(x => x.Id == Instance.Id) .SelectMany(x => x.InstancePermissionSets) .Where(x => x.PermissionSetId == id) @@ -253,7 +248,6 @@ namespace Tgstation.Server.Host.Controllers { var numDeleted = await DatabaseContext .Instances - .AsQueryable() .Where(x => x.Id == Instance.Id) .SelectMany(x => x.InstancePermissionSets) .Where(x => x.PermissionSetId == id) diff --git a/src/Tgstation.Server.Host/Controllers/JobController.cs b/src/Tgstation.Server.Host/Controllers/JobController.cs index 2b9f1b15f1..5ba72b71c6 100644 --- a/src/Tgstation.Server.Host/Controllers/JobController.cs +++ b/src/Tgstation.Server.Host/Controllers/JobController.cs @@ -71,11 +71,10 @@ namespace Tgstation.Server.Host.Controllers [ProducesResponseType(typeof(PaginatedResponse), 200)] public ValueTask Read([FromQuery] int? page, [FromQuery] int? pageSize, CancellationToken cancellationToken) => Paginated( - () => ValueTask.FromResult( + () => ValueTask.FromResult?>( new PaginatableResult( DatabaseContext .Jobs - .AsQueryable() .Include(x => x.StartedBy) .Include(x => x.CancelledBy) .Include(x => x.Instance) @@ -99,11 +98,10 @@ namespace Tgstation.Server.Host.Controllers [ProducesResponseType(typeof(PaginatedResponse), 200)] public ValueTask List([FromQuery] int? page, [FromQuery] int? pageSize, CancellationToken cancellationToken) => Paginated( - () => ValueTask.FromResult( + () => ValueTask.FromResult?>( new PaginatableResult( DatabaseContext .Jobs - .AsQueryable() .Include(x => x.StartedBy) .Include(x => x.CancelledBy) .Include(x => x.Instance) @@ -132,7 +130,6 @@ namespace Tgstation.Server.Host.Controllers // don't care if an instance post or not at this point var job = await DatabaseContext .Jobs - .AsQueryable() .Include(x => x.StartedBy) .Include(x => x.Instance) .Where(x => x.Id == id && x.Instance!.Id == Instance.Id) @@ -166,7 +163,6 @@ namespace Tgstation.Server.Host.Controllers { var job = await DatabaseContext .Jobs - .AsQueryable() .Where(x => x.Id == id && x.Instance!.Id == Instance.Id) .Include(x => x.StartedBy) .Include(x => x.CancelledBy) diff --git a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs index e99a801607..f64440c919 100644 --- a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs +++ b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs @@ -122,7 +122,6 @@ namespace Tgstation.Server.Host.Controllers var currentModel = await DatabaseContext .RepositorySettings - .AsQueryable() .Where(x => x.InstanceId == Instance.Id) .FirstOrDefaultAsync(cancellationToken); @@ -225,7 +224,6 @@ namespace Tgstation.Server.Host.Controllers { var currentModel = await DatabaseContext .RepositorySettings - .AsQueryable() .Where(x => x.InstanceId == Instance.Id) .FirstOrDefaultAsync(cancellationToken); @@ -264,7 +262,6 @@ namespace Tgstation.Server.Host.Controllers { var currentModel = await DatabaseContext .RepositorySettings - .AsQueryable() .Where(x => x.InstanceId == Instance.Id) .FirstOrDefaultAsync(cancellationToken); @@ -302,7 +299,6 @@ namespace Tgstation.Server.Host.Controllers { var currentModel = await DatabaseContext .RepositorySettings - .AsQueryable() .Where(x => x.InstanceId == Instance.Id) .FirstOrDefaultAsync(cancellationToken); @@ -388,7 +384,6 @@ namespace Tgstation.Server.Host.Controllers var currentModel = await DatabaseContext .RepositorySettings - .AsQueryable() .Where(x => x.InstanceId == Instance.Id) .FirstOrDefaultAsync(cancellationToken); diff --git a/src/Tgstation.Server.Host/Controllers/RootController.cs b/src/Tgstation.Server.Host/Controllers/RootController.cs index fcf2bd239b..ccb9bc7ced 100644 --- a/src/Tgstation.Server.Host/Controllers/RootController.cs +++ b/src/Tgstation.Server.Host/Controllers/RootController.cs @@ -48,26 +48,26 @@ namespace Tgstation.Server.Host.Controllers /// readonly IWebHostEnvironment hostEnvironment; + /// + /// The of for the . + /// + readonly IOptions generalConfigurationOptions; + + /// + /// The of for the . + /// + readonly IOptions controlPanelConfigurationOptions; + + /// + /// The of for the . + /// + readonly IOptions internalConfigurationOptions; + /// /// The for the . /// readonly ILogger logger; - /// - /// The for the . - /// - readonly GeneralConfiguration generalConfiguration; - - /// - /// The for the . - /// - readonly ControlPanelConfiguration controlPanelConfiguration; - - /// - /// The for the . - /// - readonly InternalConfiguration internalConfiguration; - /// /// Gets a giving the and action names for a given . /// @@ -96,9 +96,9 @@ namespace Tgstation.Server.Host.Controllers /// The value of . /// The value of . /// The value of . - /// The containing the value of . - /// The containing the value of . - /// The containing the value of . + /// The containing the value of . + /// The containing the value of . + /// The containing the value of . public RootController( IAssemblyInformationProvider assemblyInformationProvider, IPlatformIdentifier platformIdentifier, @@ -106,15 +106,15 @@ namespace Tgstation.Server.Host.Controllers ILogger logger, IOptions generalConfigurationOptions, IOptions controlPanelConfigurationOptions, - IOptionsSnapshot internalConfigurationOptions) + IOptions internalConfigurationOptions) { this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider)); this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier)); this.hostEnvironment = hostEnvironment ?? throw new ArgumentNullException(nameof(hostEnvironment)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); - generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); - controlPanelConfiguration = controlPanelConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(controlPanelConfigurationOptions)); - internalConfiguration = internalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(internalConfigurationOptions)); + this.generalConfigurationOptions = generalConfigurationOptions ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); + this.controlPanelConfigurationOptions = controlPanelConfigurationOptions ?? throw new ArgumentNullException(nameof(controlPanelConfigurationOptions)); + this.internalConfigurationOptions = internalConfigurationOptions ?? throw new ArgumentNullException(nameof(internalConfigurationOptions)); } /// @@ -125,8 +125,8 @@ namespace Tgstation.Server.Host.Controllers [AllowAnonymous] public IActionResult Index() { - var panelEnabled = controlPanelConfiguration.Enable; - var apiDocsEnabled = generalConfiguration.HostApiDocumentation; + var panelEnabled = controlPanelConfigurationOptions.Value.Enable; + var apiDocsEnabled = generalConfigurationOptions.Value.HostApiDocumentation; var controlPanelRoute = $"{ControlPanelController.ControlPanelRoute.TrimStart('/')}/"; if (panelEnabled && !apiDocsEnabled) @@ -143,7 +143,7 @@ namespace Tgstation.Server.Host.Controllers if (apiDocsEnabled) { - if (internalConfiguration.EnableGraphQL) + if (internalConfigurationOptions.Value.EnableGraphQL) links.Add("GraphQL API Documentation", Routes.GraphQL); links.Add("REST API Documentation", SwaggerConfiguration.DocumentationSiteRouteExtension); diff --git a/src/Tgstation.Server.Host/Controllers/SwarmController.cs b/src/Tgstation.Server.Host/Controllers/SwarmController.cs index dd57e45138..89e09c07de 100644 --- a/src/Tgstation.Server.Host/Controllers/SwarmController.cs +++ b/src/Tgstation.Server.Host/Controllers/SwarmController.cs @@ -44,22 +44,22 @@ namespace Tgstation.Server.Host.Controllers /// readonly IFileTransferStreamHandler transferService; + /// + /// The of for the . + /// + readonly IOptions swarmConfigurationOptions; + /// /// The for the . /// readonly ILogger logger; - /// - /// The for the . - /// - readonly SwarmConfiguration swarmConfiguration; - /// /// Initializes a new instance of the class. /// /// The value of . /// The value of . - /// The containing the value of . + /// The value of . /// The value of . public SwarmController( ISwarmOperations swarmOperations, @@ -69,7 +69,7 @@ namespace Tgstation.Server.Host.Controllers { this.swarmOperations = swarmOperations ?? throw new ArgumentNullException(nameof(swarmOperations)); this.transferService = transferService ?? throw new ArgumentNullException(nameof(transferService)); - swarmConfiguration = swarmConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(swarmConfigurationOptions)); + this.swarmConfigurationOptions = swarmConfigurationOptions ?? throw new ArgumentNullException(nameof(swarmConfigurationOptions)); this.logger = logger; } @@ -215,7 +215,7 @@ namespace Tgstation.Server.Host.Controllers using (LogContext.PushProperty(SerilogContextHelper.RequestPathContextProperty, $"{Request.Method} {Request.Path}")) { logger.LogTrace("Swarm request from {remoteIP}...", Request.HttpContext.Connection.RemoteIpAddress); - if (swarmConfiguration.PrivateKey == null) + if (swarmConfigurationOptions.Value.PrivateKey == null) { logger.LogDebug("Attempted swarm request without private key configured!"); return Forbid(); @@ -223,7 +223,7 @@ namespace Tgstation.Server.Host.Controllers if (!(Request.Headers.TryGetValue(SwarmConstants.ApiKeyHeader, out var apiKeyHeaderValues) && apiKeyHeaderValues.Count == 1 - && apiKeyHeaderValues.First() == swarmConfiguration.PrivateKey)) + && apiKeyHeaderValues.First() == swarmConfigurationOptions.Value.PrivateKey)) { logger.LogDebug("Unauthorized swarm request!"); return Unauthorized(); diff --git a/src/Tgstation.Server.Host/Controllers/UserController.cs b/src/Tgstation.Server.Host/Controllers/UserController.cs index f55218ef64..fc858c41bd 100644 --- a/src/Tgstation.Server.Host/Controllers/UserController.cs +++ b/src/Tgstation.Server.Host/Controllers/UserController.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; @@ -24,6 +25,7 @@ namespace Tgstation.Server.Host.Controllers /// for managing s. /// [Route(Routes.User)] + [Authorize] public sealed class UserController : ApiController { /// @@ -64,10 +66,9 @@ namespace Tgstation.Server.Host.Controllers /// created successfully. /// The requested system identifier could not be found. [HttpPut] - [TgsRestAuthorize(nameof(IUserAuthority.Create))] [ProducesResponseType(typeof(UserResponse), 201)] public ValueTask Create([FromBody] UserCreateRequest model, CancellationToken cancellationToken) - => userAuthority.InvokeTransformable(this, authority => authority.Create(model, null, cancellationToken)); + => userAuthority.InvokeTransformable(this, authority => authority.Create(model, null, cancellationToken)); /// /// Update a . @@ -80,13 +81,12 @@ namespace Tgstation.Server.Host.Controllers /// Requested does not exist. /// Requested does not exist. [HttpPost] - [TgsRestAuthorize(nameof(IUserAuthority.Update))] [ProducesResponseType(typeof(UserResponse), 200)] [ProducesResponseType(204)] [ProducesResponseType(typeof(ErrorMessageResponse), 404)] [ProducesResponseType(typeof(ErrorMessageResponse), 410)] public ValueTask Update([FromBody] UserUpdateRequest model, CancellationToken cancellationToken) - => userAuthority.InvokeTransformable(this, authority => authority.Update(model, cancellationToken)); + => userAuthority.InvokeTransformable(this, authority => authority.Update(model, cancellationToken)); /// /// Get information about the current . @@ -95,7 +95,7 @@ namespace Tgstation.Server.Host.Controllers /// A resulting in the of the operation. /// The was retrieved successfully. [HttpGet] - [TgsRestAuthorize(nameof(IUserAuthority.Read))] + [Authorize] [ProducesResponseType(typeof(UserResponse), 200)] public ValueTask Read(CancellationToken cancellationToken) => userAuthority.InvokeTransformable(this, authority => authority.Read(cancellationToken)); @@ -109,15 +109,18 @@ namespace Tgstation.Server.Host.Controllers /// A resulting in the of the operation. /// Retrieved s successfully. [HttpGet(Routes.List)] - [TgsRestAuthorize(nameof(IUserAuthority.Queryable))] [ProducesResponseType(typeof(PaginatedResponse), 200)] public ValueTask List([FromQuery] int? page, [FromQuery] int? pageSize, CancellationToken cancellationToken) => Paginated( - () => ValueTask.FromResult( - new PaginatableResult( - userAuthority.InvokeQueryable( - authority => authority.Queryable(true)) - .OrderBy(x => x.Id))), + async () => + { + var queryable = await userAuthority.InvokeQueryable( + authority => authority.Queryable(true)); + if (queryable == null) + return null; + + return new PaginatableResult(queryable.OrderBy(x => x.Id)); + }, null, page, pageSize, @@ -132,7 +135,6 @@ namespace Tgstation.Server.Host.Controllers /// The was retrieved successfully. /// The does not exist. [HttpGet("{id}")] - [TgsAuthorize] [ProducesResponseType(typeof(UserResponse), 200)] [ProducesResponseType(typeof(ErrorMessageResponse), 404)] public async ValueTask GetId(long id, CancellationToken cancellationToken) diff --git a/src/Tgstation.Server.Host/Controllers/UserGroupController.cs b/src/Tgstation.Server.Host/Controllers/UserGroupController.cs index 3ff0f0bcac..0a22a428d4 100644 --- a/src/Tgstation.Server.Host/Controllers/UserGroupController.cs +++ b/src/Tgstation.Server.Host/Controllers/UserGroupController.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; @@ -10,7 +11,6 @@ using Tgstation.Server.Api; using Tgstation.Server.Api.Models; using Tgstation.Server.Api.Models.Request; using Tgstation.Server.Api.Models.Response; -using Tgstation.Server.Api.Rights; using Tgstation.Server.Host.Authority; using Tgstation.Server.Host.Controllers.Results; using Tgstation.Server.Host.Database; @@ -24,6 +24,7 @@ namespace Tgstation.Server.Host.Controllers /// for managing s. /// [Route(Routes.UserGroup)] + [Authorize] public class UserGroupController : ApiController { /// @@ -77,7 +78,6 @@ namespace Tgstation.Server.Host.Controllers /// A resulting in the of the operation. /// created successfully. [HttpPut] - [TgsAuthorize(AdministrationRights.WriteUsers)] [ProducesResponseType(typeof(UserGroupResponse), 201)] public async ValueTask Create([FromBody] UserGroupCreateRequest model, CancellationToken cancellationToken) { @@ -103,7 +103,6 @@ namespace Tgstation.Server.Host.Controllers /// updated successfully. /// The requested does not currently exist. [HttpPost] - [TgsAuthorize(AdministrationRights.WriteUsers)] [ProducesResponseType(typeof(UserGroupResponse), 200)] public ValueTask Update([FromBody] UserGroupUpdateRequest model, CancellationToken cancellationToken) { @@ -127,7 +126,6 @@ namespace Tgstation.Server.Host.Controllers /// Retrieve successfully. /// The requested does not currently exist. [HttpGet("{id}")] - [TgsRestAuthorize(nameof(IUserGroupAuthority.GetId))] [ProducesResponseType(typeof(UserGroupResponse), 200)] [ProducesResponseType(typeof(ErrorMessageResponse), 410)] public ValueTask GetId(long id, CancellationToken cancellationToken) @@ -142,15 +140,18 @@ namespace Tgstation.Server.Host.Controllers /// A resulting in the of the request. /// Retrieved s successfully. [HttpGet(Routes.List)] - [TgsRestAuthorize(nameof(IUserGroupAuthority.Queryable))] [ProducesResponseType(typeof(PaginatedResponse), 200)] public ValueTask List([FromQuery] int? page, [FromQuery] int? pageSize, CancellationToken cancellationToken) => Paginated( - () => ValueTask.FromResult( - new PaginatableResult( - userGroupAuthority - .InvokeQueryable(authority => authority.Queryable(true)) - .OrderBy(x => x.Id))), + async () => + { + var queryable = await userGroupAuthority + .InvokeQueryable(authority => authority.Queryable(true)); + if (queryable == null) + return null; + + return new PaginatableResult(queryable.OrderBy(x => x.Id)); + }, null, page, pageSize, @@ -166,7 +167,6 @@ namespace Tgstation.Server.Host.Controllers /// The is not empty. /// The didn't exist. [HttpDelete("{id}")] - [TgsAuthorize(AdministrationRights.WriteUsers)] [ProducesResponseType(204)] [ProducesResponseType(typeof(ErrorMessageResponse), 409)] [ProducesResponseType(typeof(ErrorMessageResponse), 410)] diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index 3246b8ba0f..8b73470640 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -11,13 +11,12 @@ using Cyberboss.AspNetCore.AsyncInitializer; using Elastic.CommonSchema.Serilog; using HotChocolate.AspNetCore; -using HotChocolate.Subscriptions; -using HotChocolate.Types; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Cors.Infrastructure; using Microsoft.AspNetCore.Hosting; @@ -46,7 +45,6 @@ using Serilog.Sinks.Elasticsearch; using Tgstation.Server.Api; using Tgstation.Server.Api.Hubs; using Tgstation.Server.Api.Models; -using Tgstation.Server.Common.Http; using Tgstation.Server.Host.Authority; using Tgstation.Server.Host.Authority.Core; using Tgstation.Server.Host.Components; @@ -64,11 +62,7 @@ using Tgstation.Server.Host.Controllers; using Tgstation.Server.Host.Controllers.Results; using Tgstation.Server.Host.Database; using Tgstation.Server.Host.Extensions; -using Tgstation.Server.Host.GraphQL; -using Tgstation.Server.Host.GraphQL.Interceptors; -using Tgstation.Server.Host.GraphQL.Scalars; using Tgstation.Server.Host.GraphQL.Subscriptions; -using Tgstation.Server.Host.GraphQL.Types; using Tgstation.Server.Host.IO; using Tgstation.Server.Host.Jobs; using Tgstation.Server.Host.Properties; @@ -174,7 +168,6 @@ namespace Tgstation.Server.Host.Core services.UseStandardConfig(Configuration); services.UseStandardConfig(Configuration); services.UseStandardConfig(Configuration); - services.UseStandardConfig(Configuration); // enable options which give us config reloading services.AddOptions(); @@ -311,8 +304,12 @@ namespace Tgstation.Server.Host.Core services.AddCors(); // Enable managed HTTP clients - services.AddHttpClient(); - services.AddSingleton(); + services + .AddHttpClient() + .ConfigureHttpClientDefaults( + builder => builder.ConfigureHttpClient( + client => client.DefaultRequestHeaders.UserAgent.Add( + assemblyInformationProvider.ProductInfoHeaderValue))); // configure metrics var prometheusPort = postSetupServices.GeneralConfiguration.PrometheusPort; @@ -331,68 +328,9 @@ namespace Tgstation.Server.Host.Core // configure graphql services - .AddScoped() + .AddScoped() .AddGraphQLServer() - .AddAuthorization( - options => - { - options.AddPolicy( - TgsAuthorizeAttribute.PolicyName, - builder => builder - .RequireAuthenticatedUser() - .RequireRole(TgsAuthorizeAttribute.UserEnabledRole)); - options.AddPolicy( - "testingasdf", - builder => - { - builder.RequireAuthenticatedUser(); - builder.AuthenticationSchemes.Add(CookieAuthenticationDefaults.AuthenticationScheme); - }); - }) - .ModifyOptions(options => - { - options.EnsureAllNodesCanBeResolved = true; - options.EnableFlagEnums = true; - }) -#if DEBUG - .ModifyCostOptions(options => - { - options.EnforceCostLimits = false; - }) -#endif - .AddMutationConventions() - .AddInMemorySubscriptions( - new SubscriptionOptions - { - TopicBufferCapacity = 1024, // mainly so high for tests, not possible to DoS the server without authentication and some other access to generate messages - }) - .AddGlobalObjectIdentification() - .AddQueryFieldToMutationPayloads() - .ModifyOptions(options => - { - options.EnableDefer = true; - }) - .ModifyPagingOptions(pagingOptions => - { - pagingOptions.IncludeTotalCount = true; - pagingOptions.RequirePagingBoundaries = false; - pagingOptions.DefaultPageSize = ApiController.DefaultPageSize; - pagingOptions.MaxPageSize = ApiController.MaximumPageSize; - }) - .AddFiltering() - .AddSorting() - .AddHostTypes() - .AddErrorFilter() - .AddType() - .AddType() - .AddType() - .AddType() - .AddType() - .BindRuntimeType() - .TryAddTypeInterceptor() - .AddQueryType() - .AddMutationType() - .AddSubscriptionType(); + .ConfigureGraphQLServer(); void AddTypedContext() where TContext : DatabaseContext @@ -440,7 +378,7 @@ namespace Tgstation.Server.Host.Core services.AddSingleton(); // configure other security services - services.AddSingleton(); + services.AddScoped(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); @@ -540,6 +478,7 @@ namespace Tgstation.Server.Host.Core services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); // configure misc services services.AddSingleton(); @@ -548,7 +487,6 @@ namespace Tgstation.Server.Host.Core services.AddSingleton(); services.AddSingleton(fileSystem); services.AddHostedService(); - services.AddHostedService(); services.AddFileDownloader(); services.AddGitHub(); @@ -604,11 +542,11 @@ namespace Tgstation.Server.Host.Core ArgumentNullException.ThrowIfNull(serverPortProvider); ArgumentNullException.ThrowIfNull(assemblyInformationProvider); - var controlPanelConfiguration = controlPanelConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(controlPanelConfigurationOptions)); - var generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); - var databaseConfiguration = databaseConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(databaseConfigurationOptions)); - var swarmConfiguration = swarmConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(swarmConfigurationOptions)); - var internalConfiguration = internalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(internalConfigurationOptions)); + var controlPanelConfiguration = (controlPanelConfigurationOptions ?? throw new ArgumentNullException(nameof(controlPanelConfigurationOptions))).Value; + var generalConfiguration = (generalConfigurationOptions ?? throw new ArgumentNullException(nameof(generalConfigurationOptions))).Value; + var databaseConfiguration = (databaseConfigurationOptions ?? throw new ArgumentNullException(nameof(databaseConfigurationOptions))).Value; + var swarmConfiguration = (swarmConfigurationOptions ?? throw new ArgumentNullException(nameof(swarmConfigurationOptions))).Value; + var internalConfiguration = (internalConfigurationOptions ?? throw new ArgumentNullException(nameof(internalConfigurationOptions))).Value; ArgumentNullException.ThrowIfNull(logger); @@ -816,6 +754,10 @@ namespace Tgstation.Server.Host.Core services.AddScoped(); services.AddScoped(provider => provider.GetRequiredService()); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + // what if you // wanted to just do this: // return provider.GetRequiredService().CurrentAuthenticationContext @@ -870,6 +812,17 @@ namespace Tgstation.Server.Host.Core }; }); + services.AddAuthorization(options => + { + options.AddPolicy( + TgsAuthorizeAttribute.PolicyName, + builder => builder + .RequireAuthenticatedUser() + .RequireRole(TgsAuthorizeAttribute.UserEnabledRole)); + + options.DefaultPolicy = options.GetPolicy(TgsAuthorizeAttribute.PolicyName)!; + }); + var oidcConfig = securityConfiguration.OpenIDConnect; if (oidcConfig == null || oidcConfig.Count == 0) return; diff --git a/src/Tgstation.Server.Host/Core/CommandPipeManager.cs b/src/Tgstation.Server.Host/Core/CommandPipeManager.cs index 6a5c6325c6..f3534c9bea 100644 --- a/src/Tgstation.Server.Host/Core/CommandPipeManager.cs +++ b/src/Tgstation.Server.Host/Core/CommandPipeManager.cs @@ -30,22 +30,22 @@ namespace Tgstation.Server.Host.Core /// readonly IInstanceManager instanceManager; + /// + /// The of for the . + /// + readonly IOptions internalConfigurationOptions; + /// /// The for the . /// readonly ILogger logger; - /// - /// The for the . - /// - readonly InternalConfiguration internalConfiguration; - /// /// Initializes a new instance of the class. /// /// The value of . /// The value of . - /// The containing the value of . + /// The containing the value of . /// The value of . public CommandPipeManager( IServerControl serverControl, @@ -55,7 +55,7 @@ namespace Tgstation.Server.Host.Core { this.serverControl = serverControl ?? throw new ArgumentNullException(nameof(serverControl)); this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager)); - internalConfiguration = internalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(internalConfigurationOptions)); + this.internalConfigurationOptions = internalConfigurationOptions ?? throw new ArgumentNullException(nameof(internalConfigurationOptions)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); } @@ -65,7 +65,7 @@ namespace Tgstation.Server.Host.Core logger.LogTrace("Starting..."); // grab both pipes asap so we can close them on error - var commandPipe = internalConfiguration.CommandPipe; + var commandPipe = internalConfigurationOptions.Value.CommandPipe; var supportsPipeCommands = !String.IsNullOrWhiteSpace(commandPipe); await using var commandPipeClient = supportsPipeCommands ? new AnonymousPipeClientStream( @@ -76,7 +76,7 @@ namespace Tgstation.Server.Host.Core if (!supportsPipeCommands) logger.LogDebug("No command pipe name specified in configuration"); - var readyPipe = internalConfiguration.ReadyPipe; + var readyPipe = internalConfigurationOptions.Value.ReadyPipe; var supportsReadyNotification = !String.IsNullOrWhiteSpace(readyPipe); if (supportsReadyNotification) { diff --git a/src/Tgstation.Server.Host/Core/ServerPortProivder.cs b/src/Tgstation.Server.Host/Core/ServerPortProivder.cs index a3f39a4ab8..67f137ac51 100644 --- a/src/Tgstation.Server.Host/Core/ServerPortProivder.cs +++ b/src/Tgstation.Server.Host/Core/ServerPortProivder.cs @@ -13,28 +13,28 @@ namespace Tgstation.Server.Host.Core sealed class ServerPortProivder : IServerPortProvider { /// - public ushort HttpApiPort => generalConfiguration.ApiPort; + public ushort HttpApiPort => generalConfigurationOptions.Value.ApiPort; /// - /// The for the . + /// The of for the . /// - readonly GeneralConfiguration generalConfiguration; + readonly IOptions generalConfigurationOptions; /// /// Initializes a new instance of the class. /// - /// The containing the value of . /// The to use. + /// The containing the value of . /// The to use. public ServerPortProivder( - IOptions generalConfigurationOptions, IConfiguration configuration, + IOptions generalConfigurationOptions, ILogger logger) { - generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); ArgumentNullException.ThrowIfNull(configuration); + this.generalConfigurationOptions = generalConfigurationOptions ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); - var usingDefaultPort = generalConfiguration.ApiPort == default; + var usingDefaultPort = generalConfigurationOptions.Value.ApiPort == default; if (!usingDefaultPort) return; @@ -55,7 +55,7 @@ namespace Tgstation.Server.Host.Core if (!UInt16.TryParse(portString, out var result)) throw new InvalidOperationException($"Failed to parse HTTP EndPoint port: {httpEndpoint}"); - generalConfiguration.ApiPort = result; + this.generalConfigurationOptions.Value.ApiPort = result; } } } diff --git a/src/Tgstation.Server.Host/Core/ServerUpdater.cs b/src/Tgstation.Server.Host/Core/ServerUpdater.cs index 1170537a12..2caee4d944 100644 --- a/src/Tgstation.Server.Host/Core/ServerUpdater.cs +++ b/src/Tgstation.Server.Host/Core/ServerUpdater.cs @@ -36,21 +36,21 @@ namespace Tgstation.Server.Host.Core /// readonly IServerControl serverControl; + /// + /// The of for the . + /// + readonly IOptionsMonitor generalConfigurationOptions; + + /// + /// The of for the . + /// + readonly IOptionsMonitor updatesConfigurationOptions; + /// /// The for the . /// readonly ILogger logger; - /// - /// The for the . - /// - readonly GeneralConfiguration generalConfiguration; - - /// - /// The for the . - /// - readonly UpdatesConfiguration updatesConfiguration; - /// /// Lock used when initiating an update. /// @@ -69,24 +69,24 @@ namespace Tgstation.Server.Host.Core /// The value of . /// The value of . /// The value of . - /// The containing the value of . - /// The containing the value of . + /// The value of . + /// The value of . public ServerUpdater( IGitHubServiceFactory gitHubServiceFactory, IIOManager ioManager, IFileDownloader fileDownloader, IServerControl serverControl, ILogger logger, - IOptions generalConfigurationOptions, - IOptions updatesConfigurationOptions) + IOptionsMonitor generalConfigurationOptions, + IOptionsMonitor updatesConfigurationOptions) { this.gitHubServiceFactory = gitHubServiceFactory ?? throw new ArgumentNullException(nameof(gitHubServiceFactory)); this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); this.fileDownloader = fileDownloader ?? throw new ArgumentNullException(nameof(fileDownloader)); this.serverControl = serverControl ?? throw new ArgumentNullException(nameof(serverControl)); + this.generalConfigurationOptions = generalConfigurationOptions ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); + this.updatesConfigurationOptions = updatesConfigurationOptions ?? throw new ArgumentNullException(nameof(updatesConfigurationOptions)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); - generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); - updatesConfiguration = updatesConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(updatesConfigurationOptions)); updateInitiationLock = new object(); } @@ -290,6 +290,9 @@ namespace Tgstation.Server.Host.Core var gitHubService = await gitHubServiceFactory.CreateService(cancellationToken); var releases = await gitHubService.GetTgsReleases(cancellationToken); + + var updatesConfiguration = updatesConfigurationOptions.CurrentValue; + var generalConfiguration = generalConfigurationOptions.CurrentValue; foreach (var kvp in releases) { var version = kvp.Key; diff --git a/src/Tgstation.Server.Host/Core/VersionReportingService.cs b/src/Tgstation.Server.Host/Core/VersionReportingService.cs deleted file mode 100644 index 3a4feeed37..0000000000 --- a/src/Tgstation.Server.Host/Core/VersionReportingService.cs +++ /dev/null @@ -1,271 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -using Octokit; - -using Tgstation.Server.Common.Extensions; -using Tgstation.Server.Host.Configuration; -using Tgstation.Server.Host.Extensions; -using Tgstation.Server.Host.IO; -using Tgstation.Server.Host.Properties; -using Tgstation.Server.Host.System; -using Tgstation.Server.Host.Utils; -using Tgstation.Server.Host.Utils.GitHub; - -namespace Tgstation.Server.Host.Core -{ - /// - /// Handles TGS version reporting, if enabled. - /// - sealed class VersionReportingService : BackgroundService - { - /// - /// The for the . - /// - readonly IGitHubClientFactory gitHubClientFactory; - - /// - /// The for the . - /// - readonly IIOManager ioManager; - - /// - /// The for the . - /// - readonly IAsyncDelayer asyncDelayer; - - /// - /// The for the . - /// - readonly IAssemblyInformationProvider assemblyInformationProvider; - - /// - /// The for the . - /// - readonly ILogger logger; - - /// - /// The for the . - /// - readonly TelemetryConfiguration telemetryConfiguration; - - /// - /// The passed to . - /// - CancellationToken shutdownCancellationToken; - - /// - /// Initializes a new instance of the class. - /// - /// The value of . - /// The value of . - /// The value of . - /// The value of . - /// The containing the value of . - /// The value of . - public VersionReportingService( - IGitHubClientFactory gitHubClientFactory, - IIOManager ioManager, - IAsyncDelayer asyncDelayer, - IAssemblyInformationProvider assemblyInformationProvider, - IOptions telemetryConfigurationOptions, - ILogger logger) - { - this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory)); - this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); - this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer)); - this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider)); - telemetryConfiguration = telemetryConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(telemetryConfigurationOptions)); - this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - - /// - public override Task StopAsync(CancellationToken cancellationToken) - { - shutdownCancellationToken = cancellationToken; - return base.StopAsync(cancellationToken); - } - - /// - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - if (telemetryConfiguration.DisableVersionReporting) - { - logger.LogDebug("Version telemetry disabled"); - return; - } - - if (!telemetryConfiguration.VersionReportingRepositoryId.HasValue) - { - logger.LogError("Version reporting repository is misconfigured. Telemetry cannot be sent!"); - return; - } - - var attribute = TelemetryAppSerializedKeyAttribute.Instance; - if (attribute == null) - { - logger.LogDebug("TGS build configuration does not allow for version telemetry"); - return; - } - - logger.LogDebug("Starting..."); - - try - { - var telemetryIdDirectory = ioManager.GetPathInLocalDirectory(assemblyInformationProvider); - var telemetryIdFile = ioManager.ResolvePath( - ioManager.ConcatPath( - telemetryIdDirectory, - "telemetry.id")); - - Guid telemetryId; - if (!await ioManager.FileExists(telemetryIdFile, stoppingToken)) - { - telemetryId = Guid.NewGuid(); - await ioManager.CreateDirectory(telemetryIdDirectory, stoppingToken); - await ioManager.WriteAllBytes(telemetryIdFile, Encoding.UTF8.GetBytes(telemetryId.ToString()), stoppingToken); - logger.LogInformation("Generated telemetry ID {telemetryId} and wrote to {file}", telemetryId, telemetryIdFile); - } - else - { - var contents = await ioManager.ReadAllBytes(telemetryIdFile, stoppingToken); - - string guidStr; - try - { - guidStr = Encoding.UTF8.GetString(contents); - } - catch (Exception ex) - { - logger.LogError(ex, "Cannot decode telemetry ID from installation file ({path}). Telemetry will not be sent!", telemetryIdFile); - return; - } - - if (!Guid.TryParse(guidStr, out telemetryId)) - { - logger.LogError("Cannot parse telemetry ID from installation file ({path}). Telemetry will not be sent!", telemetryIdFile); - return; - } - } - - try - { - while (!stoppingToken.IsCancellationRequested) - { - var nextDelayHours = await TryReportVersion( - telemetryId, - attribute.SerializedKey, - telemetryConfiguration.VersionReportingRepositoryId.Value, - false, - stoppingToken) - ? 24 - : 1; - - logger.LogDebug("Next version report in {hours} hours", nextDelayHours); - await asyncDelayer.Delay(TimeSpan.FromHours(nextDelayHours), stoppingToken); - } - } - catch (OperationCanceledException ex) - { - logger.LogTrace(ex, "Inner cancellation"); - } - - shutdownCancellationToken.ThrowIfCancellationRequested(); - - logger.LogDebug("Sending shutdown telemetry"); - await TryReportVersion( - telemetryId, - attribute.SerializedKey, - telemetryConfiguration.VersionReportingRepositoryId.Value, - true, - shutdownCancellationToken); - } - catch (OperationCanceledException ex) - { - logger.LogTrace(ex, "Exiting due to outer cancellation..."); - } - catch (Exception ex) - { - logger.LogError(ex, "Crashed!"); - } - } - - /// - /// Make an attempt to report the current to the configured GitHub repository. - /// - /// The telemetry for the installation. - /// The serialized authentication for the . - /// The ID of the repository to send telemetry to. - /// If this is shutdown telemetry. - /// The for the operation. - /// A resulting in if telemetry was reported successfully, otherwise. - async ValueTask TryReportVersion(Guid telemetryId, string serializedPem, long repositoryId, bool shutdown, CancellationToken cancellationToken) - { - logger.LogDebug("Sending version telemetry..."); - - var serverFriendlyName = telemetryConfiguration.ServerFriendlyName; - if (String.IsNullOrWhiteSpace(serverFriendlyName)) - serverFriendlyName = null; - - logger.LogTrace( - "Repository ID: {repoId}, Server friendly name: {friendlyName}", - repositoryId, - serverFriendlyName == null - ? "(null)" - : $"\"{serverFriendlyName}\""); - try - { - var gitHubClient = await gitHubClientFactory.CreateClientForRepository( - serializedPem, - new RepositoryIdentifier(repositoryId), - cancellationToken); - - if (gitHubClient == null) - { - logger.LogWarning("Could not create GitHub client to connect to repository ID {repoId}!", repositoryId); - return false; - } - - // remove this lookup once https://github.com/octokit/octokit.net/pull/2960 is merged and released - var repository = await gitHubClient.Repository.Get(repositoryId); - - logger.LogTrace("Repository ID {id} resolved to {owner}/{name}", repositoryId, repository.Owner.Name, repository.Name); - - var inputs = new Dictionary - { - { "telemetry_id", telemetryId.ToString() }, - { "tgs_semver", assemblyInformationProvider.Version.Semver().ToString() }, - { "shutdown", shutdown ? "true" : "false" }, - }; - - if (serverFriendlyName != null) - inputs.Add("server_friendly_name", serverFriendlyName); - - await gitHubClient.Actions.Workflows.CreateDispatch( - repository.Owner.Login, - repository.Name, - ".github/workflows/tgs_deployments_telemetry.yml", - new CreateWorkflowDispatch("main") - { - Inputs = inputs, - }); - - logger.LogTrace("Telemetry sent successfully"); - - return true; - } - catch (Exception ex) - { - logger.LogWarning(ex, "Failed to report version!"); - return false; - } - } - } -} diff --git a/src/Tgstation.Server.Host/Database/DatabaseCollection.cs b/src/Tgstation.Server.Host/Database/DatabaseCollection.cs index b2dafa719d..94d5ef90e8 100644 --- a/src/Tgstation.Server.Host/Database/DatabaseCollection.cs +++ b/src/Tgstation.Server.Host/Database/DatabaseCollection.cs @@ -3,7 +3,6 @@ using System.Collections; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; -using System.Threading; using Microsoft.EntityFrameworkCore; @@ -48,9 +47,6 @@ namespace Tgstation.Server.Host.Database /// public void Attach(TModel model) => dbSet.Attach(model); - /// - public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) => dbSet.AsAsyncEnumerable().GetAsyncEnumerator(cancellationToken); - /// public IEnumerator GetEnumerator() => dbSet.AsQueryable().GetEnumerator(); diff --git a/src/Tgstation.Server.Host/Database/DatabaseSeeder.cs b/src/Tgstation.Server.Host/Database/DatabaseSeeder.cs index 4eaf4e21b9..03d89a64eb 100644 --- a/src/Tgstation.Server.Host/Database/DatabaseSeeder.cs +++ b/src/Tgstation.Server.Host/Database/DatabaseSeeder.cs @@ -216,7 +216,6 @@ namespace Tgstation.Server.Host.Database { var tgsUser = await databaseContext .Users - .AsQueryable() .Where(x => x.CanonicalName == User.CanonicalizeName(User.TgsSystemUserName)) .FirstOrDefaultAsync(cancellationToken); @@ -232,7 +231,6 @@ namespace Tgstation.Server.Host.Database // normalize backslashes to forward slashes var allInstances = await databaseContext .Instances - .AsQueryable() .Where(instance => instance.SwarmIdentifer == swarmConfiguration.Identifier) .ToListAsync(cancellationToken); foreach (var instance in allInstances) @@ -242,7 +240,6 @@ namespace Tgstation.Server.Host.Database { var ids = await databaseContext .DreamDaemonSettings - .AsQueryable() .Where(x => x.TopicRequestTimeout == 0) .Select(x => x.Id) .ToListAsync(cancellationToken); @@ -311,7 +308,6 @@ namespace Tgstation.Server.Host.Database { var admin = await databaseContext .Users - .AsQueryable() .Where(x => x.CanonicalName == User.CanonicalizeName(DefaultCredentials.AdminUserName)) .Include(x => x.CreatedBy) .Include(x => x.PermissionSet) diff --git a/src/Tgstation.Server.Host/Database/IDatabaseCollection.cs b/src/Tgstation.Server.Host/Database/IDatabaseCollection.cs index a9e1fe0e03..52ca1796af 100644 --- a/src/Tgstation.Server.Host/Database/IDatabaseCollection.cs +++ b/src/Tgstation.Server.Host/Database/IDatabaseCollection.cs @@ -7,7 +7,7 @@ namespace Tgstation.Server.Host.Database /// Represents a database table. /// /// The type of model. - public interface IDatabaseCollection : IQueryable, IAsyncEnumerable + public interface IDatabaseCollection : IQueryable { /// /// An of s prioritizing in the working set. diff --git a/src/Tgstation.Server.Host/Extensions/ClaimsPrincipalExtensions.cs b/src/Tgstation.Server.Host/Extensions/ClaimsPrincipalExtensions.cs new file mode 100644 index 0000000000..3b9c5c0039 --- /dev/null +++ b/src/Tgstation.Server.Host/Extensions/ClaimsPrincipalExtensions.cs @@ -0,0 +1,76 @@ +using System; +using System.Globalization; +using System.Security.Claims; + +using Microsoft.IdentityModel.JsonWebTokens; +using Microsoft.IdentityModel.Tokens; + +namespace Tgstation.Server.Host.Extensions +{ + /// + /// Extension methods for the class. + /// + static class ClaimsPrincipalExtensions + { + /// + /// Parse the out of a given authenticated . + /// + /// The to use to parse the user ID. + /// The user ID in the if it was present. + public static long? GetTgsUserId(this ClaimsPrincipal principal) + { + ArgumentNullException.ThrowIfNull(principal); + + var userIdClaim = principal.FindFirst(JwtRegisteredClaimNames.Sub); + if (userIdClaim == default) + return null; + + long userId; + try + { + userId = Int64.Parse(userIdClaim.Value, CultureInfo.InvariantCulture); + } + catch (Exception e) + { + throw new InvalidOperationException("Failed to parse user ID!", e); + } + + return userId; + } + + /// + /// Parse the out of a given authenticated . + /// + /// The to use to parse the user ID. + /// The user ID in the . + public static long RequireTgsUserId(this ClaimsPrincipal principal) + => principal.GetTgsUserId() ?? throw new InvalidOperationException($"Missing '{JwtRegisteredClaimNames.Sub}' claim!"); + + /// + /// Parse a out of a in a given . + /// + /// The containing claims. + /// The name to parse from. + /// The parsed . + public static DateTimeOffset ParseTime(this ClaimsPrincipal principal, string claimName) + { + ArgumentNullException.ThrowIfNull(principal); + ArgumentNullException.ThrowIfNull(claimName); + + var claim = principal.FindFirst(claimName); + if (claim == null) + throw new InvalidOperationException($"Missing '{claimName}' claim!"); + + try + { + return new DateTimeOffset( + EpochTime.DateTime( + Int64.Parse(claim.Value, CultureInfo.InvariantCulture))); + } + catch (Exception ex) + { + throw new InvalidOperationException($"Failed to parse claim {claimName}: '{claim.Value}'!", ex); + } + } + } +} diff --git a/src/Tgstation.Server.Host/Extensions/DataLoaderExtensions.cs b/src/Tgstation.Server.Host/Extensions/DataLoaderExtensions.cs new file mode 100644 index 0000000000..dd6a076f27 --- /dev/null +++ b/src/Tgstation.Server.Host/Extensions/DataLoaderExtensions.cs @@ -0,0 +1,50 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +using GreenDonut; +using GreenDonut.Data; + +using Tgstation.Server.Host.Authority.Core; + +namespace Tgstation.Server.Host.Extensions +{ + /// + /// Extension methods for . + /// + static class DataLoaderExtensions + { + /// + /// Convert a request for a single into a data-loader invocation for the matching . + /// + /// The of the underlying DTO being loaded. + /// The for s. + /// The active , if any. + /// The of to load. + /// The for the operation. + /// A resulting in the loaded if it exists, otherwise. + public static async ValueTask LoadAuthorityResponse( + this IDataLoader> dataLoader, + QueryContext? queryContext, + long id, + CancellationToken cancellationToken) + where TResult : class + { + ArgumentNullException.ThrowIfNull(dataLoader); + + var wrappedQueryContext = queryContext?.AuthorityResponseWrap(); + + var branchedDataLoader = dataLoader + .With(wrappedQueryContext); + + var authorityResponse = await branchedDataLoader + .LoadAsync(id, cancellationToken); + + if (authorityResponse == null) + return null; + + GraphQLAuthorityInvoker.ThrowGraphQLErrorIfNecessary(authorityResponse, false); + return authorityResponse.Result; + } + } +} diff --git a/src/Tgstation.Server.Host/Extensions/DatabaseCollectionExtensions.cs b/src/Tgstation.Server.Host/Extensions/DatabaseCollectionExtensions.cs index ea50a7af17..856b74a9f0 100644 --- a/src/Tgstation.Server.Host/Extensions/DatabaseCollectionExtensions.cs +++ b/src/Tgstation.Server.Host/Extensions/DatabaseCollectionExtensions.cs @@ -33,7 +33,6 @@ namespace Tgstation.Server.Host.Extensions ArgumentNullException.ThrowIfNull(selector); return databaseCollection - .AsQueryable() .Where(x => x.CanonicalName == User.CanonicalizeName(User.TgsSystemUserName)) .Select(selector) .FirstAsync(cancellationToken); diff --git a/src/Tgstation.Server.Host/Extensions/ExpressionExtensions.cs b/src/Tgstation.Server.Host/Extensions/ExpressionExtensions.cs new file mode 100644 index 0000000000..f30b89a78d --- /dev/null +++ b/src/Tgstation.Server.Host/Extensions/ExpressionExtensions.cs @@ -0,0 +1,39 @@ +using System; +using System.Linq.Expressions; + +using Tgstation.Server.Host.Authority.Core; + +namespace Tgstation.Server.Host.Extensions +{ + /// + /// Extension methods for s. + /// + static class ExpressionExtensions + { + /// + /// Create an to transform a given into a and output them as a . + /// + /// The input . + /// The output . + /// An to transform a given into a . + /// An to transform a given into a and output them as a . + public static Expression>> Projected(this Expression> translationExpression) + { + var parameter = Expression.Parameter(typeof(TQueried), "queried"); + var body = Expression.Invoke(translationExpression, parameter); + + var ourType = typeof(ProjectedPair); + + var expr = Expression.MemberInit( + Expression.New(ourType), + Expression.Bind( + ourType.GetProperty(nameof(ProjectedPair.Queried))!, + parameter), + Expression.Bind( + ourType.GetProperty(nameof(ProjectedPair.Result))!, + body)); + + return Expression.Lambda>>(expr, parameter); + } + } +} diff --git a/src/Tgstation.Server.Host/Extensions/InstanceManagerExtensions.cs b/src/Tgstation.Server.Host/Extensions/InstanceManagerExtensions.cs new file mode 100644 index 0000000000..5bd1f33994 --- /dev/null +++ b/src/Tgstation.Server.Host/Extensions/InstanceManagerExtensions.cs @@ -0,0 +1,25 @@ +using System; + +using Tgstation.Server.Host.Components; +using Tgstation.Server.Host.Models; + +namespace Tgstation.Server.Host.Extensions +{ + /// + /// Extensions methods for . + /// + static class InstanceManagerExtensions + { + /// + /// Get the associated with given . + /// + /// The to get the from. + /// The of the desired . + /// The associated with the given if it is online, otherwise. + public static IInstanceReference? GetInstanceReference(this IInstanceManager instanceManager, Api.Models.Instance metadata) + { + ArgumentNullException.ThrowIfNull(instanceManager); + return instanceManager.GetInstanceReference(metadata.Require(x => x.Id)); + } + } +} diff --git a/src/Tgstation.Server.Host/Extensions/QueryContextExtensions.cs b/src/Tgstation.Server.Host/Extensions/QueryContextExtensions.cs new file mode 100644 index 0000000000..cd8e9e2ed0 --- /dev/null +++ b/src/Tgstation.Server.Host/Extensions/QueryContextExtensions.cs @@ -0,0 +1,103 @@ +using System; +using System.Linq.Expressions; + +using GreenDonut.Data; + +using Tgstation.Server.Host.Authority.Core; + +namespace Tgstation.Server.Host.Extensions +{ + /// + /// Extension methods for . + /// + static class QueryContextExtensions + { + /// + /// Translate a given into one with the target wrapped in an . + /// + /// The result of the . + /// The original . + /// A with the target wrapped in an . + public static QueryContext> AuthorityResponseWrap(this QueryContext queryContext) + { + ArgumentNullException.ThrowIfNull(queryContext); + + Expression, TResult>> responseToResult = response => response.Result!; + + var authorityParameter = Expression.Parameter(typeof(AuthorityResponse), "authorityResponse"); + var resultFromAuthority = Expression.Invoke( + responseToResult, + authorityParameter); + + // we don't need to do much + Expression, AuthorityResponse>>? selector = null; + if (queryContext.Selector != null) + { + var innerInvoke = Expression.Invoke( + queryContext.Selector, + resultFromAuthority); + + var outerInvoke = Expression.Invoke( + AuthorityResponse.MappingExpression, + innerInvoke); + + selector = Expression.Lambda, AuthorityResponse>>( + outerInvoke, + authorityParameter); + } + + Expression, bool>>? predicate = null; + if (queryContext.Predicate != null) + { + predicate = Expression.Lambda, bool>>( + Expression.Invoke( + queryContext.Predicate, + resultFromAuthority), + authorityParameter); + } + + if (queryContext.Sorting?.Operations.Length > 0) + { + throw new NotImplementedException(); + } + + return new QueryContext>( + selector, + predicate); + } + + /// + /// Unwrap a given that was built from one resulting from . + /// + /// The underlying result of the . + /// The wrapped to unwrap. + /// The unwrapped for . + public static QueryContext AuthorityResponseUnwrap(this QueryContext> queryContext) + { + ArgumentNullException.ThrowIfNull(queryContext); + + // assuming we were wrapped with AuthorityResponseWrap. This is hacky but it works + // UNLESS it was compiled down, in which case we are D-O-N-E-FUCKED + Expression>? selector = null; + if (queryContext.Selector != null) + { + selector = (Expression>?)((InvocationExpression)((InvocationExpression)queryContext.Selector.Body).Arguments[0]).Expression; + } + + Expression>? predicate = null; + if (queryContext.Predicate != null) + { + predicate = (Expression>?)((InvocationExpression)queryContext.Predicate.Body).Expression; + } + + if (queryContext.Sorting?.Operations.Length > 0) + { + throw new NotImplementedException(); + } + + return new QueryContext( + selector, + predicate); + } + } +} diff --git a/src/Tgstation.Server.Host/Extensions/QueryableExtensions.cs b/src/Tgstation.Server.Host/Extensions/QueryableExtensions.cs new file mode 100644 index 0000000000..a161dce091 --- /dev/null +++ b/src/Tgstation.Server.Host/Extensions/QueryableExtensions.cs @@ -0,0 +1,130 @@ +using System; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; + +using GreenDonut.Data; + +using Microsoft.EntityFrameworkCore; + +using Tgstation.Server.Host.Authority.Core; + +namespace Tgstation.Server.Host.Extensions +{ + /// + /// Extension methods for . + /// + static class QueryableExtensions + { + /// + /// Map a given to project onto a given . + /// + /// The . + /// The . + /// A for s of /. + /// The for to map onto the . + /// A new with mapped on the . + public static IQueryable> With( + this IQueryable> queryable, + QueryContext? queryContext) + { + // taken verbatim from GreenDonut's With implementation + ArgumentNullException.ThrowIfNull(queryable); + + if (queryContext == null) + return queryable; + + var modified = false; + if (queryContext.Predicate != null) + { + queryable = queryable.Where( + TranslateLambda( + queryContext.Predicate)); + modified = true; + } + + if (queryContext.Sorting?.Operations.Length > 0) + { + var definition = new SortDefinition>( + queryContext.Sorting.Operations + .Select(MapSortBy)); + + queryable = queryable.OrderBy(definition); + modified = true; + } + + if (queryContext.Selector != null) + { + Expression, TResult, ProjectedPair>> remapSelector = + (initialProjected, selectedResult) => new ProjectedPair + { + Queried = initialProjected.Queried, + Result = selectedResult, + }; + Expression, TResult>> resultSelector = projected => projected.Result; + var parameter = Expression.Parameter(typeof(ProjectedPair), "projectedParam"); + var result = Expression.Parameter(typeof(TResult), "resultParam"); + var initialResultExpr = Expression.Invoke(resultSelector, parameter); + var resultExpr = Expression.Invoke(queryContext.Selector, initialResultExpr); + var finalInvoke = Expression.Invoke(remapSelector, parameter, resultExpr); + var finalExpr = Expression.Lambda, ProjectedPair>>(finalInvoke, parameter); + + queryable = queryable.Select(finalExpr); + modified = true; + } + + if (modified) + queryable = queryable + .TagWith("GraphQL Projections"); + + return queryable; + } + + /// + /// Map a given onto a . + /// + /// The . + /// The . + /// The for to map onto a of /. + /// A new for the . + private static ISortBy> MapSortBy(ISortBy sortBy) + { + var sortingKeyType = sortBy.GetType().GenericTypeArguments[1]; + + var factoryMethod = typeof(SortBy>) + .GetMethod( + sortBy.Ascending + ? nameof(SortBy>.Ascending) + : nameof(SortBy>.Descending))!; + + var instantiatedLambdaTranslate = typeof(QueryableExtensions) + .GetMethod(nameof(TranslateLambda), BindingFlags.NonPublic | BindingFlags.Static)! + .MakeGenericMethod(typeof(TQueried), typeof(TResult), sortingKeyType); + + var fixedLambda = instantiatedLambdaTranslate.Invoke(null, [sortBy.KeySelector])!; + + var instantiatedFactoryMethod = factoryMethod.MakeGenericMethod(sortingKeyType); + + return (ISortBy>)instantiatedFactoryMethod.Invoke(null, [fixedLambda])!; + } + + /// + /// Translate a given on a onto its . + /// + /// The . + /// The . + /// The selected . + /// A accepting a and outputting a . + /// An accepting a of / and outputting a . + private static Expression, TDesired>> TranslateLambda(LambdaExpression selectionExpression) + { + Expression, TResult>> resultSelector = projected => projected.Result; + + var parameter = Expression.Parameter(typeof(ProjectedPair), "projectedTranslateParam"); + var result = Expression.Invoke(resultSelector, parameter); + var expr = Expression.Invoke(selectionExpression, result); + + return Expression.Lambda, TDesired>>(expr, parameter); + } + } +} diff --git a/src/Tgstation.Server.Host/Extensions/RequestExecutorBuilderExtensions.cs b/src/Tgstation.Server.Host/Extensions/RequestExecutorBuilderExtensions.cs new file mode 100644 index 0000000000..73971968f0 --- /dev/null +++ b/src/Tgstation.Server.Host/Extensions/RequestExecutorBuilderExtensions.cs @@ -0,0 +1,97 @@ +using System; + +using HotChocolate.Execution.Configuration; +using HotChocolate.Subscriptions; +using HotChocolate.Types; + +using Microsoft.Extensions.DependencyInjection; + +using Tgstation.Server.Api.Rights; +using Tgstation.Server.Host.Controllers; +using Tgstation.Server.Host.GraphQL; +using Tgstation.Server.Host.GraphQL.Metadata; +using Tgstation.Server.Host.GraphQL.Scalars; +using Tgstation.Server.Host.GraphQL.Types; + +namespace Tgstation.Server.Host.Extensions +{ + /// + /// Extension methods for . + /// + public static class RequestExecutorBuilderExtensions + { + /// + /// Configure a GraphQL pipeline . + /// + /// The to configure. + public static void ConfigureGraphQLServer(this IRequestExecutorBuilder builder) + => (builder ?? throw new ArgumentNullException(nameof(builder))) + .ModifyOptions(options => + { + options.EnsureAllNodesCanBeResolved = true; + options.EnableFlagEnums = true; + }) +#if DEBUG + .ModifyCostOptions(options => + { + options.EnforceCostLimits = false; + }) +#endif + .AddMutationConventions() + .AddInMemorySubscriptions( + new SubscriptionOptions + { + TopicBufferCapacity = 1024, // mainly so high for tests, not possible to DoS the server without authentication and some other access to generate messages + }) + .AddGlobalObjectIdentification() + .AddQueryFieldToMutationPayloads() + .ModifyOptions(options => + { + options.EnableDefer = true; + }) + .ModifyPagingOptions(pagingOptions => + { + pagingOptions.IncludeTotalCount = true; + pagingOptions.RequirePagingBoundaries = false; + pagingOptions.DefaultPageSize = ApiController.DefaultPageSize; + pagingOptions.MaxPageSize = ApiController.MaximumPageSize; + }) + .AddFiltering() + .AddSorting() + .AddProjections() + .AddHostTypes() + .AddAuthorization() + .ConfigureTypes(); + + /// + /// Configure active s for a given . + /// + /// The to configure. + private static void ConfigureTypes(this IRequestExecutorBuilder builder) + => builder + .AddQueryType() + .AddMutationType() + .AddSubscriptionType() + .AddType() + .AddType() + .AddType() + .AddType() + .TryAddTypeInterceptor() + .BindRuntimeType() + .AddRightsHolders(); + + /// + /// Configure s for all s. + /// + /// The to configure. + private static void AddRightsHolders(this IRequestExecutorBuilder builder) + { + var rightsHolderGeneric = typeof(RightsHolderType<>); + foreach (var right in RightsHelper.AllRightTypes()) + { + var instantiatedRightsHolder = rightsHolderGeneric.MakeGenericType(right); + builder.AddType(instantiatedRightsHolder); + } + } + } +} diff --git a/src/Tgstation.Server.Host/GraphQL/AuthorizationHelper.cs b/src/Tgstation.Server.Host/GraphQL/AuthorizationHelper.cs new file mode 100644 index 0000000000..74bc22dc89 --- /dev/null +++ b/src/Tgstation.Server.Host/GraphQL/AuthorizationHelper.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +using HotChocolate; + +using Microsoft.AspNetCore.Authorization; + +using Tgstation.Server.Host.Security; + +namespace Tgstation.Server.Host.GraphQL +{ + /// + /// Helper for authorization functionality related to GraphQL. + /// + static class AuthorizationHelper + { + /// + /// Create a new to be thrown when a forbidden error occurs. + /// + /// The . + /// A new . + public static GraphQLException ForbiddenGraphQLException(this AuthorizationFailure authorizationFailure) + { + ArgumentNullException.ThrowIfNull(authorizationFailure); + + var messageBuilder = new StringBuilder("The current user is not authorized to access this resource."); + + foreach (var failureReason in authorizationFailure.FailureReasons) + { + messageBuilder.AppendLine(); + messageBuilder.Append("\t- "); + messageBuilder.Append(failureReason.Message); + } + + return new(ErrorBuilder.New() + .SetMessage(messageBuilder.ToString()) // Copied from graphql-platform: AuthorizeMiddleware.cs + .SetCode(ErrorCodes.Authentication.NotAuthorized) + .Build()); + } + + /// + /// Evaluate a given set of , throwing the approriate on failure. + /// + /// The authorization service to use. + /// The of s to evaluate. + /// The relevant . + /// If the should be excluded. + /// A representing the running operation. + public static async ValueTask CheckGraphQLAuthorized( + this Security.IAuthorizationService authorizationService, + IEnumerable? authorizationRequirements, + long? instanceId, + bool excludeUserSessionValidRequirement = false) + { + ArgumentNullException.ThrowIfNull(authorizationService); + ArgumentNullException.ThrowIfNull(authorizationRequirements); + + if (!excludeUserSessionValidRequirement) + authorizationRequirements = UserSessionValidRequirement.InstanceAsEnumerable.Concat(authorizationRequirements); + + var result = await authorizationService.AuthorizeAsync(authorizationRequirements, instanceId); + if (!result.Succeeded) + throw result.Failure.ForbiddenGraphQLException(); + } + } +} diff --git a/src/Tgstation.Server.Host/GraphQL/Costs.cs b/src/Tgstation.Server.Host/GraphQL/Costs.cs new file mode 100644 index 0000000000..d782f15d38 --- /dev/null +++ b/src/Tgstation.Server.Host/GraphQL/Costs.cs @@ -0,0 +1,13 @@ +namespace Tgstation.Server.Host.GraphQL +{ + /// + /// Values used with for when the default is insufficient. + /// + static class Costs + { + /// + /// Cost for non-ById queries. + /// + public const int NonIndexedQueryable = 100; + } +} diff --git a/src/Tgstation.Server.Host/GraphQL/Interfaces/IGateway.cs b/src/Tgstation.Server.Host/GraphQL/Interfaces/IGateway.cs index 1de582c06f..886ee78f47 100644 --- a/src/Tgstation.Server.Host/GraphQL/Interfaces/IGateway.cs +++ b/src/Tgstation.Server.Host/GraphQL/Interfaces/IGateway.cs @@ -1,5 +1,7 @@ using System.Linq; +using HotChocolate.Authorization; + using Tgstation.Server.Host.GraphQL.Types; namespace Tgstation.Server.Host.GraphQL.Interfaces @@ -18,7 +20,8 @@ namespace Tgstation.Server.Host.GraphQL.Interfaces /// /// Queries all s in the . /// - /// Queryable s. + /// Queryable s in the . + [Authorize] IQueryable Instances(); } } diff --git a/src/Tgstation.Server.Host/GraphQL/Metadata/RightsHolderType{TRight}.cs b/src/Tgstation.Server.Host/GraphQL/Metadata/RightsHolderType{TRight}.cs new file mode 100644 index 0000000000..2d089e69b9 --- /dev/null +++ b/src/Tgstation.Server.Host/GraphQL/Metadata/RightsHolderType{TRight}.cs @@ -0,0 +1,46 @@ +using System; + +using HotChocolate.Types; + +using Tgstation.Server.Host.GraphQL.Types; + +namespace Tgstation.Server.Host.GraphQL.Metadata +{ + /// + /// for s to give the flags proper GraphQL semantics. + /// + /// The of the . + public sealed class RightsHolderType : ObjectType> + where TRight : struct, Enum + { + /// + protected override void Configure(IObjectTypeDescriptor> descriptor) + { + ArgumentNullException.ThrowIfNull(descriptor); + + descriptor + .BindFieldsExplicitly() + .Name(typeof(TRight).Name); + + foreach (var individualRightValue in Enum.GetValues()) + { + var rightName = Enum.GetName(individualRightValue); + + descriptor + .Field($"can{rightName}") + .ResolveWith>((holder) => holder.HasContextFlag) + .Extend() + .OnBeforeCompletion( + (context, definition) => + { + definition.Member = typeof(RightsHolder).GetProperty(nameof(RightsHolder.Right)); + definition.PureResolver = context => + { + var holder = context.Parent>(); + return holder.Right.HasFlag(individualRightValue); + }; + }); + } + } + } +} diff --git a/src/Tgstation.Server.Host/GraphQL/Interceptors/RightsTypeInterceptor.cs b/src/Tgstation.Server.Host/GraphQL/Metadata/RightsTypeInterceptor.cs similarity index 80% rename from src/Tgstation.Server.Host/GraphQL/Interceptors/RightsTypeInterceptor.cs rename to src/Tgstation.Server.Host/GraphQL/Metadata/RightsTypeInterceptor.cs index 5fea9058e3..c40dec88dd 100644 --- a/src/Tgstation.Server.Host/GraphQL/Interceptors/RightsTypeInterceptor.cs +++ b/src/Tgstation.Server.Host/GraphQL/Metadata/RightsTypeInterceptor.cs @@ -5,11 +5,12 @@ using System.Reflection; using HotChocolate.Configuration; using HotChocolate.Types; +using HotChocolate.Types.Descriptors; using HotChocolate.Types.Descriptors.Definitions; using Tgstation.Server.Api.Rights; using Tgstation.Server.Host.GraphQL.Types; -namespace Tgstation.Server.Host.GraphQL.Interceptors +namespace Tgstation.Server.Host.GraphQL.Metadata { /// /// Fixes the names used for the default flags types in API rights. @@ -26,11 +27,6 @@ namespace Tgstation.Server.Host.GraphQL.Interceptors /// const string NoneFieldName = $"{IsPrefix}None"; - /// - /// Names of rights GraphQL object types. - /// - private readonly HashSet objectNames; - /// /// Names of rights GraphQL input types. /// @@ -42,28 +38,22 @@ namespace Tgstation.Server.Host.GraphQL.Interceptors public RightsTypeInterceptor() { var rightTypes = Enum.GetValues(); - objectNames = new HashSet(rightTypes.Length); inputNames = new HashSet(rightTypes.Length); foreach (var rightType in rightTypes) { var rightName = rightType.ToString(); - var flagName = $"{rightName}RightsFlags"; - - objectNames.Add(flagName); - inputNames.Add($"{flagName}Input"); + inputNames.Add($"{rightName}RightsFlagsInput"); } } /// /// Fix the "is" prefix on a given set of . /// - /// The of to correct. - /// The of s to operate on. - static void FixFields(IBindableList fields) - where TField : FieldDefinitionBase + /// The of s to operate on. + static void FixFields(IBindableList fields) { - TField? noneField = null; + InputFieldDefinition? noneField = null; foreach (var field in fields) { @@ -78,6 +68,7 @@ namespace Tgstation.Server.Host.GraphQL.Interceptors throw new InvalidOperationException("Expected flags enum type field to start with \"is\"!"); field.Name = $"can{fieldName[IsPrefix.Length..]}"; + field.Type = TypeReference.Parse($"{ScalarNames.Boolean}!"); } if (noneField == null) @@ -119,12 +110,7 @@ namespace Tgstation.Server.Host.GraphQL.Interceptors { ArgumentNullException.ThrowIfNull(definition); - if (definition is ObjectTypeDefinition objectTypeDef) - { - if (objectNames.Contains(objectTypeDef.Name)) - FixFields(objectTypeDef.Fields); - } - else if (definition is InputObjectTypeDefinition inputTypeDef) + if (definition is InputObjectTypeDefinition inputTypeDef) { const string PermissionSetInputName = $"{nameof(PermissionSet)}Input"; const string InstancePermissionSetInputName = $"{nameof(InstancePermissionSet)}Input"; diff --git a/src/Tgstation.Server.Host/GraphQL/Mutation.cs b/src/Tgstation.Server.Host/GraphQL/Mutation.cs index 286fe00619..2bb7231021 100644 --- a/src/Tgstation.Server.Host/GraphQL/Mutation.cs +++ b/src/Tgstation.Server.Host/GraphQL/Mutation.cs @@ -3,6 +3,7 @@ using System.Threading; using System.Threading.Tasks; using HotChocolate; +using HotChocolate.Authorization; using HotChocolate.Types; using Tgstation.Server.Host.Authority; @@ -14,6 +15,7 @@ namespace Tgstation.Server.Host.GraphQL /// Root type for GraphQL mutations. /// /// Intentionally left mostly empty, use type extensions to properly scope operations to domains. + [Authorize] [GraphQLDescription(GraphQLDescription)] public sealed class Mutation { @@ -28,6 +30,7 @@ namespace Tgstation.Server.Host.GraphQL /// The for the . /// The for the operation. /// A . + [AllowAnonymous] [Error(typeof(ErrorMessageException))] public ValueTask Login( [Service] IGraphQLAuthorityInvoker loginAuthority, @@ -45,6 +48,7 @@ namespace Tgstation.Server.Host.GraphQL /// The for the . /// The for the operation. /// An . + [AllowAnonymous] [Error(typeof(ErrorMessageException))] public ValueTask OAuthGateway( [Service] IGraphQLAuthorityInvoker loginAuthority, diff --git a/src/Tgstation.Server.Host/GraphQL/Mutations/AdministrationMutations.cs b/src/Tgstation.Server.Host/GraphQL/Mutations/AdministrationMutations.cs index 7390113a24..39d1513ca6 100644 --- a/src/Tgstation.Server.Host/GraphQL/Mutations/AdministrationMutations.cs +++ b/src/Tgstation.Server.Host/GraphQL/Mutations/AdministrationMutations.cs @@ -6,10 +6,8 @@ using HotChocolate; using HotChocolate.Types; using Tgstation.Server.Api.Models.Response; -using Tgstation.Server.Api.Rights; using Tgstation.Server.Host.Authority; using Tgstation.Server.Host.GraphQL.Scalars; -using Tgstation.Server.Host.Security; namespace Tgstation.Server.Host.GraphQL.Mutations { @@ -25,7 +23,6 @@ namespace Tgstation.Server.Host.GraphQL.Mutations /// /// The for the . /// A representing the running operation. - [TgsGraphQLAuthorize(nameof(IAdministrationAuthority.TriggerServerRestart))] [Error(typeof(ErrorMessageException))] public async ValueTask RestartServerNode( [Service] IGraphQLAuthorityInvoker administrationAuthority) @@ -44,7 +41,6 @@ namespace Tgstation.Server.Host.GraphQL.Mutations /// The for the . /// The for the operation. /// A representing the running operation. - [TgsGraphQLAuthorize(AdministrationRights.ChangeVersion)] [Error(typeof(ErrorMessageException))] public async ValueTask ChangeServerNodeVersionViaTrackedRepository( Version targetVersion, @@ -65,9 +61,8 @@ namespace Tgstation.Server.Host.GraphQL.Mutations /// The for the . /// The for the operation. /// A FileTicket that should be used to upload a zip containing the update data to the file transfer service. - [TgsGraphQLAuthorize(AdministrationRights.UploadVersion)] - [Error(typeof(ErrorMessageException))] [GraphQLType] + [Error(typeof(ErrorMessageException))] public async ValueTask ChangeServerNodeVersionViaUpload( Version targetVersion, [Service] IGraphQLAuthorityInvoker administrationAuthority, diff --git a/src/Tgstation.Server.Host/GraphQL/Mutations/UserGroupMutations.cs b/src/Tgstation.Server.Host/GraphQL/Mutations/UserGroupMutations.cs index 7f27d8ea76..912780cacb 100644 --- a/src/Tgstation.Server.Host/GraphQL/Mutations/UserGroupMutations.cs +++ b/src/Tgstation.Server.Host/GraphQL/Mutations/UserGroupMutations.cs @@ -10,7 +10,6 @@ using Tgstation.Server.Host.Authority; using Tgstation.Server.Host.GraphQL.Mutations.Payloads; using Tgstation.Server.Host.GraphQL.Types; using Tgstation.Server.Host.Models.Transformers; -using Tgstation.Server.Host.Security; namespace Tgstation.Server.Host.GraphQL.Mutations { @@ -43,7 +42,6 @@ namespace Tgstation.Server.Host.GraphQL.Mutations /// The for the . /// The for the operation. /// The created . - [TgsGraphQLAuthorize(nameof(IUserGroupAuthority.Create))] [Error(typeof(ErrorMessageException))] public ValueTask CreateUserGroup( string name, @@ -67,7 +65,6 @@ namespace Tgstation.Server.Host.GraphQL.Mutations /// The for the . /// The for the operation. /// The updated . - [TgsGraphQLAuthorize(nameof(IUserGroupAuthority.Update))] [Error(typeof(ErrorMessageException))] public ValueTask UpdateUserGroup( [ID(nameof(UserGroup))] long id, @@ -88,7 +85,6 @@ namespace Tgstation.Server.Host.GraphQL.Mutations /// The for the . /// The for the operation. /// The root. - [TgsGraphQLAuthorize(nameof(IUserGroupAuthority.DeleteEmpty))] [Error(typeof(ErrorMessageException))] public async ValueTask DeleteEmptyUserGroup( [ID(nameof(UserGroup))] long id, diff --git a/src/Tgstation.Server.Host/GraphQL/Mutations/UserMutations.cs b/src/Tgstation.Server.Host/GraphQL/Mutations/UserMutations.cs index 1653ef0165..b616be34d2 100644 --- a/src/Tgstation.Server.Host/GraphQL/Mutations/UserMutations.cs +++ b/src/Tgstation.Server.Host/GraphQL/Mutations/UserMutations.cs @@ -9,7 +9,6 @@ using HotChocolate.Types; using HotChocolate.Types.Relay; using Tgstation.Server.Api.Models.Request; -using Tgstation.Server.Api.Rights; using Tgstation.Server.Host.Authority; using Tgstation.Server.Host.GraphQL.Mutations.Payloads; using Tgstation.Server.Host.GraphQL.Types; @@ -38,9 +37,8 @@ namespace Tgstation.Server.Host.GraphQL.Mutations /// The for the . /// The for the operation. /// The created . - [TgsGraphQLAuthorize(nameof(IUserAuthority.Create))] [Error(typeof(ErrorMessageException))] - public ValueTask CreateUserByPasswordAndPermissionSet( + public ValueTask CreateUserByPasswordAndPermissionSet( string name, string password, bool? enabled, @@ -54,7 +52,7 @@ namespace Tgstation.Server.Host.GraphQL.Mutations ArgumentNullException.ThrowIfNull(password); ArgumentNullException.ThrowIfNull(userAuthority); - return userAuthority.InvokeTransformable( + return userAuthority.InvokeTransformable( authority => authority.Create( new UserCreateRequest { @@ -99,9 +97,8 @@ namespace Tgstation.Server.Host.GraphQL.Mutations /// The for the . /// The for the operation. /// The created . - [TgsGraphQLAuthorize(nameof(IUserAuthority.Create))] [Error(typeof(ErrorMessageException))] - public ValueTask CreateUserByPasswordAndGroup( + public ValueTask CreateUserByPasswordAndGroup( string name, string password, bool? enabled, @@ -115,7 +112,7 @@ namespace Tgstation.Server.Host.GraphQL.Mutations ArgumentNullException.ThrowIfNull(password); ArgumentNullException.ThrowIfNull(userAuthority); - return userAuthority.InvokeTransformable( + return userAuthority.InvokeTransformable( authority => authority.Create( new UserCreateRequest { @@ -156,9 +153,8 @@ namespace Tgstation.Server.Host.GraphQL.Mutations /// The for the . /// The for the operation. /// The created . - [TgsGraphQLAuthorize(nameof(IUserAuthority.Create))] [Error(typeof(ErrorMessageException))] - public ValueTask CreateUserByServiceConnectionAndPermissionSet( + public ValueTask CreateUserByServiceConnectionAndPermissionSet( string name, IEnumerable? oAuthConnections, IEnumerable? oidcConnections, @@ -171,7 +167,7 @@ namespace Tgstation.Server.Host.GraphQL.Mutations ArgumentNullException.ThrowIfNull(oAuthConnections); ArgumentNullException.ThrowIfNull(userAuthority); - return userAuthority.InvokeTransformable( + return userAuthority.InvokeTransformable( authority => authority.Create( new UserCreateRequest { @@ -215,9 +211,8 @@ namespace Tgstation.Server.Host.GraphQL.Mutations /// The for the . /// The for the operation. /// The created . - [TgsGraphQLAuthorize(nameof(IUserAuthority.Create))] [Error(typeof(ErrorMessageException))] - public ValueTask CreateUserByServiceConnectionAndGroup( + public ValueTask CreateUserByServiceConnectionAndGroup( string name, IEnumerable oAuthConnections, IEnumerable oidcConnections, @@ -230,7 +225,7 @@ namespace Tgstation.Server.Host.GraphQL.Mutations ArgumentNullException.ThrowIfNull(oAuthConnections); ArgumentNullException.ThrowIfNull(userAuthority); - return userAuthority.InvokeTransformable( + return userAuthority.InvokeTransformable( authority => authority.Create( new UserCreateRequest { @@ -271,9 +266,8 @@ namespace Tgstation.Server.Host.GraphQL.Mutations /// The for the . /// The for the operation. /// The created . - [TgsGraphQLAuthorize(nameof(IUserAuthority.Create))] [Error(typeof(ErrorMessageException))] - public ValueTask CreateUserBySystemIDAndPermissionSet( + public ValueTask CreateUserBySystemIDAndPermissionSet( string systemIdentifier, bool? enabled, IEnumerable? oAuthConnections, @@ -285,7 +279,7 @@ namespace Tgstation.Server.Host.GraphQL.Mutations ArgumentNullException.ThrowIfNull(systemIdentifier); ArgumentNullException.ThrowIfNull(userAuthority); - return userAuthority.InvokeTransformable( + return userAuthority.InvokeTransformable( authority => authority.Create( new UserCreateRequest { @@ -328,9 +322,8 @@ namespace Tgstation.Server.Host.GraphQL.Mutations /// The for the . /// The for the operation. /// The created . - [TgsGraphQLAuthorize(nameof(IUserAuthority.Create))] [Error(typeof(ErrorMessageException))] - public ValueTask CreateUserBySystemIDAndGroup( + public ValueTask CreateUserBySystemIDAndGroup( string systemIdentifier, bool? enabled, [ID(nameof(UserGroup))] long groupId, @@ -342,7 +335,7 @@ namespace Tgstation.Server.Host.GraphQL.Mutations ArgumentNullException.ThrowIfNull(systemIdentifier); ArgumentNullException.ThrowIfNull(userAuthority); - return userAuthority.InvokeTransformable( + return userAuthority.InvokeTransformable( authority => authority.Create( new UserCreateRequest { @@ -379,9 +372,8 @@ namespace Tgstation.Server.Host.GraphQL.Mutations /// The for the . /// The for the operation. /// The updated current . - [TgsGraphQLAuthorize(AdministrationRights.WriteUsers | AdministrationRights.EditOwnPassword)] [Error(typeof(ErrorMessageException))] - public ValueTask SetCurrentUserPassword( + public ValueTask SetCurrentUserPassword( string newPassword, [Service] IAuthenticationContext authenticationContext, [Service] IGraphQLAuthorityInvoker userAuthority, @@ -389,8 +381,8 @@ namespace Tgstation.Server.Host.GraphQL.Mutations { ArgumentNullException.ThrowIfNull(newPassword); ArgumentNullException.ThrowIfNull(userAuthority); - return userAuthority.InvokeTransformable( - async authority => await authority.Update( + return userAuthority.InvokeTransformable( + authority => authority.Update( new UserUpdateRequest { Id = authenticationContext.User.Id, @@ -408,9 +400,8 @@ namespace Tgstation.Server.Host.GraphQL.Mutations /// The for the . /// The for the operation. /// The updated current . - [TgsGraphQLAuthorize(AdministrationRights.WriteUsers | AdministrationRights.EditOwnServiceConnections)] [Error(typeof(ErrorMessageException))] - public ValueTask SetCurrentServiceConnections( + public ValueTask SetCurrentServiceConnections( IEnumerable? newOAuthConnections, IEnumerable? newOidcConnections, [Service] IAuthenticationContext authenticationContext, @@ -419,8 +410,8 @@ namespace Tgstation.Server.Host.GraphQL.Mutations { ArgumentNullException.ThrowIfNull(newOAuthConnections); ArgumentNullException.ThrowIfNull(userAuthority); - return userAuthority.InvokeTransformable( - async authority => await authority.Update( + return userAuthority.InvokeTransformable( + authority => authority.Update( new UserUpdateRequest { Id = authenticationContext.User.Id, @@ -454,9 +445,8 @@ namespace Tgstation.Server.Host.GraphQL.Mutations /// The for the . /// The for the operation. /// The updated . - [TgsGraphQLAuthorize(AdministrationRights.WriteUsers)] [Error(typeof(ErrorMessageException))] - public ValueTask UpdateUser( + public ValueTask UpdateUser( [ID(nameof(User))] long id, string? casingOnlyNameChange, string? newPassword, @@ -493,9 +483,8 @@ namespace Tgstation.Server.Host.GraphQL.Mutations /// The for the . /// The for the operation. /// The updated . - [TgsGraphQLAuthorize(AdministrationRights.WriteUsers)] [Error(typeof(ErrorMessageException))] - public ValueTask UpdateUserSetOwnedPermissionSet( + public ValueTask UpdateUserSetOwnedPermissionSet( [ID(nameof(User))] long id, string? casingOnlyNameChange, string? newPassword, @@ -533,9 +522,8 @@ namespace Tgstation.Server.Host.GraphQL.Mutations /// The for the . /// The for the operation. /// The updated . - [TgsGraphQLAuthorize(AdministrationRights.WriteUsers)] [Error(typeof(ErrorMessageException))] - public ValueTask UpdateUserSetGroup( + public ValueTask UpdateUserSetGroup( [ID(nameof(User))] long id, string? casingOnlyNameChange, string? newPassword, @@ -574,7 +562,7 @@ namespace Tgstation.Server.Host.GraphQL.Mutations /// The for the . /// The for the operation. /// The updated . - ValueTask UpdateUserCore( + ValueTask UpdateUserCore( [ID(nameof(User))] long id, string? casingOnlyNameChange, string? newPassword, @@ -585,8 +573,8 @@ namespace Tgstation.Server.Host.GraphQL.Mutations IEnumerable? newOidcConnections, IGraphQLAuthorityInvoker userAuthority, CancellationToken cancellationToken) - => userAuthority.InvokeTransformable( - async authority => await authority.Update( + => userAuthority.InvokeTransformable( + authority => authority.Update( new UserUpdateRequest { Id = id, diff --git a/src/Tgstation.Server.Host/GraphQL/Subscription.cs b/src/Tgstation.Server.Host/GraphQL/Subscription.cs index 170dceb49a..9361246dea 100644 --- a/src/Tgstation.Server.Host/GraphQL/Subscription.cs +++ b/src/Tgstation.Server.Host/GraphQL/Subscription.cs @@ -3,6 +3,7 @@ using System.Threading; using System.Threading.Tasks; using HotChocolate; +using HotChocolate.Authorization; using HotChocolate.Execution; using HotChocolate.Subscriptions; using HotChocolate.Types; @@ -16,6 +17,7 @@ namespace Tgstation.Server.Host.GraphQL /// Root type for GraphQL subscriptions. /// /// Intentionally left mostly empty, use type extensions to properly scope operations to domains. + [Authorize(ApplyPolicy.Validation)] // See https://github.com/ChilliCream/graphql-platform/issues/6259 [GraphQLDescription(GraphQLDescription)] public sealed class Subscription { @@ -63,7 +65,6 @@ namespace Tgstation.Server.Host.GraphQL /// The received from the publisher. /// The . [Subscribe(With = nameof(SessionInvalidatedStream))] - [TgsGraphQLAuthorize] public SessionInvalidationReason SessionInvalidated([EventMessage] SessionInvalidationReason sessionInvalidationReason) => sessionInvalidationReason; } diff --git a/src/Tgstation.Server.Host/GraphQL/Subscriptions/UserSubscriptions.cs b/src/Tgstation.Server.Host/GraphQL/Subscriptions/UserSubscriptions.cs index 35be564c52..2b23feba9e 100644 --- a/src/Tgstation.Server.Host/GraphQL/Subscriptions/UserSubscriptions.cs +++ b/src/Tgstation.Server.Host/GraphQL/Subscriptions/UserSubscriptions.cs @@ -8,7 +8,6 @@ using HotChocolate.Execution; using HotChocolate.Types; using HotChocolate.Types.Relay; -using Tgstation.Server.Api.Rights; using Tgstation.Server.Host.GraphQL.Types; using Tgstation.Server.Host.Security; @@ -68,7 +67,6 @@ namespace Tgstation.Server.Host.GraphQL.Subscriptions /// The received from the publisher. /// The updated . [Subscribe(With = nameof(UserUpdatedStream))] - [TgsGraphQLAuthorize(AdministrationRights.ReadUsers)] public User UserUpdated([EventMessage] User user) { ArgumentNullException.ThrowIfNull(user); @@ -98,7 +96,6 @@ namespace Tgstation.Server.Host.GraphQL.Subscriptions /// The received from the publisher. /// The updated . [Subscribe(With = nameof(CurrentUserUpdatedStream))] - [TgsGraphQLAuthorize] public User CurrentUserUpdated([EventMessage] User user) { ArgumentNullException.ThrowIfNull(user); diff --git a/src/Tgstation.Server.Host/GraphQL/Types/ChatBot.cs b/src/Tgstation.Server.Host/GraphQL/Types/ChatBot.cs new file mode 100644 index 0000000000..ad72301f53 --- /dev/null +++ b/src/Tgstation.Server.Host/GraphQL/Types/ChatBot.cs @@ -0,0 +1,29 @@ +using System; +using System.Threading.Tasks; + +using HotChocolate.Types.Relay; + +using Tgstation.Server.Api.Models; + +namespace Tgstation.Server.Host.GraphQL.Types +{ + /// + /// Represents a connection to a chat . + /// + [Node] + public sealed class ChatBot : Entity + { + /// + /// Node resolver for . + /// + /// The of the to retrieve. + /// The with if it exists, otherwise. + public static ValueTask GetChatBot(long id) + => throw new NotImplementedException(); + + /// + /// The the chat bot uses. + /// + public required ChatProvider Provider { get; init; } + } +} diff --git a/src/Tgstation.Server.Host/GraphQL/Types/Entity.cs b/src/Tgstation.Server.Host/GraphQL/Types/Entity.cs index 4f45c22306..9ee997a511 100644 --- a/src/Tgstation.Server.Host/GraphQL/Types/Entity.cs +++ b/src/Tgstation.Server.Host/GraphQL/Types/Entity.cs @@ -1,5 +1,6 @@ using System.Diagnostics.CodeAnalysis; +using HotChocolate.Data; using HotChocolate.Types.Relay; namespace Tgstation.Server.Host.GraphQL.Types @@ -13,7 +14,8 @@ namespace Tgstation.Server.Host.GraphQL.Types /// The ID of the . /// [ID] - public required long Id { get; init; } + [IsProjected(true)] + public required long Id { get; set; } /// /// Initializes a new instance of the class. diff --git a/src/Tgstation.Server.Host/GraphQL/Types/GatewayInformation.cs b/src/Tgstation.Server.Host/GraphQL/Types/GatewayInformation.cs index 9a1aee31cf..e175145516 100644 --- a/src/Tgstation.Server.Host/GraphQL/Types/GatewayInformation.cs +++ b/src/Tgstation.Server.Host/GraphQL/Types/GatewayInformation.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; using HotChocolate; +using HotChocolate.Authorization; using Microsoft.Extensions.Options; @@ -14,6 +16,7 @@ using Tgstation.Server.Host.GraphQL.Types.OAuth; using Tgstation.Server.Host.Properties; using Tgstation.Server.Host.Security; using Tgstation.Server.Host.Security.OAuth; +using Tgstation.Server.Host.Security.RightsEvaluation; using Tgstation.Server.Host.System; namespace Tgstation.Server.Host.GraphQL.Types @@ -23,70 +26,122 @@ namespace Tgstation.Server.Host.GraphQL.Types /// public sealed class GatewayInformation { + /// + /// Access the GraphQL API without auth. + /// + static Version GraphQLApiVersionNoAuth { get; } = global::System.Version.Parse(MasterVersionsAttribute.Instance.RawGraphQLVersion); + + /// + /// Gets the major GraphQL API number of the . + /// + public int MajorGraphQLApiVersion => GraphQLApiVersionNoAuth.Major; + /// /// Gets the minimum valid password length for TGS users. /// + /// The to use. /// The containing the . /// A specifying the minimumn valid password length for TGS users. - [TgsGraphQLAuthorize(AdministrationRights.WriteUsers | AdministrationRights.EditOwnPassword)] - public uint MinimumPasswordLength( + [Authorize] + public async ValueTask MinimumPasswordLength( + [Service] IAuthorizationService authorizationService, [Service] IOptionsSnapshot generalConfigurationOptions) { + ArgumentNullException.ThrowIfNull(authorizationService); ArgumentNullException.ThrowIfNull(generalConfigurationOptions); + + await authorizationService.CheckGraphQLAuthorized( + [new OrRightsConditional( + new FlagRightsConditional(AdministrationRights.WriteUsers), + new FlagRightsConditional(AdministrationRights.EditOwnPassword))], + null); + return generalConfigurationOptions.Value.MinimumPasswordLength; } /// /// Gets the maximum allowed attached instances for the . /// + /// The to use. /// The containing the . /// A specifying the maximum allowed attached instances for the . - [TgsGraphQLAuthorize(InstanceManagerRights.Create)] - public uint InstanceLimit( + [Authorize] + public async ValueTask InstanceLimit( + [Service] IAuthorizationService authorizationService, [Service] IOptionsSnapshot generalConfigurationOptions) { + ArgumentNullException.ThrowIfNull(authorizationService); ArgumentNullException.ThrowIfNull(generalConfigurationOptions); + + await authorizationService.CheckGraphQLAuthorized( + [new FlagRightsConditional(InstanceManagerRights.Create)], + null); + return generalConfigurationOptions.Value.InstanceLimit; } /// /// Gets the maximum allowed registered s for the . /// + /// The to use. /// The containing the . /// A specifying the maximum allowed registered users for the . /// This limit only applies to user creation attempts made via the current . - [TgsGraphQLAuthorize(AdministrationRights.WriteUsers)] - public uint UserLimit( + [Authorize] + public async ValueTask UserLimit( + [Service] IAuthorizationService authorizationService, [Service] IOptionsSnapshot generalConfigurationOptions) { + ArgumentNullException.ThrowIfNull(authorizationService); ArgumentNullException.ThrowIfNull(generalConfigurationOptions); + + await authorizationService.CheckGraphQLAuthorized( + [new FlagRightsConditional(AdministrationRights.WriteUsers)], + null); + return generalConfigurationOptions.Value.UserLimit; } /// /// Gets the maximum allowed registered s for the . /// + /// The to use. /// The containing the . /// A specifying the maximum allowed registered s for the . /// This limit only applies to creation attempts made via the current . - [TgsGraphQLAuthorize(AdministrationRights.WriteUsers)] - public uint UserGroupLimit( + [Authorize] + public async ValueTask UserGroupLimit( + [Service] IAuthorizationService authorizationService, [Service] IOptionsSnapshot generalConfigurationOptions) { + ArgumentNullException.ThrowIfNull(authorizationService); ArgumentNullException.ThrowIfNull(generalConfigurationOptions); + + await authorizationService.CheckGraphQLAuthorized( + [new FlagRightsConditional(AdministrationRights.WriteUsers)], + null); return generalConfigurationOptions.Value.UserGroupLimit; } /// /// Gets the locations s may be created or attached from if there are restrictions. /// + /// The to use. /// The containing the . /// The locations s may be created or attached from if there are restrictions, otherwise. - [TgsGraphQLAuthorize(InstanceManagerRights.Create | InstanceManagerRights.Relocate)] - public IReadOnlyCollection? ValidInstancePaths( + [Authorize] + public async ValueTask?> ValidInstancePaths( + [Service] IAuthorizationService authorizationService, [Service] IOptionsSnapshot generalConfigurationOptions) { + ArgumentNullException.ThrowIfNull(authorizationService); ArgumentNullException.ThrowIfNull(generalConfigurationOptions); + + await authorizationService.CheckGraphQLAuthorized( + [new OrRightsConditional( + new FlagRightsConditional(InstanceManagerRights.Create), + new FlagRightsConditional(InstanceManagerRights.Relocate))], + null); return generalConfigurationOptions.Value.ValidInstancePaths; } @@ -95,26 +150,29 @@ namespace Tgstation.Server.Host.GraphQL.Types /// /// The to use. /// if the runs on a Windows operating system, otherwise. - [TgsGraphQLAuthorize] + [Authorize] public bool WindowsHost( [Service] IPlatformIdentifier platformIdentifier) { ArgumentNullException.ThrowIfNull(platformIdentifier); + return platformIdentifier.IsWindows; } /// /// Gets the swarm protocol . /// - [TgsGraphQLAuthorize] - public Version SwarmProtocolVersion => global::System.Version.Parse(MasterVersionsAttribute.Instance.RawSwarmProtocolVersion); + /// The swarm protocol . + [Authorize] + public Version SwarmProtocolVersion() + => global::System.Version.Parse(MasterVersionsAttribute.Instance.RawSwarmProtocolVersion); /// /// Gets the of tgstation-server the is running. /// /// The to use. /// The of tgstation-server the is running. - [TgsGraphQLAuthorize] + [Authorize] public Version Version( [Service] IAssemblyInformationProvider assemblyInformationProvider) { @@ -122,28 +180,28 @@ namespace Tgstation.Server.Host.GraphQL.Types return assemblyInformationProvider.Version; } - /// - /// Gets the major GraphQL API number of the . - /// - public int MajorGraphQLApiVersion => GraphQLApiVersion.Major; - /// /// Gets the GraphQL API of the . /// - [TgsGraphQLAuthorize] - public Version GraphQLApiVersion => global::System.Version.Parse(MasterVersionsAttribute.Instance.RawGraphQLVersion); + /// The GraphQL API of the . + [Authorize] + public Version GraphQLApiVersion() + => GraphQLApiVersionNoAuth; /// /// Gets the REST API of the . /// - [TgsGraphQLAuthorize] - public Version ApiVersion => ApiHeaders.Version; + /// The REST API of the . + [Authorize] + public Version ApiVersion() => ApiHeaders.Version; /// /// Gets the DMAPI interop the uses. /// - [TgsGraphQLAuthorize] - public Version DMApiVersion => DMApiConstants.InteropVersion; + /// Yhe DMAPI interop the uses. + [Authorize] + public Version DMApiVersion() + => DMApiConstants.InteropVersion; /// /// Gets the information needed to perform open authentication with the . diff --git a/src/Tgstation.Server.Host/GraphQL/Types/InstancePermissionSet.cs b/src/Tgstation.Server.Host/GraphQL/Types/InstancePermissionSet.cs index ebe30ac072..651ce1e00c 100644 --- a/src/Tgstation.Server.Host/GraphQL/Types/InstancePermissionSet.cs +++ b/src/Tgstation.Server.Host/GraphQL/Types/InstancePermissionSet.cs @@ -19,36 +19,36 @@ namespace Tgstation.Server.Host.GraphQL.Types /// /// The of the . /// - public InstancePermissionSetRights? InstancePermissionSetRights { get; set; } + public required RightsHolder InstancePermissionSetRights { get; set; } /// /// The of the . /// - public EngineRights? EngineRights { get; set; } + public required RightsHolder EngineRights { get; set; } /// /// The of the . /// - public DreamDaemonRights? DreamDaemonRights { get; set; } + public required RightsHolder DreamDaemonRights { get; set; } /// /// The of the . /// - public DreamMakerRights? DreamMakerRights { get; set; } + public required RightsHolder DreamMakerRights { get; set; } /// /// The of the . /// - public RepositoryRights? RepositoryRights { get; set; } + public required RightsHolder RepositoryRights { get; set; } /// /// The of the . /// - public ChatBotRights? ChatBotRights { get; set; } + public required RightsHolder ChatBotRights { get; set; } /// /// The of the . /// - public ConfigurationRights? ConfigurationRights { get; set; } + public required RightsHolder ConfigurationRights { get; set; } } } diff --git a/src/Tgstation.Server.Host/GraphQL/Types/NamedEntity.cs b/src/Tgstation.Server.Host/GraphQL/Types/NamedEntity.cs index d0a36221cc..933e047b00 100644 --- a/src/Tgstation.Server.Host/GraphQL/Types/NamedEntity.cs +++ b/src/Tgstation.Server.Host/GraphQL/Types/NamedEntity.cs @@ -13,7 +13,7 @@ namespace Tgstation.Server.Host.GraphQL.Types /// /// The name of the . /// - public required string Name { get; init; } + public required string Name { get; set; } /// /// Initializes a new instance of the class. diff --git a/src/Tgstation.Server.Host/GraphQL/Types/OAuth/OAuthConnection.cs b/src/Tgstation.Server.Host/GraphQL/Types/OAuth/OAuthConnection.cs index 64374b2bb6..dc38accbb9 100644 --- a/src/Tgstation.Server.Host/GraphQL/Types/OAuth/OAuthConnection.cs +++ b/src/Tgstation.Server.Host/GraphQL/Types/OAuth/OAuthConnection.cs @@ -1,6 +1,4 @@ -using System; - -using Tgstation.Server.Api.Models; +using Tgstation.Server.Api.Models; namespace Tgstation.Server.Host.GraphQL.Types.OAuth { @@ -12,22 +10,11 @@ namespace Tgstation.Server.Host.GraphQL.Types.OAuth /// /// The of the . /// - public OAuthProvider Provider { get; } + public required OAuthProvider Provider { get; init; } /// /// The ID of the user in the . /// - public string ExternalUserId { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The value of . - /// The value of . - public OAuthConnection(string externalUserId, OAuthProvider provider) - { - ExternalUserId = externalUserId ?? throw new ArgumentNullException(nameof(externalUserId)); - Provider = provider; - } + public required string ExternalUserId { get; init; } } } diff --git a/src/Tgstation.Server.Host/GraphQL/Types/Oidc/OidcConnection.cs b/src/Tgstation.Server.Host/GraphQL/Types/Oidc/OidcConnection.cs index cf2157ed8f..8297bed4fb 100644 --- a/src/Tgstation.Server.Host/GraphQL/Types/Oidc/OidcConnection.cs +++ b/src/Tgstation.Server.Host/GraphQL/Types/Oidc/OidcConnection.cs @@ -1,6 +1,4 @@ -using System; - -namespace Tgstation.Server.Host.GraphQL.Types.OAuth +namespace Tgstation.Server.Host.GraphQL.Types.OAuth { /// /// Represents a valid OAuth connection. @@ -9,23 +7,12 @@ namespace Tgstation.Server.Host.GraphQL.Types.OAuth { /// /// The scheme key of the . - /// ] - public string SchemeKey { get; } + /// + public required string SchemeKey { get; init; } /// /// The ID of the user in the OIDC provider ("sub" claim). /// - public string ExternalUserId { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The value of . - /// The value of . - public OidcConnection(string externalUserId, string schemeKey) - { - ExternalUserId = externalUserId ?? throw new ArgumentNullException(nameof(externalUserId)); - SchemeKey = schemeKey ?? throw new ArgumentNullException(nameof(schemeKey)); - } + public required string ExternalUserId { get; init; } } } diff --git a/src/Tgstation.Server.Host/GraphQL/Types/PermissionSet.cs b/src/Tgstation.Server.Host/GraphQL/Types/PermissionSet.cs index 684beafdb8..b593d9e926 100644 --- a/src/Tgstation.Server.Host/GraphQL/Types/PermissionSet.cs +++ b/src/Tgstation.Server.Host/GraphQL/Types/PermissionSet.cs @@ -10,11 +10,11 @@ namespace Tgstation.Server.Host.GraphQL.Types /// /// The for the . /// - public required AdministrationRights AdministrationRights { get; init; } + public required RightsHolder AdministrationRights { get; set; } /// /// The for the . /// - public required InstanceManagerRights InstanceManagerRights { get; init; } + public required RightsHolder InstanceManagerRights { get; set; } } } diff --git a/src/Tgstation.Server.Host/GraphQL/Types/RemoteGateway.cs b/src/Tgstation.Server.Host/GraphQL/Types/RemoteGateway.cs deleted file mode 100644 index fcb9f088f4..0000000000 --- a/src/Tgstation.Server.Host/GraphQL/Types/RemoteGateway.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; -using System.Linq; - -using Tgstation.Server.Host.GraphQL.Interfaces; - -namespace Tgstation.Server.Host.GraphQL.Types -{ - /// - /// for accessing remote s. - /// - /// This is currently unimplemented. - public sealed class RemoteGateway : IGateway - { - /// - public GatewayInformation Information() => throw new NotImplementedException(); - - /// - public IQueryable Instances() => throw new NotImplementedException(); - } -} diff --git a/src/Tgstation.Server.Host/GraphQL/Types/RightsHolder{TRight}.cs b/src/Tgstation.Server.Host/GraphQL/Types/RightsHolder{TRight}.cs new file mode 100644 index 0000000000..2377afc9c2 --- /dev/null +++ b/src/Tgstation.Server.Host/GraphQL/Types/RightsHolder{TRight}.cs @@ -0,0 +1,26 @@ +using System; + +namespace Tgstation.Server.Host.GraphQL.Types +{ + /// + /// Holder for a given . + /// + /// The being held. + public sealed class RightsHolder + where TRight : struct, Enum + { + /// + /// Marker property to allow to be properly selected by the GraphQL system. Should not be used for any other purpose. + /// + public bool HasContextFlag + { + get => throw new InvalidOperationException($"{nameof(HasContextFlag)} getter should not be called!"); + set => throw new InvalidOperationException($"{nameof(HasContextFlag)} setter should not be called!"); + } + + /// + /// The held value. + /// + public required TRight Right { get; init; } + } +} diff --git a/src/Tgstation.Server.Host/GraphQL/Types/ServerSwarm.cs b/src/Tgstation.Server.Host/GraphQL/Types/ServerSwarm.cs index 35d82985dd..8c37141f04 100644 --- a/src/Tgstation.Server.Host/GraphQL/Types/ServerSwarm.cs +++ b/src/Tgstation.Server.Host/GraphQL/Types/ServerSwarm.cs @@ -3,13 +3,14 @@ using System.Collections.Generic; using System.Linq; using HotChocolate; +using HotChocolate.Authorization; using Microsoft.Extensions.Options; +using Tgstation.Server.Api.Models; using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.GraphQL.Interfaces; using Tgstation.Server.Host.Properties; -using Tgstation.Server.Host.Security; using Tgstation.Server.Host.Swarm; namespace Tgstation.Server.Host.GraphQL.Types @@ -20,24 +21,41 @@ namespace Tgstation.Server.Host.GraphQL.Types public sealed class ServerSwarm { /// - /// Gets the swarm protocol major version in use. + /// Access all instances in the . /// - [TgsGraphQLAuthorize] - public int ProtocolMajorVersion => Version.Parse(MasterVersionsAttribute.Instance.RawSwarmProtocolVersion).Major; + /// Queryable in the . + [Authorize] + public IQueryable Instances() + => throw new ErrorMessageException(ErrorCode.RemoteGatewaysNotImplemented); /// - /// Gets the swarm's . + /// Gets the swarm protocol major version in use. /// - /// A new . - [TgsGraphQLAuthorize] - public Users Users() => new(); + /// The swarm protocol major version in use. + [Authorize] + public int ProtocolMajorVersion() + => Version.Parse(MasterVersionsAttribute.Instance.RawSwarmProtocolVersion).Major; + + /// + /// Gets the swarm's . + /// + /// A new . + [Authorize] + public UsersRepository Users() => new(); + + /// + /// Gets the swarm's . + /// + /// A new . + [Authorize] + public UserGroupsRepository UserGroups() => new(); /// /// Gets the connected server. /// /// The containing the current . /// The to use. - /// A new for the local node if it is part of a swarm, otherwise. + /// The for the local node if it is part of a swarm, a otherwise. public IServerNode CurrentNode( [Service] IOptionsSnapshot swarmConfigurationOptions, [Service] ISwarmService swarmService) @@ -45,11 +63,12 @@ namespace Tgstation.Server.Host.GraphQL.Types ArgumentNullException.ThrowIfNull(swarmConfigurationOptions); ArgumentNullException.ThrowIfNull(swarmService); - var ourIdentifier = swarmConfigurationOptions.Value.Identifier; - if (ourIdentifier == null) + if (swarmConfigurationOptions.Value.PrivateKey == null) return new StandaloneNode(); - return (IServerNode?)SwarmNode.GetSwarmNode(ourIdentifier, swarmService) ?? new StandaloneNode(); + return ((IServerNode?)SwarmNode.GetSwarmNode( + swarmConfigurationOptions.Value.Identifier!, + swarmService)) ?? new StandaloneNode(); } /// @@ -57,7 +76,7 @@ namespace Tgstation.Server.Host.GraphQL.Types /// /// The to use. /// A of s if the local node is part of a swarm, otherwise. - [TgsGraphQLAuthorize] + [Authorize] public List? Nodes( [Service] ISwarmService swarmService) { @@ -69,7 +88,7 @@ namespace Tgstation.Server.Host.GraphQL.Types /// Gets the for the swarm. /// /// A new . - [TgsGraphQLAuthorize] + [Authorize] public UpdateInformation UpdateInformation() => new(); } } diff --git a/src/Tgstation.Server.Host/GraphQL/Types/SwarmNode.cs b/src/Tgstation.Server.Host/GraphQL/Types/SwarmNode.cs index 520f8bd6c7..e0a2068908 100644 --- a/src/Tgstation.Server.Host/GraphQL/Types/SwarmNode.cs +++ b/src/Tgstation.Server.Host/GraphQL/Types/SwarmNode.cs @@ -2,6 +2,7 @@ using System.Linq; using HotChocolate; +using HotChocolate.Authorization; using HotChocolate.Types.Relay; using Microsoft.Extensions.Options; @@ -10,7 +11,6 @@ using Tgstation.Server.Api.Models; using Tgstation.Server.Api.Models.Internal; using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.GraphQL.Interfaces; -using Tgstation.Server.Host.Security; using Tgstation.Server.Host.Swarm; namespace Tgstation.Server.Host.GraphQL.Types @@ -53,13 +53,14 @@ namespace Tgstation.Server.Host.GraphQL.Types /// The . /// The to load from. /// A new with the matching if found, otherwise. - [TgsGraphQLAuthorize] + [Authorize] public static SwarmNode? GetSwarmNode( string identifier, [Service] ISwarmService swarmService) { ArgumentNullException.ThrowIfNull(identifier); ArgumentNullException.ThrowIfNull(swarmService); + var info = swarmService .GetSwarmServers() ?.FirstOrDefault(x => x.Identifier == identifier); diff --git a/src/Tgstation.Server.Host/GraphQL/Types/UpdatedUser.cs b/src/Tgstation.Server.Host/GraphQL/Types/UpdatedUser.cs new file mode 100644 index 0000000000..41cd8959df --- /dev/null +++ b/src/Tgstation.Server.Host/GraphQL/Types/UpdatedUser.cs @@ -0,0 +1,45 @@ +using System; + +using HotChocolate.Types.Relay; + +using Tgstation.Server.Host.Models; +using Tgstation.Server.Host.Models.Transformers; + +namespace Tgstation.Server.Host.GraphQL.Types +{ + /// + /// Represents a that has been updated. + /// + public sealed class UpdatedUser + { + /// + /// The 's . + /// + [ID(nameof(Types.User))] + public long Id { get; } + + /// + /// The , if was authorized to be read. + /// + public User? User { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The value of containing the . + public UpdatedUser(Models.User user) + : this((user ?? throw new ArgumentNullException(nameof(user))).Require(u => u.Id)) + { + User = ((IApiTransformable)user).ToApi(); + } + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + public UpdatedUser(long id) + { + Id = id; + } + } +} diff --git a/src/Tgstation.Server.Host/GraphQL/Types/User.cs b/src/Tgstation.Server.Host/GraphQL/Types/User.cs index 7de3e7ca2a..1888f1b8cf 100644 --- a/src/Tgstation.Server.Host/GraphQL/Types/User.cs +++ b/src/Tgstation.Server.Host/GraphQL/Types/User.cs @@ -1,15 +1,21 @@ using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using GreenDonut; +using GreenDonut.Data; + using HotChocolate; +using HotChocolate.Data; using HotChocolate.Types.Relay; using Tgstation.Server.Host.Authority; +using Tgstation.Server.Host.Authority.Core; +using Tgstation.Server.Host.Extensions; using Tgstation.Server.Host.GraphQL.Interfaces; using Tgstation.Server.Host.GraphQL.Types.OAuth; using Tgstation.Server.Host.Models.Transformers; -using Tgstation.Server.Host.Security; namespace Tgstation.Server.Host.GraphQL.Types { @@ -39,34 +45,68 @@ namespace Tgstation.Server.Host.GraphQL.Types /// public required string? SystemIdentifier { get; init; } + /// + /// The for the user. + /// + public required UserGroup? Group { get; set; } + + /// + /// The for the user if the user does not belong to a . + /// + public required PermissionSet? OwnedPermissionSet { get; set; } + /// /// The of the . /// - [GraphQLIgnore] + [IsProjected(true)] + public required long? GroupId { get; init; } + + /// + /// The of the . + /// + [IsProjected(true)] public required long? CreatedById { get; init; } /// - /// The of the . + /// Implements the . /// - [GraphQLIgnore] - public required long? GroupId { get; init; } + /// The of s to load. + /// The for the . + /// The for mapped to an . + /// The for the operation. + /// A resulting in a of the requested s. + [DataLoader(AccessModifier = DataLoaderAccessModifier.PublicInterface)] + public static ValueTask>> GetUsers( + IReadOnlyList ids, + IGraphQLAuthorityInvoker userAuthority, + QueryContext>? queryContext, + CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(ids); + ArgumentNullException.ThrowIfNull(userAuthority); + + return userAuthority.ExecuteDataLoader( + (authority, id) => authority.GetId(id, false, cancellationToken), + ids, + queryContext); + } /// /// Node resolver for s. /// /// The to lookup. - /// The for the . + /// The to use. + /// The for the operation. /// The for the operation. /// A resulting in the queried , if present. - [TgsGraphQLAuthorize] public static ValueTask GetUser( long id, - [Service] IGraphQLAuthorityInvoker userAuthority, + [Service] IUsersDataLoader usersDataLoader, + QueryContext? queryContext, CancellationToken cancellationToken) { - ArgumentNullException.ThrowIfNull(userAuthority); - return userAuthority.InvokeTransformableAllowMissing( - authority => authority.GetId(id, false, false, cancellationToken)); + ArgumentNullException.ThrowIfNull(usersDataLoader); + return usersDataLoader.LoadAuthorityResponse(queryContext, id, cancellationToken); } /// @@ -148,39 +188,5 @@ namespace Tgstation.Server.Host.GraphQL.Types return permissionSetAuthority.InvokeTransformable( authority => authority.GetId(lookupId, lookupType, cancellationToken)); } - - /// - /// The owned by the , if any. - /// - /// The for the . - /// The for the operation. - /// A resulting in the owned by the , if any. - public ValueTask OwnedPermissionSet( - [Service] IGraphQLAuthorityInvoker permissionSetAuthority, - CancellationToken cancellationToken) - { - ArgumentNullException.ThrowIfNull(permissionSetAuthority); - - return permissionSetAuthority.InvokeTransformableAllowMissing( - authority => authority.GetId(Id, PermissionSetLookupType.UserId, cancellationToken)); - } - - /// - /// The asociated with the user, if any. - /// - /// The for the . - /// The for the operation. - /// A resulting in the associated with the , if any. - public async ValueTask Group( - [Service] IGraphQLAuthorityInvoker userGroupAuthority, - CancellationToken cancellationToken) - { - ArgumentNullException.ThrowIfNull(userGroupAuthority); - if (!GroupId.HasValue) - return null; - - return await userGroupAuthority.InvokeTransformable( - authority => authority.GetId(GroupId.Value, false, cancellationToken)); - } } } diff --git a/src/Tgstation.Server.Host/GraphQL/Types/UserGroup.cs b/src/Tgstation.Server.Host/GraphQL/Types/UserGroup.cs index 2511ab438a..d08987d818 100644 --- a/src/Tgstation.Server.Host/GraphQL/Types/UserGroup.cs +++ b/src/Tgstation.Server.Host/GraphQL/Types/UserGroup.cs @@ -10,7 +10,6 @@ using HotChocolate.Types.Relay; using Tgstation.Server.Host.Authority; using Tgstation.Server.Host.Models.Transformers; -using Tgstation.Server.Host.Security; namespace Tgstation.Server.Host.GraphQL.Types { @@ -27,7 +26,6 @@ namespace Tgstation.Server.Host.GraphQL.Types /// The for the . /// The for the operation. /// A resulting in the queried , if present. - [TgsGraphQLAuthorize] public static ValueTask GetUserGroup( long id, [Service] IGraphQLAuthorityInvoker userGroupAuthority, @@ -62,15 +60,14 @@ namespace Tgstation.Server.Host.GraphQL.Types [UsePaging] [UseFiltering] [UseSorting] - [TgsGraphQLAuthorize(nameof(IUserAuthority.Queryable))] - public IQueryable QueryableUsersByGroup( + public async ValueTask> QueryableUsersByGroup( [Service] IGraphQLAuthorityInvoker userAuthority) { ArgumentNullException.ThrowIfNull(userAuthority); - var dtoQueryable = userAuthority.InvokeTransformableQueryable( + var dtoQueryable = await userAuthority.InvokeTransformableQueryable( authority => authority - .Queryable(false) - .Where(user => user.GroupId == Id)); + .Queryable(false), + queryable => queryable.Where(user => user.GroupId == Id)); return dtoQueryable; } } diff --git a/src/Tgstation.Server.Host/GraphQL/Types/UserGroups.cs b/src/Tgstation.Server.Host/GraphQL/Types/UserGroupsRepository.cs similarity index 79% rename from src/Tgstation.Server.Host/GraphQL/Types/UserGroups.cs rename to src/Tgstation.Server.Host/GraphQL/Types/UserGroupsRepository.cs index eb9fc04a52..d5ff1b5a76 100644 --- a/src/Tgstation.Server.Host/GraphQL/Types/UserGroups.cs +++ b/src/Tgstation.Server.Host/GraphQL/Types/UserGroupsRepository.cs @@ -10,25 +10,26 @@ using HotChocolate.Types.Relay; using Tgstation.Server.Host.Authority; using Tgstation.Server.Host.Models.Transformers; -using Tgstation.Server.Host.Security; namespace Tgstation.Server.Host.GraphQL.Types { /// /// Wrapper for accessing s. /// - public sealed class UserGroups + public sealed class UserGroupsRepository { /// /// Gets the current . /// /// The for the . + /// The for the operation. /// A resulting in the current 's . public ValueTask Current( - [Service] IGraphQLAuthorityInvoker userGroupAuthority) + [Service] IGraphQLAuthorityInvoker userGroupAuthority, + CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(userGroupAuthority); - return userGroupAuthority.InvokeTransformableAllowMissing(authority => authority.Read()); + return userGroupAuthority.InvokeTransformableAllowMissing(authority => authority.Read(cancellationToken)); } /// @@ -38,7 +39,6 @@ namespace Tgstation.Server.Host.GraphQL.Types /// The for the . /// The for the operation. /// The represented by , if any. - [TgsGraphQLAuthorize(nameof(IUserGroupAuthority.GetId))] public ValueTask ById( [ID(nameof(UserGroup))] long id, [Service] IGraphQLAuthorityInvoker userGroupAuthority, @@ -53,12 +53,12 @@ namespace Tgstation.Server.Host.GraphQL.Types [UsePaging] [UseFiltering] [UseSorting] - [TgsGraphQLAuthorize(nameof(IUserGroupAuthority.Queryable))] - public IQueryable QueryableGroups( + public async ValueTask> QueryableGroups( [Service] IGraphQLAuthorityInvoker userGroupAuthority) { ArgumentNullException.ThrowIfNull(userGroupAuthority); - var dtoQueryable = userGroupAuthority.InvokeTransformableQueryable(authority => authority.Queryable(false)); + var dtoQueryable = await userGroupAuthority.InvokeTransformableQueryable( + authority => authority.Queryable(false)); return dtoQueryable; } @@ -71,16 +71,15 @@ namespace Tgstation.Server.Host.GraphQL.Types [UsePaging] [UseFiltering] [UseSorting] - [TgsGraphQLAuthorize(nameof(IUserAuthority.Queryable))] - public IQueryable QueryableUsersByGroupId( - [ID(nameof(UserGroup))]long groupId, + public async ValueTask> QueryableUsersByGroupId( + [ID(nameof(UserGroup))] long groupId, [Service] IGraphQLAuthorityInvoker userAuthority) { ArgumentNullException.ThrowIfNull(userAuthority); - var dtoQueryable = userAuthority.InvokeTransformableQueryable( + var dtoQueryable = await userAuthority.InvokeTransformableQueryable( authority => authority - .Queryable(false) - .Where(user => user.GroupId == groupId)); + .Queryable(false), + queryable => queryable.Where(user => user.GroupId == groupId)); return dtoQueryable; } } diff --git a/src/Tgstation.Server.Host/GraphQL/Types/UserName.cs b/src/Tgstation.Server.Host/GraphQL/Types/UserName.cs index 7175c2935e..9ba7b79cf1 100644 --- a/src/Tgstation.Server.Host/GraphQL/Types/UserName.cs +++ b/src/Tgstation.Server.Host/GraphQL/Types/UserName.cs @@ -9,7 +9,6 @@ using HotChocolate.Types.Relay; using Tgstation.Server.Host.Authority; using Tgstation.Server.Host.GraphQL.Interfaces; using Tgstation.Server.Host.Models.Transformers; -using Tgstation.Server.Host.Security; namespace Tgstation.Server.Host.GraphQL.Types { @@ -26,7 +25,6 @@ namespace Tgstation.Server.Host.GraphQL.Types /// The for the . /// The for the operation. /// A resulting in the queried , if present. - [TgsGraphQLAuthorize] public static ValueTask GetUserName( long id, [Service] IGraphQLAuthorityInvoker userAuthority, diff --git a/src/Tgstation.Server.Host/GraphQL/Types/Users.cs b/src/Tgstation.Server.Host/GraphQL/Types/UsersRepository.cs similarity index 70% rename from src/Tgstation.Server.Host/GraphQL/Types/Users.cs rename to src/Tgstation.Server.Host/GraphQL/Types/UsersRepository.cs index 8ec3667fe3..d735684565 100644 --- a/src/Tgstation.Server.Host/GraphQL/Types/Users.cs +++ b/src/Tgstation.Server.Host/GraphQL/Types/UsersRepository.cs @@ -3,6 +3,8 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using GreenDonut.Data; + using HotChocolate; using HotChocolate.Data; using HotChocolate.Types; @@ -13,30 +15,21 @@ using Microsoft.Extensions.Options; using Tgstation.Server.Host.Authority; using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.Models.Transformers; -using Tgstation.Server.Host.Security; - -#pragma warning disable CA1724 // conflict with GitLabApiClient.Models.Users. They can fuck off namespace Tgstation.Server.Host.GraphQL.Types { /// /// Wrapper for accessing s. /// - public sealed class Users + public sealed class UsersRepository { - /// - /// Gets the swarm's . - /// - /// A new . - public UserGroups Groups() => new(); - /// /// If only OIDC logins and registration is allowed. /// - /// The containing the . + /// The containing the . /// if OIDC strict mode is enabled, otherwise. public bool OidcStrictMode( - [Service] IOptions securityConfigurationOptions) + [Service] IOptionsSnapshot securityConfigurationOptions) { ArgumentNullException.ThrowIfNull(securityConfigurationOptions); return securityConfigurationOptions.Value.OidcStrictMode; @@ -48,7 +41,6 @@ namespace Tgstation.Server.Host.GraphQL.Types /// The for the . /// The for the operation. /// A resulting in the current . - [TgsGraphQLAuthorize(nameof(IUserAuthority.Read))] public ValueTask Current( [Service] IGraphQLAuthorityInvoker userAuthority, CancellationToken cancellationToken) @@ -61,16 +53,17 @@ namespace Tgstation.Server.Host.GraphQL.Types /// Gets a by . /// /// The of the . - /// The for the . + /// The to use. + /// The active . /// The for the operation. /// The represented by , if any. [Error(typeof(ErrorMessageException))] - [TgsGraphQLAuthorize(nameof(IUserAuthority.GetId))] public ValueTask ById( [ID(nameof(User))] long id, - [Service] IGraphQLAuthorityInvoker userAuthority, + [Service] IUsersDataLoader usersDataLoader, + QueryContext? queryContext, CancellationToken cancellationToken) - => User.GetUser(id, userAuthority, cancellationToken); + => User.GetUser(id, usersDataLoader, queryContext, cancellationToken); /// /// Queries all registered s. @@ -80,12 +73,12 @@ namespace Tgstation.Server.Host.GraphQL.Types [UsePaging] [UseFiltering] [UseSorting] - [TgsGraphQLAuthorize(nameof(IUserAuthority.Queryable))] - public IQueryable QueryableUsers( + public async ValueTask> QueryableUsers( [Service] IGraphQLAuthorityInvoker userAuthority) { ArgumentNullException.ThrowIfNull(userAuthority); - var dtoQueryable = userAuthority.InvokeTransformableQueryable(authority => authority.Queryable(false)); + var dtoQueryable = await userAuthority.InvokeTransformableQueryable( + authority => authority.Queryable(false)); return dtoQueryable; } } diff --git a/src/Tgstation.Server.Host/IO/DefaultIOManager.cs b/src/Tgstation.Server.Host/IO/DefaultIOManager.cs index eaf2b27736..5e30e756df 100644 --- a/src/Tgstation.Server.Host/IO/DefaultIOManager.cs +++ b/src/Tgstation.Server.Host/IO/DefaultIOManager.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using Tgstation.Server.Host.Jobs; using Tgstation.Server.Host.Utils; namespace Tgstation.Server.Host.IO @@ -253,8 +254,18 @@ namespace Tgstation.Server.Host.IO => ResolvePath(CurrentDirectory); /// - public virtual string ResolvePath(string path) - => fileSystem.Path.GetFullPath(path ?? throw new ArgumentNullException(nameof(path))); + public string ResolvePath(string path) + { + if (fileSystem.Path.IsPathRooted(path ?? throw new ArgumentNullException(nameof(path)))) + { + // Important to evaluate the path anyway to normalize front slashes to backslashes on Windows + // Some tools (looking at you netsh.exe) bitch if you pass them forward slashes as directory separators + // Can't rely on ResolvePathCore to do this either because its contract stipulates a relative path + return fileSystem.Path.GetFullPath(path); + } + + return ResolvePathCore(path); + } /// public async ValueTask WriteAllBytes(string path, byte[] contents, CancellationToken cancellationToken) @@ -330,26 +341,46 @@ namespace Tgstation.Server.Host.IO TaskScheduler.Current); /// - public Task ZipToDirectory(string path, Stream zipFile, CancellationToken cancellationToken) => Task.Factory.StartNew( - () => - { - path = ResolvePath(path); - ArgumentNullException.ThrowIfNull(zipFile); + public async ValueTask ZipToDirectory(string path, Stream zipFile, CancellationToken cancellationToken) + { + path = ResolvePath(path); + ArgumentNullException.ThrowIfNull(zipFile); #if NET9_0_OR_GREATER #error Check if zip file seeking has been addressesed. See https://github.com/tgstation/tgstation-server/issues/1531 #endif - // ZipArchive does a synchronous copy on unseekable streams we want to avoid - if (!zipFile.CanSeek) - throw new ArgumentException("Stream does not support seeking!", nameof(zipFile)); + // ZipArchive does a synchronous copy on unseekable streams we want to avoid + if (!zipFile.CanSeek) + throw new ArgumentException("Stream does not support seeking!", nameof(zipFile)); - using var archive = new ZipArchive(zipFile, ZipArchiveMode.Read, true); - archive.ExtractToDirectory(path); - }, - cancellationToken, - BlockingTaskCreationOptions, - TaskScheduler.Current); + using var archive = new ZipArchive(zipFile, ZipArchiveMode.Read, true); + + // start async context + await Task.Yield(); + foreach (var entry in archive.Entries) + { + var entryPath = fileSystem.Path.Combine(path, entry.FullName); + + if (string.IsNullOrEmpty(entry.Name)) + { + fileSystem.Directory.CreateDirectory(entryPath); + continue; + } + + var directoryPath = fileSystem.Path.GetDirectoryName(entryPath); + if (directoryPath == null) + { + throw new JobException("Zip archive concatenation resulted in a null directory path!"); + } + + fileSystem.Directory.CreateDirectory(directoryPath); + + using var entryStream = entry.Open(); + using var outputStream = fileSystem.File.Create(entryPath); + await entryStream.CopyToAsync(outputStream, cancellationToken); + } + } /// public bool PathContainsParentAccess(string path) => path @@ -417,7 +448,7 @@ namespace Tgstation.Server.Host.IO { ArgumentNullException.ThrowIfNull(subdirectoryPath); - if (!Path.IsPathRooted(subdirectoryPath)) + if (!fileSystem.Path.IsPathRooted(subdirectoryPath)) subdirectoryPath = ConcatPath( ResolvePath(), subdirectoryPath); @@ -427,6 +458,14 @@ namespace Tgstation.Server.Host.IO subdirectoryPath); } + /// + /// Resolve a given, non-rooted, . + /// + /// The non-rooted path to resolve. + /// The fully resolved path. + protected virtual string ResolvePathCore(string path) + => fileSystem.Path.GetFullPath(path ?? throw new ArgumentNullException(nameof(path))); + /// /// Copies a directory from to . /// diff --git a/src/Tgstation.Server.Host/IO/FileDownloader.cs b/src/Tgstation.Server.Host/IO/FileDownloader.cs index 30efdb6718..10b148136f 100644 --- a/src/Tgstation.Server.Host/IO/FileDownloader.cs +++ b/src/Tgstation.Server.Host/IO/FileDownloader.cs @@ -5,7 +5,6 @@ using System.Net.Http.Headers; using Microsoft.Extensions.Logging; using Tgstation.Server.Api; -using Tgstation.Server.Common.Http; namespace Tgstation.Server.Host.IO { @@ -13,9 +12,9 @@ namespace Tgstation.Server.Host.IO public sealed class FileDownloader : IFileDownloader { /// - /// The for the . + /// The for the . /// - readonly IAbstractHttpClientFactory httpClientFactory; + readonly IHttpClientFactory httpClientFactory; /// /// The for the . @@ -27,7 +26,7 @@ namespace Tgstation.Server.Host.IO /// /// The value of . /// The value of . - public FileDownloader(IAbstractHttpClientFactory httpClientFactory, ILogger logger) + public FileDownloader(IHttpClientFactory httpClientFactory, ILogger logger) { this.httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); diff --git a/src/Tgstation.Server.Host/IO/IIOManager.cs b/src/Tgstation.Server.Host/IO/IIOManager.cs index 3b81869fb8..6d505faec0 100644 --- a/src/Tgstation.Server.Host/IO/IIOManager.cs +++ b/src/Tgstation.Server.Host/IO/IIOManager.cs @@ -238,8 +238,8 @@ namespace Tgstation.Server.Host.IO /// The path to unzip to. /// The of the . Must have set to . Will be read completely and left open. will be indeterminate. /// The for the operation. - /// A representing the running operation. - Task ZipToDirectory(string path, Stream zipFile, CancellationToken cancellationToken); + /// A representing the running operation. + ValueTask ZipToDirectory(string path, Stream zipFile, CancellationToken cancellationToken); /// /// Get the of when a given was last modified. diff --git a/src/Tgstation.Server.Host/IO/RequestFileStreamProvider.cs b/src/Tgstation.Server.Host/IO/RequestFileStreamProvider.cs index 12461e4e7f..ef82a2bf53 100644 --- a/src/Tgstation.Server.Host/IO/RequestFileStreamProvider.cs +++ b/src/Tgstation.Server.Host/IO/RequestFileStreamProvider.cs @@ -14,9 +14,9 @@ namespace Tgstation.Server.Host.IO sealed class RequestFileStreamProvider : IFileStreamProvider { /// - /// The for the . + /// The for the . /// - readonly IHttpClient httpClient; + readonly HttpClient httpClient; /// /// The for the . @@ -43,7 +43,7 @@ namespace Tgstation.Server.Host.IO /// /// The value of . /// The value of . - public RequestFileStreamProvider(IHttpClient httpClient, HttpRequestMessage requestMessage) + public RequestFileStreamProvider(HttpClient httpClient, HttpRequestMessage requestMessage) { this.httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); this.requestMessage = requestMessage ?? throw new ArgumentNullException(nameof(requestMessage)); diff --git a/src/Tgstation.Server.Host/IO/ResolvingIOManager.cs b/src/Tgstation.Server.Host/IO/ResolvingIOManager.cs index 625d7da86e..0be02cd2e2 100644 --- a/src/Tgstation.Server.Host/IO/ResolvingIOManager.cs +++ b/src/Tgstation.Server.Host/IO/ResolvingIOManager.cs @@ -27,11 +27,8 @@ namespace Tgstation.Server.Host.IO } /// - public override string ResolvePath(string path) - { - if (!IsPathRooted(path)) - return base.ResolvePath(ConcatPath(subdirectory, path)); - return path; - } + protected override string ResolvePathCore(string path) + => base.ResolvePathCore( + ConcatPath(subdirectory, path)); } } diff --git a/src/Tgstation.Server.Host/Jobs/JobService.cs b/src/Tgstation.Server.Host/Jobs/JobService.cs index c36b5ca825..4c1f5e2c90 100644 --- a/src/Tgstation.Server.Host/Jobs/JobService.cs +++ b/src/Tgstation.Server.Host/Jobs/JobService.cs @@ -213,7 +213,6 @@ namespace Tgstation.Server.Host.Jobs // mark all jobs as cancelled var badJobIds = await databaseContext .Jobs - .AsQueryable() .Where(y => !y.StoppedAt.HasValue) .Select(y => y.Id!.Value) .ToListAsync(cancellationToken); @@ -535,7 +534,6 @@ namespace Tgstation.Server.Host.Jobs // DCT: Cancellation token is for job, operation should always run var finalJob = await databaseContext .Jobs - .AsQueryable() .Include(x => x.Instance) .Include(x => x.StartedBy) .Include(x => x.CancelledBy) diff --git a/src/Tgstation.Server.Host/Jobs/JobsHubGroupMapper.cs b/src/Tgstation.Server.Host/Jobs/JobsHubGroupMapper.cs index a961fbaef9..02fb87e77d 100644 --- a/src/Tgstation.Server.Host/Jobs/JobsHubGroupMapper.cs +++ b/src/Tgstation.Server.Host/Jobs/JobsHubGroupMapper.cs @@ -128,7 +128,6 @@ namespace Tgstation.Server.Host.Jobs async databaseContext => permedInstanceIds = await databaseContext .InstancePermissionSets - .AsQueryable() .Where(ips => ips.PermissionSetId == pid) .Select(ips => ips.InstanceId) .ToListAsync(cancellationToken)); @@ -162,7 +161,6 @@ namespace Tgstation.Server.Host.Jobs .ToListAsync(cancellationToken); var permissionSetAccessibleInstanceIds = await databaseContext .InstancePermissionSets - .AsQueryable() .Where(ips => ips.PermissionSetId == permissionSetId) .Select(ips => ips.InstanceId) .ToListAsync(cancellationToken); diff --git a/src/Tgstation.Server.Host/Models/ITransformer{TInput,TOutput}.cs b/src/Tgstation.Server.Host/Models/ITransformer{TInput,TOutput}.cs index 9da15bcfb7..a845c8e78d 100644 --- a/src/Tgstation.Server.Host/Models/ITransformer{TInput,TOutput}.cs +++ b/src/Tgstation.Server.Host/Models/ITransformer{TInput,TOutput}.cs @@ -1,6 +1,8 @@ using System; using System.Linq.Expressions; +using Tgstation.Server.Host.Authority.Core; + namespace Tgstation.Server.Host.Models { /// @@ -15,6 +17,11 @@ namespace Tgstation.Server.Host.Models /// Expression> Expression { get; } + /// + /// for mapping into a . + /// + Expression>> ProjectedExpression { get; } + /// /// The compiled . /// diff --git a/src/Tgstation.Server.Host/Models/Transformers/PermissionSetGraphQLTransformer.cs b/src/Tgstation.Server.Host/Models/Transformers/PermissionSetGraphQLTransformer.cs index 49f70f462f..0c4d044e1c 100644 --- a/src/Tgstation.Server.Host/Models/Transformers/PermissionSetGraphQLTransformer.cs +++ b/src/Tgstation.Server.Host/Models/Transformers/PermissionSetGraphQLTransformer.cs @@ -1,4 +1,7 @@ -namespace Tgstation.Server.Host.Models.Transformers +using Tgstation.Server.Api.Rights; +using Tgstation.Server.Host.GraphQL.Types; + +namespace Tgstation.Server.Host.Models.Transformers { /// /// for s. @@ -9,11 +12,21 @@ /// Initializes a new instance of the class. /// public PermissionSetGraphQLTransformer() - : base(model => new GraphQL.Types.PermissionSet - { - AdministrationRights = model.AdministrationRights!.Value, - InstanceManagerRights = model.InstanceManagerRights!.Value, - }) + : base( + BuildSubProjection< + InstanceManagerRights?, + AdministrationRights?, + RightsHolder, + RightsHolder, + RightsHolderGraphQLTransformer, + RightsHolderGraphQLTransformer>( + (model, instanceManagerRights, administrationRights) => new GraphQL.Types.PermissionSet + { + AdministrationRights = administrationRights ?? NotNullFallback>(), + InstanceManagerRights = instanceManagerRights ?? NotNullFallback>(), + }, + model => model.InstanceManagerRights ?? NotNullFallback(), + model => model.AdministrationRights ?? NotNullFallback())) { } } diff --git a/src/Tgstation.Server.Host/Models/Transformers/RightsHolderGraphQLTransformer{TRight}.cs b/src/Tgstation.Server.Host/Models/Transformers/RightsHolderGraphQLTransformer{TRight}.cs new file mode 100644 index 0000000000..a46cbfdd03 --- /dev/null +++ b/src/Tgstation.Server.Host/Models/Transformers/RightsHolderGraphQLTransformer{TRight}.cs @@ -0,0 +1,25 @@ +using System; + +using Tgstation.Server.Host.GraphQL.Types; + +namespace Tgstation.Server.Host.Models.Transformers +{ + /// + /// for s. + /// + /// The of the . + sealed class RightsHolderGraphQLTransformer : TransformerBase> + where TRight : struct, Enum + { + /// + /// Initializes a new instance of the class. + /// + public RightsHolderGraphQLTransformer() + : base(right => new RightsHolder + { + Right = right ?? NotNullFallback(), + }) + { + } + } +} diff --git a/src/Tgstation.Server.Host/Models/Transformers/TransformerBase{TInput,TOutput}.cs b/src/Tgstation.Server.Host/Models/Transformers/TransformerBase{TInput,TOutput}.cs index e3d16441cc..318164c617 100644 --- a/src/Tgstation.Server.Host/Models/Transformers/TransformerBase{TInput,TOutput}.cs +++ b/src/Tgstation.Server.Host/Models/Transformers/TransformerBase{TInput,TOutput}.cs @@ -1,6 +1,9 @@ using System; using System.Linq.Expressions; +using Tgstation.Server.Host.Authority.Core; +using Tgstation.Server.Host.Extensions; + namespace Tgstation.Server.Host.Models.Transformers { /// @@ -9,14 +12,131 @@ namespace Tgstation.Server.Host.Models.Transformers /// /// cache for . /// - static Func? compiledExpression; + static Func? compiledExpression; // This is safe https://stackoverflow.com/a/9647661/3976486 + + /// + /// cache for . + /// + static Expression>>? projectedExpression; /// public Expression> Expression { get; } + /// + public Expression>> ProjectedExpression { get; } + /// public Func CompiledExpression { get; } + /// + /// Gets the that should be used when a database projected non-null DTO value is null in the expression. + /// + /// The non-null a fallback is required for. + /// A fallback value. + protected static T NotNullFallback() + where T : notnull + => default!; + + /// + /// Build an for to when contains a with its own . + /// + /// The field in that needs transforming. + /// The transformed of . + /// The for /. + /// The to take a and and produce a . + /// The to select from . + /// An expression converting into based on with its other arguments generated from the transformation result of . + protected static Expression> BuildSubProjection( + Expression> transformerExpression, + Expression> subInputSelectionExpression) + where TSubOutput : class + where TTransformer : ITransformer, new() + { + var subTransformer = new TTransformer(); + + var primaryInput = global::System.Linq.Expressions.Expression.Parameter(typeof(TInput), "input"); + var subInputExpression = global::System.Linq.Expressions.Expression.Invoke(subInputSelectionExpression, primaryInput); + + var notNullExpression = global::System.Linq.Expressions.Expression.MakeBinary( + ExpressionType.NotEqual, + subInputExpression, + global::System.Linq.Expressions.Expression.Constant(null, typeof(TSubInput))); + + var subOutputExpression = global::System.Linq.Expressions.Expression.Invoke(subTransformer.Expression, subInputExpression); + + var conditionalSubOutputExpression = global::System.Linq.Expressions.Expression.Condition( + notNullExpression, + subOutputExpression, + global::System.Linq.Expressions.Expression.Constant(null, typeof(TSubOutput))); + + var outputExpression = global::System.Linq.Expressions.Expression.Invoke(transformerExpression, primaryInput, conditionalSubOutputExpression); + + return global::System.Linq.Expressions.Expression.Lambda>(outputExpression, primaryInput); + } + + /// + /// Build an for to when contains two sub-inputs with their own s. + /// + /// The first field in that needs transforming. + /// The second field in that needs transforming. + /// The first transformed of . + /// The second transformed of . + /// The for /. + /// The for /. + /// The to take a , , and and produce a . + /// The to select from . + /// The to select from . + /// An expression converting into based on with its other arguments generated from the transformation result of and . + protected static Expression> BuildSubProjection< + TSubInput1, + TSubInput2, + TSubOutput1, + TSubOutput2, + TTransformer1, + TTransformer2>( + Expression> transformerExpression, + Expression> subInput1SelectionExpression, + Expression> subInput2SelectionExpression) + where TSubOutput1 : class + where TSubOutput2 : class + where TTransformer1 : ITransformer, new() + where TTransformer2 : ITransformer, new() + { + var subTransformer1 = new TTransformer1(); + var subTransformer2 = new TTransformer2(); + + var primaryInput = global::System.Linq.Expressions.Expression.Parameter(typeof(TInput), "input"); + + var subInput1Expression = global::System.Linq.Expressions.Expression.Invoke(subInput1SelectionExpression, primaryInput); + var subInput2Expression = global::System.Linq.Expressions.Expression.Invoke(subInput2SelectionExpression, primaryInput); + + var notNullExpression1 = global::System.Linq.Expressions.Expression.MakeBinary( + ExpressionType.NotEqual, + subInput1Expression, + global::System.Linq.Expressions.Expression.Constant(null, typeof(TSubInput1))); + var notNullExpression2 = global::System.Linq.Expressions.Expression.MakeBinary( + ExpressionType.NotEqual, + subInput2Expression, + global::System.Linq.Expressions.Expression.Constant(null, typeof(TSubInput2))); + + var subOutput1Expression = global::System.Linq.Expressions.Expression.Invoke(subTransformer1.Expression, subInput1Expression); + var subOutput2Expression = global::System.Linq.Expressions.Expression.Invoke(subTransformer2.Expression, subInput2Expression); + + var conditionalSubOutput1Expression = global::System.Linq.Expressions.Expression.Condition( + notNullExpression1, + subOutput1Expression, + global::System.Linq.Expressions.Expression.Constant(null, typeof(TSubOutput1))); + + var conditionalSubOutput2Expression = global::System.Linq.Expressions.Expression.Condition( + notNullExpression2, + subOutput2Expression, + global::System.Linq.Expressions.Expression.Constant(null, typeof(TSubOutput2))); + + var outputExpression = global::System.Linq.Expressions.Expression.Invoke(transformerExpression, primaryInput, conditionalSubOutput1Expression, conditionalSubOutput2Expression); + + return global::System.Linq.Expressions.Expression.Lambda>(outputExpression, primaryInput); + } + /// /// Initializes a new instance of the class. /// @@ -25,8 +145,10 @@ namespace Tgstation.Server.Host.Models.Transformers Expression> expression) { compiledExpression ??= expression.Compile(); + projectedExpression ??= expression.Projected(); Expression = expression; CompiledExpression = compiledExpression; + ProjectedExpression = projectedExpression; } } } diff --git a/src/Tgstation.Server.Host/Models/Transformers/UpdatedUserGraphQLTransformer.cs b/src/Tgstation.Server.Host/Models/Transformers/UpdatedUserGraphQLTransformer.cs new file mode 100644 index 0000000000..89ee7a93de --- /dev/null +++ b/src/Tgstation.Server.Host/Models/Transformers/UpdatedUserGraphQLTransformer.cs @@ -0,0 +1,18 @@ +namespace Tgstation.Server.Host.Models.Transformers +{ + /// + /// for s. + /// + sealed class UpdatedUserGraphQLTransformer : TransformerBase + { + /// + /// Initializes a new instance of the class. + /// + public UpdatedUserGraphQLTransformer() + : base(model => model.User != null + ? new GraphQL.Types.UpdatedUser(model.User) + : new GraphQL.Types.UpdatedUser(model.Id)) + { + } + } +} diff --git a/src/Tgstation.Server.Host/Models/Transformers/UserGraphQLTransformer.cs b/src/Tgstation.Server.Host/Models/Transformers/UserGraphQLTransformer.cs index 9c1184ecce..2a3656cc98 100644 --- a/src/Tgstation.Server.Host/Models/Transformers/UserGraphQLTransformer.cs +++ b/src/Tgstation.Server.Host/Models/Transformers/UserGraphQLTransformer.cs @@ -1,4 +1,6 @@ -namespace Tgstation.Server.Host.Models.Transformers +using System; + +namespace Tgstation.Server.Host.Models.Transformers { /// /// for s. @@ -9,17 +11,29 @@ /// Initializes a new instance of the class. /// public UserGraphQLTransformer() - : base(model => new GraphQL.Types.User - { - CreatedAt = model.CreatedAt!.Value, - CanonicalName = model.CanonicalName!, - CreatedById = model.CreatedById, - Enabled = model.Enabled!.Value, - GroupId = model.GroupId, - Id = model.Id!.Value, - Name = model.Name!, - SystemIdentifier = model.SystemIdentifier, - }) + : base( + BuildSubProjection< + UserGroup, + PermissionSet, + GraphQL.Types.UserGroup, + GraphQL.Types.PermissionSet, + UserGroupGraphQLTransformer, + PermissionSetGraphQLTransformer>( + (model, group, permissionSet) => new GraphQL.Types.User + { + CreatedAt = model.CreatedAt ?? NotNullFallback(), + CanonicalName = model.CanonicalName ?? NotNullFallback(), + CreatedById = model.CreatedById, + Enabled = model.Enabled ?? NotNullFallback(), + GroupId = model.GroupId, + Id = model.Id!.Value, + Name = model.Name ?? NotNullFallback(), + SystemIdentifier = model.SystemIdentifier, + OwnedPermissionSet = permissionSet, + Group = group, + }, + model => model.Group, + model => model.PermissionSet)) { } } diff --git a/src/Tgstation.Server.Host/Models/UpdatedUser.cs b/src/Tgstation.Server.Host/Models/UpdatedUser.cs new file mode 100644 index 0000000000..ed7c6e2b6c --- /dev/null +++ b/src/Tgstation.Server.Host/Models/UpdatedUser.cs @@ -0,0 +1,51 @@ +using System; + +using Tgstation.Server.Api.Models.Response; +using Tgstation.Server.Host.Models.Transformers; + +namespace Tgstation.Server.Host.Models +{ + /// + /// Represents a that has been updated. + /// + public sealed class UpdatedUser : + ILegacyApiTransformable, + IApiTransformable + { + /// + /// The 's . + /// + public long Id { get; } + + /// + /// The , if it authorized to be read. + /// + public User? User { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The value of containing the . + public UpdatedUser(User user) + : this((user ?? throw new ArgumentNullException(nameof(user))).Require(u => u.Id)) + { + User = user; + } + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + public UpdatedUser(long id) + { + Id = id; + } + + /// + public UserResponse ToApi() + => User?.ToApi() ?? new UserResponse + { + Id = Id, + }; + } +} diff --git a/src/Tgstation.Server.Host/Properties/TelemetryAppSerializedKeyAttribute.cs b/src/Tgstation.Server.Host/Properties/TelemetryAppSerializedKeyAttribute.cs deleted file mode 100644 index 6d2ff1be5b..0000000000 --- a/src/Tgstation.Server.Host/Properties/TelemetryAppSerializedKeyAttribute.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.Reflection; - -namespace Tgstation.Server.Host.Properties -{ - /// - /// Attribute for bundling the GitHub App serialized private key used for version telemetry. - /// - [AttributeUsage(AttributeTargets.Assembly)] - sealed class TelemetryAppSerializedKeyAttribute : Attribute - { - /// - /// Return the 's instance of the . - /// - public static TelemetryAppSerializedKeyAttribute? Instance => Assembly - .GetExecutingAssembly() - .GetCustomAttribute(); - - /// - /// The serialized GitHub App Client ID and private key. - /// - public string SerializedKey { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The value of . - public TelemetryAppSerializedKeyAttribute(string serializedKey) - { - SerializedKey = serializedKey ?? throw new ArgumentNullException(nameof(serializedKey)); - } - } -} diff --git a/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs b/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs index 7e22505e43..a57bf7795b 100644 --- a/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs +++ b/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using System.Security.Claims; using System.Threading; @@ -12,7 +11,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.JsonWebTokens; -using Microsoft.IdentityModel.Tokens; using Tgstation.Server.Api; using Tgstation.Server.Api.Rights; @@ -47,21 +45,21 @@ namespace Tgstation.Server.Host.Security /// readonly IIdentityCache identityCache; + /// + /// The of for the . + /// + readonly IOptions swarmConfigurationOptions; + + /// + /// The of for the . + /// + readonly IOptionsSnapshot securityConfigurationOptions; + /// /// The for the . /// readonly ILogger logger; - /// - /// The for the . - /// - readonly SwarmConfiguration swarmConfiguration; - - /// - /// The for the . - /// - readonly SecurityConfiguration securityConfiguration; - /// /// Backing field for . /// @@ -77,45 +75,21 @@ namespace Tgstation.Server.Host.Security /// int initialized; - /// - /// Parse a out of a in a given . - /// - /// The containing claims. - /// The name to parse from. - /// The parsed . - static DateTimeOffset ParseTime(ClaimsPrincipal principal, string key) - { - var claim = principal.FindFirst(key); - if (claim == default) - throw new InvalidOperationException($"Missing '{key}' claim!"); - - try - { - return new DateTimeOffset( - EpochTime.DateTime( - Int64.Parse(claim.Value, CultureInfo.InvariantCulture))); - } - catch (Exception ex) - { - throw new InvalidOperationException($"Failed to parse '{key}'!", ex); - } - } - /// /// Initializes a new instance of the class. /// /// The value of . /// The value of . /// The containing the value of . - /// The containing the value of . - /// The containing the value of . + /// The value of . + /// The value of . /// The value of . public AuthenticationContextFactory( IDatabaseContext databaseContext, IIdentityCache identityCache, IApiHeadersProvider apiHeadersProvider, IOptions swarmConfigurationOptions, - IOptions securityConfigurationOptions, + IOptionsSnapshot securityConfigurationOptions, ILogger logger) { this.databaseContext = databaseContext ?? throw new ArgumentNullException(nameof(databaseContext)); @@ -123,8 +97,8 @@ namespace Tgstation.Server.Host.Security ArgumentNullException.ThrowIfNull(apiHeadersProvider); apiHeaders = apiHeadersProvider.ApiHeaders; - swarmConfiguration = swarmConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(swarmConfigurationOptions)); - securityConfiguration = securityConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(securityConfigurationOptions)); + this.swarmConfigurationOptions = swarmConfigurationOptions ?? throw new ArgumentNullException(nameof(swarmConfigurationOptions)); + this.securityConfigurationOptions = securityConfigurationOptions ?? throw new ArgumentNullException(nameof(securityConfigurationOptions)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); @@ -148,27 +122,13 @@ namespace Tgstation.Server.Host.Security throw new InvalidOperationException("Authentication context has already been loaded"); var principal = new ClaimsPrincipal(new ClaimsIdentity(jwt.Claims)); + var userId = principal.GetTgsUserId(); - var userIdClaim = principal.FindFirst(JwtRegisteredClaimNames.Sub); - if (userIdClaim == default) - throw new InvalidOperationException($"Missing '{JwtRegisteredClaimNames.Sub}' claim!"); - - long userId; - try - { - userId = Int64.Parse(userIdClaim.Value, CultureInfo.InvariantCulture); - } - catch (Exception e) - { - throw new InvalidOperationException("Failed to parse user ID!", e); - } - - var notBefore = ParseTime(principal, JwtRegisteredClaimNames.Nbf); - var expires = ParseTime(principal, JwtRegisteredClaimNames.Exp); + var notBefore = principal.ParseTime(JwtRegisteredClaimNames.Nbf); + var expires = principal.ParseTime(JwtRegisteredClaimNames.Exp); var user = await databaseContext .Users - .AsQueryable() .Where(x => x.Id == userId) .Include(x => x.CreatedBy) .Include(x => x.PermissionSet) @@ -204,9 +164,9 @@ namespace Tgstation.Server.Host.Security var instanceId = apiHeaders?.InstanceId; if (instanceId.HasValue) { - instancePermissionSet = await databaseContext.InstancePermissionSets - .AsQueryable() - .Where(x => x.PermissionSetId == userPermissionSet!.Id && x.InstanceId == instanceId && x.Instance!.SwarmIdentifer == swarmConfiguration.Identifier) + instancePermissionSet = await databaseContext + .InstancePermissionSets + .Where(x => x.PermissionSetId == userPermissionSet!.Id && x.InstanceId == instanceId && x.Instance!.SwarmIdentifer == swarmConfigurationOptions.Value.Identifier) .Include(x => x.Instance) .FirstOrDefaultAsync(cancellationToken); @@ -248,7 +208,6 @@ namespace Tgstation.Server.Host.Security var deprefixedScheme = scheme.Substring(OpenIDConnectAuthenticationSchemePrefix.Length); var connection = await databaseContext .OidcConnections - .AsQueryable() .Where(oidcConnection => oidcConnection.ExternalUserId == userId && oidcConnection.SchemeKey == deprefixedScheme) .Include(oidcConnection => oidcConnection.User) .ThenInclude(user => user!.Group) @@ -256,7 +215,7 @@ namespace Tgstation.Server.Host.Security .FirstOrDefaultAsync(cancellationToken); User user; - if (!securityConfiguration.OidcStrictMode) + if (!securityConfigurationOptions.Value.OidcStrictMode) { if (connection == default) { @@ -283,7 +242,6 @@ namespace Tgstation.Server.Host.Security UserGroup? group = groupId.HasValue ? await databaseContext .Groups - .AsQueryable() .Where(group => group.Id == groupId.Value) .Include(group => group.PermissionSet) .FirstOrDefaultAsync(cancellationToken) @@ -377,7 +335,7 @@ namespace Tgstation.Server.Host.Security await databaseContext.Save(cancellationToken); } - var expires = ParseTime(principal, JwtRegisteredClaimNames.Exp); + var expires = principal.ParseTime(JwtRegisteredClaimNames.Exp); currentAuthenticationContext.Initialize( user, diff --git a/src/Tgstation.Server.Host/Security/AuthorizationHandler.cs b/src/Tgstation.Server.Host/Security/AuthorizationHandler.cs new file mode 100644 index 0000000000..95774ee803 --- /dev/null +++ b/src/Tgstation.Server.Host/Security/AuthorizationHandler.cs @@ -0,0 +1,223 @@ +using System; +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.AspNetCore.Authorization; +using Microsoft.EntityFrameworkCore; + +using Tgstation.Server.Api.Rights; +using Tgstation.Server.Common.Extensions; +using Tgstation.Server.Host.Database; +using Tgstation.Server.Host.Extensions; +using Tgstation.Server.Host.Models; +using Tgstation.Server.Host.Security.RightsEvaluation; +using Tgstation.Server.Host.Utils; + +namespace Tgstation.Server.Host.Security +{ + /// + /// for s and s. + /// + public sealed class AuthorizationHandler : IAuthorizationHandler + { + /// + /// The for the . + /// + readonly IDatabaseContextFactory databaseContextFactory; + + /// + /// The for the . + /// + readonly IApiHeadersProvider apiHeadersProvider; + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + /// The value of . + public AuthorizationHandler(IDatabaseContextFactory databaseContextFactory, IApiHeadersProvider apiHeadersProvider) + { + this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory)); + this.apiHeadersProvider = apiHeadersProvider ?? throw new ArgumentNullException(nameof(apiHeadersProvider)); + } + + /// + public Task HandleAsync(AuthorizationHandlerContext context) + { + // https://github.com/dotnet/aspnetcore/issues/56272 + CancellationToken cancellationToken = CancellationToken.None; + + ArgumentNullException.ThrowIfNull(context); + + // all the requirements we process require authentication + if (context.User.Identity?.IsAuthenticated != true) + { + context.Fail( + new AuthorizationFailureReason(this, "User is not authenticated!")); + return Task.CompletedTask; + } + + List processingRequirements = new List(); + + foreach (var req in context.Requirements.OfType()) + processingRequirements.Add( + HandleSessionValidRequirement(context, req, cancellationToken)); + + var method = GetType().GetMethod(nameof(InvokeHandleRightsConditionalRequirement), BindingFlags.NonPublic | BindingFlags.Instance) + ?? throw new InvalidOperationException("Failed to locate rights handler function!"); + foreach (var rightType in RightsHelper.AllRightTypes()) + { + var genericMethod = method.MakeGenericMethod(rightType); + processingRequirements.AddRange((IEnumerable)genericMethod.Invoke(this, [context, cancellationToken])!); + } + + return ValueTaskExtensions.WhenAll(processingRequirements).AsTask(); + } + + /// + /// Handle invoking for given . + /// + /// The of right to invoke the requirement handler for. + /// The shared . + /// The for the operation. + /// An of s representing the running operation. + IEnumerable InvokeHandleRightsConditionalRequirement(AuthorizationHandlerContext context, CancellationToken cancellationToken) + where TRights : Enum + => context.Requirements.OfType>().Select(requirement => HandleRightsConditionalRequirement(context, requirement, cancellationToken)); + + /// + /// Handle authorization requirements. + /// + /// The shared . + /// The requirment to evaluate. + /// The for the operation. + /// A representing the running operation. + ValueTask HandleSessionValidRequirement(AuthorizationHandlerContext context, UserSessionValidRequirement requirement, CancellationToken cancellationToken) + { + var userId = context.User.GetTgsUserId(); + + var nbf = context.User.ParseTime(JwtRegisteredClaimNames.Nbf); + + return databaseContextFactory.UseContext(async databaseContext => + { + var sessionData = await databaseContext + .Users + .Where(user => user.Id == userId) + .Select(user => new + { + Enabled = user.Enabled!.Value, + user.LastPasswordUpdate, + }) + .TagWith("user_session_validation") + .FirstOrDefaultAsync(cancellationToken); + + lock (context) + { + if (sessionData == null) + context.Fail( + new AuthorizationFailureReason(this, $"Unable to retrieve user {userId}!")); + else if (!sessionData.Enabled) + context.Fail( + new AuthorizationFailureReason(this, "User is disabled!")); + else if (sessionData.LastPasswordUpdate >= nbf) + context.Fail( + new AuthorizationFailureReason(this, "User has been modified since logging in!")); + else + context.Succeed(requirement); + } + }); + } + + /// + /// Handle authorization requirements. + /// + /// The of right to invoke the requirement handler for. + /// The shared . + /// The requirment to evaluate. + /// The for the operation. + /// A representing the running operation. + ValueTask HandleRightsConditionalRequirement(AuthorizationHandlerContext context, RightsConditional requirement, CancellationToken cancellationToken) + where TRights : Enum + { + var rightsType = RightsHelper.TypeToRight(); + var isInstance = RightsHelper.IsInstanceRight(rightsType); + var userId = context.User.GetTgsUserId(); + + return databaseContextFactory.UseContext(async databaseContext => + { + var queryableUsers = databaseContext + .Users; + + var matchingUniquePermissionSetIds = queryableUsers + .Where(user => user.Id == userId && user.PermissionSet != null) + .Select(user => user.PermissionSet!.Id); + + var matchingGroupPermissionSetIds = queryableUsers + .Where(user => user.Id == userId && user.Group != null) + .Select(user => user.Group!.PermissionSet!.Id); + + object? permissionSet; + if (isInstance) + { + if (context.Resource is not long instanceId) + throw new InvalidOperationException("Instance ID should have been passed in as authorization resource!"); + + permissionSet = await databaseContext + .InstancePermissionSets + .Where(ips => ips.InstanceId == instanceId + && (matchingUniquePermissionSetIds.Contains(ips.PermissionSetId) || matchingGroupPermissionSetIds.Contains(ips.PermissionSetId))) + .TagWith("rights_authorization_handler_instance_permission_set") + .FirstOrDefaultAsync(cancellationToken); + } + else + permissionSet = await databaseContext + .PermissionSets + .Where(permissionSet => matchingUniquePermissionSetIds.Contains(permissionSet.Id) || matchingGroupPermissionSetIds.Contains(permissionSet.Id)) + .TagWith("rights_authorization_handler_permission_set") + .FirstOrDefaultAsync(cancellationToken); + + if (permissionSet == null) + { + context.Fail( + new AuthorizationFailureReason(this, $"Unable to find {(isInstance ? "instance " : String.Empty)}permission set for user.")); + return; + } + + // use the api versions because they're the ones that contain the actual properties + var requiredPermissionSetType = isInstance ? typeof(InstancePermissionSet) : typeof(PermissionSet); + + var rightsClrType = typeof(TRights); + var nullableRightsType = typeof(Nullable<>).MakeGenericType(rightsClrType); + + var rightPropertyInfo = requiredPermissionSetType + .GetProperties() + .Where(propertyInfo => propertyInfo.PropertyType == nullableRightsType && propertyInfo.CanRead) + .Single(); + + var rightPropertyGetMethod = rightPropertyInfo.GetMethod; + if (rightPropertyGetMethod == null) + throw new InvalidOperationException($"Rights property {rightPropertyInfo.Name} on {rightsClrType.FullName} has no getter!"); + + var right = rightPropertyGetMethod.Invoke( + permissionSet, + Array.Empty()) + ?? throw new InvalidOperationException("A user right was null!"); + + var result = requirement.Evaluate((TRights)right); + + lock (context) + { + if (result) + context.Succeed(requirement); + else + context.Fail( + new AuthorizationFailureReason(this, $"Failed to successfully evaluate rights requirement: {requirement}")); + } + }); + } + } +} diff --git a/src/Tgstation.Server.Host/Security/AuthorizationService.cs b/src/Tgstation.Server.Host/Security/AuthorizationService.cs new file mode 100644 index 0000000000..50356a0b53 --- /dev/null +++ b/src/Tgstation.Server.Host/Security/AuthorizationService.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; + +namespace Tgstation.Server.Host.Security +{ + /// + sealed class AuthorizationService : IAuthorizationService + { + /// + /// The for the . + /// + readonly IClaimsPrincipalAccessor claimsPrincipalAccessor; + + /// + /// The for the . + /// + readonly Microsoft.AspNetCore.Authorization.IAuthorizationService aspNetCoreAuthorizationService; + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + /// The value of . + public AuthorizationService( + IClaimsPrincipalAccessor claimsPrincipalAccessor, + Microsoft.AspNetCore.Authorization.IAuthorizationService aspNetCoreAuthorizationService) + { + this.claimsPrincipalAccessor = claimsPrincipalAccessor ?? throw new ArgumentNullException(nameof(claimsPrincipalAccessor)); + this.aspNetCoreAuthorizationService = aspNetCoreAuthorizationService ?? throw new ArgumentNullException(nameof(aspNetCoreAuthorizationService)); + } + + /// + public async ValueTask AuthorizeAsync(IEnumerable requirements, object? resource) + { + ArgumentNullException.ThrowIfNull(requirements); + + // asp net fails for an empty authorization requirement list + var bakedRequirements = requirements.ToList(); + if (bakedRequirements.Count == 0) + return AuthorizationResult.Success(); + + var result = await aspNetCoreAuthorizationService.AuthorizeAsync( + claimsPrincipalAccessor.User, + resource, + bakedRequirements); + + return result; + } + } +} diff --git a/src/Tgstation.Server.Host/Security/ClaimsPrincipalAccessor.cs b/src/Tgstation.Server.Host/Security/ClaimsPrincipalAccessor.cs new file mode 100644 index 0000000000..ae20f8b7ca --- /dev/null +++ b/src/Tgstation.Server.Host/Security/ClaimsPrincipalAccessor.cs @@ -0,0 +1,30 @@ +using System; +using System.Security.Claims; + +using Microsoft.AspNetCore.Http; + +namespace Tgstation.Server.Host.Security +{ + /// + sealed class ClaimsPrincipalAccessor : IClaimsPrincipalAccessor + { + /// + public ClaimsPrincipal User => httpContextAccessor.HttpContext?.User + ?? throw new InvalidOperationException("HTTP context was not present!"); + + /// + /// The for the . + /// + readonly IHttpContextAccessor httpContextAccessor; + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + public ClaimsPrincipalAccessor( + IHttpContextAccessor httpContextAccessor) + { + this.httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor)); + } + } +} diff --git a/src/Tgstation.Server.Host/Security/IAuthorizationService.cs b/src/Tgstation.Server.Host/Security/IAuthorizationService.cs new file mode 100644 index 0000000000..126909affb --- /dev/null +++ b/src/Tgstation.Server.Host/Security/IAuthorizationService.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +using Microsoft.AspNetCore.Authorization; + +namespace Tgstation.Server.Host.Security +{ + /// + /// Interface for evaluating s. + /// + public interface IAuthorizationService + { + /// + /// Attempt to authorize the current context with a given . + /// + /// The to authorize. + /// The resource to authorize. + /// A resulting in the . + ValueTask AuthorizeAsync(IEnumerable requirement, object? resource = null); + } +} diff --git a/src/Tgstation.Server.Host/Security/IClaimsPrincipalAccessor.cs b/src/Tgstation.Server.Host/Security/IClaimsPrincipalAccessor.cs new file mode 100644 index 0000000000..9ac3da2494 --- /dev/null +++ b/src/Tgstation.Server.Host/Security/IClaimsPrincipalAccessor.cs @@ -0,0 +1,15 @@ +using System.Security.Claims; + +namespace Tgstation.Server.Host.Security +{ + /// + /// Interface for accessing the current request's . + /// + interface IClaimsPrincipalAccessor + { + /// + /// Get the current . + /// + ClaimsPrincipal User { get; } + } +} diff --git a/src/Tgstation.Server.Host/Security/OAuth/DiscordOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/DiscordOAuthValidator.cs index 3195ec9dcf..c11a1ba991 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/DiscordOAuthValidator.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/DiscordOAuthValidator.cs @@ -1,9 +1,9 @@ using System; +using System.Net.Http; using Microsoft.Extensions.Logging; using Tgstation.Server.Api.Models; -using Tgstation.Server.Common.Http; using Tgstation.Server.Host.Configuration; namespace Tgstation.Server.Host.Security.OAuth @@ -25,11 +25,11 @@ namespace Tgstation.Server.Host.Security.OAuth /// /// Initializes a new instance of the class. /// - /// The for the . + /// The for the . /// The for the . /// The for the . public DiscordOAuthValidator( - IAbstractHttpClientFactory httpClientFactory, + IHttpClientFactory httpClientFactory, ILogger logger, OAuthConfiguration oAuthConfiguration) : base(httpClientFactory, logger, oAuthConfiguration) diff --git a/src/Tgstation.Server.Host/Security/OAuth/GenericOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/GenericOAuthValidator.cs index b68813be3c..eb5570c634 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/GenericOAuthValidator.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/GenericOAuthValidator.cs @@ -13,7 +13,6 @@ using Newtonsoft.Json.Serialization; using Tgstation.Server.Api; using Tgstation.Server.Api.Models; -using Tgstation.Server.Common.Http; using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.Extensions; @@ -53,7 +52,7 @@ namespace Tgstation.Server.Host.Security.OAuth /// /// The for the . /// - readonly IAbstractHttpClientFactory httpClientFactory; + readonly IHttpClientFactory httpClientFactory; /// /// Gets that should be used. @@ -74,7 +73,7 @@ namespace Tgstation.Server.Host.Security.OAuth /// The value of . /// The value of . public GenericOAuthValidator( - IAbstractHttpClientFactory httpClientFactory, + IHttpClientFactory httpClientFactory, ILogger logger, OAuthConfiguration oAuthConfiguration) { @@ -178,10 +177,10 @@ namespace Tgstation.Server.Host.Security.OAuth protected abstract OAuthTokenRequest CreateTokenRequest(string code); /// - /// Create a new configured . + /// Create a new configured . /// - /// A new configured . - IHttpClient CreateHttpClient() + /// A new configured . + HttpClient CreateHttpClient() { var httpClient = httpClientFactory.CreateClient(); try diff --git a/src/Tgstation.Server.Host/Security/OAuth/InvisionCommunityOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/InvisionCommunityOAuthValidator.cs index 20206e86d8..75733d0c51 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/InvisionCommunityOAuthValidator.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/InvisionCommunityOAuthValidator.cs @@ -1,9 +1,9 @@ using System; +using System.Net.Http; using Microsoft.Extensions.Logging; using Tgstation.Server.Api.Models; -using Tgstation.Server.Common.Http; using Tgstation.Server.Host.Configuration; namespace Tgstation.Server.Host.Security.OAuth @@ -27,11 +27,11 @@ namespace Tgstation.Server.Host.Security.OAuth /// /// Initializes a new instance of the class. /// - /// The for the . + /// The for the . /// The for the . /// The for the . public InvisionCommunityOAuthValidator( - IAbstractHttpClientFactory httpClientFactory, + IHttpClientFactory httpClientFactory, ILogger logger, OAuthConfiguration oAuthConfiguration) : base(httpClientFactory, logger, oAuthConfiguration) diff --git a/src/Tgstation.Server.Host/Security/OAuth/KeycloakOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/KeycloakOAuthValidator.cs index 795812b40d..ee9efcd214 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/KeycloakOAuthValidator.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/KeycloakOAuthValidator.cs @@ -1,9 +1,9 @@ using System; +using System.Net.Http; using Microsoft.Extensions.Logging; using Tgstation.Server.Api.Models; -using Tgstation.Server.Common.Http; using Tgstation.Server.Host.Configuration; namespace Tgstation.Server.Host.Security.OAuth @@ -32,11 +32,11 @@ namespace Tgstation.Server.Host.Security.OAuth /// /// Initializes a new instance of the class. /// - /// The for the . + /// The for the . /// The for the . /// The for the . public KeycloakOAuthValidator( - IAbstractHttpClientFactory httpClientFactory, + IHttpClientFactory httpClientFactory, ILogger logger, OAuthConfiguration oAuthConfiguration) : base(httpClientFactory, logger, oAuthConfiguration) diff --git a/src/Tgstation.Server.Host/Security/OAuth/OAuthProviders.cs b/src/Tgstation.Server.Host/Security/OAuth/OAuthProviders.cs index 1f67b1999f..6233b79157 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/OAuthProviders.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/OAuthProviders.cs @@ -1,12 +1,12 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net.Http; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Tgstation.Server.Api.Models; -using Tgstation.Server.Common.Http; using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.Utils.GitHub; @@ -24,18 +24,19 @@ namespace Tgstation.Server.Host.Security.OAuth /// Initializes a new instance of the class. /// /// The to use. - /// The to use. + /// The to use. /// The to use. /// The containing the to use. public OAuthProviders( IGitHubServiceFactory gitHubServiceFactory, - IAbstractHttpClientFactory httpClientFactory, + IHttpClientFactory httpClientFactory, ILoggerFactory loggerFactory, - IOptions securityConfigurationOptions) + IOptionsSnapshot securityConfigurationOptions) { ArgumentNullException.ThrowIfNull(loggerFactory); + ArgumentNullException.ThrowIfNull(securityConfigurationOptions); - var securityConfiguration = securityConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(securityConfigurationOptions)); + var securityConfiguration = securityConfigurationOptions.Value; var validatorsBuilder = new List(); validators = validatorsBuilder; @@ -66,9 +67,7 @@ namespace Tgstation.Server.Host.Security.OAuth loggerFactory.CreateLogger(), keyCloakConfig)); -#pragma warning disable CS0618 // Type or member is obsolete if (securityConfiguration.OAuth.TryGetValue(OAuthProvider.InvisionCommunity, out var invisionConfig)) -#pragma warning restore CS0618 // Type or member is obsolete validatorsBuilder.Add( new InvisionCommunityOAuthValidator( httpClientFactory, diff --git a/src/Tgstation.Server.Host/Security/RightsEvaluation/AndRightsConditional{TRights}.cs b/src/Tgstation.Server.Host/Security/RightsEvaluation/AndRightsConditional{TRights}.cs new file mode 100644 index 0000000000..4f39f1978d --- /dev/null +++ b/src/Tgstation.Server.Host/Security/RightsEvaluation/AndRightsConditional{TRights}.cs @@ -0,0 +1,41 @@ +using System; + +namespace Tgstation.Server.Host.Security.RightsEvaluation +{ + /// + /// Logical AND . + /// + /// The to evaluate. + public sealed class AndRightsConditional : RightsConditional + where TRights : Enum + { + /// + /// The left hand side operand. + /// + readonly RightsConditional lhs; + + /// + /// The right hand side operand. + /// + readonly RightsConditional rhs; + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + /// The value of . + public AndRightsConditional(RightsConditional lhs, RightsConditional rhs) + { + this.lhs = lhs ?? throw new ArgumentNullException(nameof(lhs)); + this.rhs = rhs ?? throw new ArgumentNullException(nameof(rhs)); + } + + /// + public override bool Evaluate(TRights rights) + => lhs.Evaluate(rights) && rhs.Evaluate(rights); + + /// + public override string ToString() + => $"({lhs} && {rhs})"; + } +} diff --git a/src/Tgstation.Server.Host/Security/RightsEvaluation/FlagRightsConditional{TRights}.cs b/src/Tgstation.Server.Host/Security/RightsEvaluation/FlagRightsConditional{TRights}.cs new file mode 100644 index 0000000000..722abe0de3 --- /dev/null +++ b/src/Tgstation.Server.Host/Security/RightsEvaluation/FlagRightsConditional{TRights}.cs @@ -0,0 +1,44 @@ +using System; + +namespace Tgstation.Server.Host.Security.RightsEvaluation +{ + /// + /// Single flag . + /// + /// The to evaluate. + /// An instance-based type MUST be evaluated with the of the instance attached as a resource. + public sealed class FlagRightsConditional : RightsConditional + where TRights : Enum + { + /// + /// The single bit flag of the . + /// + readonly TRights flag; + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + public FlagRightsConditional(TRights flag) + { + var asUlong = (ulong)(object)flag; + + if (asUlong == 0) + throw new ArgumentOutOfRangeException(nameof(flag), flag, "Flag cannot be zero!"); + + // https://stackoverflow.com/a/28303898/3976486 + if ((asUlong & (asUlong - 1)) != 0) + throw new ArgumentException("Right has more than one bit set!", nameof(flag)); + + this.flag = flag; + } + + /// + public override bool Evaluate(TRights rights) + => rights.HasFlag(flag); + + /// + public override string ToString() + => $"{typeof(TRights).Name}.{flag}"; + } +} diff --git a/src/Tgstation.Server.Host/Security/RightsEvaluation/OrRightsConditional{TRights}.cs b/src/Tgstation.Server.Host/Security/RightsEvaluation/OrRightsConditional{TRights}.cs new file mode 100644 index 0000000000..5e436915d7 --- /dev/null +++ b/src/Tgstation.Server.Host/Security/RightsEvaluation/OrRightsConditional{TRights}.cs @@ -0,0 +1,41 @@ +using System; + +namespace Tgstation.Server.Host.Security.RightsEvaluation +{ + /// + /// Logical OR . + /// + /// The to evaluate. + public sealed class OrRightsConditional : RightsConditional + where TRights : Enum + { + /// + /// The left hand side operand. + /// + readonly RightsConditional lhs; + + /// + /// The right hand side operand. + /// + readonly RightsConditional rhs; + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + /// The value of . + public OrRightsConditional(RightsConditional lhs, RightsConditional rhs) + { + this.lhs = lhs ?? throw new ArgumentNullException(nameof(lhs)); + this.rhs = rhs ?? throw new ArgumentNullException(nameof(rhs)); + } + + /// + public override bool Evaluate(TRights rights) + => lhs.Evaluate(rights) || rhs.Evaluate(rights); + + /// + public override string ToString() + => $"({lhs} || {rhs})"; + } +} diff --git a/src/Tgstation.Server.Host/Security/RightsEvaluation/RightsConditional{TRights}.cs b/src/Tgstation.Server.Host/Security/RightsEvaluation/RightsConditional{TRights}.cs new file mode 100644 index 0000000000..62fe39a730 --- /dev/null +++ b/src/Tgstation.Server.Host/Security/RightsEvaluation/RightsConditional{TRights}.cs @@ -0,0 +1,21 @@ +using System; + +using Microsoft.AspNetCore.Authorization; + +namespace Tgstation.Server.Host.Security.RightsEvaluation +{ + /// + /// An conditional expression of . + /// + /// The to evaluate. + public abstract class RightsConditional : IAuthorizationRequirement + where TRights : Enum + { + /// + /// Test if the is satified for the given . + /// + /// The to evaluate the conditional for. + /// if the is satisfied by the given . + public abstract bool Evaluate(TRights rights); + } +} diff --git a/src/Tgstation.Server.Host/Security/TgsGraphQLAuthorizeAttribute.cs b/src/Tgstation.Server.Host/Security/TgsGraphQLAuthorizeAttribute.cs deleted file mode 100644 index 2bd0bfac52..0000000000 --- a/src/Tgstation.Server.Host/Security/TgsGraphQLAuthorizeAttribute.cs +++ /dev/null @@ -1,137 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -using HotChocolate.Authorization; - -using Tgstation.Server.Api.Rights; - -namespace Tgstation.Server.Host.Security -{ - /// - /// Helper for using the with the system. - /// -#pragma warning disable CA1019 - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Property, AllowMultiple = true, Inherited = true)] - sealed class TgsGraphQLAuthorizeAttribute : AuthorizeAttribute - { - /// - /// Gets the associated with the if any. - /// - public RightsType? RightsType { get; } - - /// - /// Initializes a new instance of the class. - /// - public TgsGraphQLAuthorizeAttribute() - : this(Enumerable.Empty()) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The required. - public TgsGraphQLAuthorizeAttribute(AdministrationRights requiredRights) - : this(RightsHelper.RoleNames(requiredRights)) - { - RightsType = Api.Rights.RightsType.Administration; - } - - /// - /// Initializes a new instance of the class. - /// - /// The required. - public TgsGraphQLAuthorizeAttribute(InstanceManagerRights requiredRights) - : this(RightsHelper.RoleNames(requiredRights)) - { - RightsType = Api.Rights.RightsType.InstanceManager; - } - - /// - /// Initializes a new instance of the class. - /// - /// The required. - public TgsGraphQLAuthorizeAttribute(RepositoryRights requiredRights) - : this(RightsHelper.RoleNames(requiredRights)) - { - RightsType = Api.Rights.RightsType.Repository; - } - - /// - /// Initializes a new instance of the class. - /// - /// The required. - public TgsGraphQLAuthorizeAttribute(EngineRights requiredRights) - : this(RightsHelper.RoleNames(requiredRights)) - { - RightsType = Api.Rights.RightsType.Engine; - } - - /// - /// Initializes a new instance of the class. - /// - /// The required. - public TgsGraphQLAuthorizeAttribute(DreamMakerRights requiredRights) - : this(RightsHelper.RoleNames(requiredRights)) - { - RightsType = Api.Rights.RightsType.DreamMaker; - } - - /// - /// Initializes a new instance of the class. - /// - /// The required. - public TgsGraphQLAuthorizeAttribute(DreamDaemonRights requiredRights) - : this(RightsHelper.RoleNames(requiredRights)) - { - RightsType = Api.Rights.RightsType.DreamDaemon; - } - - /// - /// Initializes a new instance of the class. - /// - /// The required. - public TgsGraphQLAuthorizeAttribute(ChatBotRights requiredRights) - : this(RightsHelper.RoleNames(requiredRights)) - { - RightsType = Api.Rights.RightsType.ChatBots; - } - - /// - /// Initializes a new instance of the class. - /// - /// The required. - public TgsGraphQLAuthorizeAttribute(ConfigurationRights requiredRights) - : this(RightsHelper.RoleNames(requiredRights)) - { - RightsType = Api.Rights.RightsType.Configuration; - } - - /// - /// Initializes a new instance of the class. - /// - /// The required. - public TgsGraphQLAuthorizeAttribute(InstancePermissionSetRights requiredRights) - : this(RightsHelper.RoleNames(requiredRights)) - { - RightsType = Api.Rights.RightsType.InstancePermissionSet; - } - - /// - /// Initializes a new instance of the class. - /// - /// of role names. - private TgsGraphQLAuthorizeAttribute(IEnumerable roleNames) - { - var listRoles = roleNames.ToList(); - if (listRoles.Count != 0) - { - Roles = [.. listRoles]; - } - - Policy = TgsAuthorizeAttribute.PolicyName; - Apply = ApplyPolicy.Validation; - } - } -} diff --git a/src/Tgstation.Server.Host/Security/TgsGraphQLAuthorizeAttribute{TAuthority}.cs b/src/Tgstation.Server.Host/Security/TgsGraphQLAuthorizeAttribute{TAuthority}.cs deleted file mode 100644 index 495424073a..0000000000 --- a/src/Tgstation.Server.Host/Security/TgsGraphQLAuthorizeAttribute{TAuthority}.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using System.Reflection; - -using HotChocolate.Authorization; - -using Tgstation.Server.Host.Authority.Core; - -namespace Tgstation.Server.Host.Security -{ - /// - /// Inherits the roles of s for GraphQL endpoints. - /// - /// The being wrapped. - [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = true)] - public sealed class TgsGraphQLAuthorizeAttribute : AuthorizeAttribute - where TAuthority : IAuthority - { - /// - /// The name of the method targeted. - /// - public string MethodName { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The method name to inherit roles from. - public TgsGraphQLAuthorizeAttribute(string methodName) - { - ArgumentNullException.ThrowIfNull(methodName); - - var authorityType = typeof(TAuthority); - var authorityMethod = authorityType.GetMethod(methodName) - ?? throw new InvalidOperationException($"Could not find method {methodName} on {authorityType}!"); - var authorizeAttribute = authorityMethod.GetCustomAttribute() - ?? throw new InvalidOperationException($"Could not find method {authorityType}.{methodName}() has no {nameof(TgsAuthorizeAttribute)}!"); - MethodName = methodName; - Roles = authorizeAttribute.Roles?.Split(',', StringSplitOptions.RemoveEmptyEntries); - Apply = ApplyPolicy.Validation; - } - } -} diff --git a/src/Tgstation.Server.Host/Security/TgsRestAuthorizeAttribute{TAuthority}.cs b/src/Tgstation.Server.Host/Security/TgsRestAuthorizeAttribute{TAuthority}.cs deleted file mode 100644 index 6103eca33b..0000000000 --- a/src/Tgstation.Server.Host/Security/TgsRestAuthorizeAttribute{TAuthority}.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Reflection; - -using Microsoft.AspNetCore.Authorization; - -using Tgstation.Server.Host.Authority.Core; - -namespace Tgstation.Server.Host.Security -{ - /// - /// Inherits the roles of s for REST endpoints. - /// - /// The being wrapped. - [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = true)] - public sealed class TgsRestAuthorizeAttribute : AuthorizeAttribute - where TAuthority : IAuthority - { - /// - /// The name of the method targeted. - /// - public string MethodName { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The method name to inherit roles from. - public TgsRestAuthorizeAttribute(string methodName) - { - ArgumentNullException.ThrowIfNull(methodName); - - var authorityType = typeof(TAuthority); - var authorityMethod = authorityType.GetMethod(methodName) - ?? throw new InvalidOperationException($"Could not find method {methodName} on {authorityType}!"); - - var authorizeAttribute = authorityMethod.GetCustomAttribute() - ?? throw new InvalidOperationException($"Could not find method {authorityType}.{methodName}() has no {nameof(TgsAuthorizeAttribute)}!"); - - MethodName = methodName; - Roles = authorizeAttribute.Roles; - } - } -} diff --git a/src/Tgstation.Server.Host/Security/TokenFactory.cs b/src/Tgstation.Server.Host/Security/TokenFactory.cs index 6081aeb42f..9e2b18e668 100644 --- a/src/Tgstation.Server.Host/Security/TokenFactory.cs +++ b/src/Tgstation.Server.Host/Security/TokenFactory.cs @@ -39,9 +39,9 @@ namespace Tgstation.Server.Host.Security } /// - /// The for the . + /// The of for the . /// - readonly SecurityConfiguration securityConfiguration; + readonly IOptions securityConfigurationOptions; /// /// The used to generate s. @@ -63,7 +63,7 @@ namespace Tgstation.Server.Host.Security /// /// The used for generating the . /// The used to generate the issuer name. - /// The containing the value of . + /// The value of . public TokenFactory( ICryptographySuite cryptographySuite, IAssemblyInformationProvider assemblyInformationProvider, @@ -72,11 +72,11 @@ namespace Tgstation.Server.Host.Security ArgumentNullException.ThrowIfNull(cryptographySuite); ArgumentNullException.ThrowIfNull(assemblyInformationProvider); - securityConfiguration = securityConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(securityConfigurationOptions)); + this.securityConfigurationOptions = securityConfigurationOptions ?? throw new ArgumentNullException(nameof(securityConfigurationOptions)); - SigningKeyBytes = String.IsNullOrWhiteSpace(securityConfiguration.CustomTokenSigningKeyBase64) - ? cryptographySuite.GetSecureBytes(securityConfiguration.TokenSigningKeyByteCount) - : Convert.FromBase64String(securityConfiguration.CustomTokenSigningKeyBase64); + SigningKeyBytes = string.IsNullOrWhiteSpace(securityConfigurationOptions.Value.CustomTokenSigningKeyBase64) + ? cryptographySuite.GetSecureBytes(securityConfigurationOptions.Value.TokenSigningKeyByteCount) + : Convert.FromBase64String(securityConfigurationOptions.Value.CustomTokenSigningKeyBase64); ValidationParameters = new TokenValidationParameters { @@ -90,7 +90,7 @@ namespace Tgstation.Server.Host.Security ValidateAudience = true, ValidAudience = typeof(TokenResponse).Assembly.GetName().Name, - ClockSkew = TimeSpan.FromMinutes(securityConfiguration.TokenClockSkewMinutes), + ClockSkew = TimeSpan.FromMinutes(securityConfigurationOptions.Value.TokenClockSkewMinutes), RequireSignedTokens = true, @@ -122,8 +122,8 @@ namespace Tgstation.Server.Host.Security notBefore = now; var expiry = now.AddMinutes(serviceLogin - ? securityConfiguration.OAuthTokenExpiryMinutes - : securityConfiguration.TokenExpiryMinutes); + ? securityConfigurationOptions.Value.OAuthTokenExpiryMinutes + : securityConfigurationOptions.Value.TokenExpiryMinutes); var securityToken = new JwtSecurityToken( tokenHeader, diff --git a/src/Tgstation.Server.Host/Security/UserSessionValidRequirement.cs b/src/Tgstation.Server.Host/Security/UserSessionValidRequirement.cs new file mode 100644 index 0000000000..79a49a9564 --- /dev/null +++ b/src/Tgstation.Server.Host/Security/UserSessionValidRequirement.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; + +using Microsoft.AspNetCore.Authorization; + +namespace Tgstation.Server.Host.Security +{ + /// + /// for testing if a user is enabled and their session is valid. + /// + sealed class UserSessionValidRequirement : IAuthorizationRequirement + { + /// + /// The singleton instance of this class. + /// + public static IEnumerable InstanceAsEnumerable { get; } = + [ + new(), + ]; + + /// + /// Initializes a new instance of the class. + /// + private UserSessionValidRequirement() + { + } + } +} diff --git a/src/Tgstation.Server.Host/Server.cs b/src/Tgstation.Server.Host/Server.cs index 640b0c8517..73249c3d87 100644 --- a/src/Tgstation.Server.Host/Server.cs +++ b/src/Tgstation.Server.Host/Server.cs @@ -43,6 +43,11 @@ namespace Tgstation.Server.Host /// internal IHost? Host { get; private set; } + /// + /// The to use. + /// + readonly IIOManager ioManager; + /// /// The for the . /// @@ -63,16 +68,16 @@ namespace Tgstation.Server.Host /// readonly object restartLock; + /// + /// The of for the . + /// + IOptionsMonitor? generalConfigurationOptions; + /// /// The for the . /// ILogger? logger; - /// - /// The for the . - /// - GeneralConfiguration? generalConfiguration; - /// /// The for the . /// @@ -101,10 +106,12 @@ namespace Tgstation.Server.Host /// /// Initializes a new instance of the class. /// + /// The value of . /// The value of . /// The value of . - public Server(IHostBuilder hostBuilder, string? updatePath) + public Server(IIOManager ioManager, IHostBuilder hostBuilder, string? updatePath) { + this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); this.hostBuilder = hostBuilder ?? throw new ArgumentNullException(nameof(hostBuilder)); this.updatePath = updatePath; @@ -118,7 +125,7 @@ namespace Tgstation.Server.Host /// public async ValueTask Run(CancellationToken cancellationToken) { - var updateDirectory = updatePath != null ? Path.GetDirectoryName(updatePath) : null; + var updateDirectory = updatePath != null ? ioManager.GetDirectoryName(updatePath) : null; using (cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken)) using (var fsWatcher = updateDirectory != null ? new FileSystemWatcher(updateDirectory) : null) { @@ -144,8 +151,7 @@ namespace Tgstation.Server.Host if (await DumpGraphQLSchemaIfRequested(Host.Services, cancellationToken)) return; - var generalConfigurationOptions = Host.Services.GetRequiredService>(); - generalConfiguration = generalConfigurationOptions.Value; + generalConfigurationOptions = Host.Services.GetRequiredService>(); await Host.RunAsync(cancellationTokenSource.Token); } @@ -382,8 +388,8 @@ namespace Tgstation.Server.Host using var cts = new CancellationTokenSource( TimeSpan.FromMinutes( giveHandlersTimeToWaitAround - ? generalConfiguration!.ShutdownTimeoutMinutes - : generalConfiguration!.RestartTimeoutMinutes)); + ? generalConfigurationOptions!.CurrentValue.ShutdownTimeoutMinutes + : generalConfigurationOptions!.CurrentValue.RestartTimeoutMinutes)); var cancellationToken = cts.Token; try { @@ -421,12 +427,12 @@ namespace Tgstation.Server.Host /// /// The that sent the event. /// The . - void WatchForShutdownFileCreation(object sender, FileSystemEventArgs eventArgs) + async void WatchForShutdownFileCreation(object sender, FileSystemEventArgs eventArgs) { logger?.LogTrace("FileSystemWatcher triggered."); - // TODO: Refactor this to not use System.IO function here. - if (eventArgs.FullPath == Path.GetFullPath(updatePath!) && File.Exists(eventArgs.FullPath)) + // DCT: None available + if (eventArgs.FullPath == ioManager.ResolvePath(updatePath!) && await ioManager.FileExists(eventArgs.FullPath, CancellationToken.None)) { logger?.LogInformation("Host watchdog appears to be requesting server termination!"); lock (restartLock) diff --git a/src/Tgstation.Server.Host/ServerFactory.cs b/src/Tgstation.Server.Host/ServerFactory.cs index 5f3230ec0a..491ef0c3d1 100644 --- a/src/Tgstation.Server.Host/ServerFactory.cs +++ b/src/Tgstation.Server.Host/ServerFactory.cs @@ -182,7 +182,7 @@ namespace Tgstation.Server.Host IOManager.ResolvePath( IOManager.GetDirectoryName(assemblyInformationProvider.Path))); - return new Server(hostBuilder, updatePath); + return new Server(IOManager, hostBuilder, updatePath); } #pragma warning restore CA1506 } diff --git a/src/Tgstation.Server.Host/Setup/SetupWizard.cs b/src/Tgstation.Server.Host/Setup/SetupWizard.cs index ea1ba73d85..ef0019ef8b 100644 --- a/src/Tgstation.Server.Host/Setup/SetupWizard.cs +++ b/src/Tgstation.Server.Host/Setup/SetupWizard.cs @@ -958,31 +958,6 @@ namespace Tgstation.Server.Host.Setup }; } - /// - /// Prompts the user to create a . - /// - /// The for the operation. - /// A resulting in the new . - async ValueTask ConfigureTelemetry(CancellationToken cancellationToken) - { - bool enableReporting = await PromptYesNo("Enable version telemetry? This anonymously reports the TGS version in use.", true, cancellationToken); - - string? serverFriendlyName = null; - if (enableReporting) - { - await console.WriteAsync("(Optional) Publically associate your reported version with a friendly name:", false, cancellationToken); - serverFriendlyName = await console.ReadLineAsync(false, cancellationToken); - if (String.IsNullOrWhiteSpace(serverFriendlyName)) - serverFriendlyName = null; - } - - return new TelemetryConfiguration - { - DisableVersionReporting = !enableReporting, - ServerFriendlyName = serverFriendlyName, - }; - } - /// /// Saves a given set to . /// @@ -994,7 +969,6 @@ namespace Tgstation.Server.Host.Setup /// The to save. /// The to save. /// The to save. - /// The to save. /// The for the operation. /// A representing the running operation. async ValueTask SaveConfiguration( @@ -1006,7 +980,6 @@ namespace Tgstation.Server.Host.Setup ElasticsearchConfiguration? elasticsearchConfiguration, ControlPanelConfiguration controlPanelConfiguration, SwarmConfiguration? swarmConfiguration, - TelemetryConfiguration? telemetryConfiguration, CancellationToken cancellationToken) { newGeneralConfiguration.ApiPort = hostingPort ?? GeneralConfiguration.DefaultApiPort; @@ -1019,7 +992,6 @@ namespace Tgstation.Server.Host.Setup { ElasticsearchConfiguration.Section, elasticsearchConfiguration }, { ControlPanelConfiguration.Section, controlPanelConfiguration }, { SwarmConfiguration.Section, swarmConfiguration }, - { TelemetryConfiguration.Section, telemetryConfiguration }, }; var versionConverter = new VersionConverter(); @@ -1093,8 +1065,6 @@ namespace Tgstation.Server.Host.Setup var swarmConfiguration = await ConfigureSwarm(cancellationToken); - var telemetryConfiguration = await ConfigureTelemetry(cancellationToken); - await console.WriteAsync(null, true, cancellationToken); await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Configuration complete! Saving to {0}", userConfigFileName), true, cancellationToken); @@ -1107,7 +1077,6 @@ namespace Tgstation.Server.Host.Setup elasticSearchConfiguration, controlPanelConfiguration, swarmConfiguration, - telemetryConfiguration, cancellationToken); } @@ -1225,7 +1194,6 @@ namespace Tgstation.Server.Host.Setup AllowAnyOrigin = true, }, null, - null, cancellationToken); } else diff --git a/src/Tgstation.Server.Host/Swarm/SwarmService.cs b/src/Tgstation.Server.Host/Swarm/SwarmService.cs index dcb3737ff9..97ca8c914f 100644 --- a/src/Tgstation.Server.Host/Swarm/SwarmService.cs +++ b/src/Tgstation.Server.Host/Swarm/SwarmService.cs @@ -19,7 +19,6 @@ using Newtonsoft.Json; using Tgstation.Server.Api.Models.Internal; using Tgstation.Server.Api.Models.Response; using Tgstation.Server.Common.Extensions; -using Tgstation.Server.Common.Http; using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.Core; using Tgstation.Server.Host.Database; @@ -48,7 +47,7 @@ namespace Tgstation.Server.Host.Swarm return true; lock (swarmServers) - return swarmServers.Count - 1 >= swarmConfiguration.UpdateRequiredNodeCount; + return swarmServers.Count - 1 >= swarmConfigurationOptions.Value.UpdateRequiredNodeCount; } } @@ -56,7 +55,7 @@ namespace Tgstation.Server.Host.Swarm /// If the swarm system is enabled. /// [MemberNotNullWhen(true, nameof(serverHealthCheckTask), nameof(forceHealthCheckTcs), nameof(serverHealthCheckCancellationTokenSource), nameof(swarmServers))] - bool SwarmMode => swarmConfiguration.PrivateKey != null; + bool SwarmMode => swarmConfigurationOptions.Value.PrivateKey != null; /// /// The for the . @@ -74,9 +73,9 @@ namespace Tgstation.Server.Host.Swarm readonly IAssemblyInformationProvider assemblyInformationProvider; /// - /// The for the . + /// The for the . /// - readonly IAbstractHttpClientFactory httpClientFactory; + readonly IHttpClientFactory httpClientFactory; /// /// The for the . @@ -98,16 +97,16 @@ namespace Tgstation.Server.Host.Swarm /// readonly ITokenFactory tokenFactory; + /// + /// The of for the . + /// + readonly IOptions swarmConfigurationOptions; + /// /// The for the . /// readonly ILogger logger; - /// - /// The for the . - /// - readonly SwarmConfiguration swarmConfiguration; - /// /// The for . /// @@ -169,13 +168,13 @@ namespace Tgstation.Server.Host.Swarm /// The value of . /// The value of . /// The value of . - /// The containing the value of . + /// The containing the value of . /// The value of . public SwarmService( IDatabaseContextFactory databaseContextFactory, IDatabaseSeeder databaseSeeder, IAssemblyInformationProvider assemblyInformationProvider, - IAbstractHttpClientFactory httpClientFactory, + IHttpClientFactory httpClientFactory, IAsyncDelayer asyncDelayer, IServerUpdater serverUpdater, IFileTransferTicketProvider transferService, @@ -191,18 +190,18 @@ namespace Tgstation.Server.Host.Swarm this.serverUpdater = serverUpdater ?? throw new ArgumentNullException(nameof(serverUpdater)); this.transferService = transferService ?? throw new ArgumentNullException(nameof(transferService)); this.tokenFactory = tokenFactory ?? throw new ArgumentNullException(nameof(tokenFactory)); - swarmConfiguration = swarmConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(swarmConfigurationOptions)); + this.swarmConfigurationOptions = swarmConfigurationOptions ?? throw new ArgumentNullException(nameof(swarmConfigurationOptions)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); if (SwarmMode) { - if (swarmConfiguration.Address == null) + if (this.swarmConfigurationOptions.Value.Address == null) throw new InvalidOperationException("Swarm configuration missing Address!"); - if (String.IsNullOrWhiteSpace(swarmConfiguration.Identifier)) + if (string.IsNullOrWhiteSpace(this.swarmConfigurationOptions.Value.Identifier)) throw new InvalidOperationException("Swarm configuration missing Identifier!"); - swarmController = swarmConfiguration.ControllerAddress == null; + swarmController = this.swarmConfigurationOptions.Value.ControllerAddress == null; if (swarmController) registrationIdsAndTimes = new(); @@ -213,10 +212,10 @@ namespace Tgstation.Server.Host.Swarm { new() { - Address = swarmConfiguration.Address, - PublicAddress = swarmConfiguration.PublicAddress, + Address = this.swarmConfigurationOptions.Value.Address, + PublicAddress = this.swarmConfigurationOptions.Value.PublicAddress, Controller = swarmController, - Identifier = swarmConfiguration.Identifier, + Identifier = this.swarmConfigurationOptions.Value.Identifier, }, }; } @@ -411,15 +410,15 @@ namespace Tgstation.Server.Host.Swarm swarmController ? "Controller" : "Node", - swarmConfiguration.Identifier); + swarmConfigurationOptions.Value.Identifier); else logger.LogTrace("Swarm mode disabled"); SwarmRegistrationResult result; if (swarmController) { - if (swarmConfiguration.UpdateRequiredNodeCount > 0) - logger.LogInformation("Expecting connections from {expectedNodeCount} nodes", swarmConfiguration.UpdateRequiredNodeCount); + if (swarmConfigurationOptions.Value.UpdateRequiredNodeCount > 0) + logger.LogInformation("Expecting connections from {expectedNodeCount} nodes", swarmConfigurationOptions.Value.UpdateRequiredNodeCount); await databaseContextFactory.UseContext( databaseContext => databaseSeeder.Initialize(databaseContext, cancellationToken)); @@ -739,7 +738,7 @@ namespace Tgstation.Server.Host.Swarm if (!swarmController) return SendRemoteAbort(new SwarmServerInformation { - Address = swarmConfiguration.ControllerAddress, + Address = swarmConfigurationOptions.Value.ControllerAddress, }); lock (swarmServers!) @@ -857,7 +856,7 @@ namespace Tgstation.Server.Host.Swarm new SwarmUpdateRequest { UpdateVersion = version, - SourceNode = swarmConfiguration.Identifier, + SourceNode = swarmConfigurationOptions.Value.Identifier, DownloadTickets = downloadTickets, }); @@ -892,7 +891,7 @@ namespace Tgstation.Server.Host.Swarm return SwarmPrepareResult.Failure; } - if (!updateRequest.DownloadTickets.TryGetValue(swarmConfiguration.Identifier!, out var ticket)) + if (!updateRequest.DownloadTickets.TryGetValue(swarmConfigurationOptions.Value.Identifier!, out var ticket)) { logger.Log( swarmController @@ -972,11 +971,11 @@ namespace Tgstation.Server.Host.Swarm { logger.LogInformation("Sending remote prepare to nodes..."); - if (currentUpdateOperation.InvolvedServers.Count - 1 < swarmConfiguration.UpdateRequiredNodeCount) + if (currentUpdateOperation.InvolvedServers.Count - 1 < swarmConfigurationOptions.Value.UpdateRequiredNodeCount) { logger.LogWarning( "Aborting update, controller expects to be in sync with {requiredNodeCount} nodes but currently only has {currentNodeCount}!", - swarmConfiguration.UpdateRequiredNodeCount, + swarmConfigurationOptions.Value.UpdateRequiredNodeCount, currentUpdateOperation.InvolvedServers.Count - 1); abortUpdate = true; return SwarmPrepareResult.Failure; @@ -1009,7 +1008,7 @@ namespace Tgstation.Server.Host.Swarm : updateRequest.DownloadTickets!; var sourceNode = weAreInitiator - ? swarmConfiguration.Identifier + ? swarmConfigurationOptions.Value.Identifier : updateRequest.SourceNode; using var httpClient = httpClientFactory.CreateClient(); @@ -1120,7 +1119,7 @@ namespace Tgstation.Server.Host.Swarm false); var serversRequiringTickets = involvedServers - .Where(node => node.Identifier != swarmConfiguration.Identifier) + .Where(node => node.Identifier != swarmConfigurationOptions.Value.Identifier) .ToList(); logger.LogTrace("Creating {n} download tickets for other nodes...", serversRequiringTickets.Count); @@ -1277,7 +1276,7 @@ namespace Tgstation.Server.Host.Swarm /// A resulting in the . async ValueTask RegisterWithController(CancellationToken cancellationToken) { - logger.LogInformation("Attempting to register with swarm controller at {controllerAddress}...", swarmConfiguration.ControllerAddress); + logger.LogInformation("Attempting to register with swarm controller at {controllerAddress}...", swarmConfigurationOptions.Value.ControllerAddress); var requestedRegistrationId = Guid.NewGuid(); using var httpClient = httpClientFactory.CreateClient(); @@ -1287,9 +1286,9 @@ namespace Tgstation.Server.Host.Swarm SwarmConstants.RegisterRoute, new SwarmRegistrationRequest(Version.Parse(MasterVersionsAttribute.Instance.RawSwarmProtocolVersion)) { - Identifier = swarmConfiguration.Identifier, - Address = swarmConfiguration.Address, - PublicAddress = swarmConfiguration.PublicAddress, + Identifier = swarmConfigurationOptions.Value.Identifier, + Address = swarmConfigurationOptions.Value.Address, + PublicAddress = swarmConfigurationOptions.Value.PublicAddress, }, requestedRegistrationId); @@ -1437,7 +1436,7 @@ namespace Tgstation.Server.Host.Swarm { swarmServer ??= new SwarmServerInformation { - Address = swarmConfiguration.ControllerAddress, + Address = swarmConfigurationOptions.Value.ControllerAddress, }; var fullRoute = $"{SwarmConstants.ControllerRoute}/{route}"; @@ -1455,7 +1454,7 @@ namespace Tgstation.Server.Host.Swarm request.Headers.Accept.Clear(); request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json)); - request.Headers.Add(SwarmConstants.ApiKeyHeader, swarmConfiguration.PrivateKey); + request.Headers.Add(SwarmConstants.ApiKeyHeader, swarmConfigurationOptions.Value.PrivateKey); if (registrationIdOverride.HasValue) request.Headers.Add(SwarmConstants.RegistrationIdHeader, registrationIdOverride.Value.ToString()); else if (swarmController) diff --git a/src/Tgstation.Server.Host/System/IProcessExecutor.cs b/src/Tgstation.Server.Host/System/IProcessExecutor.cs index 0ce0c98a8c..e3e79d2bff 100644 --- a/src/Tgstation.Server.Host/System/IProcessExecutor.cs +++ b/src/Tgstation.Server.Host/System/IProcessExecutor.cs @@ -20,6 +20,7 @@ namespace Tgstation.Server.Host.System /// File to write process output and error streams to. Requires to be . /// If the process output and error streams should be read. /// If shell execute should not be used. Must be set if is set. + /// If should not be logged. /// A resulting in the new . ValueTask LaunchProcess( string fileName, @@ -29,7 +30,8 @@ namespace Tgstation.Server.Host.System IReadOnlyDictionary? environment = null, string? fileRedirect = null, bool readStandardHandles = false, - bool noShellExecute = false); + bool noShellExecute = false, + bool doNotLogArguments = false); /// /// Get a representing the running executable. diff --git a/src/Tgstation.Server.Host/System/ProcessExecutor.cs b/src/Tgstation.Server.Host/System/ProcessExecutor.cs index 01fe2c00a4..985e562a65 100644 --- a/src/Tgstation.Server.Host/System/ProcessExecutor.cs +++ b/src/Tgstation.Server.Host/System/ProcessExecutor.cs @@ -113,7 +113,8 @@ namespace Tgstation.Server.Host.System IReadOnlyDictionary? environment, string? fileRedirect, bool readStandardHandles, - bool noShellExecute) + bool noShellExecute, + bool doNotLogArguments) { ArgumentNullException.ThrowIfNull(fileName); ArgumentNullException.ThrowIfNull(workingDirectory); @@ -127,14 +128,14 @@ namespace Tgstation.Server.Host.System "Launching process in {workingDirectory}: {exe} {arguments}{environment}", workingDirectory, fileName, - arguments, + doNotLogArguments ? " readonly IHttpMessageHandlerFactory httpMessageHandlerFactory; + /// + /// The of for the . + /// + readonly IOptionsMonitor generalConfigurationOptions; + /// /// The for the . /// readonly ILogger logger; - /// - /// The for the . - /// - readonly GeneralConfiguration generalConfiguration; - /// /// Cache of created s and last used/expiry times, keyed by access token. /// @@ -76,17 +76,17 @@ namespace Tgstation.Server.Host.Utils.GitHub /// The value of . /// The value of . /// The value of . - /// The containing the value of . + /// The containing the value of . public GitHubClientFactory( IAssemblyInformationProvider assemblyInformationProvider, IHttpMessageHandlerFactory httpMessageHandlerFactory, - ILogger logger, - IOptions generalConfigurationOptions) + IOptionsMonitor generalConfigurationOptions, + ILogger logger) { this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider)); this.httpMessageHandlerFactory = httpMessageHandlerFactory ?? throw new ArgumentNullException(nameof(httpMessageHandlerFactory)); + this.generalConfigurationOptions = generalConfigurationOptions ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); - generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); clientCache = new Dictionary(); clientCacheSemaphore = new SemaphoreSlim(1, 1); @@ -98,7 +98,7 @@ namespace Tgstation.Server.Host.Utils.GitHub /// public async ValueTask CreateClient(CancellationToken cancellationToken) => (await GetOrCreateClient( - generalConfiguration.GitHubAccessToken, + generalConfigurationOptions.CurrentValue.GitHubAccessToken, null, cancellationToken))!; @@ -144,7 +144,7 @@ namespace Tgstation.Server.Host.Utils.GitHub var now = DateTimeOffset.UtcNow; cacheHit = clientCache.TryGetValue(cacheKey, out var tuple); - var tokenValid = cacheHit && (!tuple.Expiry.HasValue || tuple.Expiry.Value <= now); + var tokenValid = cacheHit && (!tuple.Expiry.HasValue || tuple.Expiry.Value > now); if (!tokenValid) { if (cacheHit) diff --git a/src/Tgstation.Server.Host/Utils/GitHub/GitHubServiceFactory.cs b/src/Tgstation.Server.Host/Utils/GitHub/GitHubServiceFactory.cs index 1d5fb5ef31..f7c1c43d9f 100644 --- a/src/Tgstation.Server.Host/Utils/GitHub/GitHubServiceFactory.cs +++ b/src/Tgstation.Server.Host/Utils/GitHub/GitHubServiceFactory.cs @@ -27,22 +27,22 @@ namespace Tgstation.Server.Host.Utils.GitHub /// /// The for the . /// - readonly UpdatesConfiguration updatesConfiguration; + readonly IOptionsMonitor updatesConfigurationOptions; /// /// Initializes a new instance of the class. /// /// The value of . /// The value of . - /// The containing value of . + /// The value of . public GitHubServiceFactory( IGitHubClientFactory gitHubClientFactory, ILoggerFactory loggerFactory, - IOptions updatesConfigurationOptions) + IOptionsMonitor updatesConfigurationOptions) { this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory)); this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); - updatesConfiguration = updatesConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(updatesConfigurationOptions)); + this.updatesConfigurationOptions = updatesConfigurationOptions ?? throw new ArgumentNullException(nameof(updatesConfigurationOptions)); } /// @@ -79,6 +79,6 @@ namespace Tgstation.Server.Host.Utils.GitHub => new( gitHubClient, loggerFactory.CreateLogger(), - updatesConfiguration); + updatesConfigurationOptions.CurrentValue); } } diff --git a/src/Tgstation.Server.Host/Utils/PortAllocator.cs b/src/Tgstation.Server.Host/Utils/PortAllocator.cs index ecaf79eeda..70c5a89d5e 100644 --- a/src/Tgstation.Server.Host/Utils/PortAllocator.cs +++ b/src/Tgstation.Server.Host/Utils/PortAllocator.cs @@ -34,16 +34,16 @@ namespace Tgstation.Server.Host.Utils /// readonly IPlatformIdentifier platformIdentifier; + /// + /// The of for the . + /// + readonly IOptions swarmConfigurationOptions; + /// /// The for the . /// readonly ILogger logger; - /// - /// The for the . - /// - readonly SwarmConfiguration swarmConfiguration; - /// /// The used to serialized port requisition requests. /// @@ -55,7 +55,7 @@ namespace Tgstation.Server.Host.Utils /// The value of . /// The value of . /// The value of . - /// The containing the value of . + /// The value of . /// The value of . public PortAllocator( IServerPortProvider serverPortProvider, @@ -67,7 +67,7 @@ namespace Tgstation.Server.Host.Utils this.serverPortProvider = serverPortProvider ?? throw new ArgumentNullException(nameof(serverPortProvider)); this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory)); this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier)); - swarmConfiguration = swarmConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(swarmConfigurationOptions)); + this.swarmConfigurationOptions = swarmConfigurationOptions ?? throw new ArgumentNullException(nameof(swarmConfigurationOptions)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); allocatorLock = new SemaphoreSlim(1); @@ -99,8 +99,7 @@ namespace Tgstation.Server.Host.Utils logger.LogTrace("Port allocation >= {basePort} requested...", basePort); var ddPorts = await databaseContext .DreamDaemonSettings - .AsQueryable() - .Where(x => x.Instance!.SwarmIdentifer == swarmConfiguration.Identifier) + .Where(x => x.Instance!.SwarmIdentifer == swarmConfigurationOptions.Value.Identifier) .Select(x => new { Port = x.Port!.Value, @@ -110,8 +109,7 @@ namespace Tgstation.Server.Host.Utils var dmPorts = await databaseContext .DreamMakerSettings - .AsQueryable() - .Where(x => x.Instance!.SwarmIdentifer == swarmConfiguration.Identifier) + .Where(x => x.Instance!.SwarmIdentifer == swarmConfigurationOptions.Value.Identifier) .Select(x => new { ApiValidationPort = x.ApiValidationPort!.Value, diff --git a/src/Tgstation.Server.Host/appsettings.yml b/src/Tgstation.Server.Host/appsettings.yml index ff244ea7e4..e0d490e6e4 100644 --- a/src/Tgstation.Server.Host/appsettings.yml +++ b/src/Tgstation.Server.Host/appsettings.yml @@ -92,7 +92,3 @@ Swarm: # Should be left empty if using swarm mode is not desired # PublicAddress: # The public address of the swarm node # ControllerAddress: # Required on non-controller nodes. The internal address of the swarm controller's API'. Should be left empty on the controller itself # UpdateRequiredNodeCount: # The number of nodes expected to be in the swarm before initiating an update. This should count every server irrespective of whether or not they are the controller MINUS 1 -Telemetry: - DisableVersionReporting: false # Prevents you installation and the version you're using from being reported on the source repository's deployments list - ServerFriendlyName: null # Sets a friendly name for your server in reported telemetry. Must be unique. First come first serve - VersionReportingRepositoryId: 841149827 # GitHub repostiory ID where the tgs_version_telemetry workflow can be found diff --git a/src/Tgstation.Server.Shared/Tgstation.Server.Shared.csproj b/src/Tgstation.Server.Shared/Tgstation.Server.Shared.csproj index e636c80a73..2f3bb04669 100644 --- a/src/Tgstation.Server.Shared/Tgstation.Server.Shared.csproj +++ b/src/Tgstation.Server.Shared/Tgstation.Server.Shared.csproj @@ -10,11 +10,11 @@ - + - + - + diff --git a/tests/DMAPI/BasicOperation/Test.dm b/tests/DMAPI/BasicOperation/Test.dm index 30b02a4bee..d6663296ef 100644 --- a/tests/DMAPI/BasicOperation/Test.dm +++ b/tests/DMAPI/BasicOperation/Test.dm @@ -26,12 +26,18 @@ FailTest("DMAPI Error: [message]") /proc/Run() + var/list/world_params = world.params + if("basic_reboot" in world_params || world_params["basic_reboot"] == "yes") + world.log << "Reboot path" + sleep(150) + world.Reboot() + return + world.log << "sleep" sleep(50) world.TgsTargetedChatBroadcast("Sample admin-only message", TRUE) world.log << "params check" - var/list/world_params = world.params if(!("test" in world_params) || world_params["test"] != "bababooey") FailTest("Expected parameter test=bababooey but did not receive", "test_fail_reason.txt") @@ -39,15 +45,16 @@ fdel("test_event_output.txt") var/test_data = "nwfiuurhfu" world.TgsTriggerEvent("test_event", list(test_data), TRUE) - if(!fexists("test_event_output.txt")) - FailTest("Expected test_event_output.txt to exist here", "test_fail_reason.txt") + if(world.TgsAvailable()) + if(!fexists("test_event_output.txt")) + FailTest("Expected test_event_output.txt to exist here", "test_fail_reason.txt") - var/test_contents = copytext(file2text("test_event_output.txt"), 1, length(test_data) + 1) - if(test_contents != test_data) - FailTest("Expected test_event_output.txt to contain [test_data] here. Got [test_contents]", "test_fail_reason.txt") + var/test_contents = copytext(file2text("test_event_output.txt"), 1, length(test_data) + 1) + if(test_contents != test_data) + FailTest("Expected test_event_output.txt to contain [test_data] here. Got [test_contents]", "test_fail_reason.txt") - world.log << "file check 1" - fdel("test_event_output.txt") + world.log << "file check 1" + fdel("test_event_output.txt") var/start_time = world.timeofday world.TgsTriggerEvent("test_event", list("asdf"), FALSE) @@ -57,10 +64,13 @@ world.log << "sleep2" sleep(150) - world.log << "Terminating..." + world.log << "Test Terminating..." world.TgsEndProcess() + if(world.TgsAvailable()) + FailTest("Expected TGS to not let us reach this point") - world.log << "You really shouldn't be able to read this" + del(world) + sleep(1) /world/Export(url) log << "Export: [url]" @@ -76,6 +86,7 @@ /world/Reboot(reason) TgsReboot() + return ..() /datum/tgs_chat_command/echo name = "echo" diff --git a/tests/DMAPI/LongRunning/Test.dm b/tests/DMAPI/LongRunning/Test.dm index d99513a6ef..de54b67098 100644 --- a/tests/DMAPI/LongRunning/Test.dm +++ b/tests/DMAPI/LongRunning/Test.dm @@ -193,6 +193,10 @@ var/run_bridge_test TestLegacyBridge() return "all gucci" + var/deploy_test = data["test_deployment_trigger"] + if(deploy_test) + return world.TgsTriggerDeployment() == TRUE ? "all gucci" : "deployment trigger failed!" + TgsChatBroadcast(new /datum/tgs_message_content("Received non-tgs topic: `[T]`")) return "feck" diff --git a/tests/DMAPI/test_setup.dm b/tests/DMAPI/test_setup.dm index ac345230b7..5c1103407f 100644 --- a/tests/DMAPI/test_setup.dm +++ b/tests/DMAPI/test_setup.dm @@ -1,5 +1,5 @@ /world/New() - log << "Starting test..." + log << "Starting test: [json_encode(params)]" text2file("SUCCESS", "test_success.txt") world.RunTest() diff --git a/tests/Tgstation.Server.Api.Tests/Models/Internal/TestEngineVersion.cs b/tests/Tgstation.Server.Api.Tests/Models/Internal/TestEngineVersion.cs index f13f09f3ff..cda2983b53 100644 --- a/tests/Tgstation.Server.Api.Tests/Models/Internal/TestEngineVersion.cs +++ b/tests/Tgstation.Server.Api.Tests/Models/Internal/TestEngineVersion.cs @@ -40,7 +40,7 @@ namespace Tgstation.Server.Api.Models.Internal.Tests Assert.IsFalse(EngineVersion.TryParse("x", out version)); Assert.IsNull(version); - Assert.ThrowsException(() => EngineVersion.Parse("x")); + Assert.ThrowsExactly(() => EngineVersion.Parse("x")); } } } diff --git a/tests/Tgstation.Server.Api.Tests/TestApiHeaders.cs b/tests/Tgstation.Server.Api.Tests/TestApiHeaders.cs index 8d6808f1b2..a0cf7fc181 100644 --- a/tests/Tgstation.Server.Api.Tests/TestApiHeaders.cs +++ b/tests/Tgstation.Server.Api.Tests/TestApiHeaders.cs @@ -21,8 +21,8 @@ namespace Tgstation.Server.Api.Tests [TestMethod] public void TestConstruction() { - Assert.ThrowsException(() => new ApiHeaders(null, null)); - Assert.ThrowsException(() => new ApiHeaders(productHeaderValue, null)); + Assert.ThrowsExactly(() => new ApiHeaders(null, null)); + Assert.ThrowsExactly(() => new ApiHeaders(productHeaderValue, null)); var headers = new ApiHeaders(productHeaderValue, new TokenResponse { Bearer = String.Empty }); headers = new ApiHeaders(productHeaderValue, String.Empty, OAuthProvider.GitHub); } @@ -54,7 +54,7 @@ namespace Tgstation.Server.Api.Tests Assert.AreEqual(ConformantHeader, header.RawUserAgent); Assert.IsNotNull(header.UserAgent); - Assert.ThrowsException(() => TestHeader(String.Empty)); + Assert.ThrowsExactly(() => TestHeader(String.Empty)); } } } diff --git a/tests/Tgstation.Server.Api.Tests/Tgstation.Server.Api.Tests.csproj b/tests/Tgstation.Server.Api.Tests/Tgstation.Server.Api.Tests.csproj index 2f6f44c567..9daf4ea33c 100644 --- a/tests/Tgstation.Server.Api.Tests/Tgstation.Server.Api.Tests.csproj +++ b/tests/Tgstation.Server.Api.Tests/Tgstation.Server.Api.Tests.csproj @@ -6,7 +6,7 @@ - + diff --git a/tests/Tgstation.Server.Client.Tests/TestApiClient.cs b/tests/Tgstation.Server.Client.Tests/TestApiClient.cs index 900d64a56f..ca18f42d63 100644 --- a/tests/Tgstation.Server.Client.Tests/TestApiClient.cs +++ b/tests/Tgstation.Server.Client.Tests/TestApiClient.cs @@ -15,6 +15,7 @@ using Tgstation.Server.Api.Models; using Tgstation.Server.Api.Models.Internal; using Tgstation.Server.Api.Models.Response; using Tgstation.Server.Common.Http; +using Tgstation.Server.Common.Tests; namespace Tgstation.Server.Client.Tests { @@ -44,11 +45,10 @@ namespace Tgstation.Server.Client.Tests Content = new StringContent(sampleJson) }; - var httpClient = new Mock(); - httpClient.Setup(x => x.SendAsync(It.IsNotNull(), It.IsAny(), It.IsAny())).Returns(Task.FromResult(response)); + var handler = new MockHttpMessageHandler((_, __) => Task.FromResult(response)); var client = new ApiClient( - httpClient.Object, + new HttpClient(handler), new Uri("http://fake.com"), new ApiHeaders( new ProductHeaderValue("fake"), @@ -84,11 +84,10 @@ namespace Tgstation.Server.Client.Tests Content = new StringContent(fakeJson) }; - var httpClient = new Mock(); - httpClient.Setup(x => x.SendAsync(It.IsNotNull(), It.IsAny(), It.IsAny())).Returns(Task.FromResult(response)); + var handler = new MockHttpMessageHandler((_, __) => Task.FromResult(response)); var client = new ApiClient( - httpClient.Object, + new HttpClient(handler), new Uri("http://fake.com"), new ApiHeaders( new ProductHeaderValue("fake"), @@ -99,7 +98,7 @@ namespace Tgstation.Server.Client.Tests null, false); - await Assert.ThrowsExceptionAsync(() => client.Read(Routes.Engine, default).AsTask()); + await Assert.ThrowsExactlyAsync(() => client.Read(Routes.Engine, default).AsTask()); } } } diff --git a/tests/Tgstation.Server.Client.Tests/TestServerClientFactory.cs b/tests/Tgstation.Server.Client.Tests/TestServerClientFactory.cs index c9af0484da..97023d7b62 100644 --- a/tests/Tgstation.Server.Client.Tests/TestServerClientFactory.cs +++ b/tests/Tgstation.Server.Client.Tests/TestServerClientFactory.cs @@ -10,7 +10,7 @@ namespace Tgstation.Server.Client.Tests [TestMethod] public void TestConstruction() { - Assert.ThrowsException(() => new RestServerClientFactory(null)); + Assert.ThrowsExactly(() => new RestServerClientFactory(null)); new RestServerClientFactory(new ProductHeaderValue("Tgstation.Server.Client.Tests", GetType().Assembly.GetName().Version.ToString())); } } diff --git a/tests/Tgstation.Server.Client.Tests/Tgstation.Server.Client.Tests.csproj b/tests/Tgstation.Server.Client.Tests/Tgstation.Server.Client.Tests.csproj index 66888f4430..9167b66ed3 100644 --- a/tests/Tgstation.Server.Client.Tests/Tgstation.Server.Client.Tests.csproj +++ b/tests/Tgstation.Server.Client.Tests/Tgstation.Server.Client.Tests.csproj @@ -6,11 +6,12 @@ - + + diff --git a/tests/Tgstation.Server.Common.Tests/Extensions/TestVersionExtensions.cs b/tests/Tgstation.Server.Common.Tests/Extensions/TestVersionExtensions.cs new file mode 100644 index 0000000000..d5f7fee277 --- /dev/null +++ b/tests/Tgstation.Server.Common.Tests/Extensions/TestVersionExtensions.cs @@ -0,0 +1,19 @@ +using System; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Tgstation.Server.Common.Extensions.Tests +{ + /// + /// Tests for . + /// + [TestClass] + public sealed class TestVersionExtensions + { + [TestMethod] + public void TestSemver() + { + Assert.AreEqual(new Version(1, 2, 3), new Version(1, 2, 3, 4).Semver()); + } + } +} diff --git a/tests/Tgstation.Server.Common.Tests/MockHttpMessageHandler.cs b/tests/Tgstation.Server.Common.Tests/MockHttpMessageHandler.cs new file mode 100644 index 0000000000..e4fc02dd89 --- /dev/null +++ b/tests/Tgstation.Server.Common.Tests/MockHttpMessageHandler.cs @@ -0,0 +1,23 @@ +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace Tgstation.Server.Common.Tests +{ + /// + /// Simple mock . + /// + public sealed class MockHttpMessageHandler : HttpMessageHandler + { + readonly Func> callback; + + public MockHttpMessageHandler(Func> callback) + { + this.callback = callback ?? throw new ArgumentNullException(nameof(callback)); + } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + => callback(request, cancellationToken); + } +} diff --git a/tests/Tgstation.Server.Common.Tests/Tgstation.Server.Common.Tests.csproj b/tests/Tgstation.Server.Common.Tests/Tgstation.Server.Common.Tests.csproj new file mode 100644 index 0000000000..e915d283b0 --- /dev/null +++ b/tests/Tgstation.Server.Common.Tests/Tgstation.Server.Common.Tests.csproj @@ -0,0 +1,12 @@ + + + + + $(TgsFrameworkVersion) + + + + + + + diff --git a/tests/Tgstation.Server.Host.Service.Tests/TestServerService.cs b/tests/Tgstation.Server.Host.Service.Tests/TestServerService.cs index 6faf32f95e..602e50e72d 100644 --- a/tests/Tgstation.Server.Host.Service.Tests/TestServerService.cs +++ b/tests/Tgstation.Server.Host.Service.Tests/TestServerService.cs @@ -19,9 +19,9 @@ namespace Tgstation.Server.Host.Service.Tests [TestMethod] public void TestConstructionAndDisposal() { - Assert.ThrowsException(() => new ServerService(null, null, default)); + Assert.ThrowsExactly(() => new ServerService(null, null, default)); var mockWatchdogFactory = new Mock(); - Assert.ThrowsException(() => new ServerService(mockWatchdogFactory.Object, null, default)); + Assert.ThrowsExactly(() => new ServerService(mockWatchdogFactory.Object, null, default)); new ServerService(mockWatchdogFactory.Object, Array.Empty(), default).Dispose(); } diff --git a/tests/Tgstation.Server.Host.Tests.Signals/Program.cs b/tests/Tgstation.Server.Host.Tests.Signals/Program.cs index bcb5aa6699..7c4ba7d972 100644 --- a/tests/Tgstation.Server.Host.Tests.Signals/Program.cs +++ b/tests/Tgstation.Server.Host.Tests.Signals/Program.cs @@ -34,7 +34,7 @@ namespace Tgstation.Server.Host.Tests.Signals Assert.IsFalse(tcs.Task.IsCompleted); - await Assert.ThrowsExceptionAsync(() => signalHandler.StartAsync(default)); + await Assert.ThrowsExactlyAsync(() => signalHandler.StartAsync(default)); Assert.IsFalse(tcs.Task.IsCompleted); diff --git a/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestDiscordProvider.cs b/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestDiscordProvider.cs index 58c63c6a99..da872d6f63 100644 --- a/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestDiscordProvider.cs +++ b/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestDiscordProvider.cs @@ -4,6 +4,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; @@ -56,17 +57,17 @@ namespace Tgstation.Server.Host.Components.Chat.Providers.Tests Instance = new Models.Instance(), }; - Assert.ThrowsException(() => new DiscordProvider(null, null, null, null, null, null)); - Assert.ThrowsException(() => new DiscordProvider(mockJobManager, null, null, null, null, null)); + Assert.ThrowsExactly(() => new DiscordProvider(null, null, null, null, null, null)); + Assert.ThrowsExactly(() => new DiscordProvider(mockJobManager, null, null, null, null, null)); var mockDel = Mock.Of(); - Assert.ThrowsException(() => new DiscordProvider(mockJobManager, mockDel, null, null, null, null)); + Assert.ThrowsExactly(() => new DiscordProvider(mockJobManager, mockDel, null, null, null, null)); var mockLogger = Mock.Of>(); - Assert.ThrowsException(() => new DiscordProvider(mockJobManager, mockDel, mockLogger, null, null, null)); + Assert.ThrowsExactly(() => new DiscordProvider(mockJobManager, mockDel, mockLogger, null, null, null)); var mockAss = Mock.Of(); - Assert.ThrowsException(() => new DiscordProvider(mockJobManager, mockDel, mockLogger, mockAss, null, null)); - Assert.ThrowsException(() => new DiscordProvider(mockJobManager, mockDel, mockLogger, mockAss, bot, null)); - var mockGen = new GeneralConfiguration(); - await new DiscordProvider(mockJobManager, mockDel, mockLogger, mockAss, bot, mockGen).DisposeAsync(); + Assert.ThrowsExactly(() => new DiscordProvider(mockJobManager, mockDel, mockLogger, mockAss, null, null)); + var mockGen = Mock.Of>(); + Assert.ThrowsExactly(() => new DiscordProvider(mockJobManager, mockDel, mockLogger, mockAss, mockGen, null)); + await new DiscordProvider(mockJobManager, mockDel, mockLogger, mockAss, mockGen, bot).DisposeAsync(); } static ValueTask InvokeConnect(IProvider provider, CancellationToken cancellationToken = default) => (ValueTask)provider.GetType().GetMethod("Connect", BindingFlags.Instance | BindingFlags.NonPublic).Invoke(provider, new object[] { cancellationToken }); @@ -75,13 +76,13 @@ namespace Tgstation.Server.Host.Components.Chat.Providers.Tests public async Task TestConnectWithFakeTokenFails() { var mockLogger = new Mock>(); - await using var provider = new DiscordProvider(mockJobManager, Mock.Of(), mockLogger.Object, Mock.Of(), new ChatBot + await using var provider = new DiscordProvider(mockJobManager, Mock.Of(), mockLogger.Object, Mock.Of(), Mock.Of>(), new ChatBot { ReconnectionInterval = 1, ConnectionString = "asdf", Instance = new Models.Instance(), - }, new GeneralConfiguration()); - await Assert.ThrowsExceptionAsync(async () => await InvokeConnect(provider)); + }); + await Assert.ThrowsExactlyAsync(async () => await InvokeConnect(provider)); Assert.IsFalse(provider.Connected); } @@ -95,7 +96,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers.Tests Assert.Fail("TGS_TEST_DISCORD_TOKEN is not a valid Discord connection string!"); var mockLogger = new Mock>(); - await using var provider = new DiscordProvider(mockJobManager, Mock.Of(), mockLogger.Object, Mock.Of(), testToken1, new GeneralConfiguration()); + await using var provider = new DiscordProvider(mockJobManager, Mock.Of(), mockLogger.Object, Mock.Of(), Mock.Of>(), testToken1); Assert.IsFalse(provider.Connected); await InvokeConnect(provider); Assert.IsTrue(provider.Connected); diff --git a/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestIrcProvider.cs b/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestIrcProvider.cs index b39f84f4c0..400bbbb563 100644 --- a/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestIrcProvider.cs +++ b/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestIrcProvider.cs @@ -4,6 +4,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; @@ -23,15 +24,13 @@ namespace Tgstation.Server.Host.Components.Chat.Providers.Tests [TestMethod] public async Task TestConstructionAndDisposal() { - Assert.ThrowsException(() => new IrcProvider(null, null, null, null, null, null)); + Assert.ThrowsExactly(() => new IrcProvider(null, null, null, null, null, null)); var mockJobManager = new Mock(); - Assert.ThrowsException(() => new IrcProvider(mockJobManager.Object, null, null, null, null, null)); + Assert.ThrowsExactly(() => new IrcProvider(mockJobManager.Object, null, null, null, null, null)); var mockAsyncDelayer = new Mock(); - Assert.ThrowsException(() => new IrcProvider(mockJobManager.Object, mockAsyncDelayer.Object, null, null, null, null)); + Assert.ThrowsExactly(() => new IrcProvider(mockJobManager.Object, mockAsyncDelayer.Object, null, null, null, null)); var mockLogger = new Mock>(); - Assert.ThrowsException(() => new IrcProvider(mockJobManager.Object, mockAsyncDelayer.Object, mockLogger.Object, null, null, null)); - var mockAss = new Mock(); - Assert.ThrowsException(() => new IrcProvider(mockJobManager.Object, mockAsyncDelayer.Object, mockLogger.Object, mockAss.Object, null, null)); + Assert.ThrowsExactly(() => new IrcProvider(mockJobManager.Object, mockAsyncDelayer.Object, mockLogger.Object, null, null, null)); var mockBot = new ChatBot { @@ -39,10 +38,14 @@ namespace Tgstation.Server.Host.Components.Chat.Providers.Tests Instance = new Models.Instance(), Provider = ChatProvider.Irc }; - Assert.ThrowsException(() => new IrcProvider(mockJobManager.Object, mockAsyncDelayer.Object, mockLogger.Object, mockAss.Object, mockBot, null)); + Assert.ThrowsExactly(() => new IrcProvider(mockJobManager.Object, mockAsyncDelayer.Object, mockLogger.Object, mockBot, null, null)); - var mockLogConf = new FileLoggingConfiguration(); - Assert.ThrowsException(() => new IrcProvider(mockJobManager.Object, mockAsyncDelayer.Object, mockLogger.Object, mockAss.Object, mockBot, mockLogConf)); + var mockAss = new Mock(); + Assert.ThrowsExactly(() => new IrcProvider(mockJobManager.Object, mockAsyncDelayer.Object, mockLogger.Object, mockBot, mockAss.Object, null)); + + var mockLogConf = new Mock>(); + mockLogConf.SetupGet(x => x.CurrentValue).Returns(new FileLoggingConfiguration()); + Assert.ThrowsExactly(() => new IrcProvider(mockJobManager.Object, mockAsyncDelayer.Object, mockLogger.Object, mockBot, mockAss.Object, mockLogConf.Object)); mockBot.ConnectionString = new IrcConnectionStringBuilder { @@ -52,7 +55,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers.Tests Port = 6667 }.ToString(); - await new IrcProvider(mockJobManager.Object, mockAsyncDelayer.Object, mockLogger.Object, mockAss.Object, mockBot, mockLogConf).DisposeAsync(); + await new IrcProvider(mockJobManager.Object, mockAsyncDelayer.Object, mockLogger.Object, mockBot, mockAss.Object, mockLogConf.Object).DisposeAsync(); } static ValueTask InvokeConnect(IProvider provider, CancellationToken cancellationToken = default) => (ValueTask)provider.GetType().GetMethod("Connect", BindingFlags.Instance | BindingFlags.NonPublic).Invoke(provider, new object[] { cancellationToken }); @@ -89,7 +92,9 @@ namespace Tgstation.Server.Host.Components.Chat.Providers.Tests Instance = new Models.Instance(), }; - await using var provider = new IrcProvider(mockJobManager, new AsyncDelayer(loggerFactory.CreateLogger()), loggerFactory.CreateLogger(), Mock.Of(), chatBot, new FileLoggingConfiguration()); + var mockLogConf = new Mock>(); + mockLogConf.SetupGet(x => x.CurrentValue).Returns(new FileLoggingConfiguration()); + await using var provider = new IrcProvider(mockJobManager, new AsyncDelayer(loggerFactory.CreateLogger()), loggerFactory.CreateLogger(), chatBot, Mock.Of(), mockLogConf.Object); Assert.IsFalse(provider.Connected); await InvokeConnect(provider); Assert.IsTrue(provider.Connected); diff --git a/tests/Tgstation.Server.Host.Tests/Components/Engine/TestOpenDreamInstaller.cs b/tests/Tgstation.Server.Host.Tests/Components/Engine/TestOpenDreamInstaller.cs index 8950acc6ab..01a53b348e 100644 --- a/tests/Tgstation.Server.Host.Tests/Components/Engine/TestOpenDreamInstaller.cs +++ b/tests/Tgstation.Server.Host.Tests/Components/Engine/TestOpenDreamInstaller.cs @@ -1,4 +1,5 @@ -using System.Threading; +using System.Net.Http; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -8,8 +9,6 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using Tgstation.Server.Api.Models; -using Tgstation.Server.Api.Models.Internal; -using Tgstation.Server.Common.Http; using Tgstation.Server.Host.Components.Repository; using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.IO; @@ -36,13 +35,13 @@ namespace Tgstation.Server.Host.Components.Engine.Tests static async Task RepoDownloadTest(bool needsClone) { - var mockGeneralConfigOptions = new Mock>(); + var mockGeneralConfigOptions = new Mock>(); var generalConfig = new GeneralConfiguration(); - var mockSessionConfigOptions = new Mock>(); + var mockSessionConfigOptions = new Mock>(); var sessionConfig = new SessionConfiguration(); Assert.IsNotNull(generalConfig.OpenDreamGitUrl); - mockGeneralConfigOptions.SetupGet(x => x.Value).Returns(generalConfig); - mockSessionConfigOptions.SetupGet(x => x.Value).Returns(sessionConfig); + mockGeneralConfigOptions.SetupGet(x => x.CurrentValue).Returns(generalConfig); + mockSessionConfigOptions.SetupGet(x => x.CurrentValue).Returns(sessionConfig); var cloneAttempts = 0; var mockRepository = new Mock(); @@ -76,7 +75,7 @@ namespace Tgstation.Server.Host.Components.Engine.Tests Mock.Of(), mockRepositoryManager.Object, Mock.Of(), - Mock.Of(), + Mock.Of(), mockGeneralConfigOptions.Object, mockSessionConfigOptions.Object); diff --git a/tests/Tgstation.Server.Host.Tests/Components/Engine/TestPosixByondInstaller.cs b/tests/Tgstation.Server.Host.Tests/Components/Engine/TestPosixByondInstaller.cs index 6dbfb155b4..3a45b4edf9 100644 --- a/tests/Tgstation.Server.Host.Tests/Components/Engine/TestPosixByondInstaller.cs +++ b/tests/Tgstation.Server.Host.Tests/Components/Engine/TestPosixByondInstaller.cs @@ -1,15 +1,18 @@ -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Moq; -using System; +using System; using System.IO; +using System.IO.Abstractions.TestingHelpers; using System.Linq; using System.Reflection; +using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Moq; + using Tgstation.Server.Api.Models; -using Tgstation.Server.Api.Models.Internal; using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.IO; @@ -21,15 +24,15 @@ namespace Tgstation.Server.Host.Components.Engine.Tests [TestMethod] public void TestConstruction() { - Assert.ThrowsException(() => new PosixByondInstaller(null, null, null, null, null)); + Assert.ThrowsExactly(() => new PosixByondInstaller(null, null, null, null, null)); var mockPostWriteHandler = new Mock(); - Assert.ThrowsException(() => new PosixByondInstaller(mockPostWriteHandler.Object, null, null, null, null)); + Assert.ThrowsExactly(() => new PosixByondInstaller(mockPostWriteHandler.Object, null, null, null, null)); var mockIOManager = new Mock(); - Assert.ThrowsException(() => new PosixByondInstaller(mockPostWriteHandler.Object, mockIOManager.Object, null, null, null)); + Assert.ThrowsExactly(() => new PosixByondInstaller(mockPostWriteHandler.Object, mockIOManager.Object, null, null, null)); var mockFileDownloader = Mock.Of(); - Assert.ThrowsException(() => new PosixByondInstaller(mockPostWriteHandler.Object, mockIOManager.Object, mockFileDownloader, null, null)); + Assert.ThrowsExactly(() => new PosixByondInstaller(mockPostWriteHandler.Object, mockIOManager.Object, mockFileDownloader, null, null)); var mockOptions = Mock.Of>(); - Assert.ThrowsException(() => new PosixByondInstaller(mockPostWriteHandler.Object, mockIOManager.Object, mockFileDownloader, mockOptions, null)); + Assert.ThrowsExactly(() => new PosixByondInstaller(mockPostWriteHandler.Object, mockIOManager.Object, mockFileDownloader, mockOptions, null)); var mockLogger = new Mock>(); _ = new PosixByondInstaller(mockPostWriteHandler.Object, mockIOManager.Object, mockFileDownloader, mockOptions, mockLogger.Object); @@ -64,7 +67,7 @@ namespace Tgstation.Server.Host.Components.Engine.Tests var installer = new PosixByondInstaller(mockPostWriteHandler.Object, mockIOManager.Object, mockFileDownloader.Object, mockOptions.Object, mockLogger.Object); - await Assert.ThrowsExceptionAsync(() => installer.DownloadVersion(null, null, default).AsTask()); + await Assert.ThrowsExactlyAsync(() => installer.DownloadVersion(null, null, default).AsTask()); var ourArray = Array.Empty(); mockFileDownloader @@ -96,6 +99,10 @@ namespace Tgstation.Server.Host.Components.Engine.Tests public async Task TestInstallByond() { var mockIOManager = new Mock(); + mockIOManager.Setup(x => x.FileExists(It.IsNotNull(), It.IsAny())).ReturnsAsync(true); + mockIOManager.Setup(x => x.CreateResolverForSubdirectory(It.IsNotNull())).Returns(mockIOManager.Object); + mockIOManager.Setup(x => x.ConcatPath(It.IsNotNull())).Returns(Path.Combine); + mockIOManager.Setup(x => x.ResolvePath(It.IsNotNull())).Returns(path => path); var mockPostWriteHandler = new Mock(); var mockLogger = new Mock>(); var mockFileDownloader = Mock.Of(); @@ -103,7 +110,7 @@ namespace Tgstation.Server.Host.Components.Engine.Tests var installer = new PosixByondInstaller(mockPostWriteHandler.Object, mockIOManager.Object, mockFileDownloader, mockOptions, mockLogger.Object); const string FakePath = "fake"; - await Assert.ThrowsExceptionAsync(() => installer.Install(null, null, false, default).AsTask()); + await Assert.ThrowsExactlyAsync(() => installer.Install(null, null, false, default).AsTask()); var byondVersion = new EngineVersion { @@ -111,7 +118,7 @@ namespace Tgstation.Server.Host.Components.Engine.Tests Version = new Version(123, 252345), }; - await Assert.ThrowsExceptionAsync(() => installer.Install(byondVersion, null, false, default).AsTask()); + await Assert.ThrowsExactlyAsync(() => installer.Install(byondVersion, null, false, default).AsTask()); byondVersion.Version = new Version(511, 1385); await installer.Install(byondVersion, FakePath, false, default); diff --git a/tests/Tgstation.Server.Host.Tests/Components/Events/TestEventScriptAttribute.cs b/tests/Tgstation.Server.Host.Tests/Components/Events/TestEventScriptAttribute.cs index 232f8767d6..6082c2dc7b 100644 --- a/tests/Tgstation.Server.Host.Tests/Components/Events/TestEventScriptAttribute.cs +++ b/tests/Tgstation.Server.Host.Tests/Components/Events/TestEventScriptAttribute.cs @@ -14,7 +14,7 @@ namespace Tgstation.Server.Host.Components.Events.Tests [TestMethod] public void TestConstruction() { - Assert.ThrowsException(() => new EventScriptAttribute(null)); + Assert.ThrowsExactly(() => new EventScriptAttribute(null)); var test = new EventScriptAttribute("test1", "test2"); Assert.IsTrue(test.ScriptNames.SequenceEqual(["test1", "test2"])); } diff --git a/tests/Tgstation.Server.Host.Tests/Components/Repository/TestRepositoryFactory.cs b/tests/Tgstation.Server.Host.Tests/Components/Repository/TestRepositoryFactory.cs index 84aea24ef6..b080816cb1 100644 --- a/tests/Tgstation.Server.Host.Tests/Components/Repository/TestRepositoryFactory.cs +++ b/tests/Tgstation.Server.Host.Tests/Components/Repository/TestRepositoryFactory.cs @@ -23,7 +23,7 @@ namespace Tgstation.Server.Host.Components.Repository.Tests .CreateFromPath(path, default)); [TestMethod] - public void TestConstructionThrows() => Assert.ThrowsException(() => new LibGit2RepositoryFactory(null)); + public void TestConstructionThrows() => Assert.ThrowsExactly(() => new LibGit2RepositoryFactory(null)); [TestMethod] public void TestInMemoryRepoCreation() diff --git a/tests/Tgstation.Server.Host.Tests/Components/Repository/TestRepositoryManager.cs b/tests/Tgstation.Server.Host.Tests/Components/Repository/TestRepositoryManager.cs index 2cdd59d19b..317798e10f 100644 --- a/tests/Tgstation.Server.Host.Tests/Components/Repository/TestRepositoryManager.cs +++ b/tests/Tgstation.Server.Host.Tests/Components/Repository/TestRepositoryManager.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using LibGit2Sharp; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; @@ -47,9 +48,9 @@ namespace Tgstation.Server.Host.Components.Repository.Tests Mock.Of(), Mock.Of(), mockGitRemoteFeaturesFactory.Object, + Mock.Of>(), Mock.Of>(), - Mock.Of>(), - new GeneralConfiguration()); + Mock.Of>()); } [TestCleanup] diff --git a/tests/Tgstation.Server.Host.Tests/Components/StaticFiles/TestConfiguration.cs b/tests/Tgstation.Server.Host.Tests/Components/StaticFiles/TestConfiguration.cs index 7314b0783a..1eeb0bdb30 100644 --- a/tests/Tgstation.Server.Host.Tests/Components/StaticFiles/TestConfiguration.cs +++ b/tests/Tgstation.Server.Host.Tests/Components/StaticFiles/TestConfiguration.cs @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; @@ -56,13 +57,13 @@ namespace Tgstation.Server.Host.Components.StaticFiles.Tests Mock.Of(), Mock.Of(), Mock.Of(), + Mock.Of>(), + Mock.Of>(), loggerFactory.CreateLogger(), new Models.Instance { Path = "Some path", - }, - new GeneralConfiguration(), - new SessionConfiguration()); + }); await configuration.StartAsync(CancellationToken.None); diff --git a/tests/Tgstation.Server.Host.Tests/Core/TestApplication.cs b/tests/Tgstation.Server.Host.Tests/Core/TestApplication.cs index b6edec53b0..67283c9b2b 100644 --- a/tests/Tgstation.Server.Host.Tests/Core/TestApplication.cs +++ b/tests/Tgstation.Server.Host.Tests/Core/TestApplication.cs @@ -26,61 +26,61 @@ namespace Tgstation.Server.Host.Core.Tests [TestMethod] public void TestMethodThrows() { - Assert.ThrowsException(() => new Application(null, null)); + Assert.ThrowsExactly(() => new Application(null, null)); var mockConfiguration = new Mock(); - Assert.ThrowsException(() => new Application(mockConfiguration.Object, null)); + Assert.ThrowsExactly(() => new Application(mockConfiguration.Object, null)); var mockHostingEnvironment = new Mock(); var app = new Application(mockConfiguration.Object, mockHostingEnvironment.Object); - Assert.ThrowsException(() => app.ConfigureServices(null, null, null)); + Assert.ThrowsExactly(() => app.ConfigureServices(null, null, null)); var mockServiceCollection = Mock.Of(); - Assert.ThrowsException(() => app.ConfigureServices(mockServiceCollection, null, null)); + Assert.ThrowsExactly(() => app.ConfigureServices(mockServiceCollection, null, null)); - Assert.ThrowsException(() => app.ConfigureServices(mockServiceCollection, null, null)); + Assert.ThrowsExactly(() => app.ConfigureServices(mockServiceCollection, null, null)); - Assert.ThrowsException(() => app.Configure(null, null, null, null, null, null, null, null, null, null, null, null)); + Assert.ThrowsExactly(() => app.Configure(null, null, null, null, null, null, null, null, null, null, null, null)); var mockAppBuilder = new Mock(); - Assert.ThrowsException(() => app.Configure(mockAppBuilder.Object, null, null, null, null, null, null, null, null, null, null, null)); + Assert.ThrowsExactly(() => app.Configure(mockAppBuilder.Object, null, null, null, null, null, null, null, null, null, null, null)); var mockServerControl = new Mock(); - Assert.ThrowsException(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, null, null, null, null, null, null, null, null, null, null)); + Assert.ThrowsExactly(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, null, null, null, null, null, null, null, null, null, null)); var mockTokenFactory = new Mock(); - Assert.ThrowsException(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, null, null, null, null, null, null, null, null, null)); + Assert.ThrowsExactly(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, null, null, null, null, null, null, null, null, null)); var mockServerPortProvider = new Mock(); mockServerPortProvider.SetupGet(x => x.HttpApiPort).Returns(5345); - Assert.ThrowsException(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, mockServerPortProvider.Object, null, null, null, null, null, null, null, null)); + Assert.ThrowsExactly(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, mockServerPortProvider.Object, null, null, null, null, null, null, null, null)); var mockAssemblyInformationProvider = Mock.Of(); - Assert.ThrowsException(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, mockServerPortProvider.Object, mockAssemblyInformationProvider, null, null, null, null, null, null, null)); + Assert.ThrowsExactly(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, mockServerPortProvider.Object, mockAssemblyInformationProvider, null, null, null, null, null, null, null)); var mockControlPanelOptions = new Mock>(); mockControlPanelOptions.SetupGet(x => x.Value).Returns(new ControlPanelConfiguration()).Verifiable(); - Assert.ThrowsException(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, mockServerPortProvider.Object, mockAssemblyInformationProvider, mockControlPanelOptions.Object, null, null, null, null, null, null)); + Assert.ThrowsExactly(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, mockServerPortProvider.Object, mockAssemblyInformationProvider, mockControlPanelOptions.Object, null, null, null, null, null, null)); var mockGeneralOptions = new Mock>(); mockGeneralOptions.SetupGet(x => x.Value).Returns(new GeneralConfiguration()).Verifiable(); - Assert.ThrowsException(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, mockServerPortProvider.Object, mockAssemblyInformationProvider, mockControlPanelOptions.Object, mockGeneralOptions.Object, null, null, null, null, null)); + Assert.ThrowsExactly(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, mockServerPortProvider.Object, mockAssemblyInformationProvider, mockControlPanelOptions.Object, mockGeneralOptions.Object, null, null, null, null, null)); var mockDatabaseOptions = new Mock>(); mockDatabaseOptions.SetupGet(x => x.Value).Returns(new DatabaseConfiguration()).Verifiable(); - Assert.ThrowsException(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, mockServerPortProvider.Object, mockAssemblyInformationProvider, mockControlPanelOptions.Object, mockGeneralOptions.Object, mockDatabaseOptions.Object, null, null, null, null)); + Assert.ThrowsExactly(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, mockServerPortProvider.Object, mockAssemblyInformationProvider, mockControlPanelOptions.Object, mockGeneralOptions.Object, mockDatabaseOptions.Object, null, null, null, null)); var mockSecurityOptions = new Mock>(); mockDatabaseOptions.SetupGet(x => x.Value).Returns(new DatabaseConfiguration()).Verifiable(); - Assert.ThrowsException(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, mockServerPortProvider.Object, mockAssemblyInformationProvider, mockControlPanelOptions.Object, mockGeneralOptions.Object, mockDatabaseOptions.Object, mockSecurityOptions.Object, null, null, null)); + Assert.ThrowsExactly(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, mockServerPortProvider.Object, mockAssemblyInformationProvider, mockControlPanelOptions.Object, mockGeneralOptions.Object, mockDatabaseOptions.Object, mockSecurityOptions.Object, null, null, null)); var mockSwarmOptions = new Mock>(); mockSwarmOptions.SetupGet(x => x.Value).Returns(new SwarmConfiguration()).Verifiable(); - Assert.ThrowsException(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, mockServerPortProvider.Object, mockAssemblyInformationProvider, mockControlPanelOptions.Object, mockGeneralOptions.Object, mockDatabaseOptions.Object, mockSecurityOptions.Object, mockSwarmOptions.Object, null, null)); + Assert.ThrowsExactly(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, mockServerPortProvider.Object, mockAssemblyInformationProvider, mockControlPanelOptions.Object, mockGeneralOptions.Object, mockDatabaseOptions.Object, mockSecurityOptions.Object, mockSwarmOptions.Object, null, null)); var mockInternalOptions = new Mock>(); mockInternalOptions.SetupGet(x => x.Value).Returns(new InternalConfiguration()).Verifiable(); - Assert.ThrowsException(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, mockServerPortProvider.Object, mockAssemblyInformationProvider, mockControlPanelOptions.Object, mockGeneralOptions.Object, mockDatabaseOptions.Object, mockSecurityOptions.Object, mockSwarmOptions.Object, mockInternalOptions.Object, null)); + Assert.ThrowsExactly(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, mockServerPortProvider.Object, mockAssemblyInformationProvider, mockControlPanelOptions.Object, mockGeneralOptions.Object, mockDatabaseOptions.Object, mockSecurityOptions.Object, mockSwarmOptions.Object, mockInternalOptions.Object, null)); mockControlPanelOptions.VerifyAll(); mockInternalOptions.VerifyAll(); diff --git a/tests/Tgstation.Server.Host.Tests/Core/TestServerUpdateInitiator.cs b/tests/Tgstation.Server.Host.Tests/Core/TestServerUpdateInitiator.cs index 41bde8f588..2b345fb8d0 100644 --- a/tests/Tgstation.Server.Host.Tests/Core/TestServerUpdateInitiator.cs +++ b/tests/Tgstation.Server.Host.Tests/Core/TestServerUpdateInitiator.cs @@ -17,8 +17,8 @@ namespace Tgstation.Server.Host.Core.Tests [TestMethod] public void TestConstructor() { - Assert.ThrowsException(() => new ServerUpdateInitiator(null, null)); - Assert.ThrowsException(() => new ServerUpdateInitiator(Mock.Of(), null)); + Assert.ThrowsExactly(() => new ServerUpdateInitiator(null, null)); + Assert.ThrowsExactly(() => new ServerUpdateInitiator(Mock.Of(), null)); _ = new ServerUpdateInitiator(Mock.Of(), Mock.Of()); } diff --git a/tests/Tgstation.Server.Host.Tests/Database/TestDatabaseConnectionFactory.cs b/tests/Tgstation.Server.Host.Tests/Database/TestDatabaseConnectionFactory.cs index b2964fd934..2076ba80bc 100644 --- a/tests/Tgstation.Server.Host.Tests/Database/TestDatabaseConnectionFactory.cs +++ b/tests/Tgstation.Server.Host.Tests/Database/TestDatabaseConnectionFactory.cs @@ -16,8 +16,8 @@ namespace Tgstation.Server.Host.Database.Tests public void TestBadParameters() { var factory = new DatabaseConnectionFactory(); - Assert.ThrowsException(() => factory.CreateConnection(null, default)); - Assert.ThrowsException(() => factory.CreateConnection(String.Empty, (DatabaseType)42)); + Assert.ThrowsExactly(() => factory.CreateConnection(null, default)); + Assert.ThrowsExactly(() => factory.CreateConnection(String.Empty, (DatabaseType)42)); } [TestMethod] diff --git a/tests/Tgstation.Server.Host.Tests/Database/TestDatabaseContextFactory.cs b/tests/Tgstation.Server.Host.Tests/Database/TestDatabaseContextFactory.cs index a0413a9158..8873270a67 100644 --- a/tests/Tgstation.Server.Host.Tests/Database/TestDatabaseContextFactory.cs +++ b/tests/Tgstation.Server.Host.Tests/Database/TestDatabaseContextFactory.cs @@ -14,14 +14,14 @@ namespace Tgstation.Server.Host.Database.Tests [TestMethod] public void TestConstructionThrows() { - Assert.ThrowsException(() => new DatabaseContextFactory(null)); + Assert.ThrowsExactly(() => new DatabaseContextFactory(null)); var mockProvider = new Mock(); mockProvider.Setup(x => x.GetService(typeof(IDatabaseContext))).Verifiable(); var mockScope = new Mock(); mockScope.SetupGet(x => x.ServiceProvider).Returns(mockProvider.Object).Verifiable(); var mockScopeFactory = new Mock(); mockScopeFactory.Setup(x => x.CreateScope()).Returns(mockScope.Object).Verifiable(); - Assert.ThrowsException(() => new DatabaseContextFactory(mockScopeFactory.Object)); + Assert.ThrowsExactly(() => new DatabaseContextFactory(mockScopeFactory.Object)); mockScopeFactory.VerifyAll(); mockScope.VerifyAll(); mockProvider.VerifyAll(); @@ -41,7 +41,7 @@ namespace Tgstation.Server.Host.Database.Tests var factory = new DatabaseContextFactory(mockScopeFactory.Object); - await Assert.ThrowsExceptionAsync(() => factory.UseContext(null).AsTask()); + await Assert.ThrowsExactlyAsync(() => factory.UseContext(null).AsTask()); await factory.UseContext(context => { diff --git a/tests/Tgstation.Server.Host.Tests/Extensions/TestGeneralConfigurationExtensions.cs b/tests/Tgstation.Server.Host.Tests/Extensions/TestGeneralConfigurationExtensions.cs index 1f7b726a6e..86a3f39f52 100644 --- a/tests/Tgstation.Server.Host.Tests/Extensions/TestGeneralConfigurationExtensions.cs +++ b/tests/Tgstation.Server.Host.Tests/Extensions/TestGeneralConfigurationExtensions.cs @@ -10,7 +10,7 @@ namespace Tgstation.Server.Host.Extensions.Tests [TestMethod] public void TestThrowsOnNullParameter() { - Assert.ThrowsException(() => GeneralConfigurationExtensions.GetCopyDirectoryTaskThrottle(null)); + Assert.ThrowsExactly(() => GeneralConfigurationExtensions.GetCopyDirectoryTaskThrottle(null)); } } } diff --git a/tests/Tgstation.Server.Host.Tests/Extensions/TestServiceCollectionExtensions.cs b/tests/Tgstation.Server.Host.Tests/Extensions/TestServiceCollectionExtensions.cs index 09c2bd502d..2c3c2e4078 100644 --- a/tests/Tgstation.Server.Host.Tests/Extensions/TestServiceCollectionExtensions.cs +++ b/tests/Tgstation.Server.Host.Tests/Extensions/TestServiceCollectionExtensions.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; @@ -37,12 +37,12 @@ namespace Tgstation.Server.Host.Extensions.Tests var serviceCollection = new ServiceCollection(); var mockConfig = new Mock(); mockConfig.Setup(x => x.GetSection(It.IsNotNull())).Returns(mockConfig.Object).Verifiable(); - Assert.ThrowsException(() => ServiceCollectionExtensions.UseStandardConfig(null, null)); - Assert.ThrowsException(() => serviceCollection.UseStandardConfig(null)); + Assert.ThrowsExactly(() => ServiceCollectionExtensions.UseStandardConfig(null, null)); + Assert.ThrowsExactly(() => serviceCollection.UseStandardConfig(null)); serviceCollection.UseStandardConfig(mockConfig.Object); - Assert.ThrowsException(() => serviceCollection.UseStandardConfig(mockConfig.Object)); - Assert.ThrowsException(() => serviceCollection.UseStandardConfig(mockConfig.Object)); - Assert.ThrowsException(() => serviceCollection.UseStandardConfig(mockConfig.Object)); + Assert.ThrowsExactly(() => serviceCollection.UseStandardConfig(mockConfig.Object)); + Assert.ThrowsExactly(() => serviceCollection.UseStandardConfig(mockConfig.Object)); + Assert.ThrowsExactly(() => serviceCollection.UseStandardConfig(mockConfig.Object)); mockConfig.Verify(); } } diff --git a/tests/Tgstation.Server.Host.Tests/IO/TestConsole.cs b/tests/Tgstation.Server.Host.Tests/IO/TestConsole.cs index f5878a7e2f..cf28360070 100644 --- a/tests/Tgstation.Server.Host.Tests/IO/TestConsole.cs +++ b/tests/Tgstation.Server.Host.Tests/IO/TestConsole.cs @@ -14,14 +14,14 @@ namespace Tgstation.Server.Host.IO.Tests [TestMethod] public void TestContructionThrows() { - Assert.ThrowsException(() => new Console(null)); + Assert.ThrowsExactly(() => new Console(null)); } [TestMethod] public async Task TestWriteLine() { var console = new Console(new PlatformIdentifier()); - await Assert.ThrowsExceptionAsync(() => console.WriteAsync(null, false, default)); + await Assert.ThrowsExactlyAsync(() => console.WriteAsync(null, false, default)); try { await console.WriteAsync(null, true, default); diff --git a/tests/Tgstation.Server.Host.Tests/IO/TestFileDownloader.cs b/tests/Tgstation.Server.Host.Tests/IO/TestFileDownloader.cs index 768be41442..07223533e2 100644 --- a/tests/Tgstation.Server.Host.Tests/IO/TestFileDownloader.cs +++ b/tests/Tgstation.Server.Host.Tests/IO/TestFileDownloader.cs @@ -1,5 +1,7 @@ using System; using System.IO; +using System.Net; +using System.Net.Http; using System.Text; using System.Threading.Tasks; @@ -8,7 +10,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; -using Tgstation.Server.Common.Http; +using Tgstation.Server.Common.Tests; using Tgstation.Server.Host.System; namespace Tgstation.Server.Host.IO.Tests @@ -26,9 +28,9 @@ Please see the following for more context: [TestMethod] public void TestConstructor() { - Assert.ThrowsException(() => new FileDownloader(null, null)); - Assert.ThrowsException(() => new FileDownloader(Mock.Of(), null)); - _ = new FileDownloader(Mock.Of(), Mock.Of>()); + Assert.ThrowsExactly(() => new FileDownloader(null, null)); + Assert.ThrowsExactly(() => new FileDownloader(Mock.Of(), null)); + _ = new FileDownloader(Mock.Of(), Mock.Of>()); } [TestMethod] @@ -53,7 +55,7 @@ Please see the following for more context: var downloader = CreateDownloader(out var loggerFactory); using (loggerFactory) { - Assert.ThrowsException(() => downloader.DownloadFile(null, null)); + Assert.ThrowsExactly(() => downloader.DownloadFile(null, null)); } } @@ -65,11 +67,21 @@ Please see the following for more context: builder.SetMinimumLevel(LogLevel.Trace); }); + var mockHttpClientFactory = new Mock(); + var httpClient = new HttpClient( + new MockHttpMessageHandler( + (_, __) => Task.FromResult( + new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(ExpectedData), + }))); + + mockHttpClientFactory.Setup(x => x.CreateClient(String.Empty)).Returns(httpClient); + try { return new FileDownloader( - new HttpClientFactory( - new AssemblyInformationProvider().ProductInfoHeaderValue), + mockHttpClientFactory.Object, loggerFactory.CreateLogger()); } catch diff --git a/tests/Tgstation.Server.Host.Tests/IO/TestFilesystemLinkFactory.cs b/tests/Tgstation.Server.Host.Tests/IO/TestFilesystemLinkFactory.cs index 16ab28a73c..0d061703d7 100644 --- a/tests/Tgstation.Server.Host.Tests/IO/TestFilesystemLinkFactory.cs +++ b/tests/Tgstation.Server.Host.Tests/IO/TestFilesystemLinkFactory.cs @@ -47,12 +47,12 @@ namespace Tgstation.Server.Host.IO.Tests if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - await Assert.ThrowsExceptionAsync(() => linkFactory.CreateHardLink(f1, f2, CancellationToken.None)); + await Assert.ThrowsExactlyAsync(() => linkFactory.CreateHardLink(f1, f2, CancellationToken.None)); Assert.Inconclusive("Windows does not support hardlinks"); } - await Assert.ThrowsExceptionAsync(() => linkFactory.CreateHardLink(null, null, CancellationToken.None)); - await Assert.ThrowsExceptionAsync(() => linkFactory.CreateHardLink(f1, null, CancellationToken.None)); + await Assert.ThrowsExactlyAsync(() => linkFactory.CreateHardLink(null, null, CancellationToken.None)); + await Assert.ThrowsExactlyAsync(() => linkFactory.CreateHardLink(f1, null, CancellationToken.None)); await linkFactory.CreateHardLink(f1, f2, default); Assert.IsTrue(File.Exists(f2)); @@ -98,8 +98,8 @@ namespace Tgstation.Server.Host.IO.Tests f2 = f1 + ".linked"; File.WriteAllText(f1, Text); - await Assert.ThrowsExceptionAsync(() => linkFactory.CreateSymbolicLink(null, null, default)); - await Assert.ThrowsExceptionAsync(() => linkFactory.CreateSymbolicLink(f1, null, default)); + await Assert.ThrowsExactlyAsync(() => linkFactory.CreateSymbolicLink(null, null, default)); + await Assert.ThrowsExactlyAsync(() => linkFactory.CreateSymbolicLink(f1, null, default)); await linkFactory.CreateSymbolicLink(f1, f2, default); Assert.IsTrue(File.Exists(f2)); @@ -131,8 +131,8 @@ namespace Tgstation.Server.Host.IO.Tests var p1 = Path.Combine(f1, FileName); File.WriteAllText(p1, Text); - await Assert.ThrowsExceptionAsync(() => linkFactory.CreateSymbolicLink(null, null, default)); - await Assert.ThrowsExceptionAsync(() => linkFactory.CreateSymbolicLink(f1, null, default)); + await Assert.ThrowsExactlyAsync(() => linkFactory.CreateSymbolicLink(null, null, default)); + await Assert.ThrowsExactlyAsync(() => linkFactory.CreateSymbolicLink(f1, null, default)); await linkFactory.CreateSymbolicLink(f1, f2, default); diff --git a/tests/Tgstation.Server.Host.Tests/IO/TestIOManager.cs b/tests/Tgstation.Server.Host.Tests/IO/TestIOManager.cs index 36dfe283fe..ef3f0b07a3 100644 --- a/tests/Tgstation.Server.Host.Tests/IO/TestIOManager.cs +++ b/tests/Tgstation.Server.Host.Tests/IO/TestIOManager.cs @@ -145,28 +145,28 @@ namespace Tgstation.Server.Host.IO.Tests var tempPath1 = Guid.NewGuid().ToString(); var tempPath2 = Guid.NewGuid().ToString(); - await Assert.ThrowsExceptionAsync(() => ioManager.CopyDirectory( + await Assert.ThrowsExactlyAsync(() => ioManager.CopyDirectory( null, null, null, tempPath2, throttle, default).AsTask()); - await Assert.ThrowsExceptionAsync(() => ioManager.CopyDirectory( + await Assert.ThrowsExactlyAsync(() => ioManager.CopyDirectory( null, null, tempPath1, null, throttle, default).AsTask()); - await Assert.ThrowsExceptionAsync(() => ioManager.CopyDirectory( + await Assert.ThrowsExactlyAsync(() => ioManager.CopyDirectory( null, null, null, null, throttle, default).AsTask()); - await Assert.ThrowsExceptionAsync(() => ioManager.CopyDirectory( + await Assert.ThrowsExactlyAsync(() => ioManager.CopyDirectory( null, null, tempPath1, diff --git a/tests/Tgstation.Server.Host.Tests/IO/TestPostWriteHandler.cs b/tests/Tgstation.Server.Host.Tests/IO/TestPostWriteHandler.cs index 71d3c3fb0b..13bcc1c491 100644 --- a/tests/Tgstation.Server.Host.Tests/IO/TestPostWriteHandler.cs +++ b/tests/Tgstation.Server.Host.Tests/IO/TestPostWriteHandler.cs @@ -23,8 +23,8 @@ namespace Tgstation.Server.Host.IO.Tests else postWriteHandler = new PosixPostWriteHandler(Mock.Of>()); - Assert.ThrowsException(() => postWriteHandler.HandleWrite(null)); - Assert.ThrowsException(() => postWriteHandler.NeedsPostWrite(null)); + Assert.ThrowsExactly(() => postWriteHandler.HandleWrite(null)); + Assert.ThrowsExactly(() => postWriteHandler.NeedsPostWrite(null)); } [TestMethod] @@ -75,7 +75,7 @@ namespace Tgstation.Server.Host.IO.Tests var postWriteHandler = new PosixPostWriteHandler(Mock.Of>()); var tmpFile = Path.GetTempFileName(); File.Delete(tmpFile); - Assert.ThrowsException(() => postWriteHandler.HandleWrite(tmpFile)); + Assert.ThrowsExactly(() => postWriteHandler.HandleWrite(tmpFile)); } } } diff --git a/tests/Tgstation.Server.Host.Tests/IO/TestRequestFileStreamProvider.cs b/tests/Tgstation.Server.Host.Tests/IO/TestRequestFileStreamProvider.cs index a950ee6b9f..99c5169cf5 100644 --- a/tests/Tgstation.Server.Host.Tests/IO/TestRequestFileStreamProvider.cs +++ b/tests/Tgstation.Server.Host.Tests/IO/TestRequestFileStreamProvider.cs @@ -10,6 +10,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using Tgstation.Server.Common.Http; +using Tgstation.Server.Common.Tests; using Tgstation.Server.Host.Extensions; namespace Tgstation.Server.Host.IO.Tests @@ -20,9 +21,9 @@ namespace Tgstation.Server.Host.IO.Tests [TestMethod] public async Task TestConstruction() { - Assert.ThrowsException(() => new RequestFileStreamProvider(null, null)); - var mockClient = Mock.Of(); - Assert.ThrowsException(() => new RequestFileStreamProvider(mockClient, null)); + Assert.ThrowsExactly(() => new RequestFileStreamProvider(null, null)); + var mockClient = new HttpClient(); + Assert.ThrowsExactly(() => new RequestFileStreamProvider(mockClient, null)); await using var test = new RequestFileStreamProvider(mockClient, new HttpRequestMessage()); } @@ -31,21 +32,23 @@ namespace Tgstation.Server.Host.IO.Tests { var sequence = new byte[] { 1, 2, 3 }; var resultMs = new MemoryStream(sequence); - var mockHttpClient = new Mock(); var response = new HttpResponseMessage() { Content = new StreamContent(resultMs), }; + var ran = false; + var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com"); + var mockHttpClient = new HttpClient( + new MockHttpMessageHandler( + (_, _) => + { + ran = true; + return Task.FromResult(response); + })); - var request = new HttpRequestMessage(); - mockHttpClient - .Setup(x => x.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, It.IsAny())) - .Returns(Task.FromResult(response)) - .Verifiable(); - - await using var downloader = new RequestFileStreamProvider(mockHttpClient.Object, request); + await using var downloader = new RequestFileStreamProvider(mockHttpClient, request); var download = await downloader.GetResult(default); @@ -55,8 +58,7 @@ namespace Tgstation.Server.Host.IO.Tests var resultSequence = buffer.ToArray(); Assert.IsTrue(sequence.SequenceEqual(resultSequence)); - - mockHttpClient.VerifyAll(); + Assert.IsTrue(ran); } [TestMethod] @@ -64,20 +66,23 @@ namespace Tgstation.Server.Host.IO.Tests { var sequence = new byte[] { 1, 2, 3 }; var resultMs = new MemoryStream(sequence); - var mockHttpClient = new Mock(); var response = new HttpResponseMessage() { Content = new StreamContent(resultMs), }; - var request = new HttpRequestMessage(); - mockHttpClient - .Setup(x => x.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, It.IsAny())) - .Returns(Task.FromResult(response)) - .Verifiable(); + int ran = 0; + var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com"); + var mockHttpClient = new HttpClient( + new MockHttpMessageHandler( + (_, _) => + { + ++ran; + return Task.FromResult(response); + })); - await using var downloader = new RequestFileStreamProvider(mockHttpClient.Object, request); + await using var downloader = new RequestFileStreamProvider(mockHttpClient, request); var task1 = downloader.GetResult(default); var task2 = downloader.GetResult(default); @@ -93,15 +98,13 @@ namespace Tgstation.Server.Host.IO.Tests Assert.AreSame(task1Result, await task2); Assert.AreSame(task1Result, await task3); - mockHttpClient.VerifyAll(); - Assert.AreEqual(1, mockHttpClient.Invocations.Count); + Assert.AreEqual(1, ran); } [TestMethod] public async Task TestInterruptedDownload() { var resultMs = new MemoryStream(); - var mockHttpClient = new Mock(); var response = new HttpResponseMessage() { @@ -110,17 +113,18 @@ namespace Tgstation.Server.Host.IO.Tests var tcs = new TaskCompletionSource(); - var request = new HttpRequestMessage(); - mockHttpClient - .Setup(x => x.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, It.IsAny())) - .Returns((request, option, cancellationToken) => - { - cancellationToken.Register(() => tcs.TrySetCanceled(cancellationToken)); - return tcs.Task; - }) - .Verifiable(); + var ran = false; + var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com"); + var mockHttpClient = new HttpClient( + new MockHttpMessageHandler( + (_, cancellationToken) => + { + ran = true; + cancellationToken.Register(() => tcs.TrySetCanceled(cancellationToken)); + return tcs.Task; + })); - await using var downloader = new RequestFileStreamProvider(mockHttpClient.Object, request); + await using var downloader = new RequestFileStreamProvider(mockHttpClient, request); using var cts1 = new CancellationTokenSource(); var task1 = downloader.GetResult(cts1.Token); @@ -133,11 +137,11 @@ namespace Tgstation.Server.Host.IO.Tests cts2.Cancel(); - await Assert.ThrowsExceptionAsync(() => task1.AsTask()); - await Assert.ThrowsExceptionAsync(() => task2.AsTask()); - await Assert.ThrowsExceptionAsync(() => task3.AsTask()); + await Assert.ThrowsExactlyAsync(task1.AsTask); + await Assert.ThrowsExactlyAsync(task2.AsTask); + await Assert.ThrowsExactlyAsync(task3.AsTask); - mockHttpClient.VerifyAll(); + Assert.IsTrue(ran); } } } diff --git a/tests/Tgstation.Server.Host.Tests/Jobs/TestJobHandler.cs b/tests/Tgstation.Server.Host.Tests/Jobs/TestJobHandler.cs index b8a61b4b29..ba43c2825e 100644 --- a/tests/Tgstation.Server.Host.Tests/Jobs/TestJobHandler.cs +++ b/tests/Tgstation.Server.Host.Tests/Jobs/TestJobHandler.cs @@ -22,7 +22,7 @@ namespace Tgstation.Server.Host.Jobs.Tests public void TestConstructionAndDisposal() { cancelled = false; - Assert.ThrowsException(() => new JobHandler(null)); + Assert.ThrowsExactly(() => new JobHandler(null)); currentWaitTask = Task.CompletedTask; new JobHandler(TestJob).Dispose(); @@ -40,9 +40,9 @@ namespace Tgstation.Server.Host.Jobs.Tests currentWaitTask = tcs.Task; cts.Cancel(); using var handler = new JobHandler(TestJob); - await Assert.ThrowsExceptionAsync(() => handler.Wait(cts.Token)); + await Assert.ThrowsExactlyAsync(() => handler.Wait(cts.Token)); handler.Start(); - await Assert.ThrowsExceptionAsync(() => handler.Wait(cts.Token)); + await Assert.ThrowsExactlyAsync(() => handler.Wait(cts.Token)); tcs.SetResult(); await handler.Wait(default); } diff --git a/tests/Tgstation.Server.Host.Tests/Security/RightsEvaluation/TestAndRightsConditional.cs b/tests/Tgstation.Server.Host.Tests/Security/RightsEvaluation/TestAndRightsConditional.cs new file mode 100644 index 0000000000..a189cc63de --- /dev/null +++ b/tests/Tgstation.Server.Host.Tests/Security/RightsEvaluation/TestAndRightsConditional.cs @@ -0,0 +1,38 @@ +using System; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Tgstation.Server.Api.Rights; +using Tgstation.Server.Host.Security.RightsEvaluation; + +namespace Tgstation.Server.Host.Tests.Security.RightsEvaluation +{ + [TestClass] + public sealed class TestAndRightsConditional + { + [TestMethod] + public void TestBasicAnding() + { + var conditional = new AndRightsConditional( + new FlagRightsConditional(RepositoryRights.ChangeCredentials), + new FlagRightsConditional(RepositoryRights.ChangeCommitter)); + + foreach (RepositoryRights right in Enum.GetValues(typeof(RepositoryRights))) + Assert.IsFalse(conditional.Evaluate(right)); + + Assert.IsTrue(conditional.Evaluate(RepositoryRights.ChangeCredentials | RepositoryRights.ChangeCommitter)); + Assert.IsTrue(conditional.Evaluate(RepositoryRights.ChangeCredentials | RepositoryRights.ChangeCommitter | RepositoryRights.SetReference)); + } + + [TestMethod] + public void TestThrows() + { + Assert.Throws(() => _ = new AndRightsConditional( + null, + null)); + Assert.Throws(() => _ = new AndRightsConditional( + new FlagRightsConditional(RepositoryRights.SetOrigin), + null)); + } + } +} diff --git a/tests/Tgstation.Server.Host.Tests/Security/RightsEvaluation/TestFlagRightsConditional.cs b/tests/Tgstation.Server.Host.Tests/Security/RightsEvaluation/TestFlagRightsConditional.cs new file mode 100644 index 0000000000..864cc34093 --- /dev/null +++ b/tests/Tgstation.Server.Host.Tests/Security/RightsEvaluation/TestFlagRightsConditional.cs @@ -0,0 +1,36 @@ +using System; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Tgstation.Server.Api.Rights; +using Tgstation.Server.Host.Security.RightsEvaluation; + +namespace Tgstation.Server.Host.Tests.Security.RightsEvaluation +{ + [TestClass] + public sealed class TestFlagRightsConditional + { + [TestMethod] + public void TestOnlyWorksForOneFlag() + { + var targetRight = RepositoryRights.ChangeAutoUpdateSettings; + var conditional = new FlagRightsConditional(targetRight); + + Assert.IsTrue(conditional.Evaluate(targetRight)); + foreach (RepositoryRights right in Enum.GetValues(typeof(RepositoryRights))) + if (right != targetRight) + { + Assert.IsFalse(conditional.Evaluate(right)); + Assert.IsTrue(conditional.Evaluate(targetRight | right)); + } + } + + [TestMethod] + public void TestThrowsOnNone() + => Assert.Throws(() => _ = new FlagRightsConditional(RepositoryRights.None)); + + [TestMethod] + public void TestThrowsOnMultiBit() + => Assert.Throws(() => _ = new FlagRightsConditional((RepositoryRights)3)); + } +} diff --git a/tests/Tgstation.Server.Host.Tests/Security/RightsEvaluation/TestOrRightsConditional.cs b/tests/Tgstation.Server.Host.Tests/Security/RightsEvaluation/TestOrRightsConditional.cs new file mode 100644 index 0000000000..b2a0d1b4f3 --- /dev/null +++ b/tests/Tgstation.Server.Host.Tests/Security/RightsEvaluation/TestOrRightsConditional.cs @@ -0,0 +1,42 @@ +using System; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Tgstation.Server.Api.Rights; +using Tgstation.Server.Host.Security.RightsEvaluation; + +namespace Tgstation.Server.Host.Tests.Security.RightsEvaluation +{ + [TestClass] + public sealed class TestOrRightsConditional + { + [TestMethod] + public void TestBasicOring() + { + var conditional = new OrRightsConditional( + new FlagRightsConditional(RepositoryRights.ChangeCredentials), + new FlagRightsConditional(RepositoryRights.ChangeCommitter)); + + foreach (RepositoryRights right in Enum.GetValues(typeof(RepositoryRights))) + if (right != RepositoryRights.ChangeCredentials && right != RepositoryRights.ChangeCommitter) + Assert.IsFalse(conditional.Evaluate(right)); + + Assert.IsTrue(conditional.Evaluate(RepositoryRights.ChangeCredentials | RepositoryRights.ChangeCommitter)); + Assert.IsTrue(conditional.Evaluate(RepositoryRights.ChangeCredentials)); + Assert.IsTrue(conditional.Evaluate(RepositoryRights.ChangeCommitter)); + Assert.IsTrue(conditional.Evaluate(RepositoryRights.ChangeCommitter | RepositoryRights.MergePullRequest)); + Assert.IsTrue(conditional.Evaluate(RepositoryRights.ChangeCredentials | RepositoryRights.ChangeCommitter | RepositoryRights.MergePullRequest)); + } + + [TestMethod] + public void TestThrows() + { + Assert.Throws(() => _ = new OrRightsConditional( + null, + null)); + Assert.Throws(() => _ = new OrRightsConditional( + new FlagRightsConditional(RepositoryRights.SetOrigin), + null)); + } + } +} diff --git a/tests/Tgstation.Server.Host.Tests/Setup/TestSetupWizard.cs b/tests/Tgstation.Server.Host.Tests/Setup/TestSetupWizard.cs index fba28a4d6e..7c3033cad4 100644 --- a/tests/Tgstation.Server.Host.Tests/Setup/TestSetupWizard.cs +++ b/tests/Tgstation.Server.Host.Tests/Setup/TestSetupWizard.cs @@ -27,27 +27,27 @@ namespace Tgstation.Server.Host.Setup.Tests [TestMethod] public void TestConstructionThrows() { - Assert.ThrowsException(() => new SetupWizard(null, null, null, null, null, null, null, null, null, null, null)); + Assert.ThrowsExactly(() => new SetupWizard(null, null, null, null, null, null, null, null, null, null, null)); var mockIOManager = new Mock(); - Assert.ThrowsException(() => new SetupWizard(mockIOManager.Object, null, null, null, null, null, null, null, null, null, null)); + Assert.ThrowsExactly(() => new SetupWizard(mockIOManager.Object, null, null, null, null, null, null, null, null, null, null)); var mockConsole = new Mock(); - Assert.ThrowsException(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, null, null, null, null, null, null, null, null, null)); + Assert.ThrowsExactly(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, null, null, null, null, null, null, null, null, null)); var mockHostingEnvironment = new Mock(); - Assert.ThrowsException(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, null, null, null, null, null, null, null, null)); + Assert.ThrowsExactly(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, null, null, null, null, null, null, null, null)); var mockAssemblyInfoProvider = new Mock(); - Assert.ThrowsException(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, mockAssemblyInfoProvider.Object, null, null, null, null, null, null, null)); + Assert.ThrowsExactly(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, mockAssemblyInfoProvider.Object, null, null, null, null, null, null, null)); var mockDBConnectionFactory = new Mock(); - Assert.ThrowsException(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, mockAssemblyInfoProvider.Object, mockDBConnectionFactory.Object, null, null, null, null, null, null)); + Assert.ThrowsExactly(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, mockAssemblyInfoProvider.Object, mockDBConnectionFactory.Object, null, null, null, null, null, null)); var mockPlatformIdentifier = new Mock(); - Assert.ThrowsException(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, mockAssemblyInfoProvider.Object, mockDBConnectionFactory.Object, mockPlatformIdentifier.Object, null, null, null, null, null)); + Assert.ThrowsExactly(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, mockAssemblyInfoProvider.Object, mockDBConnectionFactory.Object, mockPlatformIdentifier.Object, null, null, null, null, null)); var mockAsyncDelayer = new Mock(); - Assert.ThrowsException(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, mockAssemblyInfoProvider.Object, mockDBConnectionFactory.Object, mockPlatformIdentifier.Object, mockAsyncDelayer.Object, null, null, null, null)); + Assert.ThrowsExactly(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, mockAssemblyInfoProvider.Object, mockDBConnectionFactory.Object, mockPlatformIdentifier.Object, mockAsyncDelayer.Object, null, null, null, null)); var mockLifetime = new Mock(); - Assert.ThrowsException(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, mockAssemblyInfoProvider.Object, mockDBConnectionFactory.Object, mockPlatformIdentifier.Object, mockAsyncDelayer.Object, mockLifetime.Object, null, null, null)); + Assert.ThrowsExactly(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, mockAssemblyInfoProvider.Object, mockDBConnectionFactory.Object, mockPlatformIdentifier.Object, mockAsyncDelayer.Object, mockLifetime.Object, null, null, null)); var mockServices = new Mock(); - Assert.ThrowsException(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, mockAssemblyInfoProvider.Object, mockDBConnectionFactory.Object, mockPlatformIdentifier.Object, mockAsyncDelayer.Object, mockLifetime.Object, mockServices.Object, null, null)); + Assert.ThrowsExactly(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, mockAssemblyInfoProvider.Object, mockDBConnectionFactory.Object, mockPlatformIdentifier.Object, mockAsyncDelayer.Object, mockLifetime.Object, mockServices.Object, null, null)); var mockGeneralConfigurationOptions = Options.Create(new GeneralConfiguration()); - Assert.ThrowsException(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, mockAssemblyInfoProvider.Object, mockDBConnectionFactory.Object, mockPlatformIdentifier.Object, mockAsyncDelayer.Object, mockLifetime.Object, mockServices.Object, mockGeneralConfigurationOptions, null)); + Assert.ThrowsExactly(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, mockAssemblyInfoProvider.Object, mockDBConnectionFactory.Object, mockPlatformIdentifier.Object, mockAsyncDelayer.Object, mockLifetime.Object, mockServices.Object, mockGeneralConfigurationOptions, null)); } [TestMethod] @@ -96,10 +96,10 @@ namespace Tgstation.Server.Host.Setup.Tests await RunWizard(); testGeneralConfig.SetupWizardMode = SetupWizardMode.Force; - await Assert.ThrowsExceptionAsync(() => RunWizard()); + await Assert.ThrowsExactlyAsync(() => RunWizard()); testGeneralConfig.SetupWizardMode = SetupWizardMode.Only; - await Assert.ThrowsExceptionAsync(() => RunWizard()); + await Assert.ThrowsExactlyAsync(() => RunWizard()); mockConsole.SetupGet(x => x.Available).Returns(true).Verifiable(); mockIOManager.Setup(x => x.ConcatPath(testInternalConfig.AppSettingsBasePath, It.IsNotNull())).Returns(paths => @@ -195,8 +195,6 @@ namespace Tgstation.Server.Host.Setup.Tests "y", // swarm config "n", - // telemetry config - "n", //saved, now for second run //this time use defaults amap String.Empty, @@ -236,9 +234,6 @@ namespace Tgstation.Server.Host.Setup.Tests "privatekey", "n", "http://controller.com", - // telemetry config - "y", - "telemetry name", //third run, we already hit all the code coverage so just get through it String.Empty, nameof(DatabaseType.MariaDB), @@ -277,8 +272,6 @@ namespace Tgstation.Server.Host.Setup.Tests "https://controllerpublic.com", "privatekey", "y", - // telemetry config - "n", }; var inputPos = 0; @@ -342,7 +335,7 @@ namespace Tgstation.Server.Host.Setup.Tests return Task.CompletedTask; }).Verifiable(); - await Assert.ThrowsExceptionAsync(() => RunWizard()); + await Assert.ThrowsExactlyAsync(() => RunWizard()); Assert.AreEqual(finalInputSequence.Count, inputPos); mockFailCommand.VerifyAll(); diff --git a/tests/Tgstation.Server.Host.Tests/Swarm/SwarmRpcMapper.cs b/tests/Tgstation.Server.Host.Tests/Swarm/SwarmRpcMapper.cs index cb48282fd2..6135e988c2 100644 --- a/tests/Tgstation.Server.Host.Tests/Swarm/SwarmRpcMapper.cs +++ b/tests/Tgstation.Server.Host.Tests/Swarm/SwarmRpcMapper.cs @@ -21,7 +21,7 @@ using Moq; using Newtonsoft.Json; -using Tgstation.Server.Common.Http; +using Tgstation.Server.Common.Tests; using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.Controllers; using Tgstation.Server.Host.Controllers.Results; @@ -41,12 +41,10 @@ namespace Tgstation.Server.Host.Swarm.Tests int serverErrorCount; - public SwarmRpcMapper(Func createSwarmController, Mock clientMock, ILogger logger) + public SwarmRpcMapper(Func createSwarmController, ILogger logger, out HttpMessageHandler handlerMock) { this.createSwarmController = createSwarmController; - clientMock - .Setup(x => x.SendAsync(It.IsNotNull(), It.IsAny(), It.IsAny())) - .Returns(MapRequest); + handlerMock = new MockHttpMessageHandler(MapRequest); this.logger = logger; AsyncRequests = true; } @@ -63,7 +61,6 @@ namespace Tgstation.Server.Host.Swarm.Tests async Task MapRequest( HttpRequestMessage request, - HttpCompletionOption httpCompletionOption, CancellationToken cancellationToken) { var (config, transferService, node) = configToNodes.FirstOrDefault( diff --git a/tests/Tgstation.Server.Host.Tests/Swarm/TestSwarmProtocol.cs b/tests/Tgstation.Server.Host.Tests/Swarm/TestSwarmProtocol.cs index 2ae9c2e0ff..1bfff4200f 100644 --- a/tests/Tgstation.Server.Host.Tests/Swarm/TestSwarmProtocol.cs +++ b/tests/Tgstation.Server.Host.Tests/Swarm/TestSwarmProtocol.cs @@ -2,33 +2,29 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; -using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.TestTools.UnitTesting; -using Moq; - using Tgstation.Server.Common.Extensions; using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.Core; using Tgstation.Server.Host.IO; -using Tgstation.Server.Host.Transfer; namespace Tgstation.Server.Host.Swarm.Tests { [TestClass] public sealed class TestSwarmProtocol { - static readonly HashSet usedPorts = new (); - static ILoggerFactory loggerFactory; - static ILogger logger; + HashSet usedPorts = new(); + ILoggerFactory loggerFactory; + ILogger logger; - static ISeekableFileStreamProvider updateFileStreamProvider; + ISeekableFileStreamProvider updateFileStreamProvider; - [ClassInitialize] - public static async Task Initialize(TestContext _) + [TestInitialize] + public async Task Initialize() { loggerFactory = LoggerFactory.Create(builder => { @@ -44,8 +40,8 @@ namespace Tgstation.Server.Host.Swarm.Tests await updateFileStreamProvider.GetResult(default); } - [ClassCleanup] - public static async Task Shutdown() + [TestCleanup] + public async Task Shutdown() { usedPorts.Clear(); loggerFactory.Dispose(); @@ -207,7 +203,7 @@ namespace Tgstation.Server.Host.Swarm.Tests await TestSimultaneousPrepareDifferentVersionsFails(false); } - static async ValueTask TestSimultaneousPrepareDifferentVersionsFails(bool prepControllerFirst) + async ValueTask TestSimultaneousPrepareDifferentVersionsFails(bool prepControllerFirst) { await using var controller = GenNode(); await using var node1 = GenNode(controller); @@ -269,7 +265,7 @@ namespace Tgstation.Server.Host.Swarm.Tests Assert.AreEqual(SwarmCommitResult.AbortUpdate, await nodeCommitTask); } - static TestableSwarmNode GenNode(TestableSwarmNode controller = null, Version version = null) + TestableSwarmNode GenNode(TestableSwarmNode controller = null, Version version = null) { ushort randPort; do @@ -290,7 +286,7 @@ namespace Tgstation.Server.Host.Swarm.Tests return new TestableSwarmNode(loggerFactory, config, version); } - static async Task DelayMax(Action assertion, long seconds = 1) + async Task DelayMax(Action assertion, long seconds = 1) { var id = Guid.NewGuid(); logger.LogInformation("Begin DelayMax {id}: {seconds}", id, seconds); diff --git a/tests/Tgstation.Server.Host.Tests/Swarm/TestableSwarmNode.cs b/tests/Tgstation.Server.Host.Tests/Swarm/TestableSwarmNode.cs index 9b3e925536..5f5c9aec76 100644 --- a/tests/Tgstation.Server.Host.Tests/Swarm/TestableSwarmNode.cs +++ b/tests/Tgstation.Server.Host.Tests/Swarm/TestableSwarmNode.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net.Http; using System.Reflection; using System.Threading; using System.Threading.Tasks; @@ -50,7 +51,6 @@ namespace Tgstation.Server.Host.Swarm.Tests public bool Shutdown { get; private set; } - readonly Mock mockHttpClient; readonly Mock mockDBContextFactory; readonly Mock mockDatabaseSeeder; readonly ISetup mockDatabaseSeederInitialize; @@ -121,10 +121,6 @@ namespace Tgstation.Server.Host.Swarm.Tests .Setup(x => x.UseContextTaskReturn(It.IsNotNull>())) .Callback>((func) => func(mockDatabaseContext)); - var mockHttpClientFactory = new Mock(); - mockHttpClient = new Mock(); - mockHttpClientFactory.Setup(x => x.CreateClient()).Returns(mockHttpClient.Object); - var mockAsyncDelayer = new Mock(); mockAsyncDelayer.Setup( x => x.Delay(It.IsAny(), It.IsAny())) @@ -161,8 +157,8 @@ namespace Tgstation.Server.Host.Swarm.Tests targetTransfer, mockOptions.Object, loggerFactory.CreateLogger()), - mockHttpClient, - loggerFactory.CreateLogger($"SwarmRpcMapper-{swarmConfiguration.Identifier}")); + loggerFactory.CreateLogger($"SwarmRpcMapper-{swarmConfiguration.Identifier}"), + out var mockMessageHandler); mockServerUpdater .Setup(x => x.BeginUpdate(It.IsNotNull(), It.IsAny(), It.IsNotNull(), It.IsAny())) @@ -175,6 +171,9 @@ namespace Tgstation.Server.Host.Swarm.Tests var mockTokenFactory = new MockTokenFactory(); + var mockHttpClientFactory = new Mock(); + mockHttpClientFactory.Setup(x => x.CreateClient(String.Empty)).Returns(() => new HttpClient(mockMessageHandler)); + var runCount = 0; void RecreateControllerAndService() { @@ -264,7 +263,7 @@ namespace Tgstation.Server.Host.Swarm.Tests SwarmRegistrationResult? result; if (cancel) { - await Assert.ThrowsExceptionAsync(Invoke); + await Assert.ThrowsExactlyAsync(Invoke); result = null; } else diff --git a/tests/Tgstation.Server.Host.Tests/System/TestPosixSignalHandler.cs b/tests/Tgstation.Server.Host.Tests/System/TestPosixSignalHandler.cs index dd4f2debf3..7a8341ba3b 100644 --- a/tests/Tgstation.Server.Host.Tests/System/TestPosixSignalHandler.cs +++ b/tests/Tgstation.Server.Host.Tests/System/TestPosixSignalHandler.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.TestTools.UnitTesting; + using Moq; using Tgstation.Server.Host.Core; @@ -21,13 +22,13 @@ namespace Tgstation.Server.Host.System.Tests [TestMethod] public void TestConstruction() { - Assert.ThrowsException(() => new PosixSignalHandler(null, null, null)); + Assert.ThrowsExactly(() => new PosixSignalHandler(null, null, null)); var mockServerControl = Mock.Of(); - Assert.ThrowsException(() => new PosixSignalHandler(mockServerControl, null, null)); + Assert.ThrowsExactly(() => new PosixSignalHandler(mockServerControl, null, null)); var mockAsyncDelayer = Mock.Of(); - Assert.ThrowsException(() => new PosixSignalHandler(mockServerControl, mockAsyncDelayer, null)); + Assert.ThrowsExactly(() => new PosixSignalHandler(mockServerControl, mockAsyncDelayer, null)); new PosixSignalHandler(mockServerControl, mockAsyncDelayer, Mock.Of>()).Dispose(); } @@ -74,7 +75,8 @@ namespace Tgstation.Server.Host.System.Tests null, null, true, - true); + true, + false); await Task.Delay(TimeSpan.FromSeconds(10)); diff --git a/tests/Tgstation.Server.Host.Tests/TestProgram.cs b/tests/Tgstation.Server.Host.Tests/TestProgram.cs index a4f9318e13..657b1a8b8d 100644 --- a/tests/Tgstation.Server.Host.Tests/TestProgram.cs +++ b/tests/Tgstation.Server.Host.Tests/TestProgram.cs @@ -21,7 +21,7 @@ namespace Tgstation.Server.Host.Tests [TestMethod] public async Task TestIncompatibleWatchdog() { - await Assert.ThrowsExceptionAsync(() => Program.Main(new string[] { "garbage", "0.0.1" })); + await Assert.ThrowsExactlyAsync(() => Program.Main(new string[] { "garbage", "0.0.1" })); } [TestMethod] @@ -74,7 +74,7 @@ namespace Tgstation.Server.Host.Tests ServerFactory = mockServerFactory.Object }; - await Assert.ThrowsExceptionAsync(() => program.Main(Array.Empty(), null).AsTask()); + await Assert.ThrowsExactlyAsync(() => program.Main(Array.Empty(), null).AsTask()); } [TestMethod] diff --git a/tests/Tgstation.Server.Host.Tests/TestServerFactory.cs b/tests/Tgstation.Server.Host.Tests/TestServerFactory.cs index a520286782..558aa82ac1 100644 --- a/tests/Tgstation.Server.Host.Tests/TestServerFactory.cs +++ b/tests/Tgstation.Server.Host.Tests/TestServerFactory.cs @@ -21,11 +21,11 @@ namespace Tgstation.Server.Host.Tests [TestMethod] public void TestConstructor() { - Assert.ThrowsException(() => new ServerFactory(null, null, null)); + Assert.ThrowsExactly(() => new ServerFactory(null, null, null)); IAssemblyInformationProvider assemblyInformationProvider = Mock.Of(); - Assert.ThrowsException(() => new ServerFactory(assemblyInformationProvider, null, null)); + Assert.ThrowsExactly(() => new ServerFactory(assemblyInformationProvider, null, null)); IIOManager ioManager = Mock.Of(); - Assert.ThrowsException(() => new ServerFactory(assemblyInformationProvider, ioManager, null)); + Assert.ThrowsExactly(() => new ServerFactory(assemblyInformationProvider, ioManager, null)); var mockFileSystem = new MockFileSystem(); _ = new ServerFactory(assemblyInformationProvider, ioManager, mockFileSystem); } @@ -35,7 +35,7 @@ namespace Tgstation.Server.Host.Tests { var factory = Application.CreateDefaultServerFactory(); - await Assert.ThrowsExceptionAsync(() => factory.CreateServer(null, null, default).AsTask()); + await Assert.ThrowsExactlyAsync(() => factory.CreateServer(null, null, default).AsTask()); var result = await factory.CreateServer(cliArgs, null, default); Assert.IsNotNull(result); } @@ -46,8 +46,8 @@ namespace Tgstation.Server.Host.Tests var factory = Application.CreateDefaultServerFactory(); const string Path = "/test"; - await Assert.ThrowsExceptionAsync(() => factory.CreateServer(null, null, default).AsTask()); - await Assert.ThrowsExceptionAsync(() => factory.CreateServer(null, Path, default).AsTask()); + await Assert.ThrowsExactlyAsync(() => factory.CreateServer(null, null, default).AsTask()); + await Assert.ThrowsExactlyAsync(() => factory.CreateServer(null, Path, default).AsTask()); var result = await factory.CreateServer(cliArgs, Path, default); Assert.IsNotNull(result); } diff --git a/tests/Tgstation.Server.Host.Tests/Tgstation.Server.Host.Tests.csproj b/tests/Tgstation.Server.Host.Tests/Tgstation.Server.Host.Tests.csproj index 2b96d6fd6a..debff5e515 100644 --- a/tests/Tgstation.Server.Host.Tests/Tgstation.Server.Host.Tests.csproj +++ b/tests/Tgstation.Server.Host.Tests/Tgstation.Server.Host.Tests.csproj @@ -7,13 +7,14 @@ - + - + + diff --git a/tests/Tgstation.Server.Host.Tests/Utils/GitHub/TestGitHubClientFactory.cs b/tests/Tgstation.Server.Host.Tests/Utils/GitHub/TestGitHubClientFactory.cs index 01bb7bac1f..8ab83dec4a 100644 --- a/tests/Tgstation.Server.Host.Tests/Utils/GitHub/TestGitHubClientFactory.cs +++ b/tests/Tgstation.Server.Host.Tests/Utils/GitHub/TestGitHubClientFactory.cs @@ -1,4 +1,5 @@ using System; +using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Text; @@ -14,6 +15,7 @@ using Moq; using Octokit; using Tgstation.Server.Api.Models; +using Tgstation.Server.Common.Tests; using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.System; using Tgstation.Server.Host.Tests; @@ -23,10 +25,26 @@ namespace Tgstation.Server.Host.Utils.GitHub.Tests [TestClass] public sealed class TestGitHubClientFactory { - static ILoggerFactory loggerFactory; + const string FakePrivateKey = @"-----BEGIN RSA PRIVATE KEY----- +MIICWgIBAAKBgFGAf2dPjiPMmX0tD4SXtp7Mc6q4Z1uiCUuL3ms9lwAjz29d/yVv +TlJIxcv8OQBamV0XTFJK+leAjBR/8ZwEumKvV/GgZzZsNny7xFgDLIugLaan2SSp +uNab05d9q+btGw6f8VlSmz8EbCUm0vc803uSLW14tHTMw8g3pqsPaYWrAgMBAAEC +gYAR5EaTtGg8vaaYTzMP5YpGlBV0TeUKvaiSuBLMsgmbE0FvblAbtLKZ57XcUPNm +e30dbYboreMFIIgf2/J7UKJl1hNCZUeh8FU2dgArDjx7Ffl1GKyiv/j+/YBgPs0O ++fZerbQSFybN/XQnb2PRt6KyXdGTxRRszY66VUxkcrYOCQJBAJm/UOqGMpZxWKVJ +8kU6+bX7nNO/uRR5KNnjW9Yu7C2jWlbw5RHSzreCwKdh/75l0fLymQ4G/j2lSLw4 +v5vdtZUCQQCHtNcmaB6mpjgMd3779qKvsotUaltcqzmygS+odiIAMgeqcgASt3Fq +trxIb5zO8CmfKN2D9YxNroX1aToG8P4/AkBcWLF9bEWOX15jSVsfgiDi0dKMzSeZ +yHxlA07yAxURBIEKn363ietoBj05TH+UGQxV5KlR55ll5ZUemOdd83lRAkAJrSJe +MbRly5pZgTfDvY1SG9gFd+P10pu1l7KPP4UjIG4dgC1zKDNlGYyJWbQDqWo+WAqb +eym67EPPAObLUem/AkAbebRFp6RCTJBIFkI2GlJGabab96pEno4gdmn0fyUGhf+9 +IDFvEHaDd7T08wT0pD0vTs0+UH/r/AjmXnWmrCPi +-----END RSA PRIVATE KEY-----"; - [ClassInitialize] - public static void Initialize(TestContext _) + ILoggerFactory loggerFactory; + + [TestInitialize] + public void Initialize() { loggerFactory = LoggerFactory.Create(builder => { @@ -35,8 +53,8 @@ namespace Tgstation.Server.Host.Utils.GitHub.Tests }); } - [ClassCleanup] - public static void Cleanup() + [TestCleanup] + public void Cleanup() { loggerFactory.Dispose(); } @@ -44,10 +62,10 @@ namespace Tgstation.Server.Host.Utils.GitHub.Tests [TestMethod] public void TestContructionThrows() { - Assert.ThrowsException(() => new GitHubClientFactory(null, null, null, null)); - Assert.ThrowsException(() => new GitHubClientFactory(Mock.Of(), null, null, null)); - Assert.ThrowsException(() => new GitHubClientFactory(Mock.Of(), Mock.Of(), null, null)); - Assert.ThrowsException(() => new GitHubClientFactory(Mock.Of(), Mock.Of(), Mock.Of>(), null)); + Assert.ThrowsExactly(() => new GitHubClientFactory(null, null, null, null)); + Assert.ThrowsExactly(() => new GitHubClientFactory(Mock.Of(), null, null, null)); + Assert.ThrowsExactly(() => new GitHubClientFactory(Mock.Of(), Mock.Of(), null, null)); + Assert.ThrowsExactly(() => new GitHubClientFactory(Mock.Of(), Mock.Of(), Mock.Of>(), null)); } [TestMethod] @@ -56,12 +74,12 @@ namespace Tgstation.Server.Host.Utils.GitHub.Tests var mockApp = new Mock(); mockApp.SetupGet(x => x.ProductInfoHeaderValue).Returns(new ProductInfoHeaderValue("TGSTests", "1.2.3")).Verifiable(); - var mockOptions = new Mock>(); + var mockOptions = new Mock>(); var gc = new GeneralConfiguration(); Assert.IsNull(gc.GitHubAccessToken); - mockOptions.SetupGet(x => x.Value).Returns(gc); - var factory = new GitHubClientFactory(mockApp.Object, new BasicHttpMessageHandlerFactory(), loggerFactory.CreateLogger(), mockOptions.Object); + mockOptions.SetupGet(x => x.CurrentValue).Returns(gc); + var factory = new GitHubClientFactory(mockApp.Object, new BasicHttpMessageHandlerFactory(), mockOptions.Object, loggerFactory.CreateLogger()); var client = await factory.CreateClient(CancellationToken.None); Assert.IsNotNull(client); @@ -85,11 +103,11 @@ namespace Tgstation.Server.Host.Utils.GitHub.Tests var mockApp = new Mock(); mockApp.SetupGet(x => x.ProductInfoHeaderValue).Returns(new ProductInfoHeaderValue("TGSTests", "1.2.3")).Verifiable(); - var mockOptions = new Mock>(); - mockOptions.SetupGet(x => x.Value).Returns(new GeneralConfiguration()); - var factory = new GitHubClientFactory(mockApp.Object, new BasicHttpMessageHandlerFactory(), loggerFactory.CreateLogger(), mockOptions.Object); + var mockOptions = new Mock>(); + mockOptions.SetupGet(x => x.CurrentValue).Returns(new GeneralConfiguration()); + var factory = new GitHubClientFactory(mockApp.Object, new BasicHttpMessageHandlerFactory(), mockOptions.Object, loggerFactory.CreateLogger()); - await Assert.ThrowsExceptionAsync(() => factory.CreateClient(null, CancellationToken.None).AsTask()); + await Assert.ThrowsExactlyAsync(() => factory.CreateClient(null, CancellationToken.None).AsTask()); var client = await factory.CreateClient("asdf", CancellationToken.None); Assert.IsNotNull(client); @@ -107,9 +125,9 @@ namespace Tgstation.Server.Host.Utils.GitHub.Tests var mockApp = new Mock(); mockApp.SetupGet(x => x.ProductInfoHeaderValue).Returns(new ProductInfoHeaderValue("TGSTests", "1.2.3")).Verifiable(); - var mockOptions = new Mock>(); - mockOptions.SetupGet(x => x.Value).Returns(new GeneralConfiguration()); - var factory = new GitHubClientFactory(mockApp.Object, new BasicHttpMessageHandlerFactory(), loggerFactory.CreateLogger(), mockOptions.Object); + var mockOptions = new Mock>(); + mockOptions.SetupGet(x => x.CurrentValue).Returns(new GeneralConfiguration()); + var factory = new GitHubClientFactory(mockApp.Object, new BasicHttpMessageHandlerFactory(), mockOptions.Object, loggerFactory.CreateLogger()); var appID = Environment.GetEnvironmentVariable("TGS_TEST_APP_ID"); var privateKey = Environment.GetEnvironmentVariable("TGS_TEST_APP_PRIVATE_KEY"); @@ -145,39 +163,11 @@ namespace Tgstation.Server.Host.Utils.GitHub.Tests var mockApp = new Mock(); mockApp.SetupGet(x => x.ProductInfoHeaderValue).Returns(new ProductInfoHeaderValue("TGSTests", "1.2.3")).Verifiable(); - var mockOptions = new Mock>(); - mockOptions.SetupGet(x => x.Value).Returns(new GeneralConfiguration()); - var factory = new GitHubClientFactory(mockApp.Object, new BasicHttpMessageHandlerFactory(), loggerFactory.CreateLogger(), mockOptions.Object); + var mockOptions = new Mock>(); + mockOptions.SetupGet(x => x.CurrentValue).Returns(new GeneralConfiguration()); + var factory = new GitHubClientFactory(mockApp.Object, new BasicHttpMessageHandlerFactory(), mockOptions.Object, loggerFactory.CreateLogger()); - await Assert.ThrowsExceptionAsync(() => factory.CreateClient(null, CancellationToken.None).AsTask()); - - const string FakePrivateKey = @"-----BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEAq3oP6NMRwRZY8eMbm4GRLyfJ07LNpHzjRcjTvMf8LGGSVb8v -DBApR/5+TvWB5qnnh5fJBrR40sboJhIXUXkpyebIu/7lDqXhfjroAk8oNJhyLpW7 -u+8DpwTuZYQSSUgdMvNqrWBt7SMrFbhTtEVvQLW5LcwXL9E+V83pwQ1q37wCsedq -fsQbZSxXMMu4efaFoGJ1G60WqYiCCBSsJMPZY5St2G5Tn2caUVQX4V/kyU09gGyN -lJWFpZJP37zBdfmX+VkNj+UFusmcUe7GnklzYhyiXX2sVxzCekCAUxZKO3WKDHbg -CA02oZ8mfQiIbxCYG/uqYmAoAMbQJKqc7iu/0wIDAQABAoIBAEa37F/EzImpQb1g -QD59zPZ5nk7katLvfnuFO22bvHBBPSyH0EtVTvEWD9lYft42K/pLquhM/ZdP2OX6 -iAtdwNI3j4mYsba8yqZYfN6W7rojRRTZQ7dZ91OmQPs04KXAS+p7YP9nyW4HFvm6 -Lyslh6BUUa6FgPqDfQaRMVogwnbKUilob1eNncXRTw1PUQ+YOK28COYwp+XtNdEM -lecUVqZv5HKSMvP643sMKnj93PXWyisaj77pEdw/1CDQkP1cgqTyX4SaEgx2QDhR -ljaSIQH41JqQCDpWqbqrx3XiqpaWrIAnmUHhP0WRUlD8wCaW5PxY/HwkT/McgyGD -QUofyuECgYEA1XH6850JOrqYZagmTwgSzfbuOmQyYWxdvnq/oRjyy2rpcrISRdMh -3NdpwDX41EhWvMEjfA7l1AYccf72AbwGK+kzosqXn53NHcCjsVd1VzNo9Zm7l9Wj -SJE3ZLSrQ/8nlvrwGr0tgybhq3kLJZLZ+blBSvuYx2YHC4PQEuAo3DcCgYEAzaoO -tPCvpo23jHcevYaQQCYh1sAts9m99LUg2GUYHQ/U11VGq+DTUfReTI7LhXZuyvyx -V+bR5e6NPTuL0XBmESjDSUnj+SCVK8x+NMJMQKqS4OcGSZmx0CCtQCAykZvOA9NQ -zoXW8DkTtiwyQ+AMh9msYnWpJJj2y86/6pOqQ0UCgYB/B+Lu8dr4VO02Myj5iDiI -1BlcLx281Z3FK5C48/wsDGj7lfdCDzHsGVgayQRacuMMW3Ye807dLPXo8nC+/4Q8 -xgGxNRmgKW5V8rx5Yy+2wiYJZYE8EC2plqN9D/mN8mFBff9AKq7Xi2BriRKVPhz0 -fsjZM3vt0E8JD13angYzaQKBgHHkfBJ9u3grwPrbuL1SOK4dr92iPWz85zIN4GuV -yH3Hl6HMCsACWGRpRJN2/IQjawWkXH2GSLThn3vKbwqECTH1dfgvID2FarZ/n2CO -PPYOwBomNhgqMgtFHUyGyBpUwwjhTD2iZr5PjXf0D74A5E+THuDDsfCfeQSysRsx -vTdVAoGBAI/jjUMdjkY43zhe3w2piwT0fhGfqm9ikdAB9IcgcptuS0ML0ZaWV/eO -8a/G4KMV387BgLUPotWL29EHTdD5ir2yVIhF4JtQEpPucktacruu2TbGUm/5gV96 -0+rEPfoEdN+gHI+3w7N6owFmir7Wdz0VK9K8OxsjMLshW3sRlphg ------END RSA PRIVATE KEY-----"; + await Assert.ThrowsExactlyAsync(() => factory.CreateClient(null, CancellationToken.None).AsTask()); var fakeAccessString = $"{RepositorySettings.TgsAppPrivateKeyPrefix}123:{Convert.ToBase64String(Encoding.UTF8.GetBytes(FakePrivateKey))}"; @@ -188,21 +178,271 @@ vTdVAoGBAI/jjUMdjkY43zhe3w2piwT0fhGfqm9ikdAB9IcgcptuS0ML0ZaWV/eO } [TestMethod] - public async Task TestClientCaching() + public async Task TestBasicClientCaching() { var mockApp = new Mock(); mockApp.SetupGet(x => x.ProductInfoHeaderValue).Returns(new ProductInfoHeaderValue("TGSTests", "1.2.3")).Verifiable(); - var mockOptions = new Mock>(); - mockOptions.SetupGet(x => x.Value).Returns(new GeneralConfiguration()); - var factory = new GitHubClientFactory(mockApp.Object, new BasicHttpMessageHandlerFactory(), loggerFactory.CreateLogger(), mockOptions.Object); + var mockOptions = new Mock>(); + mockOptions.SetupGet(x => x.CurrentValue).Returns(new GeneralConfiguration()); + var factory = new GitHubClientFactory(mockApp.Object, new BasicHttpMessageHandlerFactory(), mockOptions.Object, loggerFactory.CreateLogger()); var client1 = await factory.CreateClient(CancellationToken.None); var client2 = await factory.CreateClient("asdf", CancellationToken.None); var client3 = await factory.CreateClient(CancellationToken.None); var client4 = await factory.CreateClient("asdf", CancellationToken.None); - Assert.ReferenceEquals(client1, client3); - Assert.ReferenceEquals(client2, client4); + Assert.AreSame(client1, client3); + Assert.AreSame(client2, client4); + } + + [TestMethod] + public async Task TestAppClientCaching() + { + var mockApp = new Mock(); + mockApp.SetupGet(x => x.ProductInfoHeaderValue).Returns(new ProductInfoHeaderValue("TGSTests", "1.2.3")).Verifiable(); + + const int MockAppId = 123; + const int MockRepoId = 42; + const int MockInstallationId = 542; + const string MockAccessToken = "asdf_2134_im_the_installation_access_token"; + + DateTimeOffset? mockExpiry = null; + + var mockMessageHandler = new MockHttpMessageHandler( + (request, cancellationToken) => + { + string json; + string path = request.RequestUri.AbsolutePath; + if (path == $"/repositories/{MockRepoId}/installation") + json = @"{ + ""id"": " + MockInstallationId + @", + ""account"": { + ""login"": ""octocat"", + ""id"": 1, + ""node_id"": ""MDQ6VXNlcjE="", + ""avatar_url"": ""https://github.com/images/error/octocat_happy.gif"", + ""gravatar_id"": """", + ""url"": ""https://api.github.com/users/octocat"", + ""html_url"": ""https://github.com/octocat"", + ""followers_url"": ""https://api.github.com/users/octocat/followers"", + ""following_url"": ""https://api.github.com/users/octocat/following{/other_user}"", + ""gists_url"": ""https://api.github.com/users/octocat/gists{/gist_id}"", + ""starred_url"": ""https://api.github.com/users/octocat/starred{/owner}{/repo}"", + ""subscriptions_url"": ""https://api.github.com/users/octocat/subscriptions"", + ""organizations_url"": ""https://api.github.com/users/octocat/orgs"", + ""repos_url"": ""https://api.github.com/users/octocat/repos"", + ""events_url"": ""https://api.github.com/users/octocat/events{/privacy}"", + ""received_events_url"": ""https://api.github.com/users/octocat/received_events"", + ""type"": ""User"", + ""site_admin"": false + }, + ""access_tokens_url"": ""https://api.github.com/app/installations/1/access_tokens"", + ""repositories_url"": ""https://api.github.com/installation/repositories"", + ""html_url"": ""https://github.com/organizations/github/settings/installations/1"", + ""app_id"": 1, + ""target_id"": 1, + ""target_type"": ""Organization"", + ""permissions"": { + ""checks"": ""write"", + ""metadata"": ""read"", + ""contents"": ""read"" + }, + ""events"": [ + ""push"", + ""pull_request"" + ], + ""single_file_name"": ""config.yaml"", + ""has_multiple_single_files"": true, + ""single_file_paths"": [ + ""config.yml"", + "".github/issue_TEMPLATE.md"" + ], + ""repository_selection"": ""selected"", + ""created_at"": ""2017-07-08T16:18:44-04:00"", + ""updated_at"": ""2017-07-08T16:18:44-04:00"", + ""app_slug"": ""github-actions"", + ""suspended_at"": null, + ""suspended_by"": null +}"; + else if (path == $"/app/installations/{MockInstallationId}/access_tokens") + json = @"{ + ""token"": """ + MockAccessToken + @""", + ""expires_at"": """ + (mockExpiry = DateTime.UtcNow.AddMinutes(15).AddSeconds(1)).Value.ToString("O") + @""", + ""permissions"": { + ""issues"": ""write"", + ""contents"": ""read"" + }, + ""repository_selection"": ""selected"", + ""repositories"": [ + { + ""id"": 1296269, + ""node_id"": ""MDEwOlJlcG9zaXRvcnkxMjk2MjY5"", + ""name"": ""Hello-World"", + ""full_name"": ""octocat/Hello-World"", + ""owner"": { + ""login"": ""octocat"", + ""id"": 1, + ""node_id"": ""MDQ6VXNlcjE="", + ""avatar_url"": ""https://github.com/images/error/octocat_happy.gif"", + ""gravatar_id"": """", + ""url"": ""https://api.github.com/users/octocat"", + ""html_url"": ""https://github.com/octocat"", + ""followers_url"": ""https://api.github.com/users/octocat/followers"", + ""following_url"": ""https://api.github.com/users/octocat/following{/other_user}"", + ""gists_url"": ""https://api.github.com/users/octocat/gists{/gist_id}"", + ""starred_url"": ""https://api.github.com/users/octocat/starred{/owner}{/repo}"", + ""subscriptions_url"": ""https://api.github.com/users/octocat/subscriptions"", + ""organizations_url"": ""https://api.github.com/users/octocat/orgs"", + ""repos_url"": ""https://api.github.com/users/octocat/repos"", + ""events_url"": ""https://api.github.com/users/octocat/events{/privacy}"", + ""received_events_url"": ""https://api.github.com/users/octocat/received_events"", + ""type"": ""User"", + ""site_admin"": false + }, + ""private"": false, + ""html_url"": ""https://github.com/octocat/Hello-World"", + ""description"": ""This your first repo!"", + ""fork"": false, + ""url"": ""https://api.github.com/repos/octocat/Hello-World"", + ""archive_url"": ""https://api.github.com/repos/octocat/Hello-World/{archive_format}{/ref}"", + ""assignees_url"": ""https://api.github.com/repos/octocat/Hello-World/assignees{/user}"", + ""blobs_url"": ""https://api.github.com/repos/octocat/Hello-World/git/blobs{/sha}"", + ""branches_url"": ""https://api.github.com/repos/octocat/Hello-World/branches{/branch}"", + ""collaborators_url"": ""https://api.github.com/repos/octocat/Hello-World/collaborators{/collaborator}"", + ""comments_url"": ""https://api.github.com/repos/octocat/Hello-World/comments{/number}"", + ""commits_url"": ""https://api.github.com/repos/octocat/Hello-World/commits{/sha}"", + ""compare_url"": ""https://api.github.com/repos/octocat/Hello-World/compare/{base}...{head}"", + ""contents_url"": ""https://api.github.com/repos/octocat/Hello-World/contents/{+path}"", + ""contributors_url"": ""https://api.github.com/repos/octocat/Hello-World/contributors"", + ""deployments_url"": ""https://api.github.com/repos/octocat/Hello-World/deployments"", + ""downloads_url"": ""https://api.github.com/repos/octocat/Hello-World/downloads"", + ""events_url"": ""https://api.github.com/repos/octocat/Hello-World/events"", + ""forks_url"": ""https://api.github.com/repos/octocat/Hello-World/forks"", + ""git_commits_url"": ""https://api.github.com/repos/octocat/Hello-World/git/commits{/sha}"", + ""git_refs_url"": ""https://api.github.com/repos/octocat/Hello-World/git/refs{/sha}"", + ""git_tags_url"": ""https://api.github.com/repos/octocat/Hello-World/git/tags{/sha}"", + ""git_url"": ""git:github.com/octocat/Hello-World.git"", + ""issue_comment_url"": ""https://api.github.com/repos/octocat/Hello-World/issues/comments{/number}"", + ""issue_events_url"": ""https://api.github.com/repos/octocat/Hello-World/issues/events{/number}"", + ""issues_url"": ""https://api.github.com/repos/octocat/Hello-World/issues{/number}"", + ""keys_url"": ""https://api.github.com/repos/octocat/Hello-World/keys{/key_id}"", + ""labels_url"": ""https://api.github.com/repos/octocat/Hello-World/labels{/name}"", + ""languages_url"": ""https://api.github.com/repos/octocat/Hello-World/languages"", + ""merges_url"": ""https://api.github.com/repos/octocat/Hello-World/merges"", + ""milestones_url"": ""https://api.github.com/repos/octocat/Hello-World/milestones{/number}"", + ""notifications_url"": ""https://api.github.com/repos/octocat/Hello-World/notifications{?since,all,participating}"", + ""pulls_url"": ""https://api.github.com/repos/octocat/Hello-World/pulls{/number}"", + ""releases_url"": ""https://api.github.com/repos/octocat/Hello-World/releases{/id}"", + ""ssh_url"": ""git@github.com:octocat/Hello-World.git"", + ""stargazers_url"": ""https://api.github.com/repos/octocat/Hello-World/stargazers"", + ""statuses_url"": ""https://api.github.com/repos/octocat/Hello-World/statuses/{sha}"", + ""subscribers_url"": ""https://api.github.com/repos/octocat/Hello-World/subscribers"", + ""subscription_url"": ""https://api.github.com/repos/octocat/Hello-World/subscription"", + ""tags_url"": ""https://api.github.com/repos/octocat/Hello-World/tags"", + ""teams_url"": ""https://api.github.com/repos/octocat/Hello-World/teams"", + ""trees_url"": ""https://api.github.com/repos/octocat/Hello-World/git/trees{/sha}"", + ""clone_url"": ""https://github.com/octocat/Hello-World.git"", + ""mirror_url"": ""git:git.example.com/octocat/Hello-World"", + ""hooks_url"": ""https://api.github.com/repos/octocat/Hello-World/hooks"", + ""svn_url"": ""https://svn.github.com/octocat/Hello-World"", + ""homepage"": ""https://github.com"", + ""language"": null, + ""forks_count"": 9, + ""stargazers_count"": 80, + ""watchers_count"": 80, + ""size"": 108, + ""default_branch"": ""master"", + ""open_issues_count"": 0, + ""is_template"": true, + ""topics"": [ + ""octocat"", + ""atom"", + ""electron"", + ""api"" + ], + ""has_issues"": true, + ""has_projects"": true, + ""has_wiki"": true, + ""has_pages"": false, + ""has_downloads"": true, + ""archived"": false, + ""disabled"": false, + ""visibility"": ""public"", + ""pushed_at"": ""2011-01-26T19:06:43Z"", + ""created_at"": ""2011-01-26T19:01:12Z"", + ""updated_at"": ""2011-01-26T19:14:43Z"", + ""permissions"": { + ""admin"": false, + ""push"": false, + ""pull"": true + }, + ""allow_rebase_merge"": true, + ""template_repository"": null, + ""temp_clone_token"": ""ABTLWHOULUVAXGTRYU7OC2876QJ2O"", + ""allow_squash_merge"": true, + ""allow_auto_merge"": false, + ""delete_branch_on_merge"": true, + ""allow_merge_commit"": true, + ""subscribers_count"": 42, + ""network_count"": 0, + ""license"": { + ""key"": ""mit"", + ""name"": ""MIT License"", + ""url"": ""https://api.github.com/licenses/mit"", + ""spdx_id"": ""MIT"", + ""node_id"": ""MDc6TGljZW5zZW1pdA=="", + ""html_url"": ""https://github.com/licenses/mit"" + }, + ""forks"": 1, + ""open_issues"": 1, + ""watchers"": 1 + } + ] +}"; + else + { + Assert.Fail($"Unrecognized request path: {request.RequestUri}"); + return Task.FromResult(new HttpResponseMessage()); + } + + var result = new HttpResponseMessage(); + result.StatusCode = HttpStatusCode.OK; + result.Content = new StringContent(json); + result.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); + + return Task.FromResult(result); + }); + + var mockMessageHandlerFactory = new Mock(); + mockMessageHandlerFactory.Setup(x => x.CreateHandler(It.IsAny())).Returns(mockMessageHandler); + + var mockOptions = new Mock>(); + mockOptions.SetupGet(x => x.CurrentValue).Returns(new GeneralConfiguration()); + var factory = new GitHubClientFactory(mockApp.Object, mockMessageHandlerFactory.Object, mockOptions.Object, loggerFactory.CreateLogger()); + + var fakeAccessString = $"{RepositorySettings.TgsAppPrivateKeyPrefix}{MockAppId}:{Convert.ToBase64String(Encoding.UTF8.GetBytes(FakePrivateKey))}"; + var repoIdentifier = new RepositoryIdentifier(MockRepoId); + + var client1 = await factory.CreateClientForRepository(fakeAccessString, repoIdentifier, CancellationToken.None); + Assert.IsNotNull(client1); + Assert.AreEqual(MockAccessToken, client1.Connection.Credentials.GetToken()); + + var client2 = await factory.CreateClientForRepository(fakeAccessString, repoIdentifier, CancellationToken.None); + Assert.AreSame(client1, client2); + + IGitHubClient client3 = null; + for (var i = 0; i < 100; ++i) + { + await Task.Delay(TimeSpan.FromMilliseconds(50)); + client3 = await factory.CreateClientForRepository(fakeAccessString, repoIdentifier, CancellationToken.None); + + if (client3 != client2) + break; + } + + Assert.AreNotSame(client2, client3); + Assert.IsNotNull(client3); + Assert.AreEqual(MockAccessToken, client3.Connection.Credentials.GetToken()); } } } diff --git a/tests/Tgstation.Server.Host.Tests/Utils/GitHub/TestGitHubServiceFactory.cs b/tests/Tgstation.Server.Host.Tests/Utils/GitHub/TestGitHubServiceFactory.cs index 24212eaa93..0bef83a84a 100644 --- a/tests/Tgstation.Server.Host.Tests/Utils/GitHub/TestGitHubServiceFactory.cs +++ b/tests/Tgstation.Server.Host.Tests/Utils/GitHub/TestGitHubServiceFactory.cs @@ -20,11 +20,11 @@ namespace Tgstation.Server.Host.Utils.GitHub.Tests [TestMethod] public void TestConstructor() { - Assert.ThrowsException(() => new GitHubServiceFactory(null, null, null)); - Assert.ThrowsException(() => new GitHubServiceFactory(Mock.Of(), null, null)); - Assert.ThrowsException(() => new GitHubServiceFactory(Mock.Of(), Mock.Of(), null)); - var mockOptions = new Mock>(); - mockOptions.SetupGet(x => x.Value).Returns(new UpdatesConfiguration()); + Assert.ThrowsExactly(() => new GitHubServiceFactory(null, null, null)); + Assert.ThrowsExactly(() => new GitHubServiceFactory(Mock.Of(), null, null)); + Assert.ThrowsExactly(() => new GitHubServiceFactory(Mock.Of(), Mock.Of(), null)); + var mockOptions = new Mock>(); + mockOptions.SetupGet(x => x.CurrentValue).Returns(new UpdatesConfiguration()); _ = new GitHubServiceFactory(Mock.Of(), Mock.Of(), mockOptions.Object); } @@ -41,12 +41,12 @@ namespace Tgstation.Server.Host.Utils.GitHub.Tests mockFactory.Setup(x => x.CreateClient(mockToken, It.IsAny())).Returns(ValueTask.FromResult(Mock.Of())).Verifiable(); #pragma warning restore CA2012 // Use ValueTasks correctly - var mockOptions = new Mock>(); - mockOptions.SetupGet(x => x.Value).Returns(new UpdatesConfiguration()); + var mockOptions = new Mock>(); + mockOptions.SetupGet(x => x.CurrentValue).Returns(new UpdatesConfiguration()); var factory = new GitHubServiceFactory(mockFactory.Object, Mock.Of(), mockOptions.Object); - await Assert.ThrowsExceptionAsync(() => factory.CreateService(null, CancellationToken.None).AsTask()); + await Assert.ThrowsExactlyAsync(() => factory.CreateService(null, CancellationToken.None).AsTask()); Assert.AreEqual(0, mockFactory.Invocations.Count); var result1 = await factory.CreateService(CancellationToken.None); diff --git a/tests/Tgstation.Server.Host.Tests/Utils/TestAsyncDelayer.cs b/tests/Tgstation.Server.Host.Tests/Utils/TestAsyncDelayer.cs index 991a03ceb3..7adaabf35b 100644 --- a/tests/Tgstation.Server.Host.Tests/Utils/TestAsyncDelayer.cs +++ b/tests/Tgstation.Server.Host.Tests/Utils/TestAsyncDelayer.cs @@ -28,7 +28,7 @@ namespace Tgstation.Server.Host.Utils.Tests var delayer = new AsyncDelayer(Mock.Of>()); using var cts = new CancellationTokenSource(); cts.Cancel(); - await Assert.ThrowsExceptionAsync(() => delayer.Delay(TimeSpan.FromSeconds(1), cts.Token).AsTask()); + await Assert.ThrowsExactlyAsync(() => delayer.Delay(TimeSpan.FromSeconds(1), cts.Token).AsTask()); } } } diff --git a/tests/Tgstation.Server.Host.Watchdog.Tests/TestWatchdog.cs b/tests/Tgstation.Server.Host.Watchdog.Tests/TestWatchdog.cs index 0ba2e31764..f12df3a125 100644 --- a/tests/Tgstation.Server.Host.Watchdog.Tests/TestWatchdog.cs +++ b/tests/Tgstation.Server.Host.Watchdog.Tests/TestWatchdog.cs @@ -13,9 +13,9 @@ namespace Tgstation.Server.Host.Watchdog.Tests [TestMethod] public void TestConstruction() { - Assert.ThrowsException(() => new Watchdog(null, null)); + Assert.ThrowsExactly(() => new Watchdog(null, null)); var mockSignalChecker = Mock.Of(); - Assert.ThrowsException(() => new Watchdog(mockSignalChecker, null)); + Assert.ThrowsExactly(() => new Watchdog(mockSignalChecker, null)); var mockLogger = Mock.Of>(); var wd = new Watchdog(mockSignalChecker, mockLogger); } diff --git a/tests/Tgstation.Server.Tests/CachingFileDownloader.cs b/tests/Tgstation.Server.Tests/CachingFileDownloader.cs index 16dce2af75..a0ebbab9da 100644 --- a/tests/Tgstation.Server.Tests/CachingFileDownloader.cs +++ b/tests/Tgstation.Server.Tests/CachingFileDownloader.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.IO.Abstractions; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; @@ -49,7 +50,7 @@ namespace Tgstation.Server.Tests var cfd = new CachingFileDownloader(loggerFactory.CreateLogger()); // this also will inject the edge version - var edgeVersion = await EngineTest.GetEdgeVersion(Api.Models.EngineType.Byond, logger, cfd, cancellationToken); + await EngineTest.GetEdgeVersion(Api.Models.EngineType.Byond, logger, cfd, cancellationToken); // predownload the target github release update asset var gitHubToken = Environment.GetEnvironmentVariable("TGS_TEST_GITHUB_TOKEN"); @@ -83,6 +84,11 @@ namespace Tgstation.Server.Tests public static async ValueTask InitializeByondVersion(ILogger logger, Version byondVersion, bool windows, CancellationToken cancellationToken, string urlCacheOverrideTemplate = null) { + if (Boolean.TryParse(Environment.GetEnvironmentVariable("TGS_TEST_OD_EXCLUSIVE"), out var odExclusive) && odExclusive) + { + return; + } + var version = new EngineVersion { Engine = Api.Models.EngineType.Byond, @@ -174,16 +180,26 @@ namespace Tgstation.Server.Tests public IFileStreamProvider DownloadFile(Uri url, string bearerToken) => new ProviderPackage(logger, url, bearerToken); - static FileDownloader CreateRealDownloader(ILogger logger) - => new( - new HttpClientFactory( - new AssemblyInformationProvider().ProductInfoHeaderValue), + public static FileDownloader CreateRealDownloader(ILogger logger) + { + var mockHttpClientFactory = new Mock(); + mockHttpClientFactory.Setup(x => x.CreateClient(String.Empty)).Returns( + () => + { + var client = new HttpClient(); + client.DefaultRequestHeaders.UserAgent.Add(new AssemblyInformationProvider().ProductInfoHeaderValue); + return client; + }); + + return new ( + mockHttpClientFactory.Object, logger != null ? new Logger( TestingUtils.CreateLoggerFactoryForLogger( logger, out _)) : Mock.Of>()); + } static async Task CacheFile(ILogger logger, Uri url, string bearerToken, string path, CancellationToken cancellationToken) { diff --git a/tests/Tgstation.Server.Tests/Live/AdministrationTest.cs b/tests/Tgstation.Server.Tests/Live/AdministrationTest.cs index d50205da90..7c3c65a564 100644 --- a/tests/Tgstation.Server.Tests/Live/AdministrationTest.cs +++ b/tests/Tgstation.Server.Tests/Live/AdministrationTest.cs @@ -43,12 +43,12 @@ namespace Tgstation.Server.Tests.Live Assert.IsTrue(logFile.LastModified <= downloadedTuple.Item1.LastModified); Assert.IsNull(logFile.FileTicket); - await ApiAssert.ThrowsException>(() => restClient.GetLog(new LogFileResponse + await ApiAssert.ThrowsExactly>(() => restClient.GetLog(new LogFileResponse { Name = "very_fake_path.log" }, cancellationToken), ErrorCode.IOError); - await ApiAssert.ThrowsException>(() => restClient.GetLog(new LogFileResponse + await ApiAssert.ThrowsExactly>(() => restClient.GetLog(new LogFileResponse { Name = "../out_of_bounds.file" }, cancellationToken)); diff --git a/tests/Tgstation.Server.Tests/Live/ApiAssert.cs b/tests/Tgstation.Server.Tests/Live/ApiAssert.cs index 2792289dd6..931fb4c643 100644 --- a/tests/Tgstation.Server.Tests/Live/ApiAssert.cs +++ b/tests/Tgstation.Server.Tests/Live/ApiAssert.cs @@ -27,7 +27,7 @@ namespace Tgstation.Server.Tests.Live /// A resulting in a . /// The expected . /// A representing the running operation, - public static async ValueTask ThrowsException(Func action, Api.Models.ErrorCode? expectedErrorCode = null) + public static async ValueTask ThrowsExactly(Func action, Api.Models.ErrorCode? expectedErrorCode = null) where TApiException : ApiException { try @@ -51,7 +51,7 @@ namespace Tgstation.Server.Tests.Live /// A resulting in a . /// The expected . /// A representing the running operation, - public static async ValueTask ThrowsException(Func> action, Api.Models.ErrorCode? expectedErrorCode = null) + public static async ValueTask ThrowsExactly(Func> action, Api.Models.ErrorCode? expectedErrorCode = null) where TApiException : ApiException { try diff --git a/tests/Tgstation.Server.Tests/Live/Instance/ChatTest.cs b/tests/Tgstation.Server.Tests/Live/Instance/ChatTest.cs index 1dcfd368da..90df485a2d 100644 --- a/tests/Tgstation.Server.Tests/Live/Instance/ChatTest.cs +++ b/tests/Tgstation.Server.Tests/Live/Instance/ChatTest.cs @@ -284,7 +284,7 @@ namespace Tgstation.Server.Tests.Live.Instance async Task RunLimitTests(CancellationToken cancellationToken) { - await ApiAssert.ThrowsException(() => chatClient.Create(new ChatBotCreateRequest + await ApiAssert.ThrowsExactly(() => chatClient.Create(new ChatBotCreateRequest { Name = "asdf", ConnectionString = "asdf", @@ -311,18 +311,18 @@ namespace Tgstation.Server.Tests.Live.Instance ChannelData = discordBotReq.Channels.First().ChannelData }); - await ApiAssert.ThrowsException(() => chatClient.Update(discordBotReq, cancellationToken), ErrorCode.ChatBotMaxChannels); + await ApiAssert.ThrowsExactly(() => chatClient.Update(discordBotReq, cancellationToken), ErrorCode.ChatBotMaxChannels); var oldChannels = discordBotReq.Channels; discordBotReq.Channels = null; discordBotReq.ChannelLimit = 0; - await ApiAssert.ThrowsException(() => chatClient.Update(discordBotReq, cancellationToken), ErrorCode.ChatBotMaxChannels); + await ApiAssert.ThrowsExactly(() => chatClient.Update(discordBotReq, cancellationToken), ErrorCode.ChatBotMaxChannels); discordBotReq.Channels = oldChannels; discordBotReq.ChannelLimit = null; - await ApiAssert.ThrowsException(() => chatClient.Update(discordBotReq, cancellationToken), ErrorCode.ChatBotMaxChannels); + await ApiAssert.ThrowsExactly(() => chatClient.Update(discordBotReq, cancellationToken), ErrorCode.ChatBotMaxChannels); - await ApiAssert.ThrowsException(() => instanceClient.Update(new InstanceUpdateRequest + await ApiAssert.ThrowsExactly(() => instanceClient.Update(new InstanceUpdateRequest { Id = metadata.Id, ChatBotLimit = 0 diff --git a/tests/Tgstation.Server.Tests/Live/Instance/ConfigurationTest.cs b/tests/Tgstation.Server.Tests/Live/Instance/ConfigurationTest.cs index 52217388c1..7480d3478f 100644 --- a/tests/Tgstation.Server.Tests/Live/Instance/ConfigurationTest.cs +++ b/tests/Tgstation.Server.Tests/Live/Instance/ConfigurationTest.cs @@ -70,7 +70,7 @@ namespace Tgstation.Server.Tests.Live.Instance Assert.AreEqual(TestString, Encoding.UTF8.GetString(downloadMemoryStream.ToArray()).Trim()); } - await ApiAssert.ThrowsException(() => configurationClient.DeleteEmptyDirectory(TestDir, cancellationToken), ErrorCode.ConfigurationDirectoryNotEmpty); + await ApiAssert.ThrowsExactly(() => configurationClient.DeleteEmptyDirectory(TestDir, cancellationToken), ErrorCode.ConfigurationDirectoryNotEmpty); file.FileTicket = null; await configurationClient.Write(new ConfigurationFileRequest diff --git a/tests/Tgstation.Server.Tests/Live/Instance/DeploymentTest.cs b/tests/Tgstation.Server.Tests/Live/Instance/DeploymentTest.cs index 839e47382e..2a3bfb3242 100644 --- a/tests/Tgstation.Server.Tests/Live/Instance/DeploymentTest.cs +++ b/tests/Tgstation.Server.Tests/Live/Instance/DeploymentTest.cs @@ -115,7 +115,7 @@ namespace Tgstation.Server.Tests.Live.Instance Assert.AreEqual(true, dmSettings.RequireDMApiValidation); #pragma warning restore CS0618 // Type or member is obsolete - await ApiAssert.ThrowsException(() => dreamMakerClient.Update(new DreamMakerRequest + await ApiAssert.ThrowsExactly(() => dreamMakerClient.Update(new DreamMakerRequest { #pragma warning disable CS0618 // Type or member is obsolete RequireDMApiValidation = true, @@ -232,12 +232,12 @@ namespace Tgstation.Server.Tests.Live.Instance await CheckDreamDaemonPriority(deploymentJobWaitTask, cancellationToken); Console.WriteLine($"PORT REUSE BUG 2: Expect Conflict, Setting I-{instanceClient.Metadata.Id} DD to {dmPort}"); - var t1 = ApiAssert.ThrowsException(() => dreamDaemonClient.Update(new DreamDaemonRequest + var t1 = ApiAssert.ThrowsExactly(() => dreamDaemonClient.Update(new DreamDaemonRequest { Port = dmPort, }, cancellationToken), ErrorCode.PortNotAvailable); Console.WriteLine($"PORT REUSE BUG 3: Expect Conflict, Setting I-{instanceClient.Metadata.Id} DM to {ddPort}"); - var t2 = ApiAssert.ThrowsException(() => dreamMakerClient.Update(new DreamMakerRequest + var t2 = ApiAssert.ThrowsExactly(() => dreamMakerClient.Update(new DreamMakerRequest { ApiValidationPort = ddPort }, cancellationToken), ErrorCode.PortNotAvailable); @@ -309,7 +309,7 @@ namespace Tgstation.Server.Tests.Live.Instance }, cancellationToken); Assert.IsFalse((updatedPS.DreamDaemonRights.Value & DreamDaemonRights.SetVisibility) != 0); - await ApiAssert.ThrowsException(() => dreamDaemonClient.Update(new DreamDaemonRequest + await ApiAssert.ThrowsExactly(() => dreamDaemonClient.Update(new DreamDaemonRequest { Visibility = DreamDaemonVisibility.Private }, cancellationToken)); diff --git a/tests/Tgstation.Server.Tests/Live/Instance/EngineTest.cs b/tests/Tgstation.Server.Tests/Live/Instance/EngineTest.cs index 1497054e7d..fe058f519a 100644 --- a/tests/Tgstation.Server.Tests/Live/Instance/EngineTest.cs +++ b/tests/Tgstation.Server.Tests/Live/Instance/EngineTest.cs @@ -102,7 +102,7 @@ namespace Tgstation.Server.Tests.Live.Instance } ValueTask TestInstallNullVersion(CancellationToken cancellationToken) - => ApiAssert.ThrowsException( + => ApiAssert.ThrowsExactly( () => engineClient.SetActiveVersion( new EngineVersionRequest { @@ -145,7 +145,7 @@ namespace Tgstation.Server.Tests.Live.Instance }, cancellationToken); await WaitForJob(deleteThisOneBecauseItWasntPartOfTheOriginalTest, EngineInstallationTimeout(), false, null, cancellationToken); - var nonExistentUninstallResponseTask = ApiAssert.ThrowsException(() => engineClient.DeleteVersion( + var nonExistentUninstallResponseTask = ApiAssert.ThrowsExactly(() => engineClient.DeleteVersion( new EngineVersionDeleteRequest { EngineVersion = new EngineVersion @@ -168,7 +168,7 @@ namespace Tgstation.Server.Tests.Live.Instance }, cancellationToken); - var badBecauseActiveResponseTask = ApiAssert.ThrowsException(() => engineClient.DeleteVersion( + var badBecauseActiveResponseTask = ApiAssert.ThrowsExactly(() => engineClient.DeleteVersion( new EngineVersionDeleteRequest { EngineVersion = new EngineVersion @@ -212,7 +212,7 @@ namespace Tgstation.Server.Tests.Live.Instance } }; - await ApiAssert.ThrowsException(() => engineClient.SetActiveVersion(newModel, null, cancellationToken), ErrorCode.ModelValidationFailure); + await ApiAssert.ThrowsExactly(() => engineClient.SetActiveVersion(newModel, null, cancellationToken), ErrorCode.ModelValidationFailure); newModel.EngineVersion.Engine = testEngine; @@ -270,8 +270,8 @@ namespace Tgstation.Server.Tests.Live.Instance { ByondZipDownloadTemplate = TestingUtils.ByondZipDownloadTemplate, }); - var sessionConfigOptionsMock = new Mock>(); - sessionConfigOptionsMock.SetupGet(x => x.Value).Returns(new SessionConfiguration()); + var sessionConfigOptionsMock = new Mock>(); + sessionConfigOptionsMock.SetupGet(x => x.CurrentValue).Returns(new SessionConfiguration()); var assemblyInformationProvider = new AssemblyInformationProvider(); @@ -347,7 +347,7 @@ namespace Tgstation.Server.Tests.Live.Instance } }, null, cancellationToken); Assert.IsNull(installResponse.InstallJob); - await ApiAssert.ThrowsException(() => engineClient.SetActiveVersion(new EngineVersionRequest + await ApiAssert.ThrowsExactly(() => engineClient.SetActiveVersion(new EngineVersionRequest { EngineVersion = new EngineVersion { diff --git a/tests/Tgstation.Server.Tests/Live/Instance/InstanceTest.cs b/tests/Tgstation.Server.Tests/Live/Instance/InstanceTest.cs index 5fd65574eb..1ba584277b 100644 --- a/tests/Tgstation.Server.Tests/Live/Instance/InstanceTest.cs +++ b/tests/Tgstation.Server.Tests/Live/Instance/InstanceTest.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO.Abstractions; using System.Linq; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; @@ -16,7 +17,6 @@ using Tgstation.Server.Api.Models.Request; using Tgstation.Server.Api.Models.Response; using Tgstation.Server.Client; using Tgstation.Server.Client.Components; -using Tgstation.Server.Common.Http; using Tgstation.Server.Host.Components; using Tgstation.Server.Host.Components.Engine; using Tgstation.Server.Host.Components.Events; @@ -122,20 +122,20 @@ namespace Tgstation.Server.Tests.Live.Instance new NoopEventConsumer(), Mock.Of(), Mock.Of(), + mockOptionsMonitor.Object, Mock.Of>(), - Mock.Of>(), - genConfig), + Mock.Of>()), Mock.Of(), - Mock.Of(), - Options.Create(genConfig), - Options.Create(new SessionConfiguration())) + Mock.Of(), + mockOptionsMonitor.Object, + Mock.Of>()) : new PlatformIdentifier().IsWindows ? new WindowsByondInstaller( Mock.Of(), Mock.Of(), fileDownloader, mockOptionsMonitor.Object, - Options.Create(new SessionConfiguration()), + Mock.Of>(), Mock.Of>()) : new PosixByondInstaller( Mock.Of(), @@ -169,20 +169,21 @@ namespace Tgstation.Server.Tests.Live.Instance async Task UpdateDMSettings() { - for (var i = 0; i < 10; ++i) + const int Limit = 10; + for (var i = 0; i < Limit; ++i) try { - global::System.Console.WriteLine($"PORT REUSE BUG 6: Setting I-{instanceClient.Metadata.Id} DM to {dmPort}"); + if (i != 0) + { + global::System.Console.WriteLine($"PORT REUSE BUG 6: Setting I-{instanceClient.Metadata.Id} DM to {dmPort}"); + } await instanceClient.DreamMaker.Update(new DreamMakerRequest { ApiValidationPort = dmPort, }, cancellationToken); } - catch (ConflictException ex) when (ex.ErrorCode == ErrorCode.PortNotAvailable) + catch (ConflictException ex) when (ex.ErrorCode == ErrorCode.PortNotAvailable && i < (Limit - 1)) { - if (i == 4) - throw; - // I have no idea why this happens sometimes await Task.Delay(TimeSpan.FromSeconds(3), cancellationToken); } diff --git a/tests/Tgstation.Server.Tests/Live/Instance/RepositoryTest.cs b/tests/Tgstation.Server.Tests/Live/Instance/RepositoryTest.cs index cef8a33713..b2f35d57e7 100644 --- a/tests/Tgstation.Server.Tests/Live/Instance/RepositoryTest.cs +++ b/tests/Tgstation.Server.Tests/Live/Instance/RepositoryTest.cs @@ -44,7 +44,7 @@ namespace Tgstation.Server.Tests.Live.Instance async Task Rest() { await Task.Yield(); - await ApiAssert.ThrowsException(() => repositoryClient.Read(cancellationToken), ErrorCode.RepoCloning); + await ApiAssert.ThrowsExactly(() => repositoryClient.Read(cancellationToken), ErrorCode.RepoCloning); Assert.IsNotNull(clone); Assert.AreEqual(cloneRequest.Origin, clone.Origin); Assert.AreEqual(workingBranch, clone.Reference); @@ -116,7 +116,7 @@ namespace Tgstation.Server.Tests.Live.Instance Assert.AreNotEqual(default, readAfterClone.RevisionInformation.Timestamp); // Specific SHA - await ApiAssert.ThrowsException(() => Checkout(new RepositoryUpdateRequest { Reference = "master", CheckoutSha = "286bb75" }, false, false, cancellationToken), ErrorCode.RepoMismatchShaAndReference); + await ApiAssert.ThrowsExactly(() => Checkout(new RepositoryUpdateRequest { Reference = "master", CheckoutSha = "286bb75" }, false, false, cancellationToken), ErrorCode.RepoMismatchShaAndReference); var updated = await Checkout(new RepositoryUpdateRequest { CheckoutSha = "286bb75" }, false, false, cancellationToken); // Fake SHA @@ -160,7 +160,7 @@ namespace Tgstation.Server.Tests.Live.Instance Assert.AreEqual(RepositoryRights.SetSha, newPerms.RepositoryRights); - await ApiAssert.ThrowsException(async () => await repositoryClient.Read(cancellationToken)); + await ApiAssert.ThrowsExactly(async () => await repositoryClient.Read(cancellationToken)); await instanceClient.PermissionSets.Update(new InstancePermissionSetRequest { diff --git a/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs b/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs index bcff8cf8e3..40f943a66d 100644 --- a/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs +++ b/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs @@ -1,18 +1,4 @@ -using Byond.TopicSender; - -using Microsoft.Extensions.Logging; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -using Mono.Unix; -using Mono.Unix.Native; - -using Moq; - -using Newtonsoft.Json; - -using Remora.Discord.Rest.Extensions; - -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.IO; @@ -27,8 +13,19 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using Byond.TopicSender; + +using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Mono.Unix; +using Mono.Unix.Native; + +using Moq; + +using Newtonsoft.Json; + using Tgstation.Server.Api.Models; -using Tgstation.Server.Api.Models.Internal; using Tgstation.Server.Api.Models.Request; using Tgstation.Server.Api.Models.Response; using Tgstation.Server.Client; @@ -44,8 +41,6 @@ using Tgstation.Server.Host.IO; using Tgstation.Server.Host.System; using Tgstation.Server.Host.Utils; -using static Microsoft.EntityFrameworkCore.DbLoggerCategory; - namespace Tgstation.Server.Tests.Live.Instance { sealed class WatchdogTest : JobsRequiredTest @@ -177,17 +172,17 @@ namespace Tgstation.Server.Tests.Live.Instance await Task.WhenAll( UpdateDDSettings(), CheckByondVersions(), - ApiAssert.ThrowsException(() => instanceClient.DreamDaemon.Update(new DreamDaemonRequest + ApiAssert.ThrowsExactly(() => instanceClient.DreamDaemon.Update(new DreamDaemonRequest { SoftShutdown = true, SoftRestart = true }, cancellationToken), ErrorCode.GameServerDoubleSoft).AsTask(), - ApiAssert.ThrowsException(() => instanceClient.DreamDaemon.Update(new DreamDaemonRequest + ApiAssert.ThrowsExactly(() => instanceClient.DreamDaemon.Update(new DreamDaemonRequest { Port = 0 }, cancellationToken), ErrorCode.ModelValidationFailure).AsTask(), - ApiAssert.ThrowsException(() => instanceClient.DreamDaemon.CreateDump(cancellationToken), ErrorCode.WatchdogNotRunning).AsTask(), - ApiAssert.ThrowsException(() => instanceClient.DreamDaemon.Restart(cancellationToken), ErrorCode.WatchdogNotRunning).AsTask()); + ApiAssert.ThrowsExactly(() => instanceClient.DreamDaemon.CreateDump(cancellationToken), ErrorCode.WatchdogNotRunning).AsTask(), + ApiAssert.ThrowsExactly(() => instanceClient.DreamDaemon.Restart(cancellationToken), ErrorCode.WatchdogNotRunning).AsTask()); await RunBasicTest(false, cancellationToken); @@ -346,7 +341,7 @@ namespace Tgstation.Server.Tests.Live.Instance { await RegressionTest1686(cancellationToken); - await ApiAssert.ThrowsException(() => instanceClient.DreamDaemon.Update(new DreamDaemonRequest + await ApiAssert.ThrowsExactly(() => instanceClient.DreamDaemon.Update(new DreamDaemonRequest { BroadcastMessage = "ksjfdksjf", }, cancellationToken), ErrorCode.BroadcastFailure); @@ -359,6 +354,8 @@ namespace Tgstation.Server.Tests.Live.Instance await TestLegacyBridgeEndpoint(cancellationToken); + await TestDeploymentTrigger(cancellationToken); + var deleteJobTask = TestDeleteByondInstallErrorCasesAndQueing(cancellationToken); SessionController.LogTopicRequests = false; @@ -557,53 +554,60 @@ namespace Tgstation.Server.Tests.Live.Instance Assert.AreEqual(1, dumpFiles.Length); File.Delete(dumpFiles.Single()); - JobResponse job; - while (true) + // fuck this test, it's flakey as a motherfucker + if (Environment.NewLine == null) { - KillDD(true); - var jobTcs = new TaskCompletionSource(); - var killTaskStarted = new TaskCompletionSource(); - var killThread = new Thread(() => + if (testVersion.Engine != EngineType.OpenDream) { - killTaskStarted.SetResult(); - while (!jobTcs.Task.IsCompleted) - KillDD(false); - }) - { - Priority = ThreadPriority.AboveNormal - }; + JobResponse job; + while (true) + { + KillDD(true); + var jobTcs = new TaskCompletionSource(); + var killTaskStarted = new TaskCompletionSource(); + var killThread = new Thread(() => + { + killTaskStarted.SetResult(); + while (!jobTcs.Task.IsCompleted) + KillDD(false); + }) + { + Priority = ThreadPriority.AboveNormal + }; - killThread.Start(); - try - { - await killTaskStarted.Task; - var dumpTask = instanceClient.DreamDaemon.CreateDump(cancellationToken); - job = await WaitForJob(await dumpTask, 20, true, null, cancellationToken); + killThread.Start(); + try + { + await killTaskStarted.Task; + var dumpTask = instanceClient.DreamDaemon.CreateDump(cancellationToken); + job = await WaitForJob(await dumpTask, 20, true, null, cancellationToken); + } + finally + { + jobTcs.SetResult(); + killThread.Join(); + } + + // these can also happen + + if (!(new PlatformIdentifier().IsWindows + && (job.ExceptionDetails.Contains("Access is denied.") + || job.ExceptionDetails.Contains("The handle is invalid.") + || job.ExceptionDetails.Contains("Unknown error") + || job.ExceptionDetails.Contains("No process is associated with this object.") + || job.ExceptionDetails.Contains("The program issued a command but the command length is incorrect.") + || job.ExceptionDetails.Contains("Only part of a ReadProcessMemory or WriteProcessMemory request was completed.") + || job.ExceptionDetails.Contains("Unknown error")))) + break; + + var restartJob = await instanceClient.DreamDaemon.Restart(cancellationToken); + await WaitForJob(restartJob, 20, false, null, cancellationToken); + } + + Assert.IsTrue(job.ErrorCode == ErrorCode.GameServerOffline || job.ErrorCode == ErrorCode.GCoreFailure, $"{job.ErrorCode}: {job.ExceptionDetails}"); } - finally - { - jobTcs.SetResult(); - killThread.Join(); - } - - // these can also happen - - if (!(new PlatformIdentifier().IsWindows - && (job.ExceptionDetails.Contains("Access is denied.") - || job.ExceptionDetails.Contains("The handle is invalid.") - || job.ExceptionDetails.Contains("Unknown error") - || job.ExceptionDetails.Contains("No process is associated with this object.") - || job.ExceptionDetails.Contains("The program issued a command but the command length is incorrect.") - || job.ExceptionDetails.Contains("Only part of a ReadProcessMemory or WriteProcessMemory request was completed.") - || job.ExceptionDetails.Contains("Unknown error")))) - break; - - var restartJob = await instanceClient.DreamDaemon.Restart(cancellationToken); - await WaitForJob(restartJob, 20, false, null, cancellationToken); } - Assert.IsTrue(job.ErrorCode == ErrorCode.GameServerOffline || job.ErrorCode == ErrorCode.GCoreFailure, $"{job.ErrorCode}: {job.ExceptionDetails}"); - var restartJob2 = await instanceClient.DreamDaemon.Restart(cancellationToken); await WaitForJob(restartJob2, 20, false, null, cancellationToken); } @@ -666,7 +670,7 @@ namespace Tgstation.Server.Tests.Live.Instance int nonLiveDirs = 0; // cleanup task is async - for(var i = 0; i < 20; ++i) + for (var i = 0; i < 20; ++i) { nonLiveDirs = CountNonLiveDirs(); if (expected == nonLiveDirs) @@ -745,6 +749,9 @@ namespace Tgstation.Server.Tests.Live.Instance await CheckDDPriority(); Assert.AreEqual(false, daemonStatus.SoftRestart); Assert.AreEqual(false, daemonStatus.SoftShutdown); + + Assert.AreEqual(skipApiValidation, !daemonStatus.WorldIteration.HasValue); + Assert.IsTrue(daemonStatus.ImmediateMemoryUsage.HasValue); Assert.AreNotEqual(0, daemonStatus.ImmediateMemoryUsage.Value); @@ -759,7 +766,37 @@ namespace Tgstation.Server.Tests.Live.Instance Assert.IsFalse(daemonStatus.LaunchTime.HasValue); await ExpectGameDirectoryCount(1, cancellationToken); - await CheckDMApiFail(daemonStatus.ActiveCompileJob, cancellationToken, false, skipApiValidation); + await CheckDMApiFail(daemonStatus.ActiveCompileJob, cancellationToken, false, false); + + if (!skipApiValidation && !watchdogRestartsProcess) + { + daemonStatus = await instanceClient.DreamDaemon.Update(new DreamDaemonRequest + { + AdditionalParameters = "basic_reboot=yes", + }, cancellationToken); + Assert.AreEqual("basic_reboot=yes", daemonStatus.AdditionalParameters); + + startJob = await StartDD(cancellationToken); + + await WaitForJob(startJob, 40, false, null, cancellationToken); + daemonStatus = await instanceClient.DreamDaemon.Read(cancellationToken); + + var initialWorldIteration = daemonStatus.WorldIteration; + var initialSessionId = daemonStatus.SessionId.Value; + Assert.IsTrue(initialWorldIteration.HasValue); + + for (int i = 0; i < 60 && daemonStatus.WorldIteration == initialWorldIteration; ++i) + { + await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken); + daemonStatus = await instanceClient.DreamDaemon.Read(cancellationToken); + } + + Assert.IsTrue(daemonStatus.WorldIteration.HasValue); + Assert.AreEqual(initialSessionId, daemonStatus.SessionId.Value); + Assert.IsTrue(initialWorldIteration.Value < daemonStatus.WorldIteration.Value); + + await GracefulWatchdogShutdown(cancellationToken); + } daemonStatus = await instanceClient.DreamDaemon.Update(new DreamDaemonRequest { @@ -1450,7 +1487,7 @@ namespace Tgstation.Server.Tests.Live.Instance bool firstTime = true; do { - if(!firstTime) + if (!firstTime) Assert.IsFalse(daemonStatus.SoftRestart); ValidateSessionId(daemonStatus, true); @@ -1497,13 +1534,15 @@ namespace Tgstation.Server.Tests.Live.Instance return ddProc != null; } - public Task TellWorldToReboot(bool waitForOnlineIfRestoring, CancellationToken cancellationToken, [CallerLineNumber]int source = 0) + public Task TellWorldToReboot(bool waitForOnlineIfRestoring, CancellationToken cancellationToken, [CallerLineNumber] int source = 0) => TellWorldToReboot2(instanceClient, instanceManager, topicClient, FindTopicPort(), waitForOnlineIfRestoring || testVersion.Engine.Value == EngineType.OpenDream, cancellationToken, source); - public static async Task TellWorldToReboot2(IInstanceClient instanceClient, IInstanceManager instanceManager, ITopicClient topicClient, ushort topicPort, bool waitForOnlineIfRestoring, CancellationToken cancellationToken, [CallerLineNumber]int source = 0, [CallerFilePath]string path = null) + public static async Task TellWorldToReboot2(IInstanceClient instanceClient, IInstanceManager instanceManager, ITopicClient topicClient, ushort topicPort, bool waitForOnlineIfRestoring, CancellationToken cancellationToken, [CallerLineNumber] int source = 0, [CallerFilePath] string path = null) { var daemonStatus = await instanceClient.DreamDaemon.Read(cancellationToken); Assert.IsNotNull(daemonStatus.StagedCompileJob); var initialSession = daemonStatus.ActiveCompileJob; + var initialSessionId = daemonStatus.SessionId.Value; + var initialIteration = daemonStatus.WorldIteration; System.Console.WriteLine($"TEST: Sending world reboot topic @ {path}#L{source}"); @@ -1512,16 +1551,17 @@ namespace Tgstation.Server.Tests.Live.Instance using var tempCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); var tempToken = tempCts.Token; + + bool SameSession() => initialSessionId == daemonStatus.SessionId && initialIteration == daemonStatus.WorldIteration; using (tempToken.Register(() => System.Console.WriteLine("TEST ERROR: Timeout in TellWorldToReboot!"))) { tempCts.CancelAfter(TimeSpan.FromMinutes(2)); - do { await Task.Delay(TimeSpan.FromSeconds(1), tempToken); daemonStatus = await instanceClient.DreamDaemon.Read(tempToken); } - while (initialSession.Id == daemonStatus.ActiveCompileJob.Id); + while (initialSession.Id == daemonStatus.ActiveCompileJob.Id || SameSession()); } if (waitForOnlineIfRestoring && daemonStatus.Status == WatchdogStatus.Restoring) @@ -1658,5 +1698,33 @@ namespace Tgstation.Server.Tests.Live.Instance Assert.AreEqual("all gucci", result.StringData); await CheckDMApiFail((await instanceClient.DreamDaemon.Read(cancellationToken)).ActiveCompileJob, cancellationToken); } + + async ValueTask TestDeploymentTrigger(CancellationToken cancellationToken) + { + System.Console.WriteLine("TEST: TestDeploymentTrigger"); + var initialJobs = await instanceClient.Jobs.List(new PaginationSettings + { + RetrieveCount = 1, + }, cancellationToken); + + var result = await SendTestTopic( + "test_deployment_trigger=1", + cancellationToken); + Assert.IsNotNull(result); + Assert.AreEqual("all gucci", result.StringData); + + var newJobs = await instanceClient.Jobs.List(new PaginationSettings + { + RetrieveCount = 1, + }, cancellationToken); + + newJobs.RemoveAll(job => initialJobs.Any(initialJob => initialJob.Id == job.Id)); + var deploymentJob = newJobs.SingleOrDefault(job => job.JobCode == JobCode.AutomaticDeployment); + Assert.IsNotNull(deploymentJob); + + await CheckDMApiFail((await instanceClient.DreamDaemon.Read(cancellationToken)).ActiveCompileJob, cancellationToken); + + await instanceClient.Jobs.Cancel(deploymentJob, cancellationToken); + } } } diff --git a/tests/Tgstation.Server.Tests/Live/InstanceManagerTest.cs b/tests/Tgstation.Server.Tests/Live/InstanceManagerTest.cs index c83b859f41..c2226b795a 100644 --- a/tests/Tgstation.Server.Tests/Live/InstanceManagerTest.cs +++ b/tests/Tgstation.Server.Tests/Live/InstanceManagerTest.cs @@ -76,14 +76,14 @@ namespace Tgstation.Server.Tests.Live Assert.IsTrue(Directory.Exists(firstTest.Path)); var firstClient = instanceManagerClient.CreateClient(firstTest); - await ApiAssert.ThrowsException(() => firstClient.DreamDaemon.Start(cancellationToken), ErrorCode.InstanceOffline); + await ApiAssert.ThrowsExactly(() => firstClient.DreamDaemon.Start(cancellationToken), ErrorCode.InstanceOffline); //cant create instances in existent directories var testNonEmpty = Path.Combine(testRootPath, Guid.NewGuid().ToString()); Directory.CreateDirectory(testNonEmpty); var testFile = Path.Combine(testNonEmpty, "asdf"); await File.WriteAllBytesAsync(testFile, Array.Empty(), cancellationToken); - await ApiAssert.ThrowsException(() => instanceManagerClient.CreateOrAttach(new InstanceCreateRequest + await ApiAssert.ThrowsExactly(() => instanceManagerClient.CreateOrAttach(new InstanceCreateRequest { Path = testNonEmpty, Name = "NonEmptyTest" @@ -97,18 +97,18 @@ namespace Tgstation.Server.Tests.Live Name = "NonEmptyTest" }, cancellationToken); - await ApiAssert.ThrowsException(() => instanceManagerClient.CreateOrAttach(FromResponse(firstTest), cancellationToken), ErrorCode.InstanceAtConflictingPath); + await ApiAssert.ThrowsExactly(() => instanceManagerClient.CreateOrAttach(FromResponse(firstTest), cancellationToken), ErrorCode.InstanceAtConflictingPath); Assert.IsTrue(Directory.Exists(firstTest.Path)); //can't create instances in installation directory - await ApiAssert.ThrowsException(() => instanceManagerClient.CreateOrAttach(new InstanceCreateRequest + await ApiAssert.ThrowsExactly(() => instanceManagerClient.CreateOrAttach(new InstanceCreateRequest { Path = "./A/Local/Path", Name = "NoInstallDirTest" }, cancellationToken), ErrorCode.InstanceAtConflictingPath); //can't create instances as children of other instances - await ApiAssert.ThrowsException(() => instanceManagerClient.CreateOrAttach(new InstanceCreateRequest + await ApiAssert.ThrowsExactly(() => instanceManagerClient.CreateOrAttach(new InstanceCreateRequest { Path = Path.Combine(firstTest.Path, "subdir"), Name = "NoOtherInstanceDirTest" @@ -116,19 +116,19 @@ namespace Tgstation.Server.Tests.Live Assert.IsTrue(Directory.Exists(firstTest.Path)); //can't move to existent directories - await ApiAssert.ThrowsException(() => instanceManagerClient.Update(new InstanceUpdateRequest + await ApiAssert.ThrowsExactly(() => instanceManagerClient.Update(new InstanceUpdateRequest { Id = firstTest.Id, Path = testNonEmpty }, cancellationToken), ErrorCode.InstanceAtExistingPath); - await ApiAssert.ThrowsException(() => instanceManagerClient.GrantPermissions(new InstanceUpdateRequest + await ApiAssert.ThrowsExactly(() => instanceManagerClient.GrantPermissions(new InstanceUpdateRequest { Id = 3482974, }, cancellationToken), ErrorCode.ResourceNotPresent); // test can't create instance outside of whitelist - await ApiAssert.ThrowsException(() => instanceManagerClient.CreateOrAttach(new InstanceCreateRequest + await ApiAssert.ThrowsExactly(() => instanceManagerClient.CreateOrAttach(new InstanceCreateRequest { Name = "TestInstanceOutsideOfWhitelist", Path = Path.Combine(testRootPath, "..", Guid.NewGuid().ToString()), @@ -164,7 +164,7 @@ namespace Tgstation.Server.Tests.Live // a couple data validation checks // check setting both fails - await ApiAssert.ThrowsException(() => instanceManagerClient.Update(new InstanceUpdateRequest + await ApiAssert.ThrowsExactly(() => instanceManagerClient.Update(new InstanceUpdateRequest { Id = firstTest.Id, AutoUpdateInterval = 9999, @@ -172,7 +172,7 @@ namespace Tgstation.Server.Tests.Live }, cancellationToken), ErrorCode.ModelValidationFailure); // check bad crons fail - await ApiAssert.ThrowsException(() => instanceManagerClient.Update(new InstanceUpdateRequest + await ApiAssert.ThrowsExactly(() => instanceManagerClient.Update(new InstanceUpdateRequest { Id = firstTest.Id, AutoUpdateCron = "not a cron" @@ -199,7 +199,7 @@ namespace Tgstation.Server.Tests.Live Assert.AreEqual(0U, updated.AutoUpdateInterval); //can't move online instance - await ApiAssert.ThrowsException(() => instanceManagerClient.Update(new InstanceUpdateRequest + await ApiAssert.ThrowsExactly(() => instanceManagerClient.Update(new InstanceUpdateRequest { Id = firstTest.Id, Path = initialPath @@ -264,7 +264,7 @@ namespace Tgstation.Server.Tests.Live firstTest = await instanceManagerClient.GetId(firstTest, cancellationToken); Assert.IsFalse(firstTest.Accessible); - await ApiAssert.ThrowsException(() => instanceClient.PermissionSets.Read(cancellationToken)); + await ApiAssert.ThrowsExactly(() => instanceClient.PermissionSets.Read(cancellationToken)); await instanceManagerClient.GrantPermissions(new InstanceUpdateRequest { @@ -279,7 +279,7 @@ namespace Tgstation.Server.Tests.Live Assert.AreEqual(RightsHelper.AllRights(), ourInstanceUser.DreamDaemonRights.Value); //can't detach online instance - await ApiAssert.ThrowsException(() => instanceManagerClient.Detach(firstTest, cancellationToken), ErrorCode.InstanceDetachOnline); + await ApiAssert.ThrowsExactly(() => instanceManagerClient.Detach(firstTest, cancellationToken), ErrorCode.InstanceDetachOnline); firstTest.Online = false; firstTest = await instanceManagerClient.Update(FromResponse(firstTest), cancellationToken); @@ -317,7 +317,7 @@ namespace Tgstation.Server.Tests.Live //but only if the attach file exists await instanceManagerClient.Detach(firstTest, cancellationToken); File.Delete(attachPath); - await ApiAssert.ThrowsException(() => instanceManagerClient.CreateOrAttach(FromResponse(firstTest), cancellationToken), ErrorCode.InstanceAtExistingPath); + await ApiAssert.ThrowsExactly(() => instanceManagerClient.CreateOrAttach(FromResponse(firstTest), cancellationToken), ErrorCode.InstanceAtExistingPath); } } } diff --git a/tests/Tgstation.Server.Tests/Live/LiveTestingServer.cs b/tests/Tgstation.Server.Tests/Live/LiveTestingServer.cs index d465adc1d6..85ca591984 100644 --- a/tests/Tgstation.Server.Tests/Live/LiveTestingServer.cs +++ b/tests/Tgstation.Server.Tests/Live/LiveTestingServer.cs @@ -89,6 +89,8 @@ namespace Tgstation.Server.Tests.Live Cleanup(BaseDirectory).GetAwaiter().GetResult(); } + System.Console.WriteLine($"RunningInGitHubActions: {TestingUtils.RunningInGitHubActions}"); + Assert.IsTrue(port >= 10000); // for testing bridge request limit Directory = BaseDirectory; @@ -156,7 +158,6 @@ namespace Tgstation.Server.Tests.Live $"General:OpenDreamGitUrl={OpenDreamUrl}", $"Security:TokenExpiryMinutes=120", // timeouts are useless for us $"General:OpenDreamSuppressInstallOutput={TestingUtils.RunningInGitHubActions}", - "Telemetry:DisableVersionReporting=true", $"General:PrometheusPort={port}", $"General:ByondZipDownloadTemplate={TestingUtils.ByondZipDownloadTemplate}" }; diff --git a/tests/Tgstation.Server.Tests/Live/RateLimitRetryingApiClient.cs b/tests/Tgstation.Server.Tests/Live/RateLimitRetryingApiClient.cs index a487a91991..7eda5965fb 100644 --- a/tests/Tgstation.Server.Tests/Live/RateLimitRetryingApiClient.cs +++ b/tests/Tgstation.Server.Tests/Live/RateLimitRetryingApiClient.cs @@ -14,7 +14,7 @@ namespace Tgstation.Server.Tests.Live sealed class RateLimitRetryingApiClient : ApiClient { public RateLimitRetryingApiClient( - IHttpClient httpClient, + HttpClient httpClient, Uri url, ApiHeaders apiHeaders, ApiHeaders tokenRefreshHeaders, diff --git a/tests/Tgstation.Server.Tests/Live/RateLimitRetryingApiClientFactory.cs b/tests/Tgstation.Server.Tests/Live/RateLimitRetryingApiClientFactory.cs index f9ba45e66e..3fea762930 100644 --- a/tests/Tgstation.Server.Tests/Live/RateLimitRetryingApiClientFactory.cs +++ b/tests/Tgstation.Server.Tests/Live/RateLimitRetryingApiClientFactory.cs @@ -1,9 +1,8 @@ using System; +using System.Net.Http; using Tgstation.Server.Api; -using Tgstation.Server.Api.Models.Response; using Tgstation.Server.Client; -using Tgstation.Server.Common.Http; namespace Tgstation.Server.Tests.Live { @@ -20,5 +19,19 @@ namespace Tgstation.Server.Tests.Live apiHeaders, tokenRefreshHeaders, authless); + + /// + public IApiClient CreateApiClient( + Uri url, + ApiHeaders apiHeaders, + ApiHeaders tokenRefreshHeaders, + HttpMessageHandler handler, + bool disposeHandler, + bool authless) => new RateLimitRetryingApiClient( + new HttpClient(handler, disposeHandler), + url, + apiHeaders, + tokenRefreshHeaders, + authless); } } diff --git a/tests/Tgstation.Server.Tests/Live/RawRequestTests.cs b/tests/Tgstation.Server.Tests/Live/RawRequestTests.cs index 492bc59b65..745a248d50 100644 --- a/tests/Tgstation.Server.Tests/Live/RawRequestTests.cs +++ b/tests/Tgstation.Server.Tests/Live/RawRequestTests.cs @@ -216,8 +216,8 @@ namespace Tgstation.Server.Tests.Live }; var badClient = clientFactory.CreateFromToken(serverClient.Url, newToken); - await ApiAssert.ThrowsException(() => badClient.Administration.Read(false, cancellationToken)); - await ApiAssert.ThrowsException(() => badClient.ServerInformation(cancellationToken)); + await ApiAssert.ThrowsExactly(() => badClient.Administration.Read(false, cancellationToken)); + await ApiAssert.ThrowsExactly(() => badClient.ServerInformation(cancellationToken)); } static async Task TestOAuthFails(IRestServerClient serverClient, CancellationToken cancellationToken) @@ -408,7 +408,7 @@ namespace Tgstation.Server.Tests.Live Assert.AreEqual(HubConnectionState.Disconnected, hubConnection.State); hubConnection.ProxyOn(proxy); - var exception = await Assert.ThrowsExceptionAsync(() => hubConnection.StartAsync(cancellationToken)); + var exception = await Assert.ThrowsExactlyAsync(() => hubConnection.StartAsync(cancellationToken)); Assert.AreEqual(HttpStatusCode.Unauthorized, exception.StatusCode); Assert.AreEqual(HubConnectionState.Disconnected, hubConnection.State); @@ -444,7 +444,7 @@ namespace Tgstation.Server.Tests.Live Assert.AreNotEqual(HubConnectionState.Connected, testUserConn1.State); - await ApiAssert.ThrowsException(async () => await testUserClient.SubscribeToJobUpdates(proxy, cancellationToken: cancellationToken)); + await ApiAssert.ThrowsExactly(async () => await testUserClient.SubscribeToJobUpdates(proxy, cancellationToken: cancellationToken)); } finally { diff --git a/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs b/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs index 8d2222c9fa..212a8f7185 100644 --- a/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs +++ b/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs @@ -17,6 +17,7 @@ using System.Threading.Tasks; using Microsoft.Data.SqlClient; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -87,7 +88,8 @@ namespace Tgstation.Server.Tests.Live { var result = new List(); - switch (engineType) { + switch (engineType) + { case EngineType.Byond: result.AddRange(System.Diagnostics.Process.GetProcessesByName("DreamDaemon")); if (new PlatformIdentifier().IsWindows) @@ -97,7 +99,7 @@ namespace Tgstation.Server.Tests.Live var potentialProcesses = System.Diagnostics.Process.GetProcessesByName("dotnet") .Where(process => { - if (GetCommandLine(process).Contains("Robust.Server")) + if (GetCommandLine(process)?.Contains("Robust.Server") == true) return true; process.Dispose(); @@ -322,7 +324,7 @@ namespace Tgstation.Server.Tests.Live update.PermissionSet.AdministrationRights &= ~right; await client.Users.Update(update, cancellationToken); - await ApiAssert.ThrowsException(action); + await ApiAssert.ThrowsExactly(action); update.PermissionSet.AdministrationRights |= right; await client.Users.Update(update, cancellationToken); @@ -409,10 +411,7 @@ namespace Tgstation.Server.Tests.Live await CheckUpdate(); // Second pass, uploaded updates - var downloader = new Host.IO.FileDownloader( - new Common.Http.HttpClientFactory( - new AssemblyInformationProvider().ProductInfoHeaderValue), - Mock.Of>()); + var downloader = CachingFileDownloader.CreateRealDownloader(Mock.Of>()); var gitHubToken = Environment.GetEnvironmentVariable("TGS_TEST_GITHUB_TOKEN"); if (String.IsNullOrWhiteSpace(gitHubToken)) gitHubToken = null; @@ -476,7 +475,7 @@ namespace Tgstation.Server.Tests.Live { var testUpdateVersion = new Version(5, 11, 20); await using var adminClient = await CreateAdminClient(server.ApiUrl, cancellationToken); - await ApiAssert.ThrowsException( + await ApiAssert.ThrowsExactly( () => adminClient.RestClient.Administration.Update( new ServerUpdateRequest { @@ -733,8 +732,8 @@ namespace Tgstation.Server.Tests.Live Assert.AreEqual(controllerInstance.Id, controllerInstanceList[0].Id); Assert.IsNotNull(await controllerClient.RestClient.Instances.GetId(controllerInstance, cancellationToken)); - await ApiAssert.ThrowsException(() => controllerClient.RestClient.Instances.GetId(node2Instance, cancellationToken), Api.Models.ErrorCode.ResourceNotPresent); - await ApiAssert.ThrowsException(() => node1Client.RestClient.Instances.GetId(controllerInstance, cancellationToken), Api.Models.ErrorCode.ResourceNotPresent); + await ApiAssert.ThrowsExactly(() => controllerClient.RestClient.Instances.GetId(node2Instance, cancellationToken), Api.Models.ErrorCode.ResourceNotPresent); + await ApiAssert.ThrowsExactly(() => node1Client.RestClient.Instances.GetId(controllerInstance, cancellationToken), Api.Models.ErrorCode.ResourceNotPresent); // test update await node1Client.Execute( @@ -784,7 +783,7 @@ namespace Tgstation.Server.Tests.Live await using var node1Client2 = await CreateAdminClient(node1.ApiUrl, cancellationToken); await controllerClient2.Execute( - async restClient => await ApiAssert.ThrowsException( + async restClient => await ApiAssert.ThrowsExactly( () => restClient.Administration.Update( new ServerUpdateRequest { @@ -828,10 +827,7 @@ namespace Tgstation.Server.Tests.Live CheckInfo(node2Info2); // also test with uploaded updates this time - var downloader = new Host.IO.FileDownloader( - new Common.Http.HttpClientFactory( - new AssemblyInformationProvider().ProductInfoHeaderValue), - Mock.Of>()); + var downloader = CachingFileDownloader.CreateRealDownloader(Mock.Of>()); var gitHubToken = Environment.GetEnvironmentVariable("TGS_TEST_GITHUB_TOKEN"); if (String.IsNullOrWhiteSpace(gitHubToken)) gitHubToken = null; @@ -1046,7 +1042,7 @@ namespace Tgstation.Server.Tests.Live // update should fail await controllerClient2.Execute( - async restClient => await ApiAssert.ThrowsException( + async restClient => await ApiAssert.ThrowsExactly( () => restClient.Administration.Update( new ServerUpdateRequest { @@ -1183,13 +1179,14 @@ namespace Tgstation.Server.Tests.Live null, null, true, - true); + true, + false); int? exitCode; using (cancellationToken.Register(gitRemoteOriginFixProc.Terminate)) exitCode = await gitRemoteOriginFixProc.Lifetime; - loggerFactory.CreateLogger("TgTest").LogInformation("git {args} output:{newLine}{output}",args, Environment.NewLine, await gitRemoteOriginFixProc.GetCombinedOutput(cancellationToken)); + loggerFactory.CreateLogger("TgTest").LogInformation("git {args} output:{newLine}{output}", args, Environment.NewLine, await gitRemoteOriginFixProc.GetCombinedOutput(cancellationToken)); Assert.AreEqual(0, exitCode); } @@ -1311,7 +1308,7 @@ namespace Tgstation.Server.Tests.Live async Task TestStandardTgsOperation(bool openDreamOnly) { - using(var currentProcess = System.Diagnostics.Process.GetCurrentProcess()) + using (var currentProcess = System.Diagnostics.Process.GetCurrentProcess()) { var currentPriorityClass = currentProcess.PriorityClass; if (currentPriorityClass != ProcessPriorityClass.Normal) @@ -1421,11 +1418,40 @@ namespace Tgstation.Server.Tests.Live for (var i = 0; i < 50; ++i) await Task.Yield(); - InstanceManager GetInstanceManager() => ((Host.Server)server.RealServer).Host.Services.GetRequiredService(); - ILogger GetLogger() => ((Host.Server)server.RealServer).Host.Services.GetRequiredService>(); + async ValueTask WaitForHost() + { + while (true) + { + var host = ((Host.Server)server.RealServer).Host; + if (host != null) + return host; + + await Task.Yield(); + } + } + + async ValueTask GetInstanceManager() + { + var host = await WaitForHost(); + return host.Services.GetRequiredService(); + } + + async ValueTask GetLogger() + { + var host = await WaitForHost(); + return host.Services.GetRequiredService>(); + } + + async ValueTask GetFileDownloader() + { + var host = await WaitForHost(); + return host.Services.GetRequiredService(); + } // main run var serverTask = server.Run(cancellationToken).AsTask(); + if (serverTask.IsFaulted) + await serverTask; var fileDownloader = ((Host.Server)server.RealServer).Host.Services.GetRequiredService(); var graphQLClientFactory = new GraphQLServerClientFactory(restClientFactory); @@ -1456,7 +1482,7 @@ namespace Tgstation.Server.Tests.Live Password = DefaultCredentials.DefaultAdminUserPassword, }, cancellationToken); - await ApiAssert.ThrowsException(() => tokenOnlyRestClient.Users.Read(cancellationToken), null); + await ApiAssert.ThrowsExactly(() => tokenOnlyRestClient.Users.Read(cancellationToken), null); } // basic graphql test, to be used everywhere eventually @@ -1583,8 +1609,8 @@ namespace Tgstation.Server.Tests.Live var instanceTest = new InstanceTest( firstAdminRestClient.Instances, - fileDownloader, - GetInstanceManager(), + await GetFileDownloader(), + await GetInstanceManager(), (ushort)server.ApiUrl.Port); async Task RunInstanceTests() @@ -1592,14 +1618,15 @@ namespace Tgstation.Server.Tests.Live var testSerialized = TestingUtils.RunningInGitHubActions; // they only have 2 cores, can't handle intense parallelization async Task ODCompatTests() { - var edgeODVersionTask = EngineTest.GetEdgeVersion(EngineType.OpenDream, GetLogger(), fileDownloader, cancellationToken); + var fileDownloader = await GetFileDownloader(); + var edgeODVersionTask = EngineTest.GetEdgeVersion(EngineType.OpenDream, await GetLogger(), fileDownloader, cancellationToken); - var ex = await Assert.ThrowsExceptionAsync( + var ex = await Assert.ThrowsExactlyAsync( () => InstanceTest.DownloadEngineVersion( new EngineVersion { Engine = EngineType.OpenDream, - SourceSHA = "f1dc153caf9d84cd1d0056e52286cc0163e3f4d3", // 1b4 verified version + SourceSHA = "f1dc153caf9d84cd1d0056e52286cc0163e3f4d3", // 1 before verified version }, fileDownloader, server.OpenDreamUrl, @@ -1619,7 +1646,7 @@ namespace Tgstation.Server.Tests.Live cancellationToken); } - var odCompatTests = FailFast(ODCompatTests()); + var odCompatTests = Task.CompletedTask ?? FailFast(ODCompatTests()); if (openDreamOnly || testSerialized) await odCompatTests; @@ -1630,14 +1657,14 @@ namespace Tgstation.Server.Tests.Live var windowsMinCompat = new Version(510, 1346); var linuxMinCompat = new Version(512, 1451); // http://www.byond.com/forum/?forum=5&command=search&scope=local&text=resolved%3a512.1451 await CachingFileDownloader.InitializeByondVersion( - GetLogger(), + await GetLogger(), new PlatformIdentifier().IsWindows ? windowsMinCompat : linuxMinCompat, new PlatformIdentifier().IsWindows, cancellationToken); - var compatTests = FailFast( + var compatTests = Task.CompletedTask ?? FailFast( instanceTest .RunCompatTests( new EngineVersion @@ -1661,7 +1688,7 @@ namespace Tgstation.Server.Tests.Live await FailFast( instanceTest .RunTests( - GetLogger(), + await GetLogger(), instanceClient, mainDMPort.Value, mainDDPort.Value, @@ -1826,7 +1853,7 @@ namespace Tgstation.Server.Tests.Live topicRequestResult = await WatchdogTest.SendTestTopic( "tgs_integration_test_tactics7=1", WatchdogTest.StaticTopicClient, - GetInstanceManager().GetInstanceReference(instanceClient.Metadata), + (await GetInstanceManager()).GetInstanceReference(instanceClient.Metadata), mainDDPort.Value, cancellationToken); @@ -1840,7 +1867,7 @@ namespace Tgstation.Server.Tests.Live dd = await WatchdogTest.TellWorldToReboot2( instanceClient, - GetInstanceManager(), + await GetInstanceManager(), WatchdogTest.StaticTopicClient, mainDDPort.Value, true, @@ -1894,7 +1921,7 @@ namespace Tgstation.Server.Tests.Live preStartupTime = DateTimeOffset.UtcNow; serverTask = server.Run(cancellationToken).AsTask(); long expectedCompileJobId, expectedStaged; - var edgeVersion = await EngineTest.GetEdgeVersion(EngineType.Byond, GetLogger(), fileDownloader, cancellationToken); + var edgeVersion = await EngineTest.GetEdgeVersion(EngineType.Byond, await GetLogger(), await GetFileDownloader(), cancellationToken); await using (var adminClient = await CreateAdminClient(server.ApiUrl, cancellationToken)) { var restAdminClient = adminClient.RestClient; @@ -1907,7 +1934,7 @@ namespace Tgstation.Server.Tests.Live Assert.AreEqual(WatchdogStatus.Online, dd.Status.Value); var compileJob = await instanceClient.DreamMaker.Compile(cancellationToken); - await using var wdt = new WatchdogTest(edgeVersion, instanceClient, GetInstanceManager(), (ushort)server.ApiUrl.Port, server.HighPriorityDreamDaemon, mainDDPort.Value, server.UsingBasicWatchdog); + await using var wdt = new WatchdogTest(edgeVersion, instanceClient, await GetInstanceManager(), (ushort)server.ApiUrl.Port, server.HighPriorityDreamDaemon, mainDDPort.Value, server.UsingBasicWatchdog); await wdt.WaitForJob(compileJob, 30, false, null, cancellationToken); dd = await instanceClient.DreamDaemon.Read(cancellationToken); @@ -1956,7 +1983,7 @@ namespace Tgstation.Server.Tests.Live Assert.AreEqual(WatchdogStatus.Online, currentDD.Status); Assert.AreEqual(expectedStaged, currentDD.StagedCompileJob.Job.Id.Value); - await using var wdt = new WatchdogTest(edgeVersion, instanceClient, GetInstanceManager(), (ushort)server.ApiUrl.Port, server.HighPriorityDreamDaemon, mainDDPort.Value, server.UsingBasicWatchdog); + await using var wdt = new WatchdogTest(edgeVersion, instanceClient, await GetInstanceManager(), (ushort)server.ApiUrl.Port, server.HighPriorityDreamDaemon, mainDDPort.Value, server.UsingBasicWatchdog); currentDD = await wdt.TellWorldToReboot(false, cancellationToken); Assert.AreEqual(expectedStaged, currentDD.ActiveCompileJob.Job.Id.Value); Assert.IsNull(currentDD.StagedCompileJob); @@ -1980,7 +2007,7 @@ namespace Tgstation.Server.Tests.Live Console.WriteLine($"[{DateTimeOffset.UtcNow}] TEST ERROR: {ex.ErrorCode}: {ex.Message}\n{ex.AdditionalServerData}"); throw; } - catch(OperationCanceledException ex) + catch (OperationCanceledException ex) { Console.WriteLine($"[{DateTimeOffset.UtcNow}] TEST ABORTED: {ex}"); throw; @@ -2026,7 +2053,7 @@ namespace Tgstation.Server.Tests.Live try { Console.WriteLine($"TEST: CreateAdminClient attempt {I}..."); - + restClientTask = restClientFactory.CreateFromLogin( url, username, diff --git a/tests/Tgstation.Server.Tests/Live/TestingGitHubService.cs b/tests/Tgstation.Server.Tests/Live/TestingGitHubService.cs index 731e653a4c..d420bea426 100644 --- a/tests/Tgstation.Server.Tests/Live/TestingGitHubService.cs +++ b/tests/Tgstation.Server.Tests/Live/TestingGitHubService.cs @@ -34,13 +34,13 @@ namespace Tgstation.Server.Tests.Live static TestingGitHubService() { - var mockOptions = new Mock>(); - mockOptions.SetupGet(x => x.Value).Returns(new GeneralConfiguration + var mockOptions = new Mock>(); + mockOptions.SetupGet(x => x.CurrentValue).Returns(new GeneralConfiguration { GitHubAccessToken = Environment.GetEnvironmentVariable("TGS_TEST_GITHUB_TOKEN") }); - var gitHubClientFactory = new GitHubClientFactory(new AssemblyInformationProvider(), new BasicHttpMessageHandlerFactory(), Mock.Of>(), mockOptions.Object); + var gitHubClientFactory = new GitHubClientFactory(new AssemblyInformationProvider(), new BasicHttpMessageHandlerFactory(), mockOptions.Object, Mock.Of>()); RealClient = gitHubClientFactory.CreateClient(CancellationToken.None).GetAwaiter().GetResult(); } diff --git a/tests/Tgstation.Server.Tests/Live/UsersTest.cs b/tests/Tgstation.Server.Tests/Live/UsersTest.cs index 37653adc46..bd8d4f9248 100644 --- a/tests/Tgstation.Server.Tests/Live/UsersTest.cs +++ b/tests/Tgstation.Server.Tests/Live/UsersTest.cs @@ -81,7 +81,7 @@ namespace Tgstation.Server.Tests.Live var gqlUser = graphQLResult.Swarm.Users.Current; return restResult.Enabled == gqlUser.Enabled && restResult.Name == gqlUser.Name - && (restResult.CreatedAt.Value.Ticks / 1000000) == (gqlUser.CreatedAt.Ticks / 1000000) + && (restResult.CreatedAt.Value.Ticks / 10000) == (gqlUser.CreatedAt.Ticks / 10000) && restResult.SystemIdentifier == gqlUser.SystemIdentifier && restResult.CreatedBy.Name == gqlUser.CreatedBy.Name; }, @@ -108,8 +108,8 @@ namespace Tgstation.Server.Tests.Live Assert.IsTrue(users.Count > 0); Assert.IsFalse(users.Any(x => x.Id == systemUser.Id)); - await ApiAssert.ThrowsException(() => client.Users.GetId(systemUser, cancellationToken)); - await ApiAssert.ThrowsException(() => client.Users.Update(new UserUpdateRequest + await ApiAssert.ThrowsExactly(() => client.Users.GetId(systemUser, cancellationToken)); + await ApiAssert.ThrowsExactly(() => client.Users.Update(new UserUpdateRequest { Id = systemUser.Id }, cancellationToken)); @@ -123,7 +123,7 @@ namespace Tgstation.Server.Tests.Live } }; - await ApiAssert.ThrowsException(() => client.Users.Update(new UserUpdateRequest + await ApiAssert.ThrowsExactly(() => client.Users.Update(new UserUpdateRequest { Id = restUser.Id, OAuthConnections = sampleOAuthConnections @@ -206,7 +206,7 @@ namespace Tgstation.Server.Tests.Live Password = string.Empty }; - await ApiAssert.ThrowsException(() => client.Users.Create((UserCreateRequest)testUserUpdate, cancellationToken), Api.Models.ErrorCode.UserPasswordLength); + await ApiAssert.ThrowsExactly(() => client.Users.Create((UserCreateRequest)testUserUpdate, cancellationToken), Api.Models.ErrorCode.UserPasswordLength); testUserUpdate.OAuthConnections = [ @@ -228,7 +228,7 @@ namespace Tgstation.Server.Tests.Live Id = group.Id }, }; - await ApiAssert.ThrowsException( + await ApiAssert.ThrowsExactly( () => client.Users.Update( testUserUpdate, cancellationToken), @@ -264,8 +264,10 @@ namespace Tgstation.Server.Tests.Live var result = await client.RunOperation(gql => gql.ListUsers.ExecuteAsync(cancellationToken), cancellationToken); result.EnsureNoErrors(); var users = result.Data.Swarm.Users.QueryableUsers; - Assert.IsTrue(users.TotalCount > 0); - Assert.AreEqual(Math.Min(ApiController.DefaultPageSize, users.TotalCount), users.Nodes.Count); + Assert.IsTrue(users.Nodes.Count > 0); + // Can't assert this, there's a race condition + // Assert.AreEqual(Math.Min(ApiController.DefaultPageSize, users.TotalCount), users.Nodes.Count); + Assert.IsTrue(Math.Min(ApiController.DefaultPageSize, users.TotalCount) >= users.Nodes.Count); var tgsUserResult = await client.RunOperation(gql => gql.GetUserNameByNodeId.ExecuteAsync(gqlUser.Swarm.Users.Current.CreatedBy.Id, cancellationToken), cancellationToken); tgsUserResult.EnsureNoErrors(); @@ -298,13 +300,13 @@ namespace Tgstation.Server.Tests.Live var testUserResult2 = await client.RunMutationEnsureNoErrors( gql => gql.UpdateUserOAuthConnections.ExecuteAsync( - testUserResult.User.Id, + testUserResult.UpdatedUser.Id, sampleOAuthConnections, cancellationToken), data => data.UpdateUser, cancellationToken); - var testUser = testUserResult2.User; + var testUser = testUserResult2.UpdatedUser.User; Assert.IsNotNull(testUser.OAuthConnections); Assert.AreEqual(1, testUser.OAuthConnections.Count); Assert.AreEqual(sampleOAuthConnections.First().ExternalUserId, testUser.OAuthConnections[0].ExternalUserId); @@ -374,7 +376,7 @@ namespace Tgstation.Server.Tests.Live gql => gql.ListUserGroups.ExecuteAsync(cancellationToken), cancellationToken); - var groups = groupsResult.Swarm.Users.Groups.QueryableGroups; + var groups = groupsResult.Swarm.UserGroups.QueryableGroups; Assert.AreEqual(2, groups.TotalCount); foreach (var igroup in groups.Nodes) @@ -389,7 +391,7 @@ namespace Tgstation.Server.Tests.Live gql => gql.ListUserGroups.ExecuteAsync(cancellationToken), cancellationToken); - groups = groupsResult.Swarm.Users.Groups.QueryableGroups; + groups = groupsResult.Swarm.UserGroups.QueryableGroups; Assert.AreEqual(1, groups.TotalCount); foreach (var igroup in groups.Nodes) @@ -451,14 +453,14 @@ namespace Tgstation.Server.Tests.Live data => data.CreateUserByServiceConnectionAndPermissionSet, cancellationToken); - var testUser2 = oAuthCreateResult.User; + var testUser2 = oAuthCreateResult.UpdatedUser; var testUser22Result = await client.RunMutationEnsureNoErrors( gql => gql.SetUserGroup.ExecuteAsync(testUser2.Id, group.Id, cancellationToken), data => data.UpdateUserSetGroup, cancellationToken); - var testUser22 = testUser22Result.User; + var testUser22 = testUser22Result.UpdatedUser.User; Assert.IsNull(testUser22.OwnedPermissionSet); Assert.IsNotNull(testUser22.Group); @@ -467,7 +469,7 @@ namespace Tgstation.Server.Tests.Live var group4Result = await client.RunQueryEnsureNoErrors( gql => gql.GetSomeGroupInfo.ExecuteAsync(group.Id, cancellationToken), cancellationToken); - var group4 = group4Result.Swarm.Users.Groups.ById; + var group4 = group4Result.Swarm.UserGroups.ById; Assert.IsNotNull(group4.QueryableUsersByGroup.Nodes); Assert.AreEqual(1, group4.QueryableUsersByGroup.TotalCount); @@ -503,13 +505,15 @@ namespace Tgstation.Server.Tests.Live CanSetChatBotLimit = true, CanSetConfiguration = true, CanSetOnline = true, + CanSetAutoStart = true, + CanSetAutoStop = true, } }, cancellationToken), data => data.UpdateUserSetOwnedPermissionSet, cancellationToken); - var testUser4 = testUser4Result.User; + var testUser4 = testUser4Result.UpdatedUser.User; Assert.IsNull(testUser4.Group); Assert.IsNotNull(testUser4.OwnedPermissionSet); }); @@ -529,7 +533,7 @@ namespace Tgstation.Server.Tests.Live if (new PlatformIdentifier().IsWindows) await restClient.Users.Create(update, cancellationToken); else - await ApiAssert.ThrowsException(() => restClient.Users.Create(update, cancellationToken), Api.Models.ErrorCode.RequiresPosixSystemIdentity); + await ApiAssert.ThrowsExactly(() => restClient.Users.Create(update, cancellationToken), Api.Models.ErrorCode.RequiresPosixSystemIdentity); }, async graphQLClient => { @@ -586,7 +590,7 @@ namespace Tgstation.Server.Tests.Live data => data.CreateUserByPasswordAndPermissionSet, cancellationToken); - ids.Add(result.User.Id); + ids.Add(result.UpdatedUser.Id); }); tasks.Add(CreateSpamUser()); @@ -619,12 +623,12 @@ namespace Tgstation.Server.Tests.Live Assert.AreEqual(expectedCount, nullSettings.Count); Assert.IsTrue(nullSettings.All(x => emptySettings.SingleOrDefault(y => x.Id == y.Id) != null)); - await ApiAssert.ThrowsException>(() => restClient.Users.List( + await ApiAssert.ThrowsExactly>(() => restClient.Users.List( new PaginationSettings { PageSize = -2143 }, cancellationToken), Api.Models.ErrorCode.ApiInvalidPageOrPageSize); - await ApiAssert.ThrowsException>(() => restClient.Users.List( + await ApiAssert.ThrowsExactly>(() => restClient.Users.List( new PaginationSettings { PageSize = ApiController.MaximumPageSize + 1, diff --git a/tests/Tgstation.Server.Tests/TestRepository.cs b/tests/Tgstation.Server.Tests/TestRepository.cs index 96a2349bea..e5c04e6cff 100644 --- a/tests/Tgstation.Server.Tests/TestRepository.cs +++ b/tests/Tgstation.Server.Tests/TestRepository.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using LibGit2Sharp; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; @@ -41,8 +42,8 @@ namespace Tgstation.Server.Tests Mock.Of(), Mock.Of(), Mock.Of(), + Mock.Of>(), Mock.Of>(), - new GeneralConfiguration(), () => { }); const string StartSha = "af4da8beb9f9b374b04a3cc4d65acca662e8cc1a"; @@ -81,9 +82,9 @@ namespace Tgstation.Server.Tests Mock.Of(), new WindowsPostWriteHandler(), Mock.Of(), + Mock.Of>(), Mock.Of>(), - Mock.Of>(), - new GeneralConfiguration()); + Mock.Of>()); try { using (await manager.CloneRepository( diff --git a/tests/Tgstation.Server.Tests/TestSystemInteraction.cs b/tests/Tgstation.Server.Tests/TestSystemInteraction.cs index f2601acdee..170c3c3ae4 100644 --- a/tests/Tgstation.Server.Tests/TestSystemInteraction.cs +++ b/tests/Tgstation.Server.Tests/TestSystemInteraction.cs @@ -6,12 +6,11 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.TestTools.UnitTesting; + using Moq; -using Tgstation.Server.Host.Extensions; using Tgstation.Server.Host.IO; using Tgstation.Server.Host.System; -using Tgstation.Server.Host.Utils; namespace Tgstation.Server.Tests { @@ -29,7 +28,16 @@ namespace Tgstation.Server.Tests Mock.Of>(), loggerFactory); - await using var process = await processExecutor.LaunchProcess("test." + platformIdentifier.ScriptFileExtension, ".", string.Empty, CancellationToken.None, null, null, true, true); + await using var process = await processExecutor.LaunchProcess( + "test." + platformIdentifier.ScriptFileExtension, + ".", + String.Empty, + CancellationToken.None, + null, + null, + true, + true, + false); using var cts = new CancellationTokenSource(); cts.CancelAfter(3000); var exitCode = await process.Lifetime.WaitAsync(cts.Token); @@ -64,7 +72,16 @@ namespace Tgstation.Server.Tests File.Delete(tempFile); try { - await using (var process = await processExecutor.LaunchProcess("test." + platformIdentifier.ScriptFileExtension, ".", string.Empty, CancellationToken.None, null, tempFile, true, true)) + await using (var process = await processExecutor.LaunchProcess( + "test." + platformIdentifier.ScriptFileExtension, + ".", + string.Empty, + CancellationToken.None, + null, + tempFile, + true, + true, + false)) { using var cts = new CancellationTokenSource(); cts.CancelAfter(3000); diff --git a/tests/Tgstation.Server.Tests/TestVersions.cs b/tests/Tgstation.Server.Tests/TestVersions.cs index d44fded9e2..503bbb110d 100644 --- a/tests/Tgstation.Server.Tests/TestVersions.cs +++ b/tests/Tgstation.Server.Tests/TestVersions.cs @@ -1,7 +1,8 @@ using System; -using System.IO; -using System.IO.Compression; using System.Globalization; +using System.IO; +using System.IO.Abstractions; +using System.IO.Compression; using System.Linq; using System.Net.Http; using System.Reflection; @@ -20,6 +21,7 @@ using Moq; using Newtonsoft.Json.Linq; using Tgstation.Server.Api; +using Tgstation.Server.Api.Models; using Tgstation.Server.Client; using Tgstation.Server.Common.Extensions; using Tgstation.Server.Host; @@ -29,11 +31,9 @@ using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.Controllers; using Tgstation.Server.Host.Database; using Tgstation.Server.Host.IO; -using Tgstation.Server.Host.System; -using Tgstation.Server.Api.Models; -using Tgstation.Server.Tests.Live; using Tgstation.Server.Host.Properties; -using System.IO.Abstractions; +using Tgstation.Server.Host.System; +using Tgstation.Server.Tests.Live; namespace Tgstation.Server.Tests { @@ -112,8 +112,8 @@ namespace Tgstation.Server.Tests { ByondZipDownloadTemplate = TestingUtils.ByondZipDownloadTemplate, }); - var mockSessionConfigurationOptions = new Mock>(); - mockSessionConfigurationOptions.SetupGet(x => x.Value).Returns(new SessionConfiguration()); + var mockSessionConfigurationOptions = new Mock>(); + mockSessionConfigurationOptions.SetupGet(x => x.CurrentValue).Returns(new SessionConfiguration()); using var loggerFactory = LoggerFactory.Create(builder => { @@ -179,8 +179,8 @@ namespace Tgstation.Server.Tests SkipAddingByondFirewallException = true, ByondZipDownloadTemplate = TestingUtils.ByondZipDownloadTemplate, }); - var mockSessionConfigurationOptions = new Mock>(); - mockSessionConfigurationOptions.SetupGet(x => x.Value).Returns(new SessionConfiguration()); + var mockSessionConfigurationOptions = new Mock>(); + mockSessionConfigurationOptions.SetupGet(x => x.CurrentValue).Returns(new SessionConfiguration()); using var loggerFactory = LoggerFactory.Create(builder => { @@ -204,10 +204,16 @@ namespace Tgstation.Server.Tests var fileDownloader = new CachingFileDownloader(Mock.Of>()); + var mockIOManager = new Mock(); + mockIOManager.Setup(x => x.FileExists(It.IsNotNull(), It.IsAny())).ReturnsAsync(true); + mockIOManager.Setup(x => x.CreateResolverForSubdirectory(It.IsNotNull())).Returns(mockIOManager.Object); + mockIOManager.Setup(x => x.ConcatPath(It.IsNotNull())).Returns(Path.Combine); + mockIOManager.Setup(x => x.ResolvePath(It.IsNotNull())).Returns(path => path); + ByondInstallerBase byondInstaller = platformIdentifier.IsWindows ? new WindowsByondInstaller( Mock.Of(), - Mock.Of(), + mockIOManager.Object, fileDownloader, mockGeneralConfigurationOptions.Object, mockSessionConfigurationOptions.Object, @@ -227,7 +233,7 @@ namespace Tgstation.Server.Tests new Lazy(() => null), new DefaultIOManager(new FileSystem()), loggerFactory.CreateLogger()), - Mock.Of(), + mockIOManager.Object, loggerFactory.CreateLogger(), loggerFactory); @@ -523,7 +529,8 @@ namespace Tgstation.Server.Tests null, null, true, - true); + true, + false); try { diff --git a/tests/Tgstation.Server.Tests/TestingUtils.cs b/tests/Tgstation.Server.Tests/TestingUtils.cs index a6a6683f36..9807e1999c 100644 --- a/tests/Tgstation.Server.Tests/TestingUtils.cs +++ b/tests/Tgstation.Server.Tests/TestingUtils.cs @@ -4,7 +4,6 @@ using System.IO; using System.IO.Abstractions; using System.IO.Compression; using System.Linq; -using System.Net.Http; using System.Reflection; using System.Text; using System.Threading; @@ -24,7 +23,13 @@ namespace Tgstation.Server.Tests { static class TestingUtils { - public static bool RunningInGitHubActions { get; } = !string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("GITHUB_RUN_ID")); + static TestingUtils() + { + RunningInGitHubActions = !string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("GITHUB_RUN_ID")); + System.Console.WriteLine($"RunningInGitHubAction: {RunningInGitHubActions}"); + } + + public static bool RunningInGitHubActions { get; } public static ILoggerFactory CreateLoggerFactoryForLogger(ILogger logger, out Mock mockLoggerFactory) { diff --git a/tgstation-server.sln b/tgstation-server.sln index 165ed3cac8..fcddd7eb1b 100644 --- a/tgstation-server.sln +++ b/tgstation-server.sln @@ -32,6 +32,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{6FF654E6 build\NewtonsoftJson.props = build\NewtonsoftJson.props build\NugetCommon.props = build\NugetCommon.props build\OpenApiValidationSettings.json = build\OpenApiValidationSettings.json + build\PackageVersions.props = build\PackageVersions.props build\SrcCommon.props = build\SrcCommon.props build\stylecop.json = build\stylecop.json build\TestCommon.props = build\TestCommon.props @@ -127,6 +128,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{E821 .github\CODEOWNERS = .github\CODEOWNERS .github\CODE_OF_CONDUCT.md = .github\CODE_OF_CONDUCT.md .github\CONTRIBUTING.md = .github\CONTRIBUTING.md + .github\dependabot.yml = .github\dependabot.yml + .github\FUNDING.yml = .github\FUNDING.yml + .github\PULL_REQUEST_TEMPLATE.md = .github\PULL_REQUEST_TEMPLATE.md EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tgstation.Server.Tests", "tests\Tgstation.Server.Tests\Tgstation.Server.Tests.csproj", "{09056964-1C74-445A-96EC-33F6DFC07916}" @@ -277,6 +281,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "nix", "nix", "{5130526C-A55 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tgstation.Server.Host.Utils.GitLab.GraphQL", "src\Tgstation.Server.Host.Utils.GitLab.GraphQL\Tgstation.Server.Host.Utils.GitLab.GraphQL.csproj", "{BF02BCC5-735C-4FF1-8EEF-FF78EA42FC85}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tgstation.Server.Common.Tests", "tests\Tgstation.Server.Common.Tests\Tgstation.Server.Common.Tests.csproj", "{73F2C6B9-54A6-4433-9D94-07F2AC0FD084}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -571,6 +577,18 @@ Global {BF02BCC5-735C-4FF1-8EEF-FF78EA42FC85}.ReleaseNoWindows|Any CPU.Build.0 = Release|Any CPU {BF02BCC5-735C-4FF1-8EEF-FF78EA42FC85}.ReleaseNoWix|Any CPU.ActiveCfg = Release|Any CPU {BF02BCC5-735C-4FF1-8EEF-FF78EA42FC85}.ReleaseNoWix|Any CPU.Build.0 = Release|Any CPU + {73F2C6B9-54A6-4433-9D94-07F2AC0FD084}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {73F2C6B9-54A6-4433-9D94-07F2AC0FD084}.Debug|Any CPU.Build.0 = Debug|Any CPU + {73F2C6B9-54A6-4433-9D94-07F2AC0FD084}.DebugNoWindows|Any CPU.ActiveCfg = Debug|Any CPU + {73F2C6B9-54A6-4433-9D94-07F2AC0FD084}.DebugNoWindows|Any CPU.Build.0 = Debug|Any CPU + {73F2C6B9-54A6-4433-9D94-07F2AC0FD084}.DebugNoWix|Any CPU.ActiveCfg = Debug|Any CPU + {73F2C6B9-54A6-4433-9D94-07F2AC0FD084}.DebugNoWix|Any CPU.Build.0 = Debug|Any CPU + {73F2C6B9-54A6-4433-9D94-07F2AC0FD084}.Release|Any CPU.ActiveCfg = Release|Any CPU + {73F2C6B9-54A6-4433-9D94-07F2AC0FD084}.Release|Any CPU.Build.0 = Release|Any CPU + {73F2C6B9-54A6-4433-9D94-07F2AC0FD084}.ReleaseNoWindows|Any CPU.ActiveCfg = Release|Any CPU + {73F2C6B9-54A6-4433-9D94-07F2AC0FD084}.ReleaseNoWindows|Any CPU.Build.0 = Release|Any CPU + {73F2C6B9-54A6-4433-9D94-07F2AC0FD084}.ReleaseNoWix|Any CPU.ActiveCfg = Release|Any CPU + {73F2C6B9-54A6-4433-9D94-07F2AC0FD084}.ReleaseNoWix|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -609,6 +627,7 @@ Global {7F7FCFDF-271D-45C2-830C-BCCB19C57077} = {A55C1117-5808-4AB2-BEA6-4D4A3E66A2F2} {EAB84FD0-5514-4254-B188-7D90ACB7284D} = {316141B0-CD21-4769-A013-D53DA9B9EC09} {5130526C-A553-493B-A9B0-3DB452949886} = {2648A85F-61AE-428E-95E1-66D06C7A3768} + {73F2C6B9-54A6-4433-9D94-07F2AC0FD084} = {316141B0-CD21-4769-A013-D53DA9B9EC09} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {DFD36C95-3E49-41C7-ACDB-86BAF5B18A79} diff --git a/tools/Tgstation.Server.LogoGenerator/Tgstation.Server.LogoGenerator.csproj b/tools/Tgstation.Server.LogoGenerator/Tgstation.Server.LogoGenerator.csproj index 5616ec05ed..0fc788a31d 100644 --- a/tools/Tgstation.Server.LogoGenerator/Tgstation.Server.LogoGenerator.csproj +++ b/tools/Tgstation.Server.LogoGenerator/Tgstation.Server.LogoGenerator.csproj @@ -8,7 +8,7 @@ - + diff --git a/tools/Tgstation.Server.ReleaseNotes/Program.cs b/tools/Tgstation.Server.ReleaseNotes/Program.cs index 8e6c689123..356f11bf87 100644 --- a/tools/Tgstation.Server.ReleaseNotes/Program.cs +++ b/tools/Tgstation.Server.ReleaseNotes/Program.cs @@ -807,7 +807,7 @@ namespace Tgstation.Server.ReleaseNotes var versionsPropertyGroup = project.Elements().First(x => x.Name == xmlNamespace + "PropertyGroup"); var coreVersion = Version.Parse(versionsPropertyGroup.Element(xmlNamespace + "TgsCoreVersion").Value); - const string BodyForPRSha = "a4278b458274559c76797a372087ff758ab87259"; + const string BodyForPRSha = "b5780571c1a436c5f687bbd7cba753b3b503762b"; var prBody = $@"# Automated Pull Request This pull request was generated by our [deployment pipeline]({actionUrl}) as a result of the release of [tgstation-server-v{coreVersion}](https://github.com/tgstation/tgstation-server/releases/tag/tgstation-server-v{coreVersion}). Validation was performed as part of the process. @@ -984,7 +984,8 @@ Note: `` is the directory's name containing the manifest you're submitting foreach (var maxVersionKvp in prResults.SelectMany(x => x.Item1) .Where(x => !releasedComponentVersions.ContainsKey(x.Key)) .GroupBy(x => x.Key) - .Select(group => { + .Select(group => + { var versions = group .Where(x => x.Value.Version != null) .ToList(); diff --git a/tools/Tgstation.Server.ReleaseNotes/Tgstation.Server.ReleaseNotes.csproj b/tools/Tgstation.Server.ReleaseNotes/Tgstation.Server.ReleaseNotes.csproj index 772bc7c717..df7a33a579 100644 --- a/tools/Tgstation.Server.ReleaseNotes/Tgstation.Server.ReleaseNotes.csproj +++ b/tools/Tgstation.Server.ReleaseNotes/Tgstation.Server.ReleaseNotes.csproj @@ -8,7 +8,7 @@ - +