mirror of
https://github.com/tgstation/tgstation-server.git
synced 2026-07-19 03:53:02 +01:00
Merge pull request #2409 from tgstation/LetsGetThisOut [DMDeploy][TGSDeploy]
v6.19.0: FUCK
This commit is contained in:
@@ -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:
|
||||
|
||||
+111
-267
@@ -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/<TgsCoreVersion>${CURRENT_TGS_VERSION}/<TgsCoreVersion>${{ 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+<PackageReference Include="Microsoft.EntityFrameworkCore" Version="([1-9][0-9]*\.[0-9]+\.[0-9]+)" \/>'
|
||||
if [[ $(cat src/Tgstation.Server.Host/Tgstation.Server.Host.csproj) =~ $regex ]]; then
|
||||
regex='\s+<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="([1-9][0-9]*\.[0-9]+\.[0-9]+)" \/>'
|
||||
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+<PackageVersion Include="StrawberryShake.Server" Version="([1-9][0-9]*\.[0-9]+\.[0-9]+)" \/>'
|
||||
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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 }}
|
||||
@@ -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
|
||||
@@ -0,0 +1,83 @@
|
||||
<Project>
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="Byond.TopicSender" Version="8.0.1" />
|
||||
<PackageVersion Include="Core.System.ServiceProcess" Version="2.0.1" />
|
||||
<PackageVersion Include="coverlet.collector" Version="6.0.4" />
|
||||
<PackageVersion Include="Cyberboss.AspNetCore.AsyncInitializer" Version="1.2.0" />
|
||||
<PackageVersion Include="Cyberboss.SmartIrc4net.Standard" Version="1.0.0" />
|
||||
<PackageVersion Include="DotEnv.Core" Version="3.1.0" />
|
||||
<PackageVersion Include="Elastic.CommonSchema.Serilog" Version="9.0.0" />
|
||||
<PackageVersion Include="GitHubActionsTestLogger" Version="2.4.1" />
|
||||
<PackageVersion Include="HotChocolate.AspNetCore" Version="15.1.9" />
|
||||
<PackageVersion Include="HotChocolate.AspNetCore.Authorization" Version="15.1.9" />
|
||||
<PackageVersion Include="HotChocolate.Data.EntityFramework" Version="15.1.9" />
|
||||
<PackageVersion Include="HotChocolate.Types.Analyzers" Version="15.1.9" />
|
||||
<PackageVersion Include="HotChocolate.Types.Scalars" Version="15.1.9" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageVersion Include="LibGit2Sharp" Version="0.31.0" />
|
||||
<PackageVersion Include="McMaster.Extensions.CommandLineUtils" Version="4.1.1" />
|
||||
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.8" />
|
||||
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.19" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="8.0.19" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Http.Extensions" Version="2.3.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.19" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.SignalR.Client" Version="9.0.8" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="9.0.8" />
|
||||
<PackageVersion Include="Microsoft.Diagnostics.NETCore.Client" Version="0.2.621003" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="9.0.8" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.8" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.8" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.8" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="8.0.19" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Systemd" Version="9.0.8" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.8" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Http" Version="2.3.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.8" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="9.0.8" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.EventLog" Version="9.0.8" />
|
||||
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.14.0" />
|
||||
<PackageVersion Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" Version="8.14.0" />
|
||||
<PackageVersion Include="MSTest.TestAdapter" Version="3.10.3" />
|
||||
<PackageVersion Include="MSTest.TestFramework" Version="3.10.3" />
|
||||
<PackageVersion Include="Mono.Posix.NETStandard" Version="1.0.0" />
|
||||
<!-- Pinned: Be VERY careful about updating https://github.com/moq/moq/issues/1372 -->
|
||||
<PackageVersion Include="Moq" Version="4.20.72" />
|
||||
<PackageVersion Include="NCrontab.Signed" Version="3.3.3" />
|
||||
<PackageVersion Include="NetEscapades.Configuration.Yaml" Version="3.1.0" />
|
||||
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
||||
<PackageVersion Include="Octokit" Version="14.0.0" />
|
||||
<PackageVersion Include="Octokit.GraphQL" Version="0.3.0-beta" />
|
||||
<PackageVersion Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0" />
|
||||
<PackageVersion Include="prometheus-net.AspNetCore.HealthChecks" Version="8.2.1" />
|
||||
<PackageVersion Include="Remora.Discord" Version="2025.2.0" />
|
||||
<PackageVersion Include="Serilog.Extensions.Logging" Version="9.0.2" />
|
||||
<PackageVersion Include="Serilog.Sinks.Async" Version="2.1.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.Elasticsearch" Version="10.0.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.File" Version="7.0.0" />
|
||||
<PackageVersion Include="StrawberryShake.Server" Version="15.1.9" />
|
||||
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
||||
<PackageVersion Include="Svg" Version="3.4.7" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore" Version="9.0.4" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore.Newtonsoft" Version="9.0.4" />
|
||||
<PackageVersion Include="System.ComponentModel.Annotations" Version="5.0.0" />
|
||||
<PackageVersion Include="System.DirectoryServices.AccountManagement" Version="9.0.8" />
|
||||
<PackageVersion Include="System.Drawing.Common" Version="9.0.8" />
|
||||
<PackageVersion Include="System.IO.Abstractions" Version="22.0.15" />
|
||||
<PackageVersion Include="System.IO.Abstractions.TestingHelpers" Version="22.0.15" />
|
||||
<PackageVersion Include="System.Management" Version="9.0.8" />
|
||||
<PackageVersion Include="System.Private.Uri" Version="4.3.2" />
|
||||
<PackageVersion Include="System.Runtime.InteropServices" Version="4.3.0" />
|
||||
<PackageVersion Include="System.ServiceProcess.ServiceController" Version="9.0.8" />
|
||||
<PackageVersion Include="System.Threading.Tasks.Extensions" Version="4.6.3" />
|
||||
<PackageVersion Include="WixToolset.Bal.wixext" Version="5.0.2" />
|
||||
<PackageVersion Include="WixToolset.Dtf.CustomAction" Version="5.0.2" />
|
||||
<PackageVersion Include="WixToolset.Heat" Version="5.0.2" />
|
||||
<PackageVersion Include="WixToolset.Netfx.wixext" Version="5.0.2" />
|
||||
<PackageVersion Include="WixToolset.Util.wixext" Version="5.0.2" />
|
||||
<PackageVersion Include="YamlDotNet" Version="16.3.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -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:<Provider Name>`: Sets the OAuth client ID and secret for a given `<Provider Name>`. 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:
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<TgsFrameworkVersion>net$(TgsNetMajorVersion).0</TgsFrameworkVersion>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<AccelerateBuildsInVisualStudio>true</AccelerateBuildsInVisualStudio>
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
<!-- This is the default but Wix hates it -->
|
||||
<!--<DebugType>Portable</DebugType> -->
|
||||
</PropertyGroup>
|
||||
|
||||
+8
-12
@@ -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
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup>
|
||||
<!-- Usage: Primary JSON library -->
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="Newtonsoft.Json" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Usage: Sourcelink -->
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All" />
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Usage: Linting -->
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
|
||||
<PackageReference Include="StyleCop.Analyzers">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
@@ -3,24 +3,24 @@
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Usage: Code coverage collection -->
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4">
|
||||
<PackageReference Include="coverlet.collector">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<!-- Usage: Logging specific for GitHub actions -->
|
||||
<PackageReference Include="GitHubActionsTestLogger" Version="2.4.1">
|
||||
<PackageReference Include="GitHubActionsTestLogger">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<!-- Usage: Hard to say what exactly this is for, but not including it removes the test icon and breaks vstest.console.exe for some reason -->
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" Condition="'$(TgsTestNoSdk)' != 'true'" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Condition="'$(TgsTestNoSdk)' != 'true'" />
|
||||
<!-- Usage: Dependency mocking for tests -->
|
||||
<!-- Pinned: Be VERY careful about updating https://github.com/moq/moq/issues/1372 -->
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
<PackageReference Include="Moq" />
|
||||
<!-- Usage: MSTest execution -->
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="3.9.3" />
|
||||
<PackageReference Include="MSTest.TestAdapter" />
|
||||
<!-- Usage: MSTest asserts etc... -->
|
||||
<PackageReference Include="MSTest.TestFramework" Version="3.9.3" />
|
||||
<PackageReference Include="MSTest.TestFramework" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
+9
-9
@@ -3,22 +3,22 @@
|
||||
<!-- Integration tests will ensure they match across the board -->
|
||||
<Import Project="WebpanelVersion.props" />
|
||||
<PropertyGroup>
|
||||
<TgsCoreVersion>6.18.2</TgsCoreVersion>
|
||||
<TgsConfigVersion>5.8.0</TgsConfigVersion>
|
||||
<TgsRestVersion>10.13.0</TgsRestVersion>
|
||||
<TgsGraphQLVersion>0.6.0</TgsGraphQLVersion>
|
||||
<TgsCoreVersion>6.19.0</TgsCoreVersion>
|
||||
<TgsConfigVersion>5.9.0</TgsConfigVersion>
|
||||
<TgsRestVersion>10.14.0</TgsRestVersion>
|
||||
<TgsGraphQLVersion>0.7.0</TgsGraphQLVersion>
|
||||
<TgsCommonLibraryVersion>7.0.0</TgsCommonLibraryVersion>
|
||||
<TgsApiLibraryVersion>18.0.0</TgsApiLibraryVersion>
|
||||
<TgsClientVersion>21.0.0</TgsClientVersion>
|
||||
<TgsDmapiVersion>7.3.3</TgsDmapiVersion>
|
||||
<TgsInteropVersion>5.10.1</TgsInteropVersion>
|
||||
<TgsApiLibraryVersion>18.2.0</TgsApiLibraryVersion>
|
||||
<TgsClientVersion>21.2.0</TgsClientVersion>
|
||||
<TgsDmapiVersion>7.4.0</TgsDmapiVersion>
|
||||
<TgsInteropVersion>5.11.0</TgsInteropVersion>
|
||||
<TgsHostWatchdogVersion>1.6.0</TgsHostWatchdogVersion>
|
||||
<TgsSwarmProtocolVersion>9.0.0</TgsSwarmProtocolVersion>
|
||||
<TgsContainerScriptVersion>1.2.1</TgsContainerScriptVersion>
|
||||
<TgsNugetNetFramework>netstandard2.0</TgsNugetNetFramework>
|
||||
<TgsNetMajorVersion>8</TgsNetMajorVersion>
|
||||
<!-- Update this frequently with dotnet runtime patches. MAJOR MUST MATCH ABOVE! -->
|
||||
<TgsDotnetRedistUrl>https://download.visualstudio.microsoft.com/download/pr/751d3fcd-72db-4da2-b8d0-709c19442225/33cc492bde704bfd6d70a2b9109005a0/dotnet-hosting-8.0.6-win.exe</TgsDotnetRedistUrl>
|
||||
<TgsDotnetRedistUrl>https://builds.dotnet.microsoft.com/dotnet/aspnetcore/Runtime/8.0.19/dotnet-hosting-8.0.19-win.exe</TgsDotnetRedistUrl>
|
||||
<TgsMariaDBRedistVersion>11.4.2</TgsMariaDBRedistVersion>
|
||||
<!-- Only have this uncommented if the mariadb servers are shitting the bed, update if the version updates -->
|
||||
<!--<TgsMariaDBFallbackRedist>https://mirror.its.dal.ca/mariadb//mariadb-10.11.8/winx64-packages/mariadb-10.11.8-winx64.msi</TgsMariaDBFallbackRedist>-->
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RuleSet Name="myrules" Description="My rule set" ToolsVersion="17.0">
|
||||
<Rules AnalyzerId="AsyncUsageAnalyzers" RuleNamespace="AsyncUsageAnalyzers">
|
||||
<Rule Id="UseConfigureAwait" Action="Warning" />
|
||||
</Rules>
|
||||
<Rules AnalyzerId="Microsoft.Analyzers.ManagedCodeAnalysis" RuleNamespace="Microsoft.Rules.Managed">
|
||||
<Rule Id="CA1000" Action="Warning" />
|
||||
<Rule Id="CA1000" Action="None" />
|
||||
<Rule Id="CA1001" Action="Warning" />
|
||||
<Rule Id="CA1003" Action="Warning" />
|
||||
<Rule Id="CA1004" Action="Warning" />
|
||||
|
||||
Executable
+6
@@ -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
|
||||
@@ -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="
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
'';
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
};
|
||||
|
||||
+2
-2
@@ -24,8 +24,8 @@
|
||||
<Content Include="Theme.xml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="WixToolset.Bal.wixext" Version="5.0.2" />
|
||||
<PackageReference Include="WixToolset.Netfx.wixext" Version="5.0.2" />
|
||||
<PackageReference Include="WixToolset.Bal.wixext" />
|
||||
<PackageReference Include="WixToolset.Netfx.wixext" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Tgstation.Server.Host.Service.Wix\Tgstation.Server.Host.Service.Wix.wixproj" />
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="WixToolset.Dtf.CustomAction" Version="5.0.2" />
|
||||
<PackageReference Include="WixToolset.Dtf.CustomAction" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
+2
-2
@@ -20,8 +20,8 @@
|
||||
</HarvestDirectory>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="WixToolset.Heat" Version="5.0.2" />
|
||||
<PackageReference Include="WixToolset.Util.wixext" Version="5.0.2" />
|
||||
<PackageReference Include="WixToolset.Heat" />
|
||||
<PackageReference Include="WixToolset.Util.wixext" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Tgstation.Server.Host.Service.Wix.Extensions\Tgstation.Server.Host.Service.Wix.Extensions.csproj" />
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"sdk": {
|
||||
"version": "8.0.0",
|
||||
"rollForward": "latestMajor",
|
||||
"rollForward": "latestMinor",
|
||||
"allowPrerelease": false
|
||||
}
|
||||
}
|
||||
|
||||
+8
-2
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -1 +1 @@
|
||||
"5.10.1"
|
||||
"5.11.0"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -493,7 +493,7 @@ namespace Tgstation.Server.Api.Models
|
||||
/// <summary>
|
||||
/// Attempted to restart a stopped watchdog.
|
||||
/// </summary>
|
||||
[Description("Cannot restart the watchdog as it is not running!")]
|
||||
[Description("Cannot perform watchdog operation as it is not running!")]
|
||||
WatchdogNotRunning,
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -14,6 +14,13 @@ namespace Tgstation.Server.Api.Models.Internal
|
||||
[ResponseOptions]
|
||||
public long? SessionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A incrementing ID for representing current iteration of servers world (i.e. after calling /world/proc/Reboot). Only unique within the current <see cref="SessionId"/>. Only tracked in game sessions with the DMAPI enabled.
|
||||
/// </summary>
|
||||
/// <example>1</example>
|
||||
[ResponseOptions]
|
||||
public long? WorldIteration { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When the current server execution was started.
|
||||
/// </summary>
|
||||
|
||||
@@ -33,6 +33,12 @@ namespace Tgstation.Server.Api.Rights
|
||||
/// <returns>The <see cref="Enum"/> <see cref="Type"/> of the given <paramref name="rightsType"/>.</returns>
|
||||
public static Type RightToType(RightsType rightsType) => TypeMap[rightsType];
|
||||
|
||||
/// <summary>
|
||||
/// Iterate the <see cref="Type"/> of each right.
|
||||
/// </summary>
|
||||
/// <returns>An <see cref="IEnumerable{T}"/> of each <see cref="Type"/> of right.</returns>
|
||||
public static IEnumerable<Type> AllRightTypes() => TypeMap.Values;
|
||||
|
||||
/// <summary>
|
||||
/// Map a given <typeparamref name="TRight"/> to its respective <see cref="RightsType"/>.
|
||||
/// </summary>
|
||||
|
||||
@@ -26,11 +26,11 @@
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Usage: HTTP constants reference -->
|
||||
<PackageReference Include="Microsoft.AspNetCore.Http.Extensions" Version="2.3.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Http.Extensions" />
|
||||
<!-- Usage: Decoding the 'nbf' property of JWTs -->
|
||||
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.13.0" />
|
||||
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" />
|
||||
<!-- Usage: Data model annotating -->
|
||||
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
|
||||
<PackageReference Include="System.ComponentModel.Annotations" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"strawberryshake.tools": {
|
||||
"version": "13.9.12",
|
||||
"version": "15.1.9",
|
||||
"commands": [
|
||||
"dotnet-graphql"
|
||||
]
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ mutation CreateSystemUserWithPermissionSet($systemIdentifier: String!) {
|
||||
message
|
||||
}
|
||||
}
|
||||
user {
|
||||
updatedUser {
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ mutation CreateUserFromOAuthConnection($name: String!, $oAuthConnections: [OAuth
|
||||
message
|
||||
}
|
||||
}
|
||||
user {
|
||||
updatedUser {
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
+20
-1
@@ -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
|
||||
|
||||
@@ -7,7 +7,7 @@ mutation CreateUserWithPassword($name: String!, $password: String!) {
|
||||
message
|
||||
}
|
||||
}
|
||||
user {
|
||||
updatedUser {
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
+6
-4
@@ -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 {
|
||||
|
||||
@@ -25,6 +25,8 @@ mutation SetFullPermsOnUserGroup($id: ID!) {
|
||||
canRead: true
|
||||
canList: true
|
||||
canRelocate: true
|
||||
canSetAutoStart: true
|
||||
canSetAutoStop: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
-4
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
query ListUserGroups {
|
||||
swarm {
|
||||
users {
|
||||
groups {
|
||||
queryableGroups {
|
||||
totalCount
|
||||
nodes {
|
||||
id
|
||||
}
|
||||
userGroups {
|
||||
queryableGroups {
|
||||
totalCount
|
||||
nodes {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,28 +9,24 @@
|
||||
|
||||
<ItemGroup>
|
||||
<!-- GraphQL connector and code generator -->
|
||||
<PackageReference Include="StrawberryShake.Server" Version="15.1.8" />
|
||||
<PackageReference Include="StrawberryShake.Server" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Tgstation.Server.Client\Tgstation.Server.Client.csproj" />
|
||||
<ProjectReference Include="..\Tgstation.Server.Host\Tgstation.Server.Host.csproj" PrivateAssets="all" ReferenceOutputAssembly="false" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- https://github.com/dotnet/msbuild/issues/2661#issuecomment-338808147 -->
|
||||
<Target Name="WorkaroundSdk939" BeforeTargets="ImportGraphQLApiSchema">
|
||||
<MSBuild Projects="..\Tgstation.Server.Host\Tgstation.Server.Host.csproj" />
|
||||
</Target>
|
||||
|
||||
<Target Name="DeleteGeneratedFiles" BeforeTargets="ImportGraphQLApiSchema">
|
||||
<RemoveDir Directories="$(IntermediateOutputPath)berry" />
|
||||
</Target>
|
||||
|
||||
<!-- https://github.com/ChilliCream/graphql-platform/blob/c0c8df525ca0f47bf3b3b409a8b22cbe37f7a9c0/src/StrawberryShake/MetaPackages/Common/MSBuild/StrawberryShake.targets#L20 -->
|
||||
<Target Name="ImportGraphQLApiSchema" BeforeTargets="_GraphQLCodeGenerationRoot" Inputs="../../artifacts/tgs-api.graphql" Outputs="schema.graphql">
|
||||
<Target Name="ImportGraphQLApiSchema" BeforeTargets="_GraphQLCodeGenerationRoot" DependsOnTargets="ResolveProjectReferences" Inputs="../../artifacts/tgs-api.graphql" Outputs="schema.graphql">
|
||||
<Copy SkipUnchangedFiles="true" SourceFiles="../../artifacts/tgs-api.graphql" DestinationFiles="schema.graphql" />
|
||||
</Target>
|
||||
|
||||
<Target Name="FixWarningsInGeneratedSchema" AfterTargets="GenerateGraphQLCode">
|
||||
<Target Name="FixWarningsInGeneratedSchema" AfterTargets="GenerateGraphQLCode" BeforeTargets="CoreCompile">
|
||||
<PropertyGroup>
|
||||
<InputFile>$(IntermediateOutputPath)berry/GraphQLClient.Client.cs</InputFile>
|
||||
<OutputFile>$(IntermediateOutputPath)berry/GraphQLClient.Client.cs</OutputFile>
|
||||
|
||||
@@ -69,9 +69,9 @@ namespace Tgstation.Server.Client
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IHttpClient"/> for the <see cref="ApiClient"/>.
|
||||
/// The <see cref="HttpClient"/> for the <see cref="ApiClient"/>.
|
||||
/// </summary>
|
||||
readonly IHttpClient httpClient;
|
||||
readonly HttpClient httpClient;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IRequestLogger"/>s used by the <see cref="ApiClient"/>.
|
||||
@@ -166,7 +166,7 @@ namespace Tgstation.Server.Client
|
||||
/// <param name="tokenRefreshHeaders">The value of <see cref="tokenRefreshHeaders"/>.</param>
|
||||
/// <param name="authless">The value of <see cref="authless"/>.</param>
|
||||
public ApiClient(
|
||||
IHttpClient httpClient,
|
||||
HttpClient httpClient,
|
||||
Uri url,
|
||||
ApiHeaders apiHeaders,
|
||||
ApiHeaders? tokenRefreshHeaders,
|
||||
|
||||
@@ -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);
|
||||
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
/// <summary>
|
||||
/// Create an <see cref="IApiClient"/>.
|
||||
/// </summary>
|
||||
/// <param name="url">The base <see cref="Uri"/>.</param>
|
||||
/// <param name="apiHeaders">The <see cref="ApiHeaders"/> for the <see cref="IApiClient"/>.</param>
|
||||
/// <param name="tokenRefreshHeaders">The <see cref="ApiHeaders"/> to use to generate a new <see cref="Api.Models.Response.TokenResponse"/>.</param>
|
||||
/// <param name="handler">The <see cref="HttpMessageHandler"/> to use with the internal <see cref="HttpClient"/>.</param>
|
||||
/// <param name="disposeHandler">If <paramref name="handler"/> should be disposed with the created <see cref="IApiClient"/>.</param>
|
||||
/// <param name="authless">If there should be no authentication performed.</param>
|
||||
/// <returns>A new <see cref="IApiClient"/>.</returns>
|
||||
public IApiClient CreateApiClient(
|
||||
Uri url,
|
||||
ApiHeaders apiHeaders,
|
||||
ApiHeaders? tokenRefreshHeaders,
|
||||
HttpMessageHandler handler,
|
||||
bool disposeHandler,
|
||||
bool authless);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Usage: Connecting to SignalR hubs in API -->
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="9.0.7" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" />
|
||||
<!-- Usage: Using target JSON serializer for API -->
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="9.0.7" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public sealed class HttpClient : IHttpClient
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public TimeSpan Timeout
|
||||
{
|
||||
get => httpClient.Timeout;
|
||||
set => httpClient.Timeout = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public HttpRequestHeaders DefaultRequestHeaders => httpClient.DefaultRequestHeaders;
|
||||
|
||||
/// <summary>
|
||||
/// The real <see cref="System.Net.Http.HttpClient"/>.
|
||||
/// </summary>
|
||||
readonly System.Net.Http.HttpClient httpClient;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HttpClient"/> class.
|
||||
/// </summary>
|
||||
/// <param name="implementation">The <see cref="System.Net.Http.HttpClient"/> to wrap.</param>
|
||||
public HttpClient(System.Net.Http.HttpClient implementation)
|
||||
{
|
||||
httpClient = implementation ?? throw new ArgumentNullException(nameof(implementation));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HttpClient"/> class.
|
||||
/// </summary>
|
||||
public HttpClient()
|
||||
: this(new System.Net.Http.HttpClient())
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose() => httpClient.Dispose();
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationToken cancellationToken)
|
||||
=> httpClient.SendAsync(request, completionOption, cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
using System;
|
||||
using System.Net.Http.Headers;
|
||||
|
||||
namespace Tgstation.Server.Common.Http
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="IAbstractHttpClientFactory"/> that creates <see cref="HttpClient"/>s.
|
||||
/// </summary>
|
||||
public sealed class HttpClientFactory : IAbstractHttpClientFactory
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public IHttpClient CreateClient()
|
||||
{
|
||||
var client = new HttpClient();
|
||||
try
|
||||
{
|
||||
client.DefaultRequestHeaders.UserAgent.Add(userAgent);
|
||||
return client;
|
||||
}
|
||||
catch
|
||||
{
|
||||
client.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ProductInfoHeaderValue"/> used as created client's User-Agent header on request.
|
||||
/// </summary>
|
||||
readonly ProductInfoHeaderValue userAgent;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HttpClientFactory"/> class.
|
||||
/// </summary>
|
||||
/// <param name="userAgent">The value of <see cref="userAgent"/>.</param>
|
||||
public HttpClientFactory(ProductInfoHeaderValue userAgent)
|
||||
{
|
||||
this.userAgent = userAgent ?? throw new ArgumentNullException(nameof(userAgent));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
namespace Tgstation.Server.Common.Http
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates <see cref="IHttpClient"/>s.
|
||||
/// </summary>
|
||||
public interface IAbstractHttpClientFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Create a <see cref="IHttpClient"/>.
|
||||
/// </summary>
|
||||
/// <returns>A new <see cref="IHttpClient"/>.</returns>
|
||||
IHttpClient CreateClient();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// For sending HTTP requests.
|
||||
/// </summary>
|
||||
public interface IHttpClient : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The request timeout.
|
||||
/// </summary>
|
||||
TimeSpan Timeout { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="HttpRequestHeaders"/> used on every request.
|
||||
/// </summary>
|
||||
HttpRequestHeaders DefaultRequestHeaders { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Send an HTTP request.
|
||||
/// </summary>
|
||||
/// <param name="request">The <see cref="HttpRequestMessage"/>.</param>
|
||||
/// <param name="completionOption">The <see cref="HttpCompletionOption"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="HttpResponseMessage"/> of the request.</returns>
|
||||
Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -12,10 +12,10 @@
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Usage: ValueTask netstandard backport -->
|
||||
<PackageReference Include="System.Threading.Tasks.Extensions" Version="4.6.3" />
|
||||
<PackageReference Include="System.Threading.Tasks.Extensions" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="IconGeneration" BeforeTargets="ResolveAssemblyReferences" Inputs="../../build/logo.svg" Outputs="../../artifacts/tgs.ico;../../artifacts/tgs.png;../../tools/Tgstation.Server.LogoGenerator/Program.cs">
|
||||
<Target Name="IconGeneration" BeforeTargets="ResolveAssemblyReferences" Condition="'$(TGS_NIX_BUILD)' == ''" Inputs="../../build/logo.svg" Outputs="../../artifacts/tgs.ico;../../artifacts/tgs.png;../../tools/Tgstation.Server.LogoGenerator/Program.cs">
|
||||
<Message Text="Generating icons from SVG..." Importance="high" />
|
||||
<Exec Command="dotnet run --project tools/Tgstation.Server.LogoGenerator -c $(Configuration)" WorkingDirectory="../.." />
|
||||
</Target>
|
||||
|
||||
@@ -13,9 +13,9 @@
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Usage: Identifying if we're running under SystemD -->
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="9.0.7" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" />
|
||||
<!-- Usage: Console logging plugin -->
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.7" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -17,25 +17,25 @@
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Usage: Installing service programatically -->
|
||||
<PackageReference Include="Core.System.ServiceProcess" Version="2.0.1" />
|
||||
<PackageReference Include="Core.System.ServiceProcess" />
|
||||
<!-- Usage: Command line argument support -->
|
||||
<PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="4.1.1" />
|
||||
<PackageReference Include="McMaster.Extensions.CommandLineUtils" />
|
||||
<!-- Usage: Identifies when we are running in the context of the Windows SCM -->
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.7" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" />
|
||||
<!-- Usage: Windows event log logging plugin -->
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.EventLog" Version="9.0.7" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.EventLog" />
|
||||
<!-- Usage: Console logging plugin -->
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.7" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" />
|
||||
<!-- Usage: Updated transitive dependency of Core.System.ServiceProcess -->
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="Newtonsoft.Json" />
|
||||
<!-- Usage: Updated transitive dependency of Core.System.ServiceProcess -->
|
||||
<PackageReference Include="System.Drawing.Common" Version="9.0.7" />
|
||||
<PackageReference Include="System.Drawing.Common" />
|
||||
<!-- Usage: Updated transitive dependency, unable to tell what of -->
|
||||
<PackageReference Include="System.Private.Uri" Version="4.3.2" />
|
||||
<PackageReference Include="System.Private.Uri" />
|
||||
<!-- Usage: OS identification -->
|
||||
<PackageReference Include="System.Runtime.InteropServices" Version="4.3.0" />
|
||||
<PackageReference Include="System.Runtime.InteropServices" />
|
||||
<!-- Usage: Windows Service Manager intergration -->
|
||||
<PackageReference Include="System.ServiceProcess.ServiceController" Version="9.0.7" />
|
||||
<PackageReference Include="System.ServiceProcess.ServiceController" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": 1,
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"strawberryshake.tools": {
|
||||
"version": "15.1.9",
|
||||
"commands": [
|
||||
"dotnet-graphql"
|
||||
],
|
||||
"rollForward": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
+6
-10
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="../../build/SrcCommon.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
@@ -7,28 +7,24 @@
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<Target Name="InstallApollo" Inputs="package.json;yarn.lock" Outputs="node_modules/.bin/apollo">
|
||||
<Message Text="Installing Apollo..." Importance="high" />
|
||||
<Exec Command="yarn install --immutable" />
|
||||
</Target>
|
||||
|
||||
<Target Name="CleanApollo" AfterTargets="Clean">
|
||||
<RemoveDir Directories="node_modules" />
|
||||
<RemoveDir Directories=".yarn" />
|
||||
</Target>
|
||||
|
||||
<Target Name="GetApi" DependsOnTargets="InstallApollo" Inputs="node_modules/.bin/apollo" Outputs="../../artifacts/gitlab-api.graphql">
|
||||
<Target Name="GetApi" Outputs="../../artifacts/gitlab-api.graphql" Condition="!Exists('../../artifacts/gitlab-api.graphql')">
|
||||
<Message Text="Fetching GitLab GraphQL API schema..." Importance="high" />
|
||||
<MakeDir Directories="../../artifacts" />
|
||||
<Exec Command="node_modules/.bin/apollo client:download-schema --endpoint=https://gitlab.com/api/graphql ../../artifacts/gitlab-api.graphql" />
|
||||
<Exec Command="dotnet tool restore" />
|
||||
<Exec Command="dotnet graphql download -f ../../artifacts/gitlab-api.graphql https://gitlab.com/api/graphql" />
|
||||
</Target>
|
||||
|
||||
<!-- https://github.com/ChilliCream/graphql-platform/blob/c0c8df525ca0f47bf3b3b409a8b22cbe37f7a9c0/src/StrawberryShake/MetaPackages/Common/MSBuild/StrawberryShake.targets#L20 -->
|
||||
<Target Name="ImportGraphQLApiSchema" DependsOnTargets="GetApi" BeforeTargets="_GraphQLCodeGenerationRoot" Inputs="../../artifacts/gitlab-api.graphql" Outputs="schema.graphql">
|
||||
<Copy SkipUnchangedFiles="true" SourceFiles="../../artifacts/gitlab-api.graphql" DestinationFiles="schema.graphql" />
|
||||
<WriteLinesToFile File="schema.graphql" Lines="$([System.IO.File]::ReadAllText('schema.graphql').Replace('\', ''))" Overwrite="true" Encoding="UTF-8" />
|
||||
</Target>
|
||||
|
||||
<!-- This should be able to be removed with ChilliCream v16+ -->
|
||||
<Target Name="FixWarningsInGeneratedSchema" AfterTargets="GenerateGraphQLCode">
|
||||
<PropertyGroup>
|
||||
<InputFile>$(IntermediateOutputPath)berry/GraphQLClient.Client.cs</InputFile>
|
||||
@@ -39,7 +35,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<!-- GraphQL code generator -->
|
||||
<PackageReference Include="StrawberryShake.Server" Version="15.1.8" />
|
||||
<PackageReference Include="StrawberryShake.Server" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"license": "AGPL-3.0-only",
|
||||
"devDependencies": {
|
||||
"apollo": "^2.34.0"
|
||||
},
|
||||
"packageManager": "yarn@4.7.0"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -10,9 +10,9 @@
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Usage: Logging abstractions -->
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.7" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" />
|
||||
<!-- Usage: POSIX support for signals -->
|
||||
<PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" />
|
||||
<PackageReference Include="Mono.Posix.NETStandard" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"dotnet-ef": {
|
||||
"version": "9.0.7",
|
||||
"version": "9.0.8",
|
||||
"commands": [
|
||||
"dotnet-ef"
|
||||
]
|
||||
|
||||
@@ -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
|
||||
/// </summary>
|
||||
readonly IMemoryCache cacheService;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IAssemblyInformationProvider"/> for the <see cref="AdministrationAuthority"/>.
|
||||
/// </summary>
|
||||
readonly IAssemblyInformationProvider assemblyInformationProvider;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IPlatformIdentifier"/> for the <see cref="AdministrationAuthority"/>.
|
||||
/// </summary>
|
||||
readonly IPlatformIdentifier platformIdentifier;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IIOManager"/> for the <see cref="AdministrationAuthority"/>.
|
||||
/// </summary>
|
||||
readonly IIOManager ioManager;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="FileLoggingConfiguration"/> for the <see cref="AdministrationAuthority"/>.
|
||||
/// </summary>
|
||||
readonly IOptionsSnapshot<FileLoggingConfiguration> fileLoggingConfigurationOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AdministrationAuthority"/> class.
|
||||
/// </summary>
|
||||
/// <param name="authenticationContext">The <see cref="IAuthenticationContext"/> to use.</param>
|
||||
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> to use.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> to use.</param>
|
||||
/// <param name="gitHubServiceFactory">The value of <see cref="gitHubServiceFactory"/>.</param>
|
||||
@@ -68,17 +92,23 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// <param name="serverUpdateInitiator">The value of <see cref="serverUpdateInitiator"/>.</param>
|
||||
/// <param name="fileTransferService">The value of <see cref="fileTransferService"/>.</param>
|
||||
/// <param name="cacheService">The value of <see cref="cacheService"/>.</param>
|
||||
/// <param name="assemblyInformationProvider">The value of <see cref="assemblyInformationProvider"/>.</param>
|
||||
/// <param name="platformIdentifier">The value of <see cref="platformIdentifier"/>.</param>
|
||||
/// <param name="ioManager">The value of <see cref="ioManager"/>.</param>
|
||||
/// <param name="fileLoggingConfigurationOptions">The value of <see cref="fileLoggingConfigurationOptions"/>.</param>
|
||||
public AdministrationAuthority(
|
||||
IAuthenticationContext authenticationContext,
|
||||
IDatabaseContext databaseContext,
|
||||
ILogger<UserAuthority> logger,
|
||||
IGitHubServiceFactory gitHubServiceFactory,
|
||||
IServerControl serverControl,
|
||||
IServerUpdateInitiator serverUpdateInitiator,
|
||||
IFileTransferTicketProvider fileTransferService,
|
||||
IMemoryCache cacheService)
|
||||
IMemoryCache cacheService,
|
||||
IAssemblyInformationProvider assemblyInformationProvider,
|
||||
IPlatformIdentifier platformIdentifier,
|
||||
IIOManager ioManager,
|
||||
IOptionsSnapshot<FileLoggingConfiguration> 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));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<AuthorityResponse<AdministrationResponse>> GetUpdateInformation(bool forceFresh, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
async Task<AdministrationResponse> CacheFactory()
|
||||
public RequirementsGated<AuthorityResponse<AdministrationResponse>> 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<AdministrationResponse> 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<AdministrationResponse> 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<AdministrationResponse>)rawCacheObject!;
|
||||
|
||||
var result = await task.WaitAsync(cancellationToken);
|
||||
return new AuthorityResponse<AdministrationResponse>(result);
|
||||
}
|
||||
catch (NotFoundException e)
|
||||
catch (RateLimitExceededException e)
|
||||
{
|
||||
Logger.LogWarning(e, "Not found exception while retrieving upstream repository info!");
|
||||
return RateLimit<AdministrationResponse>(e);
|
||||
}
|
||||
catch (ApiException e)
|
||||
{
|
||||
Logger.LogWarning(e, OctokitException);
|
||||
return new AuthorityResponse<AdministrationResponse>(
|
||||
new ErrorMessageResponse(ErrorCode.RemoteApiError)
|
||||
{
|
||||
AdditionalData = e.Message,
|
||||
},
|
||||
HttpFailureResponse.FailedDependency);
|
||||
}
|
||||
});
|
||||
|
||||
/// <inheritdoc />
|
||||
public RequirementsGated<AuthorityResponse<ServerUpdateResponse>> 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<ServerUpdateResponse>(ErrorCode.CannotChangeServerSuite);
|
||||
|
||||
if (!serverControl.WatchdogPresent)
|
||||
return new AuthorityResponse<ServerUpdateResponse>(
|
||||
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<ServerUpdateResponse>(ex);
|
||||
}
|
||||
catch (ApiException e)
|
||||
{
|
||||
Logger.LogWarning(e, OctokitException);
|
||||
return new AuthorityResponse<ServerUpdateResponse>(
|
||||
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<ServerUpdateResponse>(new ServerUpdateResponse(targetVersion, uploadTicket?.Ticket.FileTicket), HttpSuccessResponse.Accepted),
|
||||
ServerUpdateResult.ReleaseMissing => Gone<ServerUpdateResponse>(),
|
||||
ServerUpdateResult.UpdateInProgress => BadRequest<ServerUpdateResponse>(ErrorCode.ServerUpdateInProgress),
|
||||
ServerUpdateResult.SwarmIntegrityCheckFailed => new AuthorityResponse<ServerUpdateResponse>(
|
||||
new ErrorMessageResponse(ErrorCode.SwarmIntegrityCheckFailed),
|
||||
HttpFailureResponse.FailedDependency),
|
||||
_ => throw new InvalidOperationException($"Unexpected ServerUpdateResult: {updateResult}"),
|
||||
};
|
||||
}
|
||||
|
||||
var ttl = TimeSpan.FromMinutes(30);
|
||||
Task<AdministrationResponse> 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<AdministrationResponse>)rawCacheObject!;
|
||||
|
||||
var result = await task.WaitAsync(cancellationToken);
|
||||
return new AuthorityResponse<AdministrationResponse>(result);
|
||||
}
|
||||
catch (RateLimitExceededException e)
|
||||
{
|
||||
return RateLimit<AdministrationResponse>(e);
|
||||
}
|
||||
catch (ApiException e)
|
||||
{
|
||||
Logger.LogWarning(e, OctokitException);
|
||||
return new AuthorityResponse<AdministrationResponse>(
|
||||
new ErrorMessageResponse(ErrorCode.RemoteApiError)
|
||||
{
|
||||
AdditionalData = e.Message,
|
||||
},
|
||||
HttpFailureResponse.FailedDependency);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<AuthorityResponse<ServerUpdateResponse>> TriggerServerVersionChange(Version targetVersion, bool uploadZip, CancellationToken cancellationToken)
|
||||
{
|
||||
var attemptingUpload = uploadZip == true;
|
||||
if (attemptingUpload)
|
||||
{
|
||||
if (!AuthenticationContext.PermissionSet.AdministrationRights!.Value.HasFlag(AdministrationRights.UploadVersion))
|
||||
return Forbid<ServerUpdateResponse>();
|
||||
}
|
||||
else if (!AuthenticationContext.PermissionSet.AdministrationRights!.Value.HasFlag(AdministrationRights.ChangeVersion))
|
||||
return Forbid<ServerUpdateResponse>();
|
||||
|
||||
if (targetVersion.Major < 4)
|
||||
return BadRequest<ServerUpdateResponse>(ErrorCode.CannotChangeServerSuite);
|
||||
|
||||
if (!serverControl.WatchdogPresent)
|
||||
return new AuthorityResponse<ServerUpdateResponse>(
|
||||
new ErrorMessageResponse(ErrorCode.MissingHostWatchdog),
|
||||
HttpFailureResponse.UnprocessableEntity);
|
||||
|
||||
IFileUploadTicket? uploadTicket = attemptingUpload
|
||||
? fileTransferService.CreateUpload(FileUploadStreamKind.None)
|
||||
: null;
|
||||
|
||||
ServerUpdateResult updateResult;
|
||||
try
|
||||
{
|
||||
try
|
||||
public RequirementsGated<AuthorityResponse> 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<ServerUpdateResponse>(ex);
|
||||
}
|
||||
catch (ApiException e)
|
||||
{
|
||||
Logger.LogWarning(e, OctokitException);
|
||||
return new AuthorityResponse<ServerUpdateResponse>(
|
||||
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<ServerUpdateResponse>(new ServerUpdateResponse(targetVersion, uploadTicket?.Ticket.FileTicket), HttpSuccessResponse.Accepted),
|
||||
ServerUpdateResult.ReleaseMissing => Gone<ServerUpdateResponse>(),
|
||||
ServerUpdateResult.UpdateInProgress => BadRequest<ServerUpdateResponse>(ErrorCode.ServerUpdateInProgress),
|
||||
ServerUpdateResult.SwarmIntegrityCheckFailed => new AuthorityResponse<ServerUpdateResponse>(
|
||||
new ErrorMessageResponse(ErrorCode.SwarmIntegrityCheckFailed),
|
||||
HttpFailureResponse.FailedDependency),
|
||||
_ => throw new InvalidOperationException($"Unexpected ServerUpdateResult: {updateResult}"),
|
||||
};
|
||||
}
|
||||
await serverControl.Restart();
|
||||
return new AuthorityResponse();
|
||||
});
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<AuthorityResponse> TriggerServerRestart()
|
||||
public RequirementsGated<AuthorityResponse<LogFileResponse>> 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<LogFileResponse>();
|
||||
|
||||
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<LogFileResponse>(new LogFileResponse
|
||||
{
|
||||
Name = path,
|
||||
LastModified = await ioManager.GetLastModified(fullPath, cancellationToken),
|
||||
FileTicket = fileTransferTicket.FileTicket,
|
||||
});
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
return Conflict<LogFileResponse>(ErrorCode.IOError, ex.ToString());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/// <inheritdoc cref="IChatAuthority" />
|
||||
sealed class ChatAuthority : ComponentInterfacingAuthorityBase, IChatAuthority
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ChatAuthority"/> class.
|
||||
/// </summary>
|
||||
/// <param name="instanceManager">The <see cref="IInstanceManager"/> for the <see cref="ChatAuthority"/>.</param>
|
||||
/// <param name="databaseContext">The <see cref="AuthorityBase.DatabaseContext"/>.</param>
|
||||
/// <param name="logger">The <see cref="AuthorityBase.Logger"/>.</param>
|
||||
public ChatAuthority(IInstanceManager instanceManager, IDatabaseContext databaseContext, ILogger<ChatAuthority> logger)
|
||||
: base(instanceManager, databaseContext, logger)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform some basic validation of a given <paramref name="model"/>.
|
||||
/// </summary>
|
||||
/// <param name="model">The <see cref="ChatBotApiBase"/> to validate.</param>
|
||||
/// <param name="forCreation">If the <paramref name="model"/> is being created.</param>
|
||||
/// <returns>An <see cref="BadRequestObjectResult"/> to respond with or <see langword="null"/>.</returns>
|
||||
static AuthorityResponse<ChatBot>? 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<ChatBot>(ErrorCode.ChatBotWhitespaceName);
|
||||
|
||||
if (model.ConnectionString != null && String.IsNullOrWhiteSpace(model.ConnectionString))
|
||||
return BadRequest<ChatBot>(ErrorCode.ChatBotWhitespaceConnectionString);
|
||||
|
||||
var defaultMaxChannels = (ulong)Math.Max(ChatBot.DefaultChannelLimit, model.Channels?.Count ?? 0);
|
||||
if (defaultMaxChannels > UInt16.MaxValue)
|
||||
return BadRequest<ChatBot>(ErrorCode.ChatBotMaxChannels);
|
||||
|
||||
if (forCreation)
|
||||
model.ChannelLimit ??= (ushort)defaultMaxChannels;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public RequirementsGated<AuthorityResponse<ChatBot>> Create(
|
||||
IEnumerable<Models.ChatChannel> 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<ChatBot>();
|
||||
|
||||
if (query.TotalChatBots >= query.ChatBotLimit)
|
||||
return Conflict<ChatBot>(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<ChatBot>(model, HttpSuccessResponse.Created);
|
||||
},
|
||||
instanceId);
|
||||
},
|
||||
instanceId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
/// </summary>
|
||||
abstract class AuthorityBase : IAuthority
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the <see cref="IAuthenticationContext"/> for the <see cref="AuthorityBase"/>.
|
||||
/// </summary>
|
||||
protected IAuthenticationContext AuthenticationContext { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="IDatabaseContext"/> for the <see cref="AuthorityBase"/>.
|
||||
/// </summary>
|
||||
@@ -88,24 +83,57 @@ namespace Tgstation.Server.Host.Authority.Core
|
||||
/// </summary>
|
||||
/// <typeparam name="TResult">The <see cref="Type"/> of the <see cref="AuthorityResponse{TResult}.Result"/>.</typeparam>
|
||||
/// <param name="errorCode">The <see cref="ErrorCode"/>.</param>
|
||||
/// <param name="additionalData"><see cref="ErrorMessageResponse.AdditionalData"/> for the error message.</param>
|
||||
/// <returns>A new, errored <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
protected static AuthorityResponse<TResult> Conflict<TResult>(ErrorCode errorCode)
|
||||
protected static AuthorityResponse<TResult> Conflict<TResult>(ErrorCode errorCode, string? additionalData = null)
|
||||
=> new(
|
||||
new ErrorMessageResponse(errorCode),
|
||||
new ErrorMessageResponse(errorCode)
|
||||
{
|
||||
AdditionalData = additionalData,
|
||||
},
|
||||
HttpFailureResponse.Conflict);
|
||||
|
||||
/// <summary>
|
||||
/// Helper to quickly construct a <see cref="FlagRightsConditional{TRights}"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TRights">The <typeparamref name="TRights"/> to evaluate.</typeparam>
|
||||
/// <param name="flag">The single bit flag of the <typeparamref name="TRights"/>.</param>
|
||||
/// <returns>A new <see cref="FlagRightsConditional{TRights}"/>.</returns>
|
||||
protected static FlagRightsConditional<TRights> Flag<TRights>(TRights flag)
|
||||
where TRights : Enum
|
||||
=> new(flag);
|
||||
|
||||
/// <summary>
|
||||
/// Helper to quickly construct an <see cref="OrRightsConditional{TRights}"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TRights">The <typeparamref name="TRights"/> to evaluate.</typeparam>
|
||||
/// <param name="lhs">The left hand side operand.</param>
|
||||
/// <param name="rhs">The right hand side operand.</param>
|
||||
/// <returns>A new <see cref="OrRightsConditional{TRights}"/>.</returns>
|
||||
protected static OrRightsConditional<TRights> Or<TRights>(RightsConditional<TRights> lhs, RightsConditional<TRights> rhs)
|
||||
where TRights : Enum
|
||||
=> new(lhs, rhs);
|
||||
|
||||
/// <summary>
|
||||
/// Helper to quickly construct an <see cref="AndRightsConditional{TRights}"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TRights">The <typeparamref name="TRights"/> to evaluate.</typeparam>
|
||||
/// <param name="lhs">The left hand side operand.</param>
|
||||
/// <param name="rhs">The right hand side operand.</param>
|
||||
/// <returns>A new <see cref="AndRightsConditional{TRights}"/>.</returns>
|
||||
protected static AndRightsConditional<TRights> And<TRights>(RightsConditional<TRights> lhs, RightsConditional<TRights> rhs)
|
||||
where TRights : Enum
|
||||
=> new(lhs, rhs);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AuthorityBase"/> class.
|
||||
/// </summary>
|
||||
/// <param name="authenticationContext">The value of <see cref="AuthenticationContext"/>.</param>
|
||||
/// <param name="databaseContext">The value of <see cref="DatabaseContext"/>.</param>
|
||||
/// <param name="logger">The value of <see cref="Logger"/>.</param>
|
||||
protected AuthorityBase(
|
||||
IAuthenticationContext authenticationContext,
|
||||
IDatabaseContext databaseContext,
|
||||
ILogger<AuthorityBase> logger)
|
||||
{
|
||||
AuthenticationContext = authenticationContext ?? throw new ArgumentNullException(nameof(authenticationContext));
|
||||
DatabaseContext = databaseContext ?? throw new ArgumentNullException(nameof(databaseContext));
|
||||
Logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
@@ -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
|
||||
/// </summary>
|
||||
protected TAuthority Authority { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The authorization service for the <see cref="AuthorityInvokerBase{TAuthority}"/>.
|
||||
/// </summary>
|
||||
readonly Security.IAuthorizationService authorizationService;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AuthorityInvokerBase{TAuthority}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="authority">The value of <see cref="Authority"/>.</param>
|
||||
public AuthorityInvokerBase(TAuthority authority)
|
||||
/// <param name="authorizationService">The value of <see cref="authorizationService"/>.</param>
|
||||
public AuthorityInvokerBase(
|
||||
TAuthority authority,
|
||||
Security.IAuthorizationService authorizationService)
|
||||
{
|
||||
Authority = authority ?? throw new ArgumentNullException(nameof(authority));
|
||||
this.authorizationService = authorizationService ?? throw new ArgumentNullException(nameof(authorizationService));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
IQueryable<TResult> IAuthorityInvoker<TAuthority>.InvokeQueryable<TResult>(Func<TAuthority, IQueryable<TResult>> authorityInvoker)
|
||||
async ValueTask<IQueryable<TResult>?> IAuthorityInvoker<TAuthority>.InvokeQueryable<TResult>(Func<TAuthority, RequirementsGated<IQueryable<TResult>>> authorityInvoker)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(authorityInvoker);
|
||||
return authorityInvoker(Authority);
|
||||
|
||||
var requirementsGate = authorityInvoker(Authority);
|
||||
return await ExecuteIfRequirementsSatisfied(requirementsGate);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
IQueryable<TApiModel> IAuthorityInvoker<TAuthority>.InvokeTransformableQueryable<TResult, TApiModel, TTransformer>(Func<TAuthority, IQueryable<TResult>> authorityInvoker)
|
||||
/// <summary>
|
||||
/// Unwrap a <see cref="RequirementsGated{TResult}"/> result, returning <see langword="null"/> if the requirements weren't satisfied.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResult">The <see cref="Type"/> contained by the <paramref name="requirementsGate"/>.</typeparam>
|
||||
/// <param name="requirementsGate">The <see cref="RequirementsGated{TResult}"/> result.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <typeparamref name="TResult"/> if the requirements were met, <see langword="null"/> if the requirments weren't met.</returns>
|
||||
protected async ValueTask<TResult?> ExecuteIfRequirementsSatisfied<TResult>(RequirementsGated<TResult> 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);
|
||||
/// <summary>
|
||||
/// Called to handle generic behavior when requirements evaluation fails.
|
||||
/// </summary>
|
||||
/// <param name="authFailure">The <see cref="AuthorizationFailure"/>.</param>
|
||||
protected virtual void OnRequirementsFailure(AuthorizationFailure authFailure)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
/// <typeparam name="TResult">The <see cref="Type"/> of success response.</typeparam>
|
||||
public sealed class AuthorityResponse<TResult> : AuthorityResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// An expression to convert a <typeparamref name="TResult"/> into an <see cref="AuthorityResponse{TResult}"/>. The resulting <see cref="AuthorityResponse{TResult}"/> MUST NOT be used.
|
||||
/// </summary>
|
||||
public static Expression<Func<TResult, AuthorityResponse<TResult>>> MappingExpression { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
[MemberNotNullWhen(true, nameof(IsNoContent))]
|
||||
public override bool Success => base.Success;
|
||||
@@ -26,13 +32,24 @@ namespace Tgstation.Server.Host.Authority.Core
|
||||
/// <summary>
|
||||
/// The success <typeparamref name="TResult"/>.
|
||||
/// </summary>
|
||||
public TResult? Result { get; }
|
||||
public TResult? Result { get; private init; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="HttpSuccessResponse"/> for generating the <see cref="IActionResult"/>s.
|
||||
/// </summary>
|
||||
public HttpSuccessResponse? SuccessResponse { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes static members of the <see cref="AuthorityResponse{TResult}"/> class.
|
||||
/// </summary>
|
||||
static AuthorityResponse()
|
||||
{
|
||||
MappingExpression = result => new AuthorityResponse<TResult>
|
||||
{
|
||||
Result = result,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AuthorityResponse{TResult}"/> class.
|
||||
/// </summary>
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="AuthorityBase"/> for <see cref="IAuthority"/>s that need to access <see cref="IInstanceCore"/>s.
|
||||
/// </summary>
|
||||
abstract class ComponentInterfacingAuthorityBase : AuthorityBase
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="IInstanceManager"/> for the <see cref="ComponentInterfacingAuthorityBase"/>.
|
||||
/// </summary>
|
||||
readonly IInstanceManager instanceManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ComponentInterfacingAuthorityBase"/> class.
|
||||
/// </summary>
|
||||
/// <param name="instanceManager">The value of <see cref="instanceManager"/>.</param>
|
||||
/// <param name="databaseContext">The <see cref="AuthorityBase.DatabaseContext"/>.</param>
|
||||
/// <param name="logger">The <see cref="AuthorityBase.Logger"/>.</param>
|
||||
protected ComponentInterfacingAuthorityBase(
|
||||
IInstanceManager instanceManager,
|
||||
IDatabaseContext databaseContext,
|
||||
ILogger<ComponentInterfacingAuthorityBase> logger)
|
||||
: base(databaseContext, logger)
|
||||
{
|
||||
this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Run a given <paramref name="action"/> with the relevant <see cref="IInstance"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResult">The type of result the returned <see cref="AuthorityResponse{TResult}"/> uses.</typeparam>
|
||||
/// <param name="action">A <see cref="Func{T, TResult}"/> accepting the <see cref="IInstance"/> and returning a <see cref="ValueTask{TResult}"/> with the <see cref="IActionResult"/>.</param>
|
||||
/// <param name="instanceId">The <see cref="EntityId.Id"/> of <see cref="Models.Instance"/> to grab.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="AuthorityResponse{TResult}"/> that should be returned.</returns>
|
||||
/// <remarks>The context of <paramref name="action"/> should be as small as possible so as to avoid race conditions. This function can return a <see cref="ConflictResult"/> if the requested instance was offline.</remarks>
|
||||
protected async ValueTask<AuthorityResponse<TResult>> WithComponentInstance<TResult>(Func<IInstanceCore, ValueTask<AuthorityResponse<TResult>>> action, long instanceId)
|
||||
{
|
||||
using var instanceReference = instanceManager.GetInstanceReference(instanceId);
|
||||
using (LogContext.PushProperty(SerilogContextHelper.InstanceReferenceContextProperty, instanceReference?.Uid))
|
||||
{
|
||||
if (instanceReference == null)
|
||||
return Conflict<TResult>(ErrorCode.InstanceOffline);
|
||||
return await action(instanceReference);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
/// <summary>
|
||||
/// Throws a <see cref="ErrorMessageException"/> for errored <paramref name="authorityResponse"/>s.
|
||||
/// </summary>
|
||||
/// <param name="authorityResponse">The potentially errored <paramref name="authorityResponse"/>.</param>
|
||||
/// <typeparam name="TAuthorityResponse">The <see cref="AuthorityResponse"/> <see cref="Type"/> being checked.</typeparam>
|
||||
/// <param name="authorityResponse">The potentially errored <paramref name="authorityResponse"/> or <see langword="null"/> if requirements evaluation failed.</param>
|
||||
/// <param name="errorOnMissing">If an error should be raised for <see cref="HttpFailureResponse.NotFound"/> and <see cref="HttpFailureResponse.Gone"/> failures.</param>
|
||||
static void ThrowGraphQLErrorIfNecessary(AuthorityResponse authorityResponse, bool errorOnMissing)
|
||||
/// <returns><paramref name="authorityResponse"/> if an <see cref="ErrorMessageException"/> wasn't thrown.</returns>
|
||||
public static TAuthorityResponse ThrowGraphQLErrorIfNecessary<TAuthorityResponse>(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 <see cref="GraphQLAuthorityInvoker{TAuthority}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="authority">The <typeparamref name="TAuthority"/>.</param>
|
||||
public GraphQLAuthorityInvoker(TAuthority authority)
|
||||
: base(authority)
|
||||
/// <param name="authorizationService">the authorization service to use.</param>
|
||||
public GraphQLAuthorityInvoker(TAuthority authority, Security.IAuthorizationService authorizationService)
|
||||
: base(authority, authorizationService)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
async ValueTask IGraphQLAuthorityInvoker<TAuthority>.Invoke(Func<TAuthority, ValueTask<AuthorityResponse>> authorityInvoker)
|
||||
async ValueTask IGraphQLAuthorityInvoker<TAuthority>.Invoke(Func<TAuthority, RequirementsGated<AuthorityResponse>> authorityInvoker)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(authorityInvoker);
|
||||
|
||||
var authorityResponse = await authorityInvoker(Authority);
|
||||
var requirementsGate = authorityInvoker(Authority);
|
||||
var authorityResponse = await ExecuteIfRequirementsSatisfied(requirementsGate);
|
||||
ThrowGraphQLErrorIfNecessary(authorityResponse, true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
async ValueTask<TApiModel?> IGraphQLAuthorityInvoker<TAuthority>.InvokeAllowMissing<TResult, TApiModel>(Func<TAuthority, ValueTask<AuthorityResponse<TResult>>> authorityInvoker)
|
||||
async ValueTask<TApiModel?> IGraphQLAuthorityInvoker<TAuthority>.InvokeAllowMissing<TResult, TApiModel>(Func<TAuthority, RequirementsGated<AuthorityResponse<TResult>>> 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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
async ValueTask<TApiModel?> IGraphQLAuthorityInvoker<TAuthority>.InvokeTransformableAllowMissing<TResult, TApiModel, TTransformer>(Func<TAuthority, ValueTask<AuthorityResponse<TResult>>> authorityInvoker)
|
||||
async ValueTask<TApiModel?> IGraphQLAuthorityInvoker<TAuthority>.InvokeTransformableAllowMissing<TResult, TApiModel, TTransformer>(Func<TAuthority, RequirementsGated<AuthorityResponse<TResult>>> 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
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
ValueTask<TApiModel> IGraphQLAuthorityInvoker<TAuthority>.Invoke<TResult, TApiModel>(Func<TAuthority, ValueTask<AuthorityResponse<TResult>>> authorityInvoker)
|
||||
=> ((IGraphQLAuthorityInvoker<TAuthority>)this).InvokeAllowMissing<TResult, TApiModel>(authorityInvoker)!;
|
||||
async ValueTask<IQueryable<TApiModel>> IGraphQLAuthorityInvoker<TAuthority>.InvokeTransformableQueryable<TResult, TApiModel, TTransformer>(
|
||||
Func<TAuthority, RequirementsGated<IQueryable<TResult>>> authorityInvoker,
|
||||
Func<IQueryable<TResult>, IQueryable<TResult>>? 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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
ValueTask<TApiModel> IGraphQLAuthorityInvoker<TAuthority>.InvokeTransformable<TResult, TApiModel, TTransformer>(Func<TAuthority, ValueTask<AuthorityResponse<TResult>>> authorityInvoker)
|
||||
=> ((IGraphQLAuthorityInvoker<TAuthority>)this).InvokeTransformableAllowMissing<TResult, TApiModel, TTransformer>(authorityInvoker)!;
|
||||
async ValueTask<Dictionary<long, AuthorityResponse<TApiModel>>> IGraphQLAuthorityInvoker<TAuthority>.ExecuteDataLoader<TResult, TApiModel, TTransformer>(
|
||||
Func<TAuthority, long, RequirementsGated<Projectable<TResult, TApiModel>>> authorityInvoker,
|
||||
IReadOnlyList<long> ids,
|
||||
QueryContext<AuthorityResponse<TApiModel>>? 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<TResult, TApiModel>.Combine(
|
||||
queryable => queryable
|
||||
.Select(new TTransformer().ProjectedExpression)
|
||||
.With(unwrappedContext),
|
||||
results);
|
||||
|
||||
return responses;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
async ValueTask<TApiModel> IGraphQLAuthorityInvoker<TAuthority>.Invoke<TResult, TApiModel>(Func<TAuthority, RequirementsGated<AuthorityResponse<TResult>>> authorityInvoker)
|
||||
=> await ((IGraphQLAuthorityInvoker<TAuthority>)this).InvokeAllowMissing<TResult, TApiModel>(authorityInvoker)
|
||||
?? throw new InvalidOperationException("Authority invocation should have returned a non-nullable result!");
|
||||
|
||||
/// <inheritdoc />
|
||||
async ValueTask<TApiModel> IGraphQLAuthorityInvoker<TAuthority>.InvokeTransformable<TResult, TApiModel, TTransformer>(Func<TAuthority, RequirementsGated<AuthorityResponse<TResult>>> authorityInvoker)
|
||||
=> await ((IGraphQLAuthorityInvoker<TAuthority>)this).InvokeTransformableAllowMissing<TResult, TApiModel, TTransformer>(authorityInvoker)
|
||||
?? throw new InvalidOperationException("Authority invocation should have returned a non-nullable result!");
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnRequirementsFailure(AuthorizationFailure authFailure)
|
||||
=> throw authFailure.ForbiddenGraphQLException();
|
||||
|
||||
/// <summary>
|
||||
/// Unwrap a <see cref="RequirementsGated{TResult}"/> result, throwing a <see cref="GraphQLException"/> if they weren't met.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResult">The <see cref="Type"/> contained by the <paramref name="requirementsGate"/>.</typeparam>
|
||||
/// <param name="requirementsGate">The <see cref="RequirementsGated{TResult}"/> result.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <typeparamref name="TResult"/> if the requirements were met.</returns>
|
||||
/// <exception cref="GraphQLException">Throw when requirements were not met.</exception>
|
||||
new async ValueTask<TResult> ExecuteIfRequirementsSatisfied<TResult>(RequirementsGated<TResult> requirementsGate)
|
||||
where TResult : class
|
||||
=> (await base.ExecuteIfRequirementsSatisfied(requirementsGate))!; // base class throws if requirements evaluation fails
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 <typeparamref name="TAuthority"/> method and get the result.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResult">The returned <see cref="Type"/>.</typeparam>
|
||||
/// <param name="authorityInvoker">The <typeparamref name="TAuthority"/> <see cref="Func{T, TResult}"/> returning a <see cref="IQueryable{T}"/> <typeparamref name="TResult"/>.</param>
|
||||
/// <returns>A <see cref="IQueryable{T}"/> <typeparamref name="TResult"/> returned.</returns>
|
||||
IQueryable<TResult> InvokeQueryable<TResult>(Func<TAuthority, IQueryable<TResult>> authorityInvoker);
|
||||
|
||||
/// <summary>
|
||||
/// Invoke a <typeparamref name="TAuthority"/> method and get the transformed result.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResult">The <see cref="Type"/> returned by the <typeparamref name="TAuthority"/>.</typeparam>
|
||||
/// <typeparam name="TApiModel">The returned <see cref="Type"/>.</typeparam>
|
||||
/// <typeparam name="TTransformer">The <see cref="ITransformer{TInput, TOutput}"/> for converting <typeparamref name="TResult"/>s to <typeparamref name="TApiModel"/>s.</typeparam>
|
||||
/// <param name="authorityInvoker">The <typeparamref name="TAuthority"/> <see cref="Func{T, TResult}"/> returning a <see cref="IQueryable{T}"/> <typeparamref name="TResult"/>.</param>
|
||||
/// <returns>A <see cref="IQueryable{T}"/> <typeparamref name="TResult"/> returned.</returns>
|
||||
IQueryable<TApiModel> InvokeTransformableQueryable<TResult, TApiModel, TTransformer>(Func<TAuthority, IQueryable<TResult>> authorityInvoker)
|
||||
where TResult : IApiTransformable<TResult, TApiModel, TTransformer>
|
||||
where TApiModel : notnull
|
||||
where TTransformer : ITransformer<TResult, TApiModel>, new();
|
||||
/// <param name="authorityInvoker">The authority invocation returning a <see cref="IQueryable{T}"/> <typeparamref name="TResult"/>.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="IQueryable{T}"/> <typeparamref name="TResult"/> returned on success or <see langword="null"/> if the requirements weren't satisfied.</returns>
|
||||
ValueTask<IQueryable<TResult>?> InvokeQueryable<TResult>(Func<TAuthority, RequirementsGated<IQueryable<TResult>>> authorityInvoker);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// A projectable <typeparamref name="TResult"/> based on an underlying <typeparamref name="TQueried"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TQueried">The <see cref="EntityId"/> model <see cref="Type"/> queried.</typeparam>
|
||||
/// <typeparam name="TResult">The transformed result <see cref="Type"/>.</typeparam>
|
||||
public sealed class Projectable<TQueried, TResult>
|
||||
where TQueried : EntityId
|
||||
where TResult : notnull
|
||||
{
|
||||
/// <summary>
|
||||
/// The underlying <see cref="IQueryable{T}"/>. Should only select one entity.
|
||||
/// </summary>
|
||||
readonly IQueryable<TQueried> query;
|
||||
|
||||
/// <summary>
|
||||
/// The selector for the <see cref="ProjectedPair{TQueried, TResult}"/> data required by the <see cref="resultMapper"/>.
|
||||
/// </summary>
|
||||
readonly Expression<Func<ProjectedPair<TQueried, TResult>, ProjectedPair<object?, TResult>>> selector;
|
||||
|
||||
/// <summary>
|
||||
/// Mapper for transforming the <see cref="ProjectedPair{TQueried, TResult}"/> <typeparamref name="TResult"/> into an <see cref="AuthorityResponse{TResult}"/>. Called with the output of <see cref="selector"/> as <see cref="ProjectedPair{TQueried, TResult}.Queried"/>.
|
||||
/// </summary>
|
||||
readonly Func<ProjectedPair<object?, TResult>?, AuthorityResponse<TResult>> resultMapper;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="CancellationToken"/> for the operation.
|
||||
/// </summary>
|
||||
readonly CancellationToken cancellationToken;
|
||||
|
||||
/// <summary>
|
||||
/// Combine a set of <see cref="Projectable{TQueried, TResult}"/>s into the resulting <see cref="AuthorityResponse{TResult}"/>s.
|
||||
/// </summary>
|
||||
/// <param name="projection">The projection used to <see cref="Resolve(Func{IQueryable{TQueried}, IQueryable{ProjectedPair{TQueried, TResult}}})"/> each of the <paramref name="inputs"/>.</param>
|
||||
/// <param name="inputs">The <see cref="Projectable{TQueried, TResult}"/>s to combine.</param>
|
||||
/// <returns>A <see cref="Dictionary{TKey, TValue}"/> of the resulting <see cref="AuthorityResponse{TResult}"/>s keyed by their <see cref="EntityId.Id"/>.</returns>
|
||||
public static async ValueTask<Dictionary<long, AuthorityResponse<TResult>>> Combine(Func<IQueryable<TQueried>, IQueryable<ProjectedPair<TQueried, TResult>>> projection, params Projectable<TQueried, TResult>[] inputs)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(projection);
|
||||
ArgumentNullException.ThrowIfNull(inputs);
|
||||
|
||||
if (inputs.Length == 0)
|
||||
return new Dictionary<long, AuthorityResponse<TResult>>();
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a <see cref="Projectable{TQueried, TResult}"/>.
|
||||
/// </summary>
|
||||
/// <param name="query">The underlying <see cref="IQueryable{T}"/>.</param>
|
||||
/// <param name="resultMapper">A mapper for taking a new <see cref="ProjectedPair{TQueried, TResult}"/> (Who's <see cref="ProjectedPair{TQueried, TResult}.Queried"/> will be <see langword="null"/>) with its <see cref="ProjectedPair{TQueried, TResult}.Result"/> set amd converting it into an <see cref="AuthorityResponse{TResult}"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operations on <paramref name="query"/>.</param>
|
||||
/// <returns>A new <see cref="Projectable{TQueried, TResult}"/>.</returns>
|
||||
public static Projectable<TQueried, TResult> Create(
|
||||
IQueryable<TQueried> query,
|
||||
Func<ProjectedPair<object?, TResult>?, AuthorityResponse<TResult>> resultMapper,
|
||||
CancellationToken cancellationToken)
|
||||
=> Create(
|
||||
query,
|
||||
projected => new ProjectedPair<object?, TResult>
|
||||
{
|
||||
Queried = null,
|
||||
Result = projected.Result,
|
||||
},
|
||||
resultMapper,
|
||||
cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Create a <see cref="Projectable{TQueried, TResult}"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TSelection">A <see cref="Type"/> selected out of <typeparamref name="TQueried"/> to be used in <paramref name="resultMapper"/>.</typeparam>
|
||||
/// <param name="query">The underlying <see cref="IQueryable{T}"/>.</param>
|
||||
/// <param name="selector">Selects a value to be set as the <see cref="ProjectedPair{TQueried, TResult}.Queried"/> value for later use in <paramref name="resultMapper"/>.</param>
|
||||
/// <param name="resultMapper">A mapper for taking a new <see cref="ProjectedPair{TQueried, TResult}"/> (Who's <see cref="ProjectedPair{TQueried, TResult}.Queried"/> will be the result of <paramref name="selector"/> on the original <paramref name="query"/>.) with its <see cref="ProjectedPair{TQueried, TResult}.Result"/> set amd converting it into an <see cref="AuthorityResponse{TResult}"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operations on <paramref name="query"/>.</param>
|
||||
/// <returns>A new <see cref="Projectable{TQueried, TResult}"/>.</returns>
|
||||
public static Projectable<TQueried, TResult> Create<TSelection>(
|
||||
IQueryable<TQueried> query,
|
||||
Expression<Func<ProjectedPair<TQueried, TResult>, ProjectedPair<TSelection, TResult>>> selector,
|
||||
Func<ProjectedPair<TSelection, TResult>?, AuthorityResponse<TResult>> resultMapper,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
Expression<Func<ProjectedPair<TSelection, TResult>, ProjectedPair<object?, TResult>>> makeProjectedGeneric = projected => new ProjectedPair<object?, TResult>
|
||||
{
|
||||
Queried = projected.Queried,
|
||||
Result = projected.Result,
|
||||
};
|
||||
|
||||
var parameter = Expression.Parameter(typeof(ProjectedPair<TQueried, TResult>), "innerProjectedParam");
|
||||
return new(
|
||||
query,
|
||||
Expression.Lambda<Func<ProjectedPair<TQueried, TResult>, ProjectedPair<object?, TResult>>>(
|
||||
Expression.Invoke(
|
||||
makeProjectedGeneric,
|
||||
Expression.Invoke(
|
||||
selector,
|
||||
parameter)),
|
||||
parameter),
|
||||
projected => resultMapper(
|
||||
projected != null
|
||||
? new ProjectedPair<TSelection, TResult>
|
||||
{
|
||||
Queried = (TSelection)projected.Queried!,
|
||||
Result = projected.Result,
|
||||
}
|
||||
: null),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create an <see cref="Expression{TDelegate}"/> to transform a <see cref="ProjectedPair{TQueried, TResult}"/> into a <see cref="CombinedProjection"/>.
|
||||
/// </summary>
|
||||
/// <param name="selector">The selector used for to get business data for the <see cref="resultMapper"/>.</param>
|
||||
/// <returns>A new <see cref="Expression{TDelegate}"/> transforming a <see cref="ProjectedPair{TQueried, TResult}"/> into a <see cref="CombinedProjection"/>.</returns>
|
||||
private static Expression<Func<ProjectedPair<TQueried, TResult>, CombinedProjection>> CombinedProjectionExpression(Expression<Func<ProjectedPair<TQueried, TResult>, ProjectedPair<object?, TResult>>> selector)
|
||||
{
|
||||
Expression<Func<ProjectedPair<TQueried, TResult>, long>> idSelector = projected => projected.Queried.Id!.Value;
|
||||
|
||||
var parameter = Expression.Parameter(typeof(ProjectedPair<TQueried, TResult>), "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<Func<ProjectedPair<TQueried, TResult>, CombinedProjection>>(memberInitExpr, parameter);
|
||||
|
||||
return finalExpr;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Projectable{TQueried, TResult}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="query">The value of <see cref="query"/>.</param>
|
||||
/// <param name="selector">The value of <see cref="selector"/>.</param>
|
||||
/// <param name="resultMapper">The value of <see cref="resultMapper"/>.</param>
|
||||
/// <param name="cancellationToken">The value of <see cref="cancellationToken"/>.</param>
|
||||
private Projectable(
|
||||
IQueryable<TQueried> query,
|
||||
Expression<Func<ProjectedPair<TQueried, TResult>, ProjectedPair<object?, TResult>>> selector,
|
||||
Func<ProjectedPair<object?, TResult>?, AuthorityResponse<TResult>> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolve the <see cref="Projectable{TQueried, TResult}"/>.
|
||||
/// </summary>
|
||||
/// <param name="projection">The mapping from <typeparamref name="TQueried"/> to <typeparamref name="TResult"/>.</param>
|
||||
/// <returns>The resolved <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
public async ValueTask<AuthorityResponse<TResult>> Resolve(Func<IQueryable<TQueried>, IQueryable<ProjectedPair<TQueried, TResult>>> projection)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(projection);
|
||||
var selection = await projection(query)
|
||||
.Select(selector)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
return resultMapper(selection);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DTO for selecting the <see cref="Id"/> from <typeparamref name="TQueried"/> alongside the selection <see cref="ProjectedPair{TQueried, TResult}"/>.
|
||||
/// </summary>
|
||||
private class CombinedProjection
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="EntityId.Id"/> of <typeparamref name="TQueried"/>.
|
||||
/// </summary>
|
||||
public required long Id { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The selection/<typeparamref name="TResult"/> <see cref="ProjectedPair{TQueried, TResult}"/>.
|
||||
/// </summary>
|
||||
public required ProjectedPair<object?, TResult> Projected { get; init; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
namespace Tgstation.Server.Host.Authority.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// DTO for moving database projected <see cref="object"/>s through the system.
|
||||
/// </summary>
|
||||
/// <typeparam name="TQueried">The originally queried <see cref="global::System.Type"/>.</typeparam>
|
||||
/// <typeparam name="TResult">The output DTO <see cref="global::System.Type"/>.</typeparam>
|
||||
public sealed class ProjectedPair<TQueried, TResult>
|
||||
{
|
||||
/// <summary>
|
||||
/// The originally queried <typeparamref name="TQueried"/>.
|
||||
/// </summary>
|
||||
public required TQueried Queried { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The output DTO <typeparamref name="TResult"/>.
|
||||
/// </summary>
|
||||
public required TResult Result { get; init; }
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Evaluates a set of <see cref="IAuthorizationRequirement"/>s to be checked before executing a response.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResult">The <see cref="Type"/> of object the response generates.</typeparam>
|
||||
public sealed class RequirementsGated<TResult>
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="Api.Models.EntityId.Id"/> of the relevant instance.
|
||||
/// </summary>
|
||||
public long? InstanceId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IAuthorizationRequirement"/> retrieval function. <see cref="UserSessionValidRequirement"/> is included automatically.
|
||||
/// </summary>
|
||||
readonly Func<ValueTask<IEnumerable<IAuthorizationRequirement>>> getRequirements;
|
||||
|
||||
/// <summary>
|
||||
/// The response generation function.
|
||||
/// </summary>
|
||||
readonly Func<Security.IAuthorizationService, ValueTask<TResult>> getResponse;
|
||||
|
||||
/// <summary>
|
||||
/// If the <see cref="UserSessionValidRequirement"/> should not be added.
|
||||
/// </summary>
|
||||
readonly bool doNotAddUserSessionValidRequirement;
|
||||
|
||||
/// <summary>
|
||||
/// Convert a given <paramref name="result"/> into a <see cref="RequirementsGated{TResult}"/>.
|
||||
/// </summary>
|
||||
/// <param name="result">The <typeparamref name="TResult"/> to convert.</param>
|
||||
/// <returns>A new <see cref="RequirementsGated{TResult}"/> based on <paramref name="result"/>.</returns>
|
||||
public static RequirementsGated<TResult> FromResult(TResult result)
|
||||
=> new(
|
||||
() => (IAuthorizationRequirement?)null,
|
||||
() => ValueTask.FromResult(result));
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RequirementsGated{TResult}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="getRequirement">The value of <see cref="getRequirements"/>. Resulting in a <see langword="null"/> value is eqivalent to returning an empty <see cref="IEnumerable{T}"/> of <see cref="IAuthorizationRequirement"/>s.</param>
|
||||
/// <param name="getResponse">The value of <see cref="getResponse"/>.</param>
|
||||
public RequirementsGated(
|
||||
Func<ValueTask<IAuthorizationRequirement?>> getRequirement,
|
||||
Func<ValueTask<TResult>> getResponse)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(getRequirement);
|
||||
ArgumentNullException.ThrowIfNull(getResponse);
|
||||
getRequirements = async () =>
|
||||
{
|
||||
var requirement = await getRequirement();
|
||||
if (requirement == null)
|
||||
return Enumerable.Empty<IAuthorizationRequirement>();
|
||||
|
||||
return new List<IAuthorizationRequirement>
|
||||
{
|
||||
requirement,
|
||||
};
|
||||
};
|
||||
this.getResponse = _ => getResponse();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RequirementsGated{TResult}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="getRequirements">The value of <see cref="getRequirements"/>.</param>
|
||||
/// <param name="getResponse">The value of <see cref="getResponse"/>.</param>
|
||||
public RequirementsGated(
|
||||
Func<IEnumerable<IAuthorizationRequirement>> getRequirements,
|
||||
Func<ValueTask<TResult>> getResponse)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(getRequirements);
|
||||
ArgumentNullException.ThrowIfNull(getResponse);
|
||||
this.getRequirements = () => ValueTask.FromResult(getRequirements());
|
||||
this.getResponse = _ => getResponse();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RequirementsGated{TResult}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="getRequirement">The value of <see cref="getRequirements"/>. Resulting in a <see langword="null"/> value is eqivalent to returning an empty <see cref="IEnumerable{T}"/> of <see cref="IAuthorizationRequirement"/>s.</param>
|
||||
/// <param name="getResponse">The value of <see cref="getResponse"/>.</param>
|
||||
/// <param name="instanceId">The value of <see cref="InstanceId"/>.</param>
|
||||
/// <param name="doNotAddUserSessionValidRequirement">The value of <see cref="doNotAddUserSessionValidRequirement"/>.</param>
|
||||
public RequirementsGated(
|
||||
Func<IAuthorizationRequirement?> getRequirement,
|
||||
Func<ValueTask<TResult>> 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<IAuthorizationRequirement>());
|
||||
|
||||
return ValueTask.FromResult<IEnumerable<IAuthorizationRequirement>>(
|
||||
new List<IAuthorizationRequirement>
|
||||
{
|
||||
requirement,
|
||||
});
|
||||
};
|
||||
|
||||
this.getResponse = _ => getResponse();
|
||||
|
||||
this.doNotAddUserSessionValidRequirement = doNotAddUserSessionValidRequirement;
|
||||
InstanceId = instanceId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RequirementsGated{TResult}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="getRequirement">The value of <see cref="getRequirements"/>. Resulting in a <see langword="null"/> value is eqivalent to returning an empty <see cref="IEnumerable{T}"/> of <see cref="IAuthorizationRequirement"/>s.</param>
|
||||
/// <param name="getResponse">The value of <see cref="getResponse"/>.</param>
|
||||
public RequirementsGated(
|
||||
Func<IAuthorizationRequirement?> getRequirement,
|
||||
Func<Security.IAuthorizationService, ValueTask<TResult>> getResponse)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(getRequirement);
|
||||
getRequirements = () =>
|
||||
{
|
||||
var requirement = getRequirement();
|
||||
if (requirement == null)
|
||||
return ValueTask.FromResult(Enumerable.Empty<IAuthorizationRequirement>());
|
||||
|
||||
return ValueTask.FromResult<IEnumerable<IAuthorizationRequirement>>(
|
||||
new List<IAuthorizationRequirement>
|
||||
{
|
||||
requirement,
|
||||
});
|
||||
};
|
||||
|
||||
this.getResponse = getResponse ?? throw new ArgumentNullException(nameof(getResponse));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates the <see cref="IAuthorizationRequirement"/>s of the request.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="IAuthorizationRequirement"/>s for the request.</returns>
|
||||
public async ValueTask<IEnumerable<IAuthorizationRequirement>> GetRequirements()
|
||||
{
|
||||
var requirements = await getRequirements();
|
||||
if (!doNotAddUserSessionValidRequirement)
|
||||
requirements = UserSessionValidRequirement.InstanceAsEnumerable.Concat(requirements);
|
||||
|
||||
return requirements;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes the request.
|
||||
/// </summary>
|
||||
/// <param name="authorizationService">The authorization service to use.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the request <typeparamref name="TResult"/>.</returns>
|
||||
public ValueTask<TResult> Execute(Security.IAuthorizationService authorizationService)
|
||||
=> getResponse(authorizationService);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
/// <returns>An <see cref="IActionResult"/> for the <paramref name="authorityResponse"/>.</returns>
|
||||
/// <typeparam name="TResult">The result <see cref="Type"/> returned in the <paramref name="authorityResponse"/>.</typeparam>
|
||||
/// <typeparam name="TApiModel">The REST API result model built from <paramref name="authorityResponse"/>.</typeparam>
|
||||
static IActionResult CreateSuccessfulActionResult<TResult, TApiModel>(ApiController controller, Func<TResult, TApiModel> resultTransformer, AuthorityResponse<TResult> authorityResponse)
|
||||
static IActionResult CreateSuccessfulActionResult<TResult, TApiModel>(
|
||||
ApiController controller,
|
||||
Func<TResult, TApiModel> resultTransformer,
|
||||
AuthorityResponse<TResult> authorityResponse)
|
||||
where TApiModel : notnull
|
||||
{
|
||||
if (authorityResponse.IsNoContent!.Value)
|
||||
@@ -44,9 +48,14 @@ namespace Tgstation.Server.Host.Authority.Core
|
||||
/// </summary>
|
||||
/// <param name="controller">The <see cref="ApiController"/> to use.</param>
|
||||
/// <param name="authorityResponse">The <see cref="AuthorityResponse"/>.</param>
|
||||
/// <returns>An <see cref="IActionResult"/> if the <paramref name="authorityResponse"/> is not successful, <see langword="null"/> otherwise.</returns>
|
||||
static IActionResult? CreateErroredActionResult(ApiController controller, AuthorityResponse authorityResponse)
|
||||
/// <returns>An <see cref="IActionResult"/> if the <paramref name="authorityResponse"/> is not successful, <see langword="null"/> otherwise. If <see langword="null"/> is returned, <paramref name="authorityResponse"/> is not <see langword="null"/>.</returns>
|
||||
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 <see cref="RestAuthorityInvoker{TAuthority}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="authority">The <typeparamref name="TAuthority"/>.</param>
|
||||
public RestAuthorityInvoker(TAuthority authority)
|
||||
: base(authority)
|
||||
/// <param name="authorizationService">The <see cref="IAuthorizationService"/> to use.</param>
|
||||
public RestAuthorityInvoker(TAuthority authority, IAuthorizationService authorizationService)
|
||||
: base(authority, authorizationService)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
async ValueTask<IActionResult> IRestAuthorityInvoker<TAuthority>.Invoke(ApiController controller, Func<TAuthority, ValueTask<AuthorityResponse>> authorityInvoker)
|
||||
async ValueTask<IActionResult> IRestAuthorityInvoker<TAuthority>.Invoke(ApiController controller, Func<TAuthority, RequirementsGated<AuthorityResponse>> 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();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
async ValueTask<IActionResult> IRestAuthorityInvoker<TAuthority>.Invoke<TResult, TApiModel>(ApiController controller, Func<TAuthority, ValueTask<AuthorityResponse<TResult>>> authorityInvoker)
|
||||
async ValueTask<IActionResult> IRestAuthorityInvoker<TAuthority>.Invoke<TResult, TApiModel>(ApiController controller, Func<TAuthority, RequirementsGated<AuthorityResponse<TResult>>> 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!);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
async ValueTask<IActionResult> IRestAuthorityInvoker<TAuthority>.InvokeTransformable<TResult, TApiModel>(ApiController controller, Func<TAuthority, ValueTask<AuthorityResponse<TResult>>> authorityInvoker)
|
||||
async ValueTask<IActionResult> IRestAuthorityInvoker<TAuthority>.InvokeTransformable<TResult, TApiModel>(ApiController controller, Func<TAuthority, RequirementsGated<AuthorityResponse<TResult>>> 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!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
/// </summary>
|
||||
/// <param name="forceFresh">Bypass the caching that the authority performs for this request, forcing it to contact GitHub.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="AdministrationResponse"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
[TgsAuthorize(AdministrationRights.ChangeVersion)]
|
||||
ValueTask<AuthorityResponse<AdministrationResponse>> GetUpdateInformation(bool forceFresh, CancellationToken cancellationToken);
|
||||
/// <returns>A <see cref="RequirementsGated{TResult}"/> <see cref="AdministrationResponse"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
RequirementsGated<AuthorityResponse<AdministrationResponse>> GetUpdateInformation(bool forceFresh, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Triggers a restart of tgstation-server without terminating running game instances, setting its version to a given <paramref name="targetVersion"/>.
|
||||
@@ -29,15 +25,21 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// <param name="targetVersion">The <see cref="Version"/> TGS will switch to upon reboot.</param>
|
||||
/// <param name="uploadZip">If <see langword="true"/> a <see cref="FileTicketResponse.FileTicket"/> will be returned and the call must provide an uploaded zip file containing the update data to the file transfer service.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="ServerUpdateResponse"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
[TgsAuthorize(AdministrationRights.ChangeVersion | AdministrationRights.UploadVersion)]
|
||||
ValueTask<AuthorityResponse<ServerUpdateResponse>> TriggerServerVersionChange(Version targetVersion, bool uploadZip, CancellationToken cancellationToken);
|
||||
/// <returns>A <see cref="RequirementsGated{TResult}"/> <see cref="ServerUpdateResponse"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
RequirementsGated<AuthorityResponse<ServerUpdateResponse>> TriggerServerVersionChange(Version targetVersion, bool uploadZip, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Triggers a restart of tgstation-server without terminating running game instances.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="AuthorityResponse"/>.</returns>
|
||||
[TgsAuthorize(AdministrationRights.RestartHost)]
|
||||
ValueTask<AuthorityResponse> TriggerServerRestart();
|
||||
/// <returns>A <see cref="RequirementsGated{TResult}"/> <see cref="AuthorityResponse"/>.</returns>
|
||||
RequirementsGated<AuthorityResponse> TriggerServerRestart();
|
||||
|
||||
/// <summary>
|
||||
/// Get a ticket for downloading a log file at a given <paramref name="path"/>.
|
||||
/// </summary>
|
||||
/// <param name="path">The relative path to the log file in the directory.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="RequirementsGated{TResult}"/> <see cref="LogFileResponse"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
RequirementsGated<AuthorityResponse<LogFileResponse>> GetLog(string path, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="IAuthority"/> for manipulating chat bots.
|
||||
/// </summary>
|
||||
public interface IChatAuthority : IAuthority
|
||||
{
|
||||
/// <summary>
|
||||
/// Create a new <see cref="ChatBot"/>.
|
||||
/// </summary>
|
||||
/// <param name="initialChannels">The initial <see cref="ChatChannel"/>s for the chat bot. Must have been previously validated to be compatible with the <paramref name="provider"/>.</param>
|
||||
/// <param name="name">The name of the chat bot.</param>
|
||||
/// <param name="connectionString">The connection string for the chat bot.</param>
|
||||
/// <param name="provider">The <see cref="ChatProvider"/> to use.</param>
|
||||
/// <param name="instanceId">The ID of the instance the chat bot belongs to.</param>
|
||||
/// <param name="reconnectionInterval">The interval in minutes that TGS attempts to reconnect the chat provider if it disconnects while it is enabled.</param>
|
||||
/// <param name="channelLimit">The maximum number of channels that can be created for the chat bot.</param>
|
||||
/// <param name="enabled">If the chat bot is enabled.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for operations.</param>
|
||||
/// <returns>A <see cref="RequirementsGated{TResult}"/> <see cref="AuthorityResponse{TResult}"/> for the created <see cref="ChatBot"/>.</returns>
|
||||
RequirementsGated<AuthorityResponse<ChatBot>> Create(
|
||||
IEnumerable<Models.ChatChannel> initialChannels,
|
||||
string name,
|
||||
string connectionString,
|
||||
ChatProvider provider,
|
||||
long instanceId,
|
||||
uint? reconnectionInterval,
|
||||
ushort? channelLimit,
|
||||
bool enabled,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -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 <typeparamref name="TAuthority"/>s from GraphQL endpoints.
|
||||
/// </summary>
|
||||
/// <typeparam name="TAuthority">The <see cref="IAuthority"/> invoked.</typeparam>
|
||||
/// <remarks>We take the approach that fields should be non-nullable if that is the case under ideal circumstances. Authorization issues should throw.</remarks>
|
||||
public interface IGraphQLAuthorityInvoker<TAuthority> : IAuthorityInvoker<TAuthority>
|
||||
where TAuthority : IAuthority
|
||||
{
|
||||
/// <summary>
|
||||
/// Invoke a <typeparamref name="TAuthority"/> method with no success result.
|
||||
/// </summary>
|
||||
/// <param name="authorityInvoker">The <typeparamref name="TAuthority"/> <see cref="Func{T, TResult}"/> returning a <see cref="ValueTask{TResult}"/> resulting in the <see cref="AuthorityResponse"/>.</param>
|
||||
/// <param name="authorityInvoker">The <typeparamref name="TAuthority"/> <see cref="Func{T, TResult}"/> resulting in the <see cref="RequirementsGated{TResult}"/> <see cref="AuthorityResponse"/>.</param>
|
||||
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
|
||||
ValueTask Invoke(Func<TAuthority, ValueTask<AuthorityResponse>> authorityInvoker);
|
||||
ValueTask Invoke(Func<TAuthority, RequirementsGated<AuthorityResponse>> authorityInvoker);
|
||||
|
||||
/// <summary>
|
||||
/// Invoke a <typeparamref name="TAuthority"/> method and get the result.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResult">The <see cref="AuthorityResponse{TResult}.Result"/> <see cref="Type"/>.</typeparam>
|
||||
/// <typeparam name="TApiModel">The resulting <see cref="Type"/> of the return value.</typeparam>
|
||||
/// <param name="authorityInvoker">The <typeparamref name="TAuthority"/> <see cref="Func{T, TResult}"/> returning a <see cref="ValueTask{TResult}"/> resulting in the <see cref="AuthorityResponse{TResult}"/>.</param>
|
||||
/// <param name="authorityInvoker">The <typeparamref name="TAuthority"/> <see cref="Func{T, TResult}"/> resulting in the <see cref="RequirementsGated{TResult}"/> <see cref="AuthorityResponse{TResult}"/>.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <typeparamref name="TApiModel"/> generated for the resulting <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
ValueTask<TApiModel?> InvokeAllowMissing<TResult, TApiModel>(Func<TAuthority, ValueTask<AuthorityResponse<TResult>>> authorityInvoker)
|
||||
ValueTask<TApiModel?> InvokeAllowMissing<TResult, TApiModel>(Func<TAuthority, RequirementsGated<AuthorityResponse<TResult>>> authorityInvoker)
|
||||
where TResult : TApiModel
|
||||
where TApiModel : notnull;
|
||||
|
||||
@@ -37,9 +44,9 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// <typeparam name="TResult">The <see cref="AuthorityResponse{TResult}.Result"/> <see cref="Type"/>.</typeparam>
|
||||
/// <typeparam name="TApiModel">The resulting <see cref="Type"/> of the return value.</typeparam>
|
||||
/// <typeparam name="TTransformer">The <see cref="ITransformer{TInput, TOutput}"/> for converting <typeparamref name="TResult"/>s to <typeparamref name="TApiModel"/>s.</typeparam>
|
||||
/// <param name="authorityInvoker">The <typeparamref name="TAuthority"/> <see cref="Func{T, TResult}"/> returning a <see cref="ValueTask{TResult}"/> resulting in the <see cref="AuthorityResponse{TResult}"/>.</param>
|
||||
/// <param name="authorityInvoker">The <typeparamref name="TAuthority"/> <see cref="Func{T, TResult}"/> resulting in the <see cref="RequirementsGated{TResult}"/> <see cref="AuthorityResponse{TResult}"/>.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <typeparamref name="TApiModel"/> generated for the resulting <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
ValueTask<TApiModel?> InvokeTransformableAllowMissing<TResult, TApiModel, TTransformer>(Func<TAuthority, ValueTask<AuthorityResponse<TResult>>> authorityInvoker)
|
||||
ValueTask<TApiModel?> InvokeTransformableAllowMissing<TResult, TApiModel, TTransformer>(Func<TAuthority, RequirementsGated<AuthorityResponse<TResult>>> authorityInvoker)
|
||||
where TResult : notnull, IApiTransformable<TResult, TApiModel, TTransformer>
|
||||
where TApiModel : notnull
|
||||
where TTransformer : ITransformer<TResult, TApiModel>, new();
|
||||
@@ -49,9 +56,9 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// </summary>
|
||||
/// <typeparam name="TResult">The <see cref="AuthorityResponse{TResult}.Result"/> <see cref="Type"/>.</typeparam>
|
||||
/// <typeparam name="TApiModel">The resulting <see cref="Type"/> of the return value.</typeparam>
|
||||
/// <param name="authorityInvoker">The <typeparamref name="TAuthority"/> <see cref="Func{T, TResult}"/> returning a <see cref="ValueTask{TResult}"/> resulting in the <see cref="AuthorityResponse{TResult}"/>.</param>
|
||||
/// <param name="authorityInvoker">The <typeparamref name="TAuthority"/> <see cref="Func{T, TResult}"/> resulting in the <see cref="RequirementsGated{TResult}"/> <see cref="AuthorityResponse{TResult}"/>.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <typeparamref name="TApiModel"/> generated for the resulting <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
ValueTask<TApiModel> Invoke<TResult, TApiModel>(Func<TAuthority, ValueTask<AuthorityResponse<TResult>>> authorityInvoker)
|
||||
ValueTask<TApiModel> Invoke<TResult, TApiModel>(Func<TAuthority, RequirementsGated<AuthorityResponse<TResult>>> authorityInvoker)
|
||||
where TResult : TApiModel
|
||||
where TApiModel : notnull;
|
||||
|
||||
@@ -61,11 +68,45 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// <typeparam name="TResult">The <see cref="AuthorityResponse{TResult}.Result"/> <see cref="Type"/>.</typeparam>
|
||||
/// <typeparam name="TApiModel">The resulting <see cref="Type"/> of the return value.</typeparam>
|
||||
/// <typeparam name="TTransformer">The <see cref="ITransformer{TInput, TOutput}"/> for converting <typeparamref name="TResult"/>s to <typeparamref name="TApiModel"/>s.</typeparam>
|
||||
/// <param name="authorityInvoker">The <typeparamref name="TAuthority"/> <see cref="Func{T, TResult}"/> returning a <see cref="ValueTask{TResult}"/> resulting in the <see cref="AuthorityResponse{TResult}"/>.</param>
|
||||
/// <param name="authorityInvoker">The <typeparamref name="TAuthority"/> <see cref="Func{T, TResult}"/> resulting in the <see cref="RequirementsGated{TResult}"/> <see cref="AuthorityResponse{TResult}"/>.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <typeparamref name="TApiModel"/> generated for the resulting <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
ValueTask<TApiModel> InvokeTransformable<TResult, TApiModel, TTransformer>(Func<TAuthority, ValueTask<AuthorityResponse<TResult>>> authorityInvoker)
|
||||
ValueTask<TApiModel> InvokeTransformable<TResult, TApiModel, TTransformer>(Func<TAuthority, RequirementsGated<AuthorityResponse<TResult>>> authorityInvoker)
|
||||
where TResult : notnull, IApiTransformable<TResult, TApiModel, TTransformer>
|
||||
where TApiModel : notnull
|
||||
where TTransformer : ITransformer<TResult, TApiModel>, new();
|
||||
|
||||
/// <summary>
|
||||
/// Invoke a <typeparamref name="TAuthority"/> method and get the transformed result.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResult">The <see cref="Type"/> returned by the <typeparamref name="TAuthority"/>.</typeparam>
|
||||
/// <typeparam name="TApiModel">The returned <see cref="Type"/>.</typeparam>
|
||||
/// <typeparam name="TTransformer">The <see cref="ITransformer{TInput, TOutput}"/> for converting <typeparamref name="TResult"/>s to <typeparamref name="TApiModel"/>s.</typeparam>
|
||||
/// <param name="authorityInvoker">The <typeparamref name="TAuthority"/> <see cref="Func{T, TResult}"/> returning a <see cref="IQueryable{T}"/> <typeparamref name="TResult"/>.</param>
|
||||
/// <param name="preTransformer">Optional transformer for the <see cref="IQueryable{T}"/> run once it has been acquired.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="IQueryable{T}"/> <typeparamref name="TResult"/> returned on success or <see langword="null"/> if the requirements weren't satisfied.</returns>
|
||||
ValueTask<IQueryable<TApiModel>> InvokeTransformableQueryable<TResult, TApiModel, TTransformer>(
|
||||
Func<TAuthority, RequirementsGated<IQueryable<TResult>>> authorityInvoker,
|
||||
Func<IQueryable<TResult>, IQueryable<TResult>>? preTransformer = null)
|
||||
where TResult : IApiTransformable<TResult, TApiModel, TTransformer>
|
||||
where TApiModel : notnull
|
||||
where TTransformer : ITransformer<TResult, TApiModel>, new();
|
||||
|
||||
/// <summary>
|
||||
/// Execute a batch loaded call on a given <paramref name="authorityInvoker"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResult">The queried result of the <typeparamref name="TAuthority"/>.</typeparam>
|
||||
/// <typeparam name="TApiModel">The returned <see cref="Type"/>.</typeparam>
|
||||
/// <typeparam name="TTransformer">The <see cref="ITransformer{TInput, TOutput}"/> for converting <typeparamref name="TResult"/>s to <typeparamref name="TApiModel"/>s.</typeparam>
|
||||
/// <param name="authorityInvoker">The <typeparamref name="TAuthority"/> <see cref="Func{T, TResult}"/> returning a <see cref="RequirementsGated{TResult}"/> <see cref="Projectable{TQueried, TResult}"/> from <typeparamref name="TResult"/> to <typeparamref name="TApiModel"/>.</param>
|
||||
/// <param name="ids">The list of <see cref="EntityId.Id"/>s to batch load.</param>
|
||||
/// <param name="queryContext">The active <see cref="QueryContext{TEntity}"/> mapped to an <see cref="AuthorityResponse{TResult}"/> of the <typeparamref name="TApiModel"/>. MUST have been wrapped with <see cref="Extensions.QueryContextExtensions.AuthorityResponseWrap{TResult}(QueryContext{TResult})"/>. This will be unwrapped so only a <see cref="QueryContext{TEntity}"/> of <typeparamref name="TApiModel"/> will be used.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a <see cref="Dictionary{TKey, TValue}"/> of loaded <see cref="EntityId.Id"/>s to <see cref="AuthorityResponse{TResult}"/>s for the loaded <typeparamref name="TApiModel"/>s.</returns>
|
||||
ValueTask<Dictionary<long, AuthorityResponse<TApiModel>>> ExecuteDataLoader<TResult, TApiModel, TTransformer>(
|
||||
Func<TAuthority, long, RequirementsGated<Projectable<TResult, TApiModel>>> authorityInvoker,
|
||||
IReadOnlyList<long> ids,
|
||||
QueryContext<AuthorityResponse<TApiModel>>? queryContext)
|
||||
where TResult : EntityId, IApiTransformable<TResult, TApiModel, TTransformer>
|
||||
where TApiModel : Entity
|
||||
where TTransformer : ITransformer<TResult, TApiModel>, new();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,13 +16,13 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a <see cref="LoginResult"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
ValueTask<AuthorityResponse<LoginResult>> AttemptLogin(CancellationToken cancellationToken);
|
||||
RequirementsGated<AuthorityResponse<LoginResult>> AttemptLogin(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Attempt to login to an OAuth service with the current OAuth credentials.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in an <see cref="OAuthGatewayLoginResult"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
ValueTask<AuthorityResponse<OAuthGatewayLoginResult>> AttemptOAuthGatewayLogin(CancellationToken cancellationToken);
|
||||
RequirementsGated<AuthorityResponse<OAuthGatewayLoginResult>> AttemptOAuthGatewayLogin(CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
/// <param name="lookupType">The <see cref="PermissionSetLookupType"/> of <paramref name="id"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a <see cref="PermissionSet"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
[TgsAuthorize(AdministrationRights.ReadUsers)]
|
||||
ValueTask<AuthorityResponse<PermissionSet>> GetId(long id, PermissionSetLookupType lookupType, CancellationToken cancellationToken);
|
||||
RequirementsGated<AuthorityResponse<PermissionSet>> GetId(long id, PermissionSetLookupType lookupType, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,9 +20,9 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// Invoke a <typeparamref name="TAuthority"/> method with no success result.
|
||||
/// </summary>
|
||||
/// <param name="controller">The <see cref="ApiController"/> invoking the <typeparamref name="TAuthority"/>.</param>
|
||||
/// <param name="authorityInvoker">The <typeparamref name="TAuthority"/> <see cref="Func{T, TResult}"/> returning a <see cref="ValueTask{TResult}"/> resulting in the <see cref="AuthorityResponse"/>.</param>
|
||||
/// <param name="authorityInvoker">The <typeparamref name="TAuthority"/> <see cref="Func{T, TResult}"/> resulting in the <see cref="RequirementsGated{TResult}"/> <see cref="AuthorityResponse"/>.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="IActionResult"/> generated for the resulting <see cref="AuthorityResponse"/>.</returns>
|
||||
ValueTask<IActionResult> Invoke(ApiController controller, Func<TAuthority, ValueTask<AuthorityResponse>> authorityInvoker);
|
||||
ValueTask<IActionResult> Invoke(ApiController controller, Func<TAuthority, RequirementsGated<AuthorityResponse>> authorityInvoker);
|
||||
|
||||
/// <summary>
|
||||
/// Invoke a <typeparamref name="TAuthority"/> method and get the result.
|
||||
@@ -30,9 +30,9 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// <typeparam name="TResult">The <see cref="AuthorityResponse{TResult}.Result"/> <see cref="Type"/>.</typeparam>
|
||||
/// <typeparam name="TApiModel">The resulting <see cref="Type"/> of the <see cref="IActionResult"/>.</typeparam>
|
||||
/// <param name="controller">The <see cref="ApiController"/> invoking the <typeparamref name="TAuthority"/>.</param>
|
||||
/// <param name="authorityInvoker">The <typeparamref name="TAuthority"/> <see cref="Func{T, TResult}"/> returning a <see cref="ValueTask{TResult}"/> resulting in the <see cref="AuthorityResponse{TResult}"/>.</param>
|
||||
/// <param name="authorityInvoker">The <typeparamref name="TAuthority"/> <see cref="Func{T, TResult}"/> resulting in the <see cref="RequirementsGated{TResult}"/> <see cref="AuthorityResponse{TResult}"/>.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="IActionResult"/> generated for the resulting <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
ValueTask<IActionResult> Invoke<TResult, TApiModel>(ApiController controller, Func<TAuthority, ValueTask<AuthorityResponse<TResult>>> authorityInvoker)
|
||||
ValueTask<IActionResult> Invoke<TResult, TApiModel>(ApiController controller, Func<TAuthority, RequirementsGated<AuthorityResponse<TResult>>> authorityInvoker)
|
||||
where TResult : TApiModel
|
||||
where TApiModel : notnull;
|
||||
|
||||
@@ -42,9 +42,9 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// <typeparam name="TResult">The <see cref="AuthorityResponse{TResult}.Result"/> <see cref="Type"/>.</typeparam>
|
||||
/// <typeparam name="TApiModel">The returned REST <see cref="Type"/>.</typeparam>
|
||||
/// <param name="controller">The <see cref="ApiController"/> invoking the <typeparamref name="TAuthority"/>.</param>
|
||||
/// <param name="authorityInvoker">The <typeparamref name="TAuthority"/> <see cref="Func{T, TResult}"/> returning a <see cref="ValueTask{TResult}"/> resulting in the <see cref="AuthorityResponse{TResult}"/>.</param>
|
||||
/// <param name="authorityInvoker">The <typeparamref name="TAuthority"/> <see cref="Func{T, TResult}"/> resulting in the <see cref="RequirementsGated{TResult}"/> <see cref="AuthorityResponse{TResult}"/>.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="IActionResult"/> generated for the resulting <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
ValueTask<IActionResult> InvokeTransformable<TResult, TApiModel>(ApiController controller, Func<TAuthority, ValueTask<AuthorityResponse<TResult>>> authorityInvoker)
|
||||
ValueTask<IActionResult> InvokeTransformable<TResult, TApiModel>(ApiController controller, Func<TAuthority, RequirementsGated<AuthorityResponse<TResult>>> authorityInvoker)
|
||||
where TResult : notnull, ILegacyApiTransformable<TApiModel>
|
||||
where TApiModel : notnull;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a <see cref="User"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
[TgsAuthorize]
|
||||
ValueTask<AuthorityResponse<User>> Read(CancellationToken cancellationToken);
|
||||
/// <returns>A <see cref="RequirementsGated{TResult}"/> <see cref="User"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
RequirementsGated<AuthorityResponse<User>> Read(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="User"/> with a given <paramref name="id"/>.
|
||||
@@ -31,33 +27,42 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// <param name="includeJoins">If related entities should be loaded.</param>
|
||||
/// <param name="allowSystemUser">If the <see cref="User.TgsSystemUserName"/> may be returned.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a <see cref="User"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
[TgsAuthorize(AdministrationRights.ReadUsers)]
|
||||
ValueTask<AuthorityResponse<User>> GetId(long id, bool includeJoins, bool allowSystemUser, CancellationToken cancellationToken);
|
||||
/// <returns>A <see cref="RequirementsGated{TResult}"/> <see cref="User"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
RequirementsGated<AuthorityResponse<User>> GetId(long id, bool includeJoins, bool allowSystemUser, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="User"/> with a given <paramref name="id"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResult">The result type after projection.</typeparam>
|
||||
/// <param name="id">The <see cref="EntityId.Id"/> of the <see cref="User"/>.</param>
|
||||
/// <param name="allowSystemUser">If the <see cref="User.TgsSystemUserName"/> may be returned.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="RequirementsGated{TResult}"/> <see cref="Projectable{TQueried, TResult}"/> <see cref="User"/> for <typeparamref name="TResult"/>.</returns>
|
||||
RequirementsGated<Projectable<User, TResult>> GetId<TResult>(long id, bool allowSystemUser, CancellationToken cancellationToken)
|
||||
where TResult : class;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="GraphQL.Types.OAuth.OAuthConnection"/>s for the <see cref="User"/> with a given <paramref name="userId"/>.
|
||||
/// </summary>
|
||||
/// <param name="userId">The <see cref="EntityId.Id"/> of the <see cref="User"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in an <see cref="global::System.Array"/> of <see cref="GraphQL.Types.OAuth.OAuthConnection"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
ValueTask<AuthorityResponse<GraphQL.Types.OAuth.OAuthConnection[]>> OAuthConnections(long userId, CancellationToken cancellationToken);
|
||||
/// <returns>A <see cref="RequirementsGated{TResult}"/> <see cref="global::System.Array"/> of <see cref="GraphQL.Types.OAuth.OAuthConnection"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
RequirementsGated<AuthorityResponse<GraphQL.Types.OAuth.OAuthConnection[]>> OAuthConnections(long userId, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="GraphQL.Types.OAuth.OidcConnection"/>s for the <see cref="User"/> with a given <paramref name="userId"/>.
|
||||
/// </summary>
|
||||
/// <param name="userId">The <see cref="EntityId.Id"/> of the <see cref="User"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in an <see cref="global::System.Array"/> of <see cref="GraphQL.Types.OAuth.OidcConnection"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
ValueTask<AuthorityResponse<GraphQL.Types.OAuth.OidcConnection[]>> OidcConnections(long userId, CancellationToken cancellationToken);
|
||||
/// <returns>A <see cref="RequirementsGated{TResult}"/> <see cref="global::System.Array"/> of <see cref="GraphQL.Types.OAuth.OidcConnection"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
RequirementsGated<AuthorityResponse<GraphQL.Types.OAuth.OidcConnection[]>> OidcConnections(long userId, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all registered <see cref="User"/>s.
|
||||
/// </summary>
|
||||
/// <param name="includeJoins">If related entities should be loaded.</param>
|
||||
/// <returns>A <see cref="IQueryable{T}"/> of <see cref="User"/>s.</returns>
|
||||
[TgsAuthorize(AdministrationRights.ReadUsers)]
|
||||
IQueryable<User> Queryable(bool includeJoins);
|
||||
/// <returns>A <see cref="RequirementsGated{TResult}"/> <see cref="IQueryable{T}"/> of <see cref="User"/>s.</returns>
|
||||
RequirementsGated<IQueryable<User>> Queryable(bool includeJoins);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="User"/>.
|
||||
@@ -65,9 +70,8 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// <param name="createRequest">The <see cref="UserCreateRequest"/>.</param>
|
||||
/// <param name="needZeroLengthPasswordWithOAuthConnections">If a zero-length <see cref="UserUpdateRequest.Password"/> indicates and OAuth only user.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in am <see cref="AuthorityResponse{TResult}"/> for the created <see cref="User"/>.</returns>
|
||||
[TgsAuthorize(AdministrationRights.WriteUsers)]
|
||||
ValueTask<AuthorityResponse<User>> Create(
|
||||
/// <returns>A <see cref="RequirementsGated{TResult}"/> <see cref="AuthorityResponse{TResult}"/> for the created <see cref="UpdatedUser"/>.</returns>
|
||||
RequirementsGated<AuthorityResponse<UpdatedUser>> Create(
|
||||
UserCreateRequest createRequest,
|
||||
bool? needZeroLengthPasswordWithOAuthConnections,
|
||||
CancellationToken cancellationToken);
|
||||
@@ -77,8 +81,7 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// </summary>
|
||||
/// <param name="updateRequest">The <see cref="UserUpdateRequest"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in am <see cref="AuthorityResponse{TResult}"/> for the created <see cref="User"/>.</returns>
|
||||
[TgsAuthorize(AdministrationRights.WriteUsers | AdministrationRights.EditOwnPassword | AdministrationRights.EditOwnServiceConnections)]
|
||||
ValueTask<AuthorityResponse<User>> Update(UserUpdateRequest updateRequest, CancellationToken cancellationToken);
|
||||
/// <returns>A <see cref="RequirementsGated{TResult}"/> <see cref="AuthorityResponse{TResult}"/> for the created <see cref="UpdatedUser"/>.</returns>
|
||||
RequirementsGated<AuthorityResponse<UpdatedUser>> Update(UserUpdateRequest updateRequest, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,8 +17,9 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// <summary>
|
||||
/// Gets the current <see cref="UserGroup"/>.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a <see cref="UserGroup"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
ValueTask<AuthorityResponse<UserGroup>> Read();
|
||||
RequirementsGated<AuthorityResponse<UserGroup>> Read(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="UserGroup"/> with a given <paramref name="id"/>.
|
||||
@@ -26,17 +27,17 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// <param name="id">The <see cref="Api.Models.EntityId.Id"/> of the <see cref="UserGroup"/>.</param>
|
||||
/// <param name="includeJoins">If related entities should be loaded.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a <see cref="User"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
/// <returns>A <see cref="RequirementsGated{TResult}"/> <see cref="User"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
[TgsAuthorize(AdministrationRights.ReadUsers)]
|
||||
ValueTask<AuthorityResponse<UserGroup>> GetId(long id, bool includeJoins, CancellationToken cancellationToken);
|
||||
RequirementsGated<AuthorityResponse<UserGroup>> GetId(long id, bool includeJoins, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all registered <see cref="UserGroup"/>s.
|
||||
/// </summary>
|
||||
/// <param name="includeJoins">If related entities should be loaded.</param>
|
||||
/// <returns>A <see cref="IQueryable{T}"/> of <see cref="UserGroup"/>s.</returns>
|
||||
/// <returns>A <see cref="RequirementsGated{TResult}"/> <see cref="IQueryable{T}"/> of <see cref="UserGroup"/>s.</returns>
|
||||
[TgsAuthorize(AdministrationRights.ReadUsers)]
|
||||
IQueryable<UserGroup> Queryable(bool includeJoins);
|
||||
RequirementsGated<IQueryable<UserGroup>> Queryable(bool includeJoins);
|
||||
|
||||
/// <summary>
|
||||
/// Create a <see cref="UserGroup"/>.
|
||||
@@ -44,9 +45,9 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// <param name="name">The created <see cref="UserGroup"/>'s <see cref="Api.Models.NamedEntity.Name"/>.</param>
|
||||
/// <param name="permissionSet">The created <see cref="UserGroup"/>'s <see cref="UserGroup.PermissionSet"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a <see cref="UserGroup"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
/// <returns>A <see cref="RequirementsGated{TResult}"/> <see cref="UserGroup"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
[TgsAuthorize(AdministrationRights.WriteUsers)]
|
||||
ValueTask<AuthorityResponse<UserGroup>> Create(string name, PermissionSet? permissionSet, CancellationToken cancellationToken);
|
||||
RequirementsGated<AuthorityResponse<UserGroup>> Create(string name, PermissionSet? permissionSet, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Updates a <see cref="UserGroup"/>.
|
||||
@@ -55,17 +56,17 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// <param name="newName">The optional new <see cref="Api.Models.NamedEntity.Name"/> for the <see cref="UserGroup"/>.</param>
|
||||
/// <param name="newPermissionSet">The optional new <see cref="UserGroup.PermissionSet"/> for the <see cref="UserGroup"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a <see cref="UserGroup"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a <see cref="RequirementsGated{TResult}"/> <see cref="UserGroup"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
[TgsAuthorize(AdministrationRights.WriteUsers)]
|
||||
ValueTask<AuthorityResponse<UserGroup>> Update(long id, string? newName, PermissionSet? newPermissionSet, CancellationToken cancellationToken);
|
||||
RequirementsGated<AuthorityResponse<UserGroup>> Update(long id, string? newName, PermissionSet? newPermissionSet, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes an empty <see cref="UserGroup"/>.
|
||||
/// </summary>
|
||||
/// <param name="id">The <see cref="Api.Models.EntityId.Id"/> of the <see cref="UserGroup"/> to delete.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
|
||||
/// <returns>A <see cref="RequirementsGated{TResult}"/> <see cref="AuthorityResponse"/> representing the running operation.</returns>
|
||||
[TgsAuthorize(AdministrationRights.WriteUsers)]
|
||||
ValueTask<AuthorityResponse> DeleteEmpty(long id, CancellationToken cancellationToken);
|
||||
RequirementsGated<AuthorityResponse> DeleteEmpty(long id, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="SecurityConfiguration"/> for the <see cref="LoginAuthority"/>.
|
||||
/// The <see cref="IOptionsSnapshot{TOptions}"/> containing the <see cref="SecurityConfiguration"/> for the <see cref="LoginAuthority"/>.
|
||||
/// </summary>
|
||||
readonly SecurityConfiguration securityConfiguration;
|
||||
readonly IOptionsSnapshot<SecurityConfiguration> securityConfigurationOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Generate an <see cref="AuthorityResponse{TResult}"/> for a given <paramref name="headersException"/>.
|
||||
@@ -103,7 +104,6 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LoginAuthority"/> class.
|
||||
/// </summary>
|
||||
/// <param name="authenticationContext">The <see cref="IAuthenticationContext"/> to use.</param>
|
||||
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> to use.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> to use.</param>
|
||||
/// <param name="apiHeadersProvider">The value of <see cref="apiHeadersProvider"/>.</param>
|
||||
@@ -113,9 +113,8 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// <param name="cryptographySuite">The value of <see cref="cryptographySuite"/>.</param>
|
||||
/// <param name="identityCache">The value of <see cref="identityCache"/>.</param>
|
||||
/// <param name="sessionInvalidationTracker">The value of <see cref="sessionInvalidationTracker"/>.</param>
|
||||
/// <param name="securityConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="securityConfiguration"/>.</param>
|
||||
/// <param name="securityConfigurationOptions">The value of <see cref="securityConfigurationOptions"/>.</param>
|
||||
public LoginAuthority(
|
||||
IAuthenticationContext authenticationContext,
|
||||
IDatabaseContext databaseContext,
|
||||
ILogger<LoginAuthority> logger,
|
||||
IApiHeadersProvider apiHeadersProvider,
|
||||
@@ -125,9 +124,8 @@ namespace Tgstation.Server.Host.Authority
|
||||
ICryptographySuite cryptographySuite,
|
||||
IIdentityCache identityCache,
|
||||
ISessionInvalidationTracker sessionInvalidationTracker,
|
||||
IOptions<SecurityConfiguration> securityConfigurationOptions)
|
||||
IOptionsSnapshot<SecurityConfiguration> 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));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<AuthorityResponse<LoginResult>> AttemptLogin(CancellationToken cancellationToken)
|
||||
public RequirementsGated<AuthorityResponse<LoginResult>> AttemptLogin(CancellationToken cancellationToken)
|
||||
=> new(
|
||||
() => null,
|
||||
() => AttemptLoginImpl(cancellationToken),
|
||||
doNotAddUserSessionValidRequirement: true);
|
||||
|
||||
/// <inheritdoc />
|
||||
public RequirementsGated<AuthorityResponse<OAuthGatewayLoginResult>> AttemptOAuthGatewayLogin(CancellationToken cancellationToken)
|
||||
=> new(
|
||||
() => (IAuthorizationRequirement?)null,
|
||||
async () =>
|
||||
{
|
||||
var headers = apiHeadersProvider.ApiHeaders;
|
||||
if (headers == null)
|
||||
return GenerateHeadersExceptionResponse<OAuthGatewayLoginResult>(apiHeadersProvider.HeadersException!);
|
||||
|
||||
var oAuthProvider = headers.OAuthProvider;
|
||||
if (!oAuthProvider.HasValue)
|
||||
return BadRequest<OAuthGatewayLoginResult>(ErrorCode.BadHeaders);
|
||||
|
||||
var (errorResponse, oAuthResult) = await TryOAuthenticate<OAuthGatewayLoginResult>(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,
|
||||
});
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// Login process.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="AuthorityResponse{TResult}"/> for the <see cref="LoginResult"/>.</returns>
|
||||
private async ValueTask<AuthorityResponse<LoginResult>> AttemptLoginImpl(CancellationToken cancellationToken)
|
||||
{
|
||||
// password and oauth logins disabled
|
||||
if (securityConfiguration.OidcStrictMode)
|
||||
if (securityConfigurationOptions.Value.OidcStrictMode)
|
||||
return Unauthorized<LoginResult>();
|
||||
|
||||
var headers = apiHeadersProvider.ApiHeaders;
|
||||
@@ -172,7 +208,7 @@ namespace Tgstation.Server.Host.Authority
|
||||
using (systemIdentity)
|
||||
{
|
||||
// Get the user from the database
|
||||
IQueryable<User> query = DatabaseContext.Users.AsQueryable();
|
||||
IQueryable<User> query = DatabaseContext.Users;
|
||||
if (oAuthLogin)
|
||||
{
|
||||
var oAuthProvider = headers.OAuthProvider!.Value;
|
||||
@@ -278,30 +314,6 @@ namespace Tgstation.Server.Host.Authority
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<AuthorityResponse<OAuthGatewayLoginResult>> AttemptOAuthGatewayLogin(CancellationToken cancellationToken)
|
||||
{
|
||||
var headers = apiHeadersProvider.ApiHeaders;
|
||||
if (headers == null)
|
||||
return GenerateHeadersExceptionResponse<OAuthGatewayLoginResult>(apiHeadersProvider.HeadersException!);
|
||||
|
||||
var oAuthProvider = headers.OAuthProvider;
|
||||
if (!oAuthProvider.HasValue)
|
||||
return BadRequest<OAuthGatewayLoginResult>(ErrorCode.BadHeaders);
|
||||
|
||||
var (errorResponse, oAuthResult) = await TryOAuthenticate<OAuthGatewayLoginResult>(headers, oAuthProvider.Value, false, cancellationToken);
|
||||
if (errorResponse != null)
|
||||
return errorResponse;
|
||||
|
||||
Logger.LogDebug("Generated {provider} OAuth AccessCode", oAuthProvider.Value);
|
||||
|
||||
return new AuthorityResponse<OAuthGatewayLoginResult>(
|
||||
new OAuthGatewayLoginResult
|
||||
{
|
||||
AccessCode = oAuthResult!.Value.AccessCode,
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a given <paramref name="systemIdentity"/> to the <see cref="identityCache"/>.
|
||||
/// </summary>
|
||||
|
||||
@@ -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
|
||||
/// </summary>
|
||||
readonly IPermissionSetsDataLoader permissionSetsDataLoader;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IClaimsPrincipalAccessor"/> for the <see cref="PermissionSetAuthority"/>.
|
||||
/// </summary>
|
||||
readonly IClaimsPrincipalAccessor claimsPrincipalAccessor;
|
||||
|
||||
/// <summary>
|
||||
/// Implements <see cref="permissionSetsDataLoader"/>.
|
||||
/// </summary>
|
||||
@@ -84,34 +90,58 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PermissionSetAuthority"/> class.
|
||||
/// </summary>
|
||||
/// <param name="authenticationContext">The <see cref="IAuthenticationContext"/> to use.</param>
|
||||
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> to use.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> to use.</param>
|
||||
/// <param name="permissionSetsDataLoader">The value of <see cref="permissionSetsDataLoader"/>.</param>
|
||||
/// <param name="claimsPrincipalAccessor">The value of <see cref="claimsPrincipalAccessor"/>.</param>
|
||||
public PermissionSetAuthority(
|
||||
IAuthenticationContext authenticationContext,
|
||||
IDatabaseContext databaseContext,
|
||||
ILogger<AuthorityBase> 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));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<AuthorityResponse<PermissionSet>> GetId(long id, PermissionSetLookupType lookupType, CancellationToken cancellationToken)
|
||||
public RequirementsGated<AuthorityResponse<PermissionSet>> GetId(long id, PermissionSetLookupType lookupType, CancellationToken cancellationToken)
|
||||
{
|
||||
if (id != AuthenticationContext.PermissionSet.Id && !((AdministrationRights)AuthenticationContext.GetRight(RightsType.Administration)).HasFlag(AdministrationRights.ReadUsers))
|
||||
return Forbid<PermissionSet>();
|
||||
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<PermissionSet>();
|
||||
var groupIdQuery = DatabaseContext
|
||||
.Users
|
||||
.Where(user => user.Id == userId)
|
||||
.Select(user => user.GroupId);
|
||||
|
||||
return new AuthorityResponse<PermissionSet>(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<PermissionSet>();
|
||||
|
||||
return new AuthorityResponse<PermissionSet>(permissionSet);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
/// </summary>
|
||||
readonly ITopicEventSender topicEventSender;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IClaimsPrincipalAccessor"/> for the <see cref="UserAuthority"/>.
|
||||
/// </summary>
|
||||
readonly IClaimsPrincipalAccessor claimsPrincipalAccessor;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IOptionsSnapshot{TOptions}"/> of <see cref="GeneralConfiguration"/> for the <see cref="UserAuthority"/>.
|
||||
/// </summary>
|
||||
readonly IOptionsSnapshot<GeneralConfiguration> generalConfigurationOptions;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IOptions{TOptions}"/> of <see cref="SecurityConfiguration"/> for the <see cref="UserAuthority"/>.
|
||||
/// The <see cref="IOptionsSnapshot{TOptions}"/> of <see cref="SecurityConfiguration"/> for the <see cref="UserAuthority"/>.
|
||||
/// </summary>
|
||||
readonly IOptions<SecurityConfiguration> securityConfigurationOptions;
|
||||
readonly IOptionsSnapshot<SecurityConfiguration> securityConfigurationOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Implements the <see cref="usersDataLoader"/>.
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -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!,
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -164,22 +177,21 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// <param name="model">The <see cref="UserUpdateRequest"/> to check.</param>
|
||||
/// <param name="newUser">If this is a new <see cref="User"/>.</param>
|
||||
/// <returns><see langword="null"/> if <paramref name="model"/> is valid, an <see cref="AuthorityResponse{TResult}"/> errored otherwise.</returns>
|
||||
static AuthorityResponse<User>? CheckValidName(UserUpdateRequest model, bool newUser)
|
||||
static AuthorityResponse<UpdatedUser>? 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<User>(ErrorCode.UserMissingName);
|
||||
return BadRequest<UpdatedUser>(ErrorCode.UserMissingName);
|
||||
|
||||
model.Name = model.Name?.Trim();
|
||||
if (model.Name != null && model.Name.Contains(':', StringComparison.InvariantCulture))
|
||||
return BadRequest<User>(ErrorCode.UserColonInName);
|
||||
return BadRequest<UpdatedUser>(ErrorCode.UserColonInName);
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UserAuthority"/> class.
|
||||
/// </summary>
|
||||
/// <param name="authenticationContext">The <see cref="IAuthenticationContext"/> to use.</param>
|
||||
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> to use.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> to use.</param>
|
||||
/// <param name="usersDataLoader">The value of <see cref="usersDataLoader"/>.</param>
|
||||
@@ -190,10 +202,10 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// <param name="cryptographySuite">The value of <see cref="cryptographySuite"/>.</param>
|
||||
/// <param name="sessionInvalidationTracker">The value of <see cref="sessionInvalidationTracker"/>.</param>
|
||||
/// <param name="topicEventSender">The value of <see cref="topicEventSender"/>.</param>
|
||||
/// <param name="claimsPrincipalAccessor">The value of <see cref="claimsPrincipalAccessor"/>.</param>
|
||||
/// <param name="generalConfigurationOptions">The value of <see cref="generalConfigurationOptions"/>.</param>
|
||||
/// <param name="securityConfigurationOptions">The value of <see cref="securityConfigurationOptions"/>.</param>
|
||||
public UserAuthority(
|
||||
IAuthenticationContext authenticationContext,
|
||||
IDatabaseContext databaseContext,
|
||||
ILogger<UserAuthority> logger,
|
||||
IUsersDataLoader usersDataLoader,
|
||||
@@ -204,10 +216,10 @@ namespace Tgstation.Server.Host.Authority
|
||||
ICryptographySuite cryptographySuite,
|
||||
ISessionInvalidationTracker sessionInvalidationTracker,
|
||||
ITopicEventSender topicEventSender,
|
||||
IClaimsPrincipalAccessor claimsPrincipalAccessor,
|
||||
IOptionsSnapshot<GeneralConfiguration> generalConfigurationOptions,
|
||||
IOptions<SecurityConfiguration> securityConfigurationOptions)
|
||||
IOptionsSnapshot<SecurityConfiguration> 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<User>? failResponse)
|
||||
[NotNullWhen(true)] out AuthorityResponse<UpdatedUser>? failResponse)
|
||||
{
|
||||
if (createRequest.OAuthConnections?.Any(x => x == null) == true)
|
||||
{
|
||||
failResponse = BadRequest<User>(ErrorCode.ModelValidationFailure);
|
||||
failResponse = BadRequest<UpdatedUser>(ErrorCode.ModelValidationFailure);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -247,7 +260,7 @@ namespace Tgstation.Server.Host.Authority
|
||||
if ((hasNonNullPassword && hasNonNullSystemIdentifier)
|
||||
|| (!hasNonNullPassword && !hasNonNullSystemIdentifier && !hasOAuthConnections))
|
||||
{
|
||||
failResponse = BadRequest<User>(ErrorCode.UserMismatchPasswordSid);
|
||||
failResponse = BadRequest<UpdatedUser>(ErrorCode.UserMismatchPasswordSid);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -261,20 +274,20 @@ namespace Tgstation.Server.Host.Authority
|
||||
|
||||
if (createRequest.OAuthConnections.Count == 0)
|
||||
{
|
||||
failResponse = BadRequest<User>(ErrorCode.ModelValidationFailure);
|
||||
failResponse = BadRequest<UpdatedUser>(ErrorCode.ModelValidationFailure);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if (hasZeroLengthPassword)
|
||||
{
|
||||
failResponse = BadRequest<User>(ErrorCode.ModelValidationFailure);
|
||||
failResponse = BadRequest<UpdatedUser>(ErrorCode.ModelValidationFailure);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (createRequest.Group != null && createRequest.PermissionSet != null)
|
||||
{
|
||||
failResponse = BadRequest<User>(ErrorCode.UserGroupAndPermissionSet);
|
||||
failResponse = BadRequest<UpdatedUser>(ErrorCode.UserGroupAndPermissionSet);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -284,7 +297,7 @@ namespace Tgstation.Server.Host.Authority
|
||||
|
||||
if (!(createRequest.Name == null ^ createRequest.SystemIdentifier == null))
|
||||
{
|
||||
failResponse = BadRequest<User>(ErrorCode.UserMismatchNameSid);
|
||||
failResponse = BadRequest<UpdatedUser>(ErrorCode.UserMismatchNameSid);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -293,15 +306,384 @@ namespace Tgstation.Server.Host.Authority
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask<AuthorityResponse<User>> Read(CancellationToken cancellationToken)
|
||||
=> ValueTask.FromResult(new AuthorityResponse<User>(AuthenticationContext.User));
|
||||
public RequirementsGated<AuthorityResponse<User>> Read(CancellationToken cancellationToken)
|
||||
=> new(
|
||||
() => Enumerable.Empty<IAuthorizationRequirement>(),
|
||||
() => GetIdImpl(claimsPrincipalAccessor.User.RequireTgsUserId(), true, false, cancellationToken));
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<AuthorityResponse<User>> GetId(long id, bool includeJoins, bool allowSystemUser, CancellationToken cancellationToken)
|
||||
{
|
||||
if (id != AuthenticationContext.User.Id && !((AdministrationRights)AuthenticationContext.GetRight(RightsType.Administration)).HasFlag(AdministrationRights.ReadUsers))
|
||||
return Forbid<User>();
|
||||
public RequirementsGated<AuthorityResponse<User>> GetId(long id, bool includeJoins, bool allowSystemUser, CancellationToken cancellationToken)
|
||||
=> new(
|
||||
() =>
|
||||
{
|
||||
if (id != claimsPrincipalAccessor.User.GetTgsUserId())
|
||||
return Enumerable.Empty<IAuthorizationRequirement>();
|
||||
|
||||
return new List<IAuthorizationRequirement>
|
||||
{
|
||||
Flag(AdministrationRights.ReadUsers),
|
||||
};
|
||||
},
|
||||
() => GetIdImpl(id, includeJoins, allowSystemUser, cancellationToken));
|
||||
|
||||
/// <inheritdoc />
|
||||
public RequirementsGated<IQueryable<User>> Queryable(bool includeJoins)
|
||||
=> new(
|
||||
() => Flag(AdministrationRights.ReadUsers),
|
||||
() => ValueTask.FromResult(Queryable(includeJoins, false)));
|
||||
|
||||
/// <inheritdoc />
|
||||
public RequirementsGated<AuthorityResponse<GraphQL.Types.OAuth.OAuthConnection[]>> OAuthConnections(long userId, CancellationToken cancellationToken)
|
||||
=> new(
|
||||
() => claimsPrincipalAccessor.User.GetTgsUserId() != userId
|
||||
? Flag(AdministrationRights.ReadUsers)
|
||||
: null,
|
||||
async () => new AuthorityResponse<GraphQL.Types.OAuth.OAuthConnection[]>(
|
||||
await oAuthConnectionsDataLoader.LoadRequiredAsync(userId, cancellationToken)));
|
||||
|
||||
/// <inheritdoc />
|
||||
public RequirementsGated<AuthorityResponse<GraphQL.Types.OAuth.OidcConnection[]>> OidcConnections(long userId, CancellationToken cancellationToken)
|
||||
=> new(
|
||||
() => claimsPrincipalAccessor.User.GetTgsUserId() != userId
|
||||
? Flag(AdministrationRights.ReadUsers)
|
||||
: null,
|
||||
async () => new AuthorityResponse<GraphQL.Types.OAuth.OidcConnection[]>(
|
||||
await oidcConnectionsDataLoader.LoadRequiredAsync(userId, cancellationToken)));
|
||||
|
||||
/// <inheritdoc />
|
||||
#pragma warning disable CA1506 // TODO: Decomplexify
|
||||
public RequirementsGated<AuthorityResponse<UpdatedUser>> 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<UpdatedUser>(ErrorCode.UserLimitReached);
|
||||
|
||||
var dbUser = await CreateNewUserFromModel(
|
||||
createRequest,
|
||||
cancellationToken);
|
||||
if (dbUser == null)
|
||||
return Gone<UpdatedUser>();
|
||||
|
||||
if (createRequest.SystemIdentifier != null)
|
||||
try
|
||||
{
|
||||
using var sysIdentity = await systemIdentityFactory.CreateSystemIdentity(dbUser, cancellationToken);
|
||||
if (sysIdentity == null)
|
||||
return Gone<UpdatedUser>();
|
||||
dbUser.Name = sysIdentity.Username;
|
||||
dbUser.SystemIdentifier = sysIdentity.Uid;
|
||||
}
|
||||
catch (NotImplementedException ex)
|
||||
{
|
||||
Logger.LogTrace(ex, "System identities not implemented!");
|
||||
return new AuthorityResponse<UpdatedUser>(
|
||||
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;
|
||||
});
|
||||
|
||||
/// <inheritdoc />
|
||||
#pragma warning disable CA1502
|
||||
#pragma warning disable CA1506 // TODO: Decomplexify
|
||||
public RequirementsGated<AuthorityResponse<UpdatedUser>> Update(UserUpdateRequest model, CancellationToken cancellationToken)
|
||||
#pragma warning restore CA1502
|
||||
#pragma warning restore CA1506
|
||||
=> new(
|
||||
() =>
|
||||
{
|
||||
RightsConditional<AdministrationRights>? 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<UpdatedUser>(ErrorCode.ModelValidationFailure);
|
||||
|
||||
if (model.Group != null && model.PermissionSet != null)
|
||||
return BadRequest<UpdatedUser>(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<UpdatedUser>();
|
||||
|
||||
if (originalUser.CanonicalName == User.CanonicalizeName(User.TgsSystemUserName))
|
||||
return Forbid<UpdatedUser>();
|
||||
|
||||
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<UpdatedUser>(ErrorCode.UserSidChange);
|
||||
|
||||
if (model.Password != null)
|
||||
{
|
||||
if (originalUserHasSid)
|
||||
return BadRequest<UpdatedUser>(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<UpdatedUser>(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<UpdatedUser>(ErrorCode.BadUserEditDueToOidcStrictMode);
|
||||
|
||||
if (originalUser.CanonicalName == User.CanonicalizeName(DefaultCredentials.AdminUserName))
|
||||
return BadRequest<UpdatedUser>(ErrorCode.AdminUserCannotHaveServiceConnection);
|
||||
|
||||
if (model.OAuthConnections.Count == 0 && originalUser.PasswordHash == null && originalUser.SystemIdentifier == null)
|
||||
return BadRequest<UpdatedUser>(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<UpdatedUser>(ErrorCode.BadUserEditDueToOidcStrictMode);
|
||||
|
||||
if (originalUser.CanonicalName == User.CanonicalizeName(DefaultCredentials.AdminUserName))
|
||||
return BadRequest<UpdatedUser>(ErrorCode.AdminUserCannotHaveServiceConnection);
|
||||
|
||||
if (model.OidcConnections.Count == 0 && originalUser.PasswordHash == null && originalUser.SystemIdentifier == null)
|
||||
return BadRequest<UpdatedUser>(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<UpdatedUser>(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<UpdatedUser>();
|
||||
|
||||
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<UpdatedUser>(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<UpdatedUser>(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;
|
||||
});
|
||||
|
||||
/// <inheritdoc />
|
||||
public RequirementsGated<Projectable<User, TResult>> GetId<TResult>(long id, bool allowSystemUser, CancellationToken cancellationToken)
|
||||
where TResult : class
|
||||
=> new(
|
||||
() =>
|
||||
{
|
||||
if (id != claimsPrincipalAccessor.User.GetTgsUserId())
|
||||
return Enumerable.Empty<IAuthorizationRequirement>();
|
||||
|
||||
return new List<IAuthorizationRequirement>
|
||||
{
|
||||
Flag(AdministrationRights.ReadUsers),
|
||||
};
|
||||
},
|
||||
() => ValueTask.FromResult(
|
||||
Projectable<User, TResult>.Create(
|
||||
Queryable(true, allowSystemUser)
|
||||
.Where(user => user.Id == id)
|
||||
.TagWith("User by ID"),
|
||||
projected => new ProjectedPair<string, TResult>
|
||||
{
|
||||
Queried = projected.Queried.CanonicalName!,
|
||||
Result = projected.Result,
|
||||
},
|
||||
projected =>
|
||||
{
|
||||
if (projected == default)
|
||||
return NotFound<TResult>();
|
||||
|
||||
string canonicalName = projected.Queried;
|
||||
if (!allowSystemUser && canonicalName == User.CanonicalizeName(User.TgsSystemUserName))
|
||||
return Forbid<TResult>();
|
||||
|
||||
return new AuthorityResponse<TResult>(projected.Result);
|
||||
},
|
||||
cancellationToken)));
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of retrieving a <see cref="User"/> by ID.
|
||||
/// </summary>
|
||||
/// <param name="id">The <see cref="EntityId.Id"/> of the user to retrieve.</param>
|
||||
/// <param name="includeJoins">If related entities should be loaded.</param>
|
||||
/// <param name="allowSystemUser">If the <see cref="User.TgsSystemUserName"/> may be returned.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="User"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
async ValueTask<AuthorityResponse<User>> 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>(user);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IQueryable<User> Queryable(bool includeJoins)
|
||||
=> Queryable(includeJoins, false);
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<AuthorityResponse<GraphQL.Types.OAuth.OAuthConnection[]>> OAuthConnections(long userId, CancellationToken cancellationToken)
|
||||
=> new AuthorityResponse<GraphQL.Types.OAuth.OAuthConnection[]>(
|
||||
await oAuthConnectionsDataLoader.LoadRequiredAsync(userId, cancellationToken));
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<AuthorityResponse<GraphQL.Types.OAuth.OidcConnection[]>> OidcConnections(long userId, CancellationToken cancellationToken)
|
||||
=> new AuthorityResponse<GraphQL.Types.OAuth.OidcConnection[]>(
|
||||
await oidcConnectionsDataLoader.LoadRequiredAsync(userId, cancellationToken));
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<AuthorityResponse<User>> Create(
|
||||
UserCreateRequest createRequest,
|
||||
bool? needZeroLengthPasswordWithOAuthConnections,
|
||||
CancellationToken cancellationToken)
|
||||
/// <summary>
|
||||
/// Create the <see cref="AuthorityResponse{TResult}"/> for an <see cref="UpdatedUser"/>.
|
||||
/// </summary>
|
||||
/// <param name="authorizationService">The authorization service to use.</param>
|
||||
/// <param name="user">The <see cref="User"/> for the result.</param>
|
||||
/// <param name="successResponse">The <see cref="HttpSuccessResponse"/> to use.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="UpdatedUser"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
async ValueTask<AuthorityResponse<UpdatedUser>> 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<User>(ErrorCode.UserLimitReached);
|
||||
|
||||
var dbUser = await CreateNewUserFromModel(createRequest, cancellationToken);
|
||||
if (dbUser == null)
|
||||
return Gone<User>();
|
||||
|
||||
if (createRequest.SystemIdentifier != null)
|
||||
try
|
||||
{
|
||||
using var sysIdentity = await systemIdentityFactory.CreateSystemIdentity(dbUser, cancellationToken);
|
||||
if (sysIdentity == null)
|
||||
return Gone<User>();
|
||||
dbUser.Name = sysIdentity.Username;
|
||||
dbUser.SystemIdentifier = sysIdentity.Uid;
|
||||
}
|
||||
catch (NotImplementedException ex)
|
||||
{
|
||||
Logger.LogTrace(ex, "System identities not implemented!");
|
||||
return new AuthorityResponse<User>(
|
||||
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<User>(dbUser, HttpSuccessResponse.Created);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
#pragma warning disable CA1502
|
||||
#pragma warning disable CA1506 // TODO: Decomplexify
|
||||
public async ValueTask<AuthorityResponse<User>> 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<User>(ErrorCode.ModelValidationFailure);
|
||||
|
||||
if (model.Group != null && model.PermissionSet != null)
|
||||
return BadRequest<User>(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<User>();
|
||||
|
||||
if (originalUser.CanonicalName == User.CanonicalizeName(User.TgsSystemUserName))
|
||||
return Forbid<User>();
|
||||
|
||||
// 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<User>();
|
||||
|
||||
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<User>(ErrorCode.UserSidChange);
|
||||
|
||||
if (model.Password != null)
|
||||
{
|
||||
if (originalUserHasSid)
|
||||
return BadRequest<User>(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<User>(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<User>(ErrorCode.BadUserEditDueToOidcStrictMode);
|
||||
|
||||
if (originalUser.CanonicalName == User.CanonicalizeName(DefaultCredentials.AdminUserName))
|
||||
return BadRequest<User>(ErrorCode.AdminUserCannotHaveServiceConnection);
|
||||
|
||||
if (model.OAuthConnections.Count == 0 && originalUser.PasswordHash == null && originalUser.SystemIdentifier == null)
|
||||
return BadRequest<User>(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<User>(ErrorCode.BadUserEditDueToOidcStrictMode);
|
||||
|
||||
if (originalUser.CanonicalName == User.CanonicalizeName(DefaultCredentials.AdminUserName))
|
||||
return BadRequest<User>(ErrorCode.AdminUserCannotHaveServiceConnection);
|
||||
|
||||
if (model.OidcConnections.Count == 0 && originalUser.PasswordHash == null && originalUser.SystemIdentifier == null)
|
||||
return BadRequest<User>(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<User>(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<User>();
|
||||
|
||||
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<User>(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<User>(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<User>(originalUser)
|
||||
: new AuthorityResponse<User>();
|
||||
var userId = user.Require(u => u.Id);
|
||||
var canReadBack = claimsPrincipalAccessor.User.GetTgsUserId() == userId
|
||||
|| (await authorizationService.AuthorizeAsync(
|
||||
[Flag(AdministrationRights.ReadUsers)])).Succeeded;
|
||||
|
||||
return new AuthorityResponse<UpdatedUser>(
|
||||
canReadBack
|
||||
? new UpdatedUser(user)
|
||||
: new UpdatedUser(userId),
|
||||
successResponse);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -628,9 +753,8 @@ namespace Tgstation.Server.Host.Authority
|
||||
IQueryable<User> Queryable(bool includeJoins, bool allowSystemUser)
|
||||
{
|
||||
var tgsUserCanonicalName = User.CanonicalizeName(User.TgsSystemUserName);
|
||||
var queryable = DatabaseContext
|
||||
.Users
|
||||
.AsQueryable();
|
||||
IQueryable<User> 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
|
||||
/// <param name="newPassword">The new password.</param>
|
||||
/// <param name="newUser">If this is for a new <see cref="UserResponse"/>.</param>
|
||||
/// <returns><see langword="null"/> on success, an errored <see cref="AuthorityResponse{TResult}"/> if <paramref name="newPassword"/> is too short.</returns>
|
||||
AuthorityResponse<User>? TrySetPassword(User dbUser, string newPassword, bool newUser)
|
||||
AuthorityResponse<UpdatedUser>? TrySetPassword(User dbUser, string newPassword, bool newUser)
|
||||
{
|
||||
newPassword ??= String.Empty;
|
||||
if (newPassword.Length < generalConfigurationOptions.Value.MinimumPasswordLength)
|
||||
return new AuthorityResponse<User>(
|
||||
return new AuthorityResponse<UpdatedUser>(
|
||||
new ErrorMessageResponse(ErrorCode.UserPasswordLength)
|
||||
{
|
||||
AdditionalData = $"Required password length: {generalConfigurationOptions.Value.MinimumPasswordLength}",
|
||||
|
||||
@@ -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
|
||||
/// </summary>
|
||||
readonly IUserGroupsDataLoader userGroupsDataLoader;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IClaimsPrincipalAccessor"/> for the <see cref="UserGroupAuthority"/>.
|
||||
/// </summary>
|
||||
readonly IClaimsPrincipalAccessor claimsPrincipalAccessor;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IOptionsSnapshot{TOptions}"/> of the <see cref="GeneralConfiguration"/>.
|
||||
/// </summary>
|
||||
@@ -59,62 +66,179 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UserGroupAuthority"/> class.
|
||||
/// </summary>
|
||||
/// <param name="authenticationContext">The <see cref="IAuthenticationContext"/> to use.</param>
|
||||
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> to use.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> to use.</param>
|
||||
/// <param name="claimsPrincipalAccessor">The value of <see cref="claimsPrincipalAccessor"/>.</param>
|
||||
/// <param name="userGroupsDataLoader">The value of <see cref="userGroupsDataLoader"/>.</param>
|
||||
/// <param name="generalConfigurationOptions">The value of <see cref="generalConfigurationOptions"/>.</param>
|
||||
public UserGroupAuthority(
|
||||
IAuthenticationContext authenticationContext,
|
||||
IDatabaseContext databaseContext,
|
||||
ILogger<UserGroupAuthority> logger,
|
||||
IUserGroupsDataLoader userGroupsDataLoader,
|
||||
IClaimsPrincipalAccessor claimsPrincipalAccessor,
|
||||
IOptionsSnapshot<GeneralConfiguration> 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));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<AuthorityResponse<UserGroup>> GetId(long id, bool includeJoins, CancellationToken cancellationToken)
|
||||
public RequirementsGated<AuthorityResponse<UserGroup>> 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<UserGroup>();
|
||||
|
||||
return new AuthorityResponse<UserGroup>(userGroup);
|
||||
});
|
||||
|
||||
/// <inheritdoc />
|
||||
public RequirementsGated<AuthorityResponse<UserGroup>> 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<UserGroup>();
|
||||
|
||||
return new AuthorityResponse<UserGroup>(group);
|
||||
});
|
||||
|
||||
/// <inheritdoc />
|
||||
public RequirementsGated<IQueryable<UserGroup>> Queryable(bool includeJoins)
|
||||
=> new(
|
||||
() => Flag(AdministrationRights.ReadUsers),
|
||||
() => ValueTask.FromResult(QueryableImpl(includeJoins)));
|
||||
|
||||
/// <inheritdoc />
|
||||
public RequirementsGated<AuthorityResponse<UserGroup>> Create(string name, Models.PermissionSet? permissionSet, CancellationToken cancellationToken)
|
||||
{
|
||||
if (id != AuthenticationContext.User.GroupId && !((AdministrationRights)AuthenticationContext.GetRight(RightsType.Administration)).HasFlag(AdministrationRights.ReadUsers))
|
||||
return Forbid<UserGroup>();
|
||||
ArgumentNullException.ThrowIfNull(name);
|
||||
return new(
|
||||
() => Flag(AdministrationRights.WriteUsers),
|
||||
async () =>
|
||||
{
|
||||
var totalGroups = await DatabaseContext
|
||||
.Groups
|
||||
.CountAsync(cancellationToken);
|
||||
if (totalGroups >= generalConfigurationOptions.Value.UserGroupLimit)
|
||||
return Conflict<UserGroup>(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<UserGroup>();
|
||||
var dbGroup = new UserGroup
|
||||
{
|
||||
Name = name,
|
||||
PermissionSet = modelPermissionSet,
|
||||
};
|
||||
|
||||
return new AuthorityResponse<UserGroup>(userGroup);
|
||||
DatabaseContext.Groups.Add(dbGroup);
|
||||
await DatabaseContext.Save(cancellationToken);
|
||||
Logger.LogInformation("Created new user group {groupName} ({groupId})", dbGroup.Name, dbGroup.Id);
|
||||
|
||||
return new AuthorityResponse<UserGroup>(
|
||||
dbGroup,
|
||||
HttpSuccessResponse.Created);
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask<AuthorityResponse<UserGroup>> Read()
|
||||
{
|
||||
var group = AuthenticationContext.User!.Group;
|
||||
if (group == null)
|
||||
return ValueTask.FromResult(Gone<UserGroup>());
|
||||
public RequirementsGated<AuthorityResponse<UserGroup>> 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<UserGroup>(group));
|
||||
}
|
||||
if (currentGroup == default)
|
||||
return Gone<UserGroup>();
|
||||
|
||||
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<UserGroup>(currentGroup);
|
||||
});
|
||||
|
||||
/// <inheritdoc />
|
||||
public IQueryable<UserGroup> Queryable(bool includeJoins)
|
||||
public RequirementsGated<AuthorityResponse> 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);
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// Get the <see cref="IQueryable{T}"/> <see cref="UserGroup"/>s.
|
||||
/// </summary>
|
||||
/// <param name="includeJoins">If <see cref="UserGroup.Users"/> and <see cref="UserGroup.PermissionSet"/> should be included.</param>
|
||||
/// <returns>An <see cref="IQueryable{T}"/> of <see cref="UserGroup"/>s.</returns>
|
||||
IQueryable<UserGroup> QueryableImpl(bool includeJoins)
|
||||
{
|
||||
var queryable = DatabaseContext
|
||||
.Groups
|
||||
.AsQueryable();
|
||||
IQueryable<UserGroup> queryable = DatabaseContext
|
||||
.Groups;
|
||||
|
||||
if (includeJoins)
|
||||
queryable = queryable
|
||||
@@ -123,92 +247,5 @@ namespace Tgstation.Server.Host.Authority
|
||||
|
||||
return queryable;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<AuthorityResponse<UserGroup>> 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<UserGroup>(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<UserGroup>(
|
||||
dbGroup,
|
||||
HttpSuccessResponse.Created);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<AuthorityResponse<UserGroup>> 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<UserGroup>();
|
||||
|
||||
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<UserGroup>(currentGroup);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<AuthorityResponse> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="GeneralConfiguration"/> for the <see cref="DiscordProvider"/>.
|
||||
/// The <see cref="GeneralConfiguration"/> <see cref="IOptionsMonitor{TOptions}"/> for the <see cref="DiscordProvider"/>.
|
||||
/// </summary>
|
||||
readonly GeneralConfiguration generalConfiguration;
|
||||
readonly IOptionsMonitor<GeneralConfiguration> generalConfigurationOptions;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ServiceProvider"/> containing Discord services.
|
||||
@@ -141,18 +142,18 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="Provider"/>.</param>
|
||||
/// <param name="assemblyInformationProvider">The value of <see cref="assemblyInformationProvider"/>.</param>
|
||||
/// <param name="chatBot">The <see cref="ChatBot"/> for the <see cref="Provider"/>.</param>
|
||||
/// <param name="generalConfiguration">The value of <see cref="generalConfiguration"/>.</param>
|
||||
/// <param name="generalConfigurationOptions">The value of <see cref="generalConfigurationOptions"/>.</param>
|
||||
public DiscordProvider(
|
||||
IJobManager jobManager,
|
||||
IAsyncDelayer asyncDelayer,
|
||||
ILogger<DiscordProvider> logger,
|
||||
IAssemblyInformationProvider assemblyInformationProvider,
|
||||
ChatBot chatBot,
|
||||
GeneralConfiguration generalConfiguration)
|
||||
IOptionsMonitor<GeneralConfiguration> 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<ulong>();
|
||||
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}"),
|
||||
};
|
||||
|
||||
@@ -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
|
||||
/// <summary>
|
||||
/// The <see cref="FileLoggingConfiguration"/> for the <see cref="IrcProvider"/>.
|
||||
/// </summary>
|
||||
readonly FileLoggingConfiguration loggingConfiguration;
|
||||
readonly IOptionsMonitor<FileLoggingConfiguration> loggingConfigurationOptions;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IrcFeatures"/> client.
|
||||
@@ -117,18 +118,18 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="Provider"/>.</param>
|
||||
/// <param name="assemblyInformationProvider">The <see cref="IAssemblyInformationProvider"/> to get the <see cref="IAssemblyInformationProvider.VersionString"/> from.</param>
|
||||
/// <param name="chatBot">The <see cref="Models.ChatBot"/> for the <see cref="Provider"/>.</param>
|
||||
/// <param name="loggingConfiguration">The <see cref="FileLoggingConfiguration"/> for the <see cref="Provider"/>.</param>
|
||||
/// <param name="loggingConfigurationOptions">The value of <see cref="loggingConfigurationOptions"/>.</param>
|
||||
public IrcProvider(
|
||||
IJobManager jobManager,
|
||||
IAsyncDelayer asyncDelayer,
|
||||
ILogger<IrcProvider> logger,
|
||||
IAssemblyInformationProvider assemblyInformationProvider,
|
||||
Models.ChatBot chatBot,
|
||||
FileLoggingConfiguration loggingConfiguration)
|
||||
IAssemblyInformationProvider assemblyInformationProvider,
|
||||
IOptionsMonitor<FileLoggingConfiguration> 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);
|
||||
|
||||
@@ -36,14 +36,14 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
||||
readonly ILoggerFactory loggerFactory;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="GeneralConfiguration"/> for the <see cref="ProviderFactory"/>.
|
||||
/// The <see cref="GeneralConfiguration"/> <see cref="IOptionsMonitor{TOptions}"/> for the <see cref="ProviderFactory"/>.
|
||||
/// </summary>
|
||||
readonly GeneralConfiguration generalConfiguration;
|
||||
readonly IOptionsMonitor<GeneralConfiguration> generalConfigurationOptions;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="FileLoggingConfiguration"/> for the <see cref="ProviderFactory"/>.
|
||||
/// The <see cref="FileLoggingConfiguration"/> <see cref="IOptionsMonitor{TOptions}"/> for the <see cref="ProviderFactory"/>.
|
||||
/// </summary>
|
||||
readonly FileLoggingConfiguration loggingConfiguration;
|
||||
readonly IOptionsMonitor<FileLoggingConfiguration> loggingConfigurationOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ProviderFactory"/> class.
|
||||
@@ -52,22 +52,22 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
||||
/// <param name="assemblyInformationProvider">The value of <see cref="assemblyInformationProvider"/>.</param>
|
||||
/// <param name="asyncDelayer">The value of <see cref="asyncDelayer"/>.</param>
|
||||
/// <param name="loggerFactory">The value of <see cref="loggerFactory"/>.</param>
|
||||
/// <param name="generalConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="generalConfiguration"/>.</param>
|
||||
/// <param name="loggingConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="loggingConfiguration"/>.</param>
|
||||
/// <param name="generalConfigurationOptions">The value of <see cref="generalConfigurationOptions"/>.</param>
|
||||
/// <param name="loggingConfigurationOptions">The value of <see cref="loggingConfigurationOptions"/>.</param>
|
||||
public ProviderFactory(
|
||||
IJobManager jobManager,
|
||||
IAssemblyInformationProvider assemblyInformationProvider,
|
||||
IAsyncDelayer asyncDelayer,
|
||||
ILoggerFactory loggerFactory,
|
||||
IOptions<GeneralConfiguration> generalConfigurationOptions,
|
||||
IOptions<FileLoggingConfiguration> loggingConfigurationOptions)
|
||||
IOptionsMonitor<GeneralConfiguration> generalConfigurationOptions,
|
||||
IOptionsMonitor<FileLoggingConfiguration> 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));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -80,16 +80,16 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
||||
jobManager,
|
||||
asyncDelayer,
|
||||
loggerFactory.CreateLogger<IrcProvider>(),
|
||||
assemblyInformationProvider,
|
||||
settings,
|
||||
loggingConfiguration),
|
||||
assemblyInformationProvider,
|
||||
loggingConfigurationOptions),
|
||||
ChatProvider.Discord => new DiscordProvider(
|
||||
jobManager,
|
||||
asyncDelayer,
|
||||
loggerFactory.CreateLogger<DiscordProvider>(),
|
||||
assemblyInformationProvider,
|
||||
settings,
|
||||
generalConfiguration),
|
||||
generalConfigurationOptions,
|
||||
settings),
|
||||
_ => throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid ChatProvider: {0}", settings.Provider)),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ namespace Tgstation.Server.Host.Components.Deployment
|
||||
/// </summary>
|
||||
/// <param name="dmbLock">The <see cref="DmbLock"/> to get a description of.</param>
|
||||
/// <returns>A verbose description of <paramref name="dmbLock"/>.</returns>
|
||||
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)}";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DeploymentLockManager"/> class.
|
||||
|
||||
@@ -81,7 +81,7 @@ namespace Tgstation.Server.Host.Components.Deployment
|
||||
readonly CancellationTokenSource cleanupCts;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="CancellationTokenSource"/> for <see cref="LogLockStates"/>.
|
||||
/// The <see cref="CancellationTokenSource"/> for <see cref="LogLockStatesLoop"/>.
|
||||
/// </summary>
|
||||
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());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="IDmbProvider"/> and potentially the <see cref="DeploymentLockManager"/> for a given <see cref="CompileJob"/>.
|
||||
/// </summary>
|
||||
@@ -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<string> { ioManager.ResolvePath(directory) }, true, cancellationToken);
|
||||
await eventConsumer.HandleEvent(EventType.DeploymentCleanup, new List<string> { ioManager.ResolvePath(directory) }, false, true, cancellationToken);
|
||||
await ioManager.DeleteDirectory(directory, cancellationToken);
|
||||
}
|
||||
|
||||
@@ -517,7 +526,7 @@ namespace Tgstation.Server.Host.Components.Deployment
|
||||
/// Lock all <see cref="DeploymentLockManager"/>s states.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
|
||||
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)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user