mirror of
https://github.com/tgstation/tgstation-server.git
synced 2026-08-29 16:11:05 +01:00
Merge pull request #2164 from tgstation/6.15.0 [TGSDeploy]
Release v6.15.0
This commit is contained in:
@@ -38,7 +38,7 @@ env:
|
||||
OD_MIN_COMPAT_DOTNET_VERSION: 7
|
||||
OD_DOTNET_VERSION: 9
|
||||
TGS_DOTNET_QUALITY: ga
|
||||
TGS_WEBPANEL_NODE_VERSION: 20.x
|
||||
TGS_NODE_VERSION: 20.x
|
||||
TGS_TEST_GITHUB_TOKEN: ${{ secrets.LIVE_TESTS_TOKEN }}
|
||||
PACKAGING_PRIVATE_KEY_PASSPHRASE: ${{ secrets.PACKAGING_PRIVATE_KEY_PASSPHRASE }}
|
||||
|
||||
@@ -109,6 +109,14 @@ jobs:
|
||||
dotnet-version: ${{ env.TGS_DOTNET_VERSION }}.0.x
|
||||
dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }}
|
||||
|
||||
- name: Setup Node.JS
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.TGS_NODE_VERSION }}
|
||||
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
|
||||
- name: Checkout (Branch)
|
||||
uses: actions/checkout@v4
|
||||
if: github.event_name == 'push' || github.event_name == 'schedule'
|
||||
@@ -475,7 +483,7 @@ jobs:
|
||||
- name: Setup Node.JS
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.TGS_WEBPANEL_NODE_VERSION }}
|
||||
node-version: ${{ env.TGS_NODE_VERSION }}
|
||||
|
||||
- name: Checkout (Branch)
|
||||
uses: actions/checkout@v4
|
||||
@@ -542,7 +550,7 @@ jobs:
|
||||
- name: Setup Node.JS
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.TGS_WEBPANEL_NODE_VERSION }}
|
||||
node-version: ${{ env.TGS_NODE_VERSION }}
|
||||
|
||||
- name: Checkout (Branch)
|
||||
uses: actions/checkout@v4
|
||||
@@ -624,7 +632,7 @@ jobs:
|
||||
- name: Setup Node.JS
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.TGS_WEBPANEL_NODE_VERSION }}
|
||||
node-version: ${{ env.TGS_NODE_VERSION }}
|
||||
|
||||
- name: Set TGS_TEST_DUMP_API_SPEC
|
||||
if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'Advanced' && matrix.database-type == 'SqlServer' }}
|
||||
@@ -882,7 +890,7 @@ jobs:
|
||||
- name: Setup Node.JS
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.TGS_WEBPANEL_NODE_VERSION }}
|
||||
node-version: ${{ env.TGS_NODE_VERSION }}
|
||||
|
||||
- name: Set Sqlite Connection Info
|
||||
if: ${{ matrix.database-type == 'Sqlite' }}
|
||||
@@ -1461,7 +1469,7 @@ jobs:
|
||||
- name: Setup Node.JS
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.TGS_WEBPANEL_NODE_VERSION }}
|
||||
node-version: ${{ env.TGS_NODE_VERSION }}
|
||||
|
||||
- name: Checkout (Branch)
|
||||
uses: actions/checkout@v4
|
||||
@@ -2031,7 +2039,7 @@ jobs:
|
||||
- name: Setup Node.JS
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.TGS_WEBPANEL_NODE_VERSION }}
|
||||
node-version: ${{ env.TGS_NODE_VERSION }}
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
@@ -21,6 +21,9 @@ artifacts/
|
||||
/src/Tgstation.Server.Host/wwwroot
|
||||
/src/Tgstation.Server.Host/--applicationName
|
||||
/src/Tgstation.Server.Host/ClientApp
|
||||
/src/Tgstation.Server.Host.Utils.GitLab.GraphQL/.yarn
|
||||
/src/Tgstation.Server.Host.Utils.GitLab.GraphQL/node_modules
|
||||
/src/Tgstation.Server.Host.Utils.GitLab.GraphQL/schema.graphql
|
||||
/tools/Tgstation.Server.ReleaseNotes/release_notes.md
|
||||
launchSettings.json
|
||||
release_notes.md
|
||||
@@ -29,3 +32,4 @@ changelog.yml
|
||||
*.sqlite3
|
||||
packaging/
|
||||
yarn-error.log*
|
||||
.graphqlrc.json
|
||||
|
||||
@@ -321,11 +321,19 @@ 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.
|
||||
|
||||
- `Security:OAuth:<Provider Name>`: Sets the OAuth client ID and secret for a given `<Provider Name>`. The currently supported providers are `Keycloak`, `GitHub`, `Discord`, `InvisionCommunity` and `TGForums`. 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:
|
||||
- `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`, `InvisionCommunity` and `TGForums`. 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:
|
||||
```yml
|
||||
Security:
|
||||
OAuth:
|
||||
Keycloak:
|
||||
InvisionCommunity:
|
||||
ClientId: "..."
|
||||
ClientSecret: "..."
|
||||
RedirectUrl: "..."
|
||||
@@ -336,22 +344,30 @@ Security:
|
||||
The following providers use the `RedirectUrl` setting:
|
||||
|
||||
- GitHub
|
||||
- TGForums
|
||||
- Keycloak
|
||||
- InvisionCommunity
|
||||
|
||||
The following providers use the `ServerUrl` setting:
|
||||
|
||||
- Keycloak
|
||||
- InvisionCommunity
|
||||
|
||||
Gateway auth simply allows the users to authenticate with the service using the configuration you provide and have their impersonation token passed back to the client. An example of this is using GitHub gateway auth to allow clients to enumerate pull requests without getting rate limited.
|
||||
|
||||
- `Telemetry:DisableVersionReporting`: Prevents you installation and the version you're using from being reported on the source repository's deployments list
|
||||
#### OpenID Connect Configuration
|
||||
- `Security:Oidc:<Scheme Key>`: Sets up an OpenID Connect Provider with given "Scheme Key". Sample entry:
|
||||
```yml
|
||||
Security:
|
||||
Oidc:
|
||||
Example:
|
||||
Authority: "..." # This is the root of the URL containing the "/.well-known/openid-configuration" endpoint
|
||||
ClientId: "..."
|
||||
ClientSecret: "..."
|
||||
FriendlyName: "Example Provider" # Friendly name is how this provider is displayed in UIs
|
||||
ThemeColour: "#ff0000" # (Optional) Hex color code indicating the color used to theme UI elements relating to this provider
|
||||
ThemeIconUrl: "..." # (Optional) Public URL of an image that can be used to theme UI elements relating to this provider
|
||||
UsernameClaim: "preferred_username" # (Optional) Name of claim used to set TGS usernames upon registration. By default "preferred_username" is used. Only applies when using OidcStrictMode.
|
||||
```
|
||||
|
||||
- `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.
|
||||
- `Security:OidcStrictMode`: Boolean flag that, when `true`, disables password and OAuth logins, password changes, individual permission set assignment, and enables user registration using OpenID Connect providers. The claim name `tgstation-server-group-id` is used to dictate what TGS group users are registered to.
|
||||
|
||||
### Database Configuration
|
||||
|
||||
|
||||
@@ -18,9 +18,9 @@
|
||||
<!-- Pinned: Be VERY careful about updating https://github.com/moq/moq/issues/1372 -->
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
<!-- Usage: MSTest execution -->
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="3.8.0" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="3.8.2" />
|
||||
<!-- Usage: MSTest asserts etc... -->
|
||||
<PackageReference Include="MSTest.TestFramework" Version="3.8.0" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="3.8.2" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
+7
-7
@@ -3,17 +3,17 @@
|
||||
<!-- Integration tests will ensure they match across the board -->
|
||||
<Import Project="WebpanelVersion.props" />
|
||||
<PropertyGroup>
|
||||
<TgsCoreVersion>6.14.1</TgsCoreVersion>
|
||||
<TgsConfigVersion>5.5.0</TgsConfigVersion>
|
||||
<TgsRestVersion>10.12.1</TgsRestVersion>
|
||||
<TgsGraphQLVersion>0.5.0</TgsGraphQLVersion>
|
||||
<TgsCoreVersion>6.15.0</TgsCoreVersion>
|
||||
<TgsConfigVersion>5.6.0</TgsConfigVersion>
|
||||
<TgsRestVersion>10.13.0</TgsRestVersion>
|
||||
<TgsGraphQLVersion>0.6.0</TgsGraphQLVersion>
|
||||
<TgsCommonLibraryVersion>7.0.0</TgsCommonLibraryVersion>
|
||||
<TgsApiLibraryVersion>17.0.1</TgsApiLibraryVersion>
|
||||
<TgsClientVersion>20.0.0</TgsClientVersion>
|
||||
<TgsApiLibraryVersion>18.0.0</TgsApiLibraryVersion>
|
||||
<TgsClientVersion>21.0.0</TgsClientVersion>
|
||||
<TgsDmapiVersion>7.3.1</TgsDmapiVersion>
|
||||
<TgsInteropVersion>5.10.0</TgsInteropVersion>
|
||||
<TgsHostWatchdogVersion>1.6.0</TgsHostWatchdogVersion>
|
||||
<TgsSwarmProtocolVersion>8.0.0</TgsSwarmProtocolVersion>
|
||||
<TgsSwarmProtocolVersion>9.0.0</TgsSwarmProtocolVersion>
|
||||
<TgsContainerScriptVersion>1.2.1</TgsContainerScriptVersion>
|
||||
<TgsNugetNetFramework>netstandard2.0</TgsNugetNetFramework>
|
||||
<TgsNetMajorVersion>8</TgsNetMajorVersion>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<!-- This is in it's own file to help incremental building, changing it causes a complete rebuild of the web panel -->
|
||||
<TgsWebpanelVersion>6.7.3</TgsWebpanelVersion>
|
||||
<TgsWebpanelVersion>6.9.0</TgsWebpanelVersion>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
||||
@@ -584,7 +584,7 @@ namespace Tgstation.Server.Api.Models
|
||||
/// Attempted to set <see cref="Internal.UserApiBase.OAuthConnections"/> for the admin user.
|
||||
/// </summary>
|
||||
[Description("The admin user cannot use OAuth connections!")]
|
||||
AdminUserCannotOAuth,
|
||||
AdminUserCannotHaveServiceConnection,
|
||||
|
||||
/// <summary>
|
||||
/// Attempted to login with a disabled OAuth provider.
|
||||
@@ -669,5 +669,11 @@ namespace Tgstation.Server.Api.Models
|
||||
/// </summary>
|
||||
[Description("GraphQL swarm remote gateways not implemented!")]
|
||||
RemoteGatewaysNotImplemented,
|
||||
|
||||
/// <summary>
|
||||
/// Attempted to edit a user security property which is locked down due to the server being in <see cref="Response.ServerInformationResponse.OidcStrictMode"/>.
|
||||
/// </summary>
|
||||
[Description("One or more of the fields requested to be updated cannot be due to the server being in OIDC strict mode.")]
|
||||
BadUserEditDueToOidcStrictMode,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,11 @@ namespace Tgstation.Server.Api.Models.Internal
|
||||
/// </summary>
|
||||
public ICollection<OAuthConnection>? OAuthConnections { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// List of <see cref="OidcConnection"/>s associated with the user.
|
||||
/// </summary>
|
||||
public ICollection<OidcConnection>? OidcConnections { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Models.PermissionSet"/> directly associated with the user.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
|
||||
namespace Tgstation.Server.Api.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// List of OAuth providers supported by TGS.
|
||||
/// List of OAuth2.0 providers supported by TGS that do not support OIDC.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public enum OAuthProvider
|
||||
@@ -20,13 +22,15 @@ namespace Tgstation.Server.Api.Models
|
||||
Discord,
|
||||
|
||||
/// <summary>
|
||||
/// https://tgstation13.org.
|
||||
/// Pre-hostening https://tgstation13.org. No longer supported.
|
||||
/// </summary>
|
||||
[Obsolete("tgstation13.org no longer has a custom OAuth solution. This option will be removed in a future TGS version.")]
|
||||
TGForums,
|
||||
|
||||
/// <summary>
|
||||
/// https://www.keycloak.org.
|
||||
/// </summary>
|
||||
[Obsolete("This should now be implemented as an OIDC provider. This option will be removed in a future TGS version.")]
|
||||
Keycloak,
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Tgstation.Server.Api.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a valid OIDC connection.
|
||||
/// </summary>
|
||||
public class OidcConnection
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="OidcProviderInfo.SchemeKey"/> of the <see cref="OidcConnection"/>.
|
||||
/// </summary>]
|
||||
[Required]
|
||||
[RequestOptions(FieldPresence.Required)]
|
||||
[StringLength(Limits.MaximumIndexableStringLength, MinimumLength = 1)]
|
||||
public string? SchemeKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the user in the OIDC proivder ("sub" claim).
|
||||
/// </summary>
|
||||
[Required]
|
||||
[RequestOptions(FieldPresence.Required)]
|
||||
[StringLength(Limits.MaximumIndexableStringLength, MinimumLength = 1)]
|
||||
public string? ExternalUserId { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
|
||||
namespace Tgstation.Server.Api.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a configured OIDC provider.
|
||||
/// </summary>
|
||||
public sealed class OidcProviderInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// The scheme key used to reference the <see cref="OidcProviderInfo"/>. Unique amongst providers.
|
||||
/// </summary>
|
||||
public string? SchemeKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The provider's name as it should be displayed to the user.
|
||||
/// </summary>
|
||||
public string? FriendlyName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Colour that should be used to theme this OIDC provider.
|
||||
/// </summary>
|
||||
public string? ThemeColour { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Image URL that should be used to theme this OIDC provider.
|
||||
/// </summary>
|
||||
public Uri? ThemeIconUrl { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,16 @@ namespace Tgstation.Server.Api.Models.Response
|
||||
/// </summary>
|
||||
public Dictionary<OAuthProvider, OAuthProviderInfo>? OAuthProviderInfos { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// List of configured <see cref="OidcProviderInfo"/>s.
|
||||
/// </summary>
|
||||
public List<OidcProviderInfo>? OidcProviderInfos { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If only OIDC logins and registration is allowed.
|
||||
/// </summary>
|
||||
public bool OidcStrictMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If there is a server update in progress.
|
||||
/// </summary>
|
||||
|
||||
@@ -44,9 +44,9 @@ namespace Tgstation.Server.Api.Rights
|
||||
DownloadLogs = 1 << 5,
|
||||
|
||||
/// <summary>
|
||||
/// User can modify their own <see cref="Models.Internal.UserApiBase.OAuthConnections"/>.
|
||||
/// User can modify their own <see cref="Models.Internal.UserApiBase.OAuthConnections"/> and <see cref="Models.Internal.UserApiBase.OidcConnections"/>.
|
||||
/// </summary>
|
||||
EditOwnOAuthConnections = 1 << 6,
|
||||
EditOwnServiceConnections = 1 << 6,
|
||||
|
||||
/// <summary>
|
||||
/// User can upgrade/downgrade TGS using file uploads through the API.
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<!-- Usage: HTTP constants reference -->
|
||||
<PackageReference Include="Microsoft.AspNetCore.Http.Extensions" Version="2.3.0" />
|
||||
<!-- Usage: Decoding the 'nbf' property of JWTs -->
|
||||
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.5.0" />
|
||||
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.6.1" />
|
||||
<!-- Usage: Data model annotating -->
|
||||
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
mutation CreateUserFromOAuthConnection($name: String!, $oAuthConnections: [OAuthConnectionInput!]!) {
|
||||
createUserByOAuthAndPermissionSet(input: { name: $name, oAuthConnections: $oAuthConnections }) {
|
||||
createUserByServiceConnectionAndPermissionSet(input: { name: $name, oAuthConnections: $oAuthConnections }) {
|
||||
errors {
|
||||
... on ErrorMessageError {
|
||||
additionalData
|
||||
|
||||
@@ -7,7 +7,7 @@ mutation CreateUserGroup($name: String!) {
|
||||
administrationRights {
|
||||
canChangeVersion
|
||||
canDownloadLogs
|
||||
canEditOwnOAuthConnections
|
||||
canEditOwnServiceConnections
|
||||
canEditOwnPassword
|
||||
canReadUsers
|
||||
canRestartHost
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ mutation CreateUserGroupWithInstanceListPerm($name: String!) {
|
||||
administrationRights {
|
||||
canChangeVersion
|
||||
canDownloadLogs
|
||||
canEditOwnOAuthConnections
|
||||
canEditOwnServiceConnections
|
||||
canEditOwnPassword
|
||||
canReadUsers
|
||||
canRestartHost
|
||||
|
||||
@@ -6,7 +6,7 @@ mutation SetFullPermsOnUserGroup($id: ID!) {
|
||||
administrationRights: {
|
||||
canChangeVersion: true
|
||||
canDownloadLogs: true
|
||||
canEditOwnOAuthConnections: true
|
||||
canEditOwnServiceConnections: true
|
||||
canEditOwnPassword: true
|
||||
canReadUsers: true
|
||||
canWriteUsers: true
|
||||
@@ -36,7 +36,7 @@ mutation SetFullPermsOnUserGroup($id: ID!) {
|
||||
administrationRights {
|
||||
canChangeVersion
|
||||
canDownloadLogs
|
||||
canEditOwnOAuthConnections
|
||||
canEditOwnServiceConnections
|
||||
canEditOwnPassword
|
||||
canReadUsers
|
||||
canRestartHost
|
||||
|
||||
@@ -10,27 +10,27 @@ mutation SetUserGroup($id: ID!, $newGroupId: ID!) {
|
||||
user {
|
||||
ownedPermissionSet {
|
||||
instanceManagerRights {
|
||||
canCreate
|
||||
canDelete
|
||||
canGrantPermissions
|
||||
canList
|
||||
canRead
|
||||
canRelocate
|
||||
canRename
|
||||
canSetAutoUpdate
|
||||
canSetChatBotLimit
|
||||
canSetConfiguration
|
||||
canSetOnline
|
||||
canCreate
|
||||
canDelete
|
||||
canGrantPermissions
|
||||
canList
|
||||
canRead
|
||||
canRelocate
|
||||
canRename
|
||||
canSetAutoUpdate
|
||||
canSetChatBotLimit
|
||||
canSetConfiguration
|
||||
canSetOnline
|
||||
}
|
||||
administrationRights {
|
||||
canChangeVersion
|
||||
canDownloadLogs
|
||||
canEditOwnOAuthConnections
|
||||
canEditOwnPassword
|
||||
canReadUsers
|
||||
canRestartHost
|
||||
canUploadVersion
|
||||
canWriteUsers
|
||||
canChangeVersion
|
||||
canDownloadLogs
|
||||
canEditOwnServiceConnections
|
||||
canEditOwnPassword
|
||||
canReadUsers
|
||||
canRestartHost
|
||||
canUploadVersion
|
||||
canWriteUsers
|
||||
}
|
||||
}
|
||||
group {
|
||||
|
||||
@@ -12,7 +12,7 @@ mutation SetUserPermissionSet($id: ID!, $permissionSet: PermissionSetInput!) {
|
||||
administrationRights {
|
||||
canChangeVersion
|
||||
canDownloadLogs
|
||||
canEditOwnOAuthConnections
|
||||
canEditOwnServiceConnections
|
||||
canEditOwnPassword
|
||||
canReadUsers
|
||||
canRestartHost
|
||||
@@ -37,7 +37,7 @@ mutation SetUserPermissionSet($id: ID!, $permissionSet: PermissionSetInput!) {
|
||||
administrationRights {
|
||||
canChangeVersion
|
||||
canDownloadLogs
|
||||
canEditOwnOAuthConnections
|
||||
canEditOwnServiceConnections
|
||||
canEditOwnPassword
|
||||
canReadUsers
|
||||
canRestartHost
|
||||
|
||||
@@ -20,7 +20,7 @@ query GetSomeGroupInfo($id: ID!) {
|
||||
administrationRights {
|
||||
canChangeVersion
|
||||
canDownloadLogs
|
||||
canEditOwnOAuthConnections
|
||||
canEditOwnServiceConnections
|
||||
canEditOwnPassword
|
||||
canReadUsers
|
||||
canRestartHost
|
||||
|
||||
@@ -20,7 +20,7 @@ query ReadCurrentUser {
|
||||
administrationRights {
|
||||
canChangeVersion
|
||||
canDownloadLogs
|
||||
canEditOwnOAuthConnections
|
||||
canEditOwnServiceConnections
|
||||
canEditOwnPassword
|
||||
canReadUsers
|
||||
canRestartHost
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<!-- GraphQL connector and code generator -->
|
||||
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.2" />
|
||||
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.3" />
|
||||
<PackageReference Include="StrawberryShake.Server" Version="15.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Usage: Connecting to SignalR hubs in API -->
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="9.0.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="9.0.3" />
|
||||
<!-- Usage: Using target JSON serializer for API -->
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="9.0.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="9.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -13,9 +13,9 @@
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Usage: Identifying if we're running under SystemD -->
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="9.0.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="9.0.3" />
|
||||
<!-- Usage: Console logging plugin -->
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -23,19 +23,19 @@
|
||||
<!-- Usage: Identifies when we are running in the context of the Windows SCM -->
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.2" />
|
||||
<!-- Usage: Windows event log logging plugin -->
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.EventLog" Version="9.0.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.EventLog" Version="9.0.3" />
|
||||
<!-- Usage: Console logging plugin -->
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.2" />
|
||||
<!-- Usage: Updated transitive dependency of Core.System.ServiceProcess -->
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<!-- Usage: Updated transitive dependency of Core.System.ServiceProcess -->
|
||||
<PackageReference Include="System.Drawing.Common" Version="9.0.2" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="9.0.3" />
|
||||
<!-- Usage: Updated transitive dependency, unable to tell what of -->
|
||||
<PackageReference Include="System.Private.Uri" Version="4.3.2" />
|
||||
<!-- Usage: OS identification -->
|
||||
<PackageReference Include="System.Runtime.InteropServices" Version="4.3.0" />
|
||||
<!-- Usage: Windows Service Manager intergration -->
|
||||
<PackageReference Include="System.ServiceProcess.ServiceController" Version="9.0.2" />
|
||||
<PackageReference Include="System.ServiceProcess.ServiceController" Version="9.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"schema": "schema.graphql",
|
||||
"documents": "**/*.graphql",
|
||||
"extensions": {
|
||||
"strawberryShake": {
|
||||
"name": "GraphQLClient",
|
||||
"url": "../../artifacts/gitlab-api.graphql",
|
||||
"namespace": "Tgstation.Server.Host.Utils.GitLab.GraphQL",
|
||||
"records": {
|
||||
"inputs": false,
|
||||
"entities": false
|
||||
},
|
||||
"transportProfiles": [
|
||||
{
|
||||
"default": "Http",
|
||||
"subscription": "Http"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
compressionLevel: mixed
|
||||
|
||||
enableGlobalCache: false
|
||||
|
||||
enableScripts: false
|
||||
|
||||
logFilters:
|
||||
- code: YN0004
|
||||
level: discard
|
||||
- code: YN0062
|
||||
level: discard
|
||||
|
||||
nodeLinker: node-modules
|
||||
|
||||
preferInteractive: true
|
||||
@@ -0,0 +1,12 @@
|
||||
mutation CreateNote($id: NoteableID!, $body: String!) {
|
||||
createNote(input: { noteableId: $id, body: $body }) {
|
||||
note {
|
||||
id
|
||||
body
|
||||
discussion {
|
||||
id
|
||||
}
|
||||
}
|
||||
errors
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
mutation ModifyNote($id: NoteID!, $body: String!) {
|
||||
updateNote(input: { id: $id, body: $body }) {
|
||||
note {
|
||||
id
|
||||
body
|
||||
}
|
||||
errors
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
query GetCurrentUser {
|
||||
currentUser
|
||||
{
|
||||
username
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
query GetMergeRequest($project: ID!, $number: String!) {
|
||||
project(fullPath: $project) {
|
||||
mergeRequest(iid: $number) {
|
||||
author { username }
|
||||
description
|
||||
title
|
||||
diffHeadSha
|
||||
mergeCommitSha
|
||||
webUrl
|
||||
iid
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
query GetMergeRequestNotes($project: ID!, $number: String!) {
|
||||
project(fullPath: $project) {
|
||||
mergeRequest(iid: $number) {
|
||||
iid
|
||||
id
|
||||
notes {
|
||||
nodes {
|
||||
author { username }
|
||||
body
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
query GetMergeRequests($project: ID!, $numbers: [String!]!) {
|
||||
project(fullPath: $project) {
|
||||
mergeRequests(iids: $numbers) {
|
||||
nodes {
|
||||
state
|
||||
diffHeadSha
|
||||
mergeCommitSha
|
||||
closedAt
|
||||
iid
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Tgstation.Server.Host.Utils.GitLab.GraphQL
|
||||
{
|
||||
/// <inheritdoc />
|
||||
sealed class GraphQLGitLabClient : IGraphQLGitLabClient
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public IGraphQLClient GraphQL { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ServiceProvider"/> containing the <see cref="GraphQL"/> client.
|
||||
/// </summary>
|
||||
readonly ServiceProvider serviceProvider;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GraphQLGitLabClient"/> class.
|
||||
/// </summary>
|
||||
/// <param name="serviceProvider">The value of <see cref="serviceProvider"/>.</param>
|
||||
public GraphQLGitLabClient(ServiceProvider serviceProvider)
|
||||
{
|
||||
this.serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
|
||||
GraphQL = serviceProvider.GetService<IGraphQLClient>() ?? throw new ArgumentException($"Expected an {nameof(IGraphQLClient)} service in the provider!", nameof(serviceProvider));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask DisposeAsync()
|
||||
=> serviceProvider.DisposeAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using System;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Tgstation.Server.Host.Utils.GitLab.GraphQL
|
||||
{
|
||||
/// <summary>
|
||||
/// Factory for creating <see cref="IGraphQLGitLabClient"/>s.
|
||||
/// </summary>
|
||||
public sealed class GraphQLGitLabClientFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Sets up a <see cref="IGraphQLGitLabClient"/>.
|
||||
/// </summary>
|
||||
/// <param name="bearerToken">The token to use for authentication, if any.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a new <see cref="IGraphQLGitLabClient"/>.</returns>
|
||||
public static async ValueTask<IGraphQLGitLabClient> CreateClient(string? bearerToken = null)
|
||||
{
|
||||
var serviceCollection = new ServiceCollection();
|
||||
|
||||
var clientBuilder = serviceCollection
|
||||
.AddGraphQLClient();
|
||||
var graphQLEndpoint = new Uri("https://gitlab.com/api/graphql");
|
||||
|
||||
clientBuilder.ConfigureHttpClient(
|
||||
client =>
|
||||
{
|
||||
client.BaseAddress = new Uri("https://gitlab.com/api/graphql");
|
||||
if (bearerToken != null)
|
||||
{
|
||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", bearerToken);
|
||||
}
|
||||
});
|
||||
|
||||
var serviceProvider = serviceCollection.BuildServiceProvider();
|
||||
try
|
||||
{
|
||||
return new GraphQLGitLabClient(serviceProvider);
|
||||
}
|
||||
catch
|
||||
{
|
||||
await serviceProvider.DisposeAsync();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
|
||||
namespace Tgstation.Server.Host.Utils.GitLab.GraphQL
|
||||
{
|
||||
/// <summary>
|
||||
/// Wrapper for using a GitLab <see cref="IGraphQLClient"/>.
|
||||
/// </summary>
|
||||
public interface IGraphQLGitLabClient : IAsyncDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the underlying <see cref="IGraphQLClient"/>.
|
||||
/// </summary>
|
||||
IGraphQLClient GraphQL { get; }
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="../../build/SrcCommon.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>$(TgsFrameworkVersion)</TargetFrameworks>
|
||||
<Version>$(TgsCoreVersion)</Version>
|
||||
<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">
|
||||
<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" />
|
||||
</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>
|
||||
|
||||
<Target Name="FixWarningsInGeneratedSchema" AfterTargets="GenerateGraphQLCode">
|
||||
<PropertyGroup>
|
||||
<InputFile>$(IntermediateOutputPath)berry/GraphQLClient.Client.cs</InputFile>
|
||||
<OutputFile>$(IntermediateOutputPath)berry/GraphQLClient.Client.cs</OutputFile>
|
||||
</PropertyGroup>
|
||||
<WriteLinesToFile File="$(OutputFile)" Lines="$([System.IO.File]::ReadAllText($(InputFile)).Replace('/ <auto-generated/>','/ <auto-generated />%0d%0a#pragma warning disable'))" Overwrite="true" Encoding="Unicode" />
|
||||
</Target>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="StrawberryShake.Server" Version="15.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="GQL\Queries\" />
|
||||
<Folder Include="GQL\Mutations\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"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,7 +10,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Usage: Logging abstractions -->
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.3" />
|
||||
<!-- Usage: POSIX support for signals -->
|
||||
<PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"dotnet-ef": {
|
||||
"version": "8.0.13",
|
||||
"version": "9.0.3",
|
||||
"commands": [
|
||||
"dotnet-ef"
|
||||
]
|
||||
|
||||
@@ -43,6 +43,14 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// <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);
|
||||
|
||||
/// <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);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all registered <see cref="User"/>s.
|
||||
/// </summary>
|
||||
@@ -70,7 +78,7 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// <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.EditOwnOAuthConnections)]
|
||||
[TgsAuthorize(AdministrationRights.WriteUsers | AdministrationRights.EditOwnPassword | AdministrationRights.EditOwnServiceConnections)]
|
||||
ValueTask<AuthorityResponse<User>> Update(UserUpdateRequest updateRequest, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,13 @@ using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
using Tgstation.Server.Api;
|
||||
using Tgstation.Server.Api.Models;
|
||||
using Tgstation.Server.Api.Models.Response;
|
||||
using Tgstation.Server.Host.Authority.Core;
|
||||
using Tgstation.Server.Host.Configuration;
|
||||
using Tgstation.Server.Host.Database;
|
||||
using Tgstation.Server.Host.GraphQL.Mutations.Payloads;
|
||||
using Tgstation.Server.Host.Models;
|
||||
@@ -58,6 +60,11 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// </summary>
|
||||
readonly ISessionInvalidationTracker sessionInvalidationTracker;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="SecurityConfiguration"/> for the <see cref="LoginAuthority"/>.
|
||||
/// </summary>
|
||||
readonly SecurityConfiguration securityConfiguration;
|
||||
|
||||
/// <summary>
|
||||
/// Generate an <see cref="AuthorityResponse{TResult}"/> for a given <paramref name="headersException"/>.
|
||||
/// </summary>
|
||||
@@ -106,6 +113,7 @@ 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>
|
||||
public LoginAuthority(
|
||||
IAuthenticationContext authenticationContext,
|
||||
IDatabaseContext databaseContext,
|
||||
@@ -116,7 +124,8 @@ namespace Tgstation.Server.Host.Authority
|
||||
ITokenFactory tokenFactory,
|
||||
ICryptographySuite cryptographySuite,
|
||||
IIdentityCache identityCache,
|
||||
ISessionInvalidationTracker sessionInvalidationTracker)
|
||||
ISessionInvalidationTracker sessionInvalidationTracker,
|
||||
IOptions<SecurityConfiguration> securityConfigurationOptions)
|
||||
: base(
|
||||
authenticationContext,
|
||||
databaseContext,
|
||||
@@ -129,11 +138,16 @@ 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));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<AuthorityResponse<LoginResult>> AttemptLogin(CancellationToken cancellationToken)
|
||||
{
|
||||
// password and oauth logins disabled
|
||||
if (securityConfiguration.OidcStrictMode)
|
||||
return Unauthorized<LoginResult>();
|
||||
|
||||
var headers = apiHeadersProvider.ApiHeaders;
|
||||
if (headers == null)
|
||||
return GenerateHeadersExceptionResponse<LoginResult>(apiHeadersProvider.HeadersException!);
|
||||
@@ -318,6 +332,12 @@ namespace Tgstation.Server.Host.Authority
|
||||
(string? UserID, string AccessCode)? oauthResult;
|
||||
try
|
||||
{
|
||||
// minor special case here until its removal
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
if (oAuthProvider == OAuthProvider.TGForums)
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
return (Unauthorized<TResult>(), null);
|
||||
|
||||
var validator = oAuthProviders
|
||||
.GetValidator(oAuthProvider, forLogin);
|
||||
|
||||
|
||||
@@ -41,6 +41,11 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// </summary>
|
||||
readonly IOAuthConnectionsDataLoader oAuthConnectionsDataLoader;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IOidcConnectionsDataLoader"/> for the <see cref="UserAuthority"/>.
|
||||
/// </summary>
|
||||
readonly IOidcConnectionsDataLoader oidcConnectionsDataLoader;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ISystemIdentityFactory"/> for the <see cref="UserAuthority"/>.
|
||||
/// </summary>
|
||||
@@ -71,6 +76,11 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// </summary>
|
||||
readonly IOptionsSnapshot<GeneralConfiguration> generalConfigurationOptions;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IOptions{TOptions}"/> of <see cref="SecurityConfiguration"/> for the <see cref="UserAuthority"/>.
|
||||
/// </summary>
|
||||
readonly IOptions<SecurityConfiguration> securityConfigurationOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Implements the <see cref="usersDataLoader"/>.
|
||||
/// </summary>
|
||||
@@ -95,7 +105,7 @@ namespace Tgstation.Server.Host.Authority
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implements the <see cref="usersDataLoader"/>.
|
||||
/// Implements the <see cref="oAuthConnectionsDataLoader"/>.
|
||||
/// </summary>
|
||||
/// <param name="userIds">The <see cref="IReadOnlyCollection{T}"/> of <see cref="User"/> <see cref="EntityId.Id"/>s to load the OAuthConnections for.</param>
|
||||
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> to load from.</param>
|
||||
@@ -117,10 +127,37 @@ namespace Tgstation.Server.Host.Authority
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return list.ToLookup(
|
||||
oauthConnection => oauthConnection.UserId,
|
||||
oAuthConnection => oAuthConnection.UserId,
|
||||
x => new GraphQL.Types.OAuth.OAuthConnection(x.ExternalUserId!, x.Provider));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implements the <see cref="oidcConnectionsDataLoader"/>.
|
||||
/// </summary>
|
||||
/// <param name="userIds">The <see cref="IReadOnlyCollection{T}"/> of <see cref="User"/> <see cref="EntityId.Id"/>s to load the OidcConnections for.</param>
|
||||
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> to load from.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a <see cref="Dictionary{TKey, TValue}"/> of the requested <see cref="User"/>s.</returns>
|
||||
[DataLoader]
|
||||
public static async ValueTask<ILookup<long, GraphQL.Types.OAuth.OidcConnection>> GetOidcConnections(
|
||||
IReadOnlyList<long> userIds,
|
||||
IDatabaseContext databaseContext,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(userIds);
|
||||
ArgumentNullException.ThrowIfNull(databaseContext);
|
||||
|
||||
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!));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if a given <paramref name="model"/> has a valid <see cref="UserName.Name"/> specified.
|
||||
/// </summary>
|
||||
@@ -147,24 +184,28 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// <param name="logger">The <see cref="ILogger"/> to use.</param>
|
||||
/// <param name="usersDataLoader">The value of <see cref="usersDataLoader"/>.</param>
|
||||
/// <param name="oAuthConnectionsDataLoader">The value of <see cref="oAuthConnectionsDataLoader"/>.</param>
|
||||
/// <param name="oidcConnectionsDataLoader">The value of <see cref="oidcConnectionsDataLoader"/>.</param>
|
||||
/// <param name="systemIdentityFactory">The value of <see cref="systemIdentityFactory"/>.</param>
|
||||
/// <param name="permissionsUpdateNotifyee">The value of <see cref="permissionsUpdateNotifyee"/>.</param>
|
||||
/// <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="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,
|
||||
IOAuthConnectionsDataLoader oAuthConnectionsDataLoader,
|
||||
IOidcConnectionsDataLoader oidcConnectionsDataLoader,
|
||||
ISystemIdentityFactory systemIdentityFactory,
|
||||
IPermissionsUpdateNotifyee permissionsUpdateNotifyee,
|
||||
ICryptographySuite cryptographySuite,
|
||||
ISessionInvalidationTracker sessionInvalidationTracker,
|
||||
ITopicEventSender topicEventSender,
|
||||
IOptionsSnapshot<GeneralConfiguration> generalConfigurationOptions)
|
||||
IOptionsSnapshot<GeneralConfiguration> generalConfigurationOptions,
|
||||
IOptions<SecurityConfiguration> securityConfigurationOptions)
|
||||
: base(
|
||||
authenticationContext,
|
||||
databaseContext,
|
||||
@@ -172,12 +213,14 @@ namespace Tgstation.Server.Host.Authority
|
||||
{
|
||||
this.usersDataLoader = usersDataLoader ?? throw new ArgumentNullException(nameof(usersDataLoader));
|
||||
this.oAuthConnectionsDataLoader = oAuthConnectionsDataLoader ?? throw new ArgumentNullException(nameof(oAuthConnectionsDataLoader));
|
||||
this.oidcConnectionsDataLoader = oidcConnectionsDataLoader ?? throw new ArgumentNullException(nameof(oidcConnectionsDataLoader));
|
||||
this.systemIdentityFactory = systemIdentityFactory ?? throw new ArgumentNullException(nameof(systemIdentityFactory));
|
||||
this.permissionsUpdateNotifyee = permissionsUpdateNotifyee ?? throw new ArgumentNullException(nameof(permissionsUpdateNotifyee));
|
||||
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.generalConfigurationOptions = generalConfigurationOptions ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
|
||||
this.securityConfigurationOptions = securityConfigurationOptions ?? throw new ArgumentNullException(nameof(securityConfigurationOptions));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -289,6 +332,11 @@ namespace Tgstation.Server.Host.Authority
|
||||
=> 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,
|
||||
@@ -372,7 +420,7 @@ namespace Tgstation.Server.Host.Authority
|
||||
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.EditOwnOAuthConnections);
|
||||
var oAuthEdit = canEditAllUsers || callerAdministrationRights.HasFlag(AdministrationRights.EditOwnServiceConnections);
|
||||
|
||||
var originalUser = !canEditAllUsers
|
||||
? AuthenticationContext.User
|
||||
@@ -382,6 +430,7 @@ namespace Tgstation.Server.Host.Authority
|
||||
.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)
|
||||
@@ -437,8 +486,11 @@ namespace Tgstation.Server.Host.Authority
|
||||
&& (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.AdminUserCannotOAuth);
|
||||
return BadRequest<User>(ErrorCode.AdminUserCannotHaveServiceConnection);
|
||||
|
||||
if (model.OAuthConnections.Count == 0 && originalUser.PasswordHash == null && originalUser.SystemIdentifier == null)
|
||||
return BadRequest<User>(ErrorCode.CannotRemoveLastAuthenticationOption);
|
||||
@@ -452,8 +504,33 @@ namespace Tgstation.Server.Host.Authority
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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()
|
||||
@@ -474,6 +551,9 @@ namespace Tgstation.Server.Host.Authority
|
||||
}
|
||||
else if (model.PermissionSet != null)
|
||||
{
|
||||
if (securityConfigurationOptions.Value.OidcStrictMode)
|
||||
return BadRequest<User>(ErrorCode.BadUserEditDueToOidcStrictMode);
|
||||
|
||||
if (originalUser.PermissionSet == null)
|
||||
{
|
||||
Logger.LogTrace("Creating new permission set...");
|
||||
@@ -495,6 +575,9 @@ namespace Tgstation.Server.Host.Authority
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -554,6 +637,7 @@ namespace Tgstation.Server.Host.Authority
|
||||
queryable = queryable
|
||||
.Include(x => x.CreatedBy)
|
||||
.Include(x => x.OAuthConnections)
|
||||
.Include(x => x.OidcConnections)
|
||||
.Include(x => x.Group!)
|
||||
.ThenInclude(x => x.PermissionSet)
|
||||
.Include(x => x.PermissionSet);
|
||||
@@ -603,6 +687,15 @@ namespace Tgstation.Server.Host.Authority
|
||||
})
|
||||
.ToList()
|
||||
?? new List<Models.OAuthConnection>(),
|
||||
OidcConnections = model
|
||||
.OidcConnections
|
||||
?.Select(x => new Models.OidcConnection
|
||||
{
|
||||
SchemeKey = x.SchemeKey,
|
||||
ExternalUserId = x.ExternalUserId,
|
||||
})
|
||||
.ToList()
|
||||
?? new List<Models.OidcConnection>(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -377,6 +377,7 @@ namespace Tgstation.Server.Host.Components.Chat
|
||||
/// <inheritdoc />
|
||||
public Func<string?, string, Action<bool>> QueueDeploymentMessage(
|
||||
Models.RevisionInformation revisionInformation,
|
||||
Models.RevisionInformation? previousRevisionInformation,
|
||||
EngineVersion engineVersion,
|
||||
DateTimeOffset? estimatedCompletionTime,
|
||||
string? gitHubOwner,
|
||||
@@ -407,6 +408,7 @@ namespace Tgstation.Server.Host.Components.Chat
|
||||
{
|
||||
var callback = await provider.SendUpdateMessage(
|
||||
revisionInformation,
|
||||
previousRevisionInformation,
|
||||
engineVersion,
|
||||
estimatedCompletionTime,
|
||||
gitHubOwner,
|
||||
|
||||
@@ -61,6 +61,7 @@ namespace Tgstation.Server.Host.Components.Chat
|
||||
/// Send the message for a deployment to configured deployment channels.
|
||||
/// </summary>
|
||||
/// <param name="revisionInformation">The <see cref="RevisionInformation"/> of the deployment.</param>
|
||||
/// <param name="previousRevisionInformation">The optional <see cref="RevisionInformation"/> of the previous deployment.</param>
|
||||
/// <param name="engineVersion">The <see cref="Api.Models.EngineVersion"/> of the deployment.</param>
|
||||
/// <param name="estimatedCompletionTime">The optional <see cref="DateTimeOffset"/> the deployment is expected to be completed at.</param>
|
||||
/// <param name="gitHubOwner">The repository GitHub owner, if any.</param>
|
||||
@@ -69,6 +70,7 @@ namespace Tgstation.Server.Host.Components.Chat
|
||||
/// <returns>A <see cref="Func{T1, T2, TResult}"/> to call to update the message at the deployment's conclusion. Parameters: Error message if any, DreamMaker output if any. Returns an <see cref="Action"/> to call to mark the deployment as active/inactive. Parameter: If the deployment is being activated or inactivated.</returns>
|
||||
Func<string?, string, Action<bool>> QueueDeploymentMessage(
|
||||
Models.RevisionInformation revisionInformation,
|
||||
Models.RevisionInformation? previousRevisionInformation,
|
||||
Api.Models.EngineVersion engineVersion,
|
||||
DateTimeOffset? estimatedCompletionTime,
|
||||
string? gitHubOwner,
|
||||
|
||||
@@ -303,6 +303,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
||||
/// <inheritdoc />
|
||||
public override async ValueTask<Func<string?, string, ValueTask<Func<bool, ValueTask>>>> SendUpdateMessage(
|
||||
Models.RevisionInformation revisionInformation,
|
||||
Models.RevisionInformation? previousRevisionInformation,
|
||||
EngineVersion engineVersion,
|
||||
DateTimeOffset? estimatedCompletionTime,
|
||||
string? gitHubOwner,
|
||||
@@ -316,7 +317,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
||||
|
||||
localCommitPushed |= revisionInformation.CommitSha == revisionInformation.OriginCommitSha;
|
||||
|
||||
var fields = BuildUpdateEmbedFields(revisionInformation, engineVersion, gitHubOwner, gitHubRepo, localCommitPushed);
|
||||
var fields = BuildUpdateEmbedFields(revisionInformation, previousRevisionInformation, engineVersion, gitHubOwner, gitHubRepo, localCommitPushed);
|
||||
Optional<IEmbedAuthor> author = new EmbedAuthor(assemblyInformationProvider.VersionPrefix)
|
||||
{
|
||||
Url = "https://github.com/tgstation/tgstation-server",
|
||||
@@ -900,6 +901,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
||||
/// Create a <see cref="List{T}"/> of <see cref="IEmbedField"/>s for a discord update embed.
|
||||
/// </summary>
|
||||
/// <param name="revisionInformation">The <see cref="RevisionInformation"/> of the deployment.</param>
|
||||
/// <param name="previousRevisionInformation">The optional <see cref="RevisionInformation"/> of the previous deployment.</param>
|
||||
/// <param name="engineVersion">The <see cref="EngineVersion"/> of the deployment.</param>
|
||||
/// <param name="gitHubOwner">The repository GitHub owner, if any.</param>
|
||||
/// <param name="gitHubRepo">The repository GitHub name, if any.</param>
|
||||
@@ -907,6 +909,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
||||
/// <returns>A new <see cref="List{T}"/> of <see cref="IEmbedField"/>s to use.</returns>
|
||||
List<IEmbedField> BuildUpdateEmbedFields(
|
||||
Models.RevisionInformation revisionInformation,
|
||||
Models.RevisionInformation? previousRevisionInformation,
|
||||
EngineVersion engineVersion,
|
||||
string? gitHubOwner,
|
||||
string? gitHubRepo,
|
||||
@@ -936,6 +939,31 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
||||
if (gitHubOwner == null || gitHubRepo == null)
|
||||
return fields;
|
||||
|
||||
var previousTestMerges = (IEnumerable<RevInfoTestMerge>?)previousRevisionInformation?.ActiveTestMerges ?? Enumerable.Empty<RevInfoTestMerge>();
|
||||
var currentTestMerges = (IEnumerable<RevInfoTestMerge>?)revisionInformation.ActiveTestMerges ?? Enumerable.Empty<RevInfoTestMerge>();
|
||||
|
||||
// determine what TMs were changed and how
|
||||
var addedTestMerges = currentTestMerges
|
||||
.Select(x => x.TestMerge)
|
||||
.Where(x => !previousTestMerges
|
||||
.Any(y => y.TestMerge.Number == x.Number))
|
||||
.ToList();
|
||||
var removedTestMerges = previousTestMerges
|
||||
.Select(x => x.TestMerge)
|
||||
.Where(x => !currentTestMerges
|
||||
.Any(y => y.TestMerge.Number == x.Number))
|
||||
.ToList();
|
||||
var updatedTestMerges = currentTestMerges
|
||||
.Select(x => x.TestMerge)
|
||||
.Where(x => previousTestMerges
|
||||
.Any(y => y.TestMerge.Number == x.Number && y.TestMerge.TargetCommitSha != x.TargetCommitSha))
|
||||
.ToList();
|
||||
var unchangedTestMerges = currentTestMerges
|
||||
.Select(x => x.TestMerge)
|
||||
.Where(x => previousTestMerges
|
||||
.Any(y => y.TestMerge.Number == x.Number && y.TestMerge.TargetCommitSha == x.TargetCommitSha))
|
||||
.ToList();
|
||||
|
||||
fields.Add(
|
||||
new EmbedField(
|
||||
"Local Commit",
|
||||
@@ -952,13 +980,33 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
||||
: revisionOriginSha[..7],
|
||||
true));
|
||||
|
||||
fields.AddRange((revisionInformation.ActiveTestMerges ?? Enumerable.Empty<RevInfoTestMerge>())
|
||||
.Select(x => x.TestMerge)
|
||||
fields.AddRange(addedTestMerges
|
||||
.Select(x => new EmbedField(
|
||||
$"#{x.Number} (Added)",
|
||||
$"[{x.TitleAtMerge}]({x.Url}) by _[@{x.Author}](https://github.com/{x.Author})_{Environment.NewLine}Commit: [{x.TargetCommitSha![..7]}](https://github.com/{gitHubOwner}/{gitHubRepo}/commit/{x.TargetCommitSha}){(String.IsNullOrWhiteSpace(x.Comment) ? String.Empty : $"{Environment.NewLine}_**{x.Comment}**_")}",
|
||||
false)));
|
||||
|
||||
fields.AddRange(updatedTestMerges
|
||||
.Select(x => new EmbedField(
|
||||
$"#{x.Number} (Updated)",
|
||||
$"[{x.TitleAtMerge}]({x.Url}) by _[@{x.Author}](https://github.com/{x.Author})_{Environment.NewLine}Commit: [{x.TargetCommitSha![..7]}](https://github.com/{gitHubOwner}/{gitHubRepo}/commit/{x.TargetCommitSha}){(String.IsNullOrWhiteSpace(x.Comment) ? String.Empty : $"{Environment.NewLine}_**{x.Comment}**_")}",
|
||||
false)));
|
||||
|
||||
fields.AddRange(unchangedTestMerges
|
||||
.Select(x => new EmbedField(
|
||||
$"#{x.Number}",
|
||||
$"[{x.TitleAtMerge}]({x.Url}) by _[@{x.Author}](https://github.com/{x.Author})_{Environment.NewLine}Commit: [{x.TargetCommitSha![..7]}](https://github.com/{gitHubOwner}/{gitHubRepo}/commit/{x.TargetCommitSha}){(String.IsNullOrWhiteSpace(x.Comment) ? String.Empty : $"{Environment.NewLine}_**{x.Comment}**_")}",
|
||||
false)));
|
||||
|
||||
if (removedTestMerges.Count != 0)
|
||||
fields.Add(
|
||||
new EmbedField(
|
||||
"Removed:",
|
||||
String.Join(
|
||||
Environment.NewLine,
|
||||
removedTestMerges
|
||||
.Select(x => $"- #{x.Number} [{x.TitleAtMerge}]({x.Url}) by _[@{x.Author}](https://github.com/{x.Author})_"))));
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
|
||||
@@ -84,6 +84,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
||||
/// Send the message for a deployment.
|
||||
/// </summary>
|
||||
/// <param name="revisionInformation">The <see cref="RevisionInformation"/> of the deployment.</param>
|
||||
/// <param name="previousRevisionInformation">The <see cref="RevisionInformation"/> of the previous deployment if any.</param>
|
||||
/// <param name="engineVersion">The <see cref="Api.Models.EngineVersion"/> of the deployment.</param>
|
||||
/// <param name="estimatedCompletionTime">The optional <see cref="DateTimeOffset"/> the deployment is expected to be completed at.</param>
|
||||
/// <param name="gitHubOwner">The repository GitHub owner, if any.</param>
|
||||
@@ -94,6 +95,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a <see cref="Func{T1, T2, TResult}"/> to call to update the message at the deployment's conclusion. Parameters: Error message if any, DreamMaker output if any. Returns another callback which should be called to mark the deployment as active.</returns>
|
||||
ValueTask<Func<string?, string, ValueTask<Func<bool, ValueTask>>>> SendUpdateMessage(
|
||||
Models.RevisionInformation revisionInformation,
|
||||
Models.RevisionInformation? previousRevisionInformation,
|
||||
Api.Models.EngineVersion engineVersion,
|
||||
DateTimeOffset? estimatedCompletionTime,
|
||||
string? gitHubOwner,
|
||||
|
||||
@@ -17,6 +17,7 @@ using Tgstation.Server.Host.Configuration;
|
||||
using Tgstation.Server.Host.Extensions;
|
||||
using Tgstation.Server.Host.IO;
|
||||
using Tgstation.Server.Host.Jobs;
|
||||
using Tgstation.Server.Host.Models;
|
||||
using Tgstation.Server.Host.System;
|
||||
using Tgstation.Server.Host.Utils;
|
||||
|
||||
@@ -217,6 +218,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
||||
/// <inheritdoc />
|
||||
public override async ValueTask<Func<string?, string, ValueTask<Func<bool, ValueTask>>>> SendUpdateMessage(
|
||||
Models.RevisionInformation revisionInformation,
|
||||
Models.RevisionInformation? previousRevisionInformation,
|
||||
EngineVersion engineVersion,
|
||||
DateTimeOffset? estimatedCompletionTime,
|
||||
string? gitHubOwner,
|
||||
@@ -228,6 +230,9 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
||||
ArgumentNullException.ThrowIfNull(revisionInformation);
|
||||
ArgumentNullException.ThrowIfNull(engineVersion);
|
||||
|
||||
var previousTestMerges = (IEnumerable<RevInfoTestMerge>?)previousRevisionInformation?.ActiveTestMerges ?? Enumerable.Empty<RevInfoTestMerge>();
|
||||
var currentTestMerges = (IEnumerable<RevInfoTestMerge>?)revisionInformation.ActiveTestMerges ?? Enumerable.Empty<RevInfoTestMerge>();
|
||||
|
||||
var commitInsert = revisionInformation.CommitSha![..7];
|
||||
string remoteCommitInsert;
|
||||
if (revisionInformation.CommitSha == revisionInformation.OriginCommitSha)
|
||||
@@ -238,21 +243,35 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
||||
else
|
||||
remoteCommitInsert = String.Format(CultureInfo.InvariantCulture, ". Remote commit: ^{0}", revisionInformation.OriginCommitSha![..7]);
|
||||
|
||||
var testmergeInsert = (revisionInformation.ActiveTestMerges?.Count ?? 0) == 0
|
||||
var testmergeInsert = !currentTestMerges.Any()
|
||||
? String.Empty
|
||||
: String.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
" (Test Merges: {0})",
|
||||
String.Join(
|
||||
", ",
|
||||
revisionInformation
|
||||
.ActiveTestMerges!
|
||||
currentTestMerges
|
||||
.Select(x => x.TestMerge)
|
||||
.Select(x =>
|
||||
{
|
||||
var result = String.Format(CultureInfo.InvariantCulture, "#{0} at {1}", x.Number, x.TargetCommitSha![..7]);
|
||||
if (x.Comment != null)
|
||||
result += String.Format(CultureInfo.InvariantCulture, " ({0})", x.Comment);
|
||||
var status = string.Empty;
|
||||
if (!previousTestMerges.Any(y => y.TestMerge.Number == x.Number))
|
||||
status = "Added";
|
||||
else if (previousTestMerges.Any(y => y.TestMerge.Number == x.Number && y.TestMerge.TargetCommitSha != x.TargetCommitSha))
|
||||
status = "Updated";
|
||||
|
||||
var result = $"#{x.Number} at {x.TargetCommitSha![..7]}";
|
||||
|
||||
if (!string.IsNullOrEmpty(x.Comment))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(status))
|
||||
result += $" ({status} - {x.Comment})";
|
||||
else
|
||||
result += $" ({x.Comment})";
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(status))
|
||||
result += $" ({status})";
|
||||
|
||||
return result;
|
||||
})));
|
||||
|
||||
|
||||
@@ -199,6 +199,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
||||
/// <inheritdoc />
|
||||
public abstract ValueTask<Func<string?, string, ValueTask<Func<bool, ValueTask>>>> SendUpdateMessage(
|
||||
RevisionInformation revisionInformation,
|
||||
RevisionInformation? previousRevisionInformation,
|
||||
Api.Models.EngineVersion engineVersion,
|
||||
DateTimeOffset? estimatedCompletionTime,
|
||||
string? gitHubOwner,
|
||||
|
||||
@@ -336,13 +336,17 @@ namespace Tgstation.Server.Host.Components.Deployment
|
||||
}
|
||||
});
|
||||
|
||||
var likelyPushedTestMergeCommit =
|
||||
repositorySettings!.PushTestMergeCommits!.Value
|
||||
&& repositorySettings.AccessToken != null
|
||||
&& repositorySettings.AccessUser != null;
|
||||
Models.CompileJob? oldCompileJob;
|
||||
using (repo)
|
||||
{
|
||||
var likelyPushedTestMergeCommit =
|
||||
repositorySettings!.PushTestMergeCommits!.Value
|
||||
&& repositorySettings.AccessToken != null
|
||||
&& repositorySettings.AccessUser != null;
|
||||
oldCompileJob = await compileJobConsumer.LatestCompileJob();
|
||||
compileJob = await Compile(
|
||||
job,
|
||||
oldCompileJob,
|
||||
revInfo!,
|
||||
dreamMakerSettings!,
|
||||
ddSettings!,
|
||||
@@ -352,8 +356,8 @@ namespace Tgstation.Server.Host.Components.Deployment
|
||||
averageSpan,
|
||||
likelyPushedTestMergeCommit,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
var activeCompileJob = await compileJobConsumer.LatestCompileJob();
|
||||
try
|
||||
{
|
||||
await databaseContextFactory.UseContext(
|
||||
@@ -402,7 +406,7 @@ namespace Tgstation.Server.Host.Components.Deployment
|
||||
|
||||
var commentsTask = remoteDeploymentManager!.PostDeploymentComments(
|
||||
compileJob,
|
||||
activeCompileJob?.RevisionInformation,
|
||||
oldCompileJob?.RevisionInformation,
|
||||
repositorySettings,
|
||||
repoOwner,
|
||||
repoName,
|
||||
@@ -479,6 +483,7 @@ namespace Tgstation.Server.Host.Components.Deployment
|
||||
/// Run the compile implementation.
|
||||
/// </summary>
|
||||
/// <param name="job">The currently running <see cref="Job"/>.</param>
|
||||
/// <param name="oldCompileJob">The optional <see cref="CompileJob"/> of the previous deployment.</param>
|
||||
/// <param name="revisionInformation">The <see cref="RevisionInformation"/>.</param>
|
||||
/// <param name="dreamMakerSettings">The <see cref="Api.Models.Internal.DreamMakerSettings"/>.</param>
|
||||
/// <param name="launchParameters">The <see cref="DreamDaemonLaunchParameters"/>.</param>
|
||||
@@ -491,6 +496,7 @@ namespace Tgstation.Server.Host.Components.Deployment
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the completed <see cref="CompileJob"/>.</returns>
|
||||
async ValueTask<Models.CompileJob> Compile(
|
||||
Models.Job job,
|
||||
Models.CompileJob? oldCompileJob,
|
||||
Models.RevisionInformation revisionInformation,
|
||||
Api.Models.Internal.DreamMakerSettings dreamMakerSettings,
|
||||
DreamDaemonLaunchParameters launchParameters,
|
||||
@@ -512,6 +518,7 @@ namespace Tgstation.Server.Host.Components.Deployment
|
||||
using var engineLock = await engineManager.UseExecutables(null, null, cancellationToken);
|
||||
currentChatCallback = chatManager.QueueDeploymentMessage(
|
||||
revisionInformation,
|
||||
oldCompileJob?.RevisionInformation,
|
||||
engineLock.Version,
|
||||
DateTimeOffset.UtcNow + estimatedDuration,
|
||||
repository.RemoteRepositoryOwner,
|
||||
|
||||
+38
-20
@@ -18,6 +18,11 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote
|
||||
/// </summary>
|
||||
abstract class BaseRemoteDeploymentManager : IRemoteDeploymentManager
|
||||
{
|
||||
/// <summary>
|
||||
/// The header comment that begins every deployment message comment/note.
|
||||
/// </summary>
|
||||
public const string DeploymentMsgHeaderStart = "<!-- tgs_test_merge_comment -->";
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Api.Models.Instance"/> for the <see cref="BaseRemoteDeploymentManager"/>.
|
||||
/// </summary>
|
||||
@@ -66,36 +71,28 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote
|
||||
if (repositorySettings.AccessToken == null)
|
||||
return;
|
||||
|
||||
var deployedRevisionInformation = compileJob.RevisionInformation;
|
||||
if ((previousRevisionInformation != null && previousRevisionInformation.CommitSha == deployedRevisionInformation.CommitSha)
|
||||
var revisionInformation = compileJob.RevisionInformation;
|
||||
if ((previousRevisionInformation != null && previousRevisionInformation.CommitSha == revisionInformation.CommitSha)
|
||||
|| !repositorySettings.PostTestMergeComment!.Value)
|
||||
return;
|
||||
|
||||
previousRevisionInformation ??= new RevisionInformation();
|
||||
previousRevisionInformation.ActiveTestMerges ??= new List<RevInfoTestMerge>();
|
||||
var previousTestMerges = (IEnumerable<RevInfoTestMerge>?)previousRevisionInformation?.ActiveTestMerges ?? Enumerable.Empty<RevInfoTestMerge>();
|
||||
var currentTestMerges = (IEnumerable<RevInfoTestMerge>?)revisionInformation.ActiveTestMerges ?? Enumerable.Empty<RevInfoTestMerge>();
|
||||
|
||||
deployedRevisionInformation.ActiveTestMerges ??= new List<RevInfoTestMerge>();
|
||||
|
||||
// added prs
|
||||
var addedTestMerges = deployedRevisionInformation
|
||||
.ActiveTestMerges
|
||||
// determine what TMs were changed and how
|
||||
var addedTestMerges = currentTestMerges
|
||||
.Select(x => x.TestMerge)
|
||||
.Where(x => !previousRevisionInformation
|
||||
.ActiveTestMerges
|
||||
.Where(x => !previousTestMerges
|
||||
.Any(y => y.TestMerge.Number == x.Number))
|
||||
.ToList();
|
||||
var removedTestMerges = previousRevisionInformation
|
||||
.ActiveTestMerges
|
||||
var removedTestMerges = previousTestMerges
|
||||
.Select(x => x.TestMerge)
|
||||
.Where(x => !deployedRevisionInformation
|
||||
.ActiveTestMerges
|
||||
.Where(x => !currentTestMerges
|
||||
.Any(y => y.TestMerge.Number == x.Number))
|
||||
.ToList();
|
||||
var updatedTestMerges = deployedRevisionInformation
|
||||
.ActiveTestMerges
|
||||
var updatedTestMerges = currentTestMerges
|
||||
.Select(x => x.TestMerge)
|
||||
.Where(x => previousRevisionInformation
|
||||
.ActiveTestMerges
|
||||
.Where(x => previousTestMerges
|
||||
.Any(y => y.TestMerge.Number == x.Number))
|
||||
.ToList();
|
||||
|
||||
@@ -133,7 +130,12 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote
|
||||
repositorySettings,
|
||||
repoOwner,
|
||||
repoName,
|
||||
"#### Test Merge Removed",
|
||||
FormatTestMergeRemoval(
|
||||
repositorySettings,
|
||||
compileJob,
|
||||
removedTestMerge,
|
||||
repoOwner,
|
||||
repoName),
|
||||
removedTestMerge.Number,
|
||||
cancellationToken);
|
||||
tasks.Add(removeCommentTask);
|
||||
@@ -253,6 +255,22 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote
|
||||
string remoteRepositoryName,
|
||||
bool updated);
|
||||
|
||||
/// <summary>
|
||||
/// Formats a comment for a given <paramref name="testMerge"/> removal.
|
||||
/// </summary>
|
||||
/// <param name="repositorySettings">The <see cref="RepositorySettings"/> to use.</param>
|
||||
/// <param name="compileJob">The test merge's <see cref="CompileJob"/>.</param>
|
||||
/// <param name="testMerge">The <see cref="TestMerge"/>.</param>
|
||||
/// <param name="remoteRepositoryOwner">The <see cref="Api.Models.Internal.IGitRemoteInformation.RemoteRepositoryOwner"/>.</param>
|
||||
/// <param name="remoteRepositoryName">The <see cref="Api.Models.Internal.IGitRemoteInformation.RemoteRepositoryName"/>.</param>
|
||||
/// <returns>A formatted <see cref="string"/> for posting a informative comment about the <paramref name="testMerge"/> removal.</returns>
|
||||
protected abstract string FormatTestMergeRemoval(
|
||||
RepositorySettings repositorySettings,
|
||||
CompileJob compileJob,
|
||||
TestMerge testMerge,
|
||||
string remoteRepositoryOwner,
|
||||
string remoteRepositoryName);
|
||||
|
||||
/// <summary>
|
||||
/// Create a comment of a given <paramref name="testMergeNumber"/>'s source.
|
||||
/// </summary>
|
||||
|
||||
+38
-15
@@ -282,7 +282,17 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote
|
||||
|
||||
try
|
||||
{
|
||||
await gitHubService.CommentOnIssue(remoteRepositoryOwner, remoteRepositoryName, comment, testMergeNumber, cancellationToken);
|
||||
string header = String.Format(CultureInfo.InvariantCulture, "{1}{0}## Test merge deployment history:{0}{0}", Environment.NewLine, DeploymentMsgHeaderStart);
|
||||
|
||||
var existingComment = await gitHubService.GetExistingCommentOnIssue(remoteRepositoryOwner, remoteRepositoryName, DeploymentMsgHeaderStart, testMergeNumber, cancellationToken);
|
||||
if (existingComment != null)
|
||||
{
|
||||
await gitHubService.AppendCommentOnIssue(remoteRepositoryOwner, remoteRepositoryName, comment, existingComment, cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
await gitHubService.CommentOnIssue(remoteRepositoryOwner, remoteRepositoryName, header + comment, testMergeNumber, cancellationToken);
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||
{
|
||||
@@ -299,30 +309,43 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote
|
||||
string remoteRepositoryName,
|
||||
bool updated) => String.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"#### Test Merge {4}{0}{0}<details><summary>Details</summary>{0}{0}##### Server Instance{0}{5}{1}{0}{0}##### Revision{0}Origin: {6}{0}Pull Request: {2}{0}Server: {7}{3}{8}{0}</details>",
|
||||
Environment.NewLine,
|
||||
"<details><summary>Test Merge {4} @ {8}:</summary>{0}{0}##### Server Instance{0}{5}{1}{0}{0}##### Revision{0}Origin: {6}{0}Pull Request: {2}{0}Server: {7}{3}{0}</details>{0}",
|
||||
Environment.NewLine, // 0
|
||||
repositorySettings.ShowTestMergeCommitters!.Value
|
||||
? String.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"{0}{0}##### Merged By{0}{1}",
|
||||
Environment.NewLine,
|
||||
testMerge.MergedBy!.Name)
|
||||
: String.Empty,
|
||||
testMerge.TargetCommitSha,
|
||||
testMerge.Comment != null
|
||||
? String.Format(
|
||||
: String.Empty, // 1
|
||||
testMerge.TargetCommitSha, // 2
|
||||
String.IsNullOrEmpty(testMerge.Comment)
|
||||
? String.Empty
|
||||
: String.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"{0}{0}##### Comment{0}{1}",
|
||||
Environment.NewLine,
|
||||
testMerge.Comment)
|
||||
: String.Empty,
|
||||
updated ? "Updated" : "Deployed",
|
||||
Metadata.Name,
|
||||
compileJob.RevisionInformation.OriginCommitSha,
|
||||
compileJob.RevisionInformation.CommitSha,
|
||||
testMerge.Comment), // 3
|
||||
updated ? "Updated" : "Deployed", // 4
|
||||
compileJob.GitHubDeploymentId.HasValue
|
||||
? $"{Environment.NewLine}[GitHub Deployments](https://github.com/{remoteRepositoryOwner}/{remoteRepositoryName}/deployments/activity_log?environment=TGS%3A+{Metadata.Name!.Replace(" ", "+", StringComparison.Ordinal)})"
|
||||
: String.Empty);
|
||||
? $"{Environment.NewLine}[{Metadata.Name}](https://github.com/{remoteRepositoryOwner}/{remoteRepositoryName}/deployments/activity_log?environment=TGS%3A+{Metadata.Name!.Replace(" ", "+", StringComparison.Ordinal)})"
|
||||
: Metadata.Name, // 5
|
||||
compileJob.RevisionInformation.OriginCommitSha, // 6
|
||||
compileJob.RevisionInformation.CommitSha, // 7
|
||||
compileJob.Job.StartedAt); // 8
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string FormatTestMergeRemoval(
|
||||
RepositorySettings repositorySettings,
|
||||
CompileJob compileJob,
|
||||
TestMerge testMerge,
|
||||
string remoteRepositoryOwner,
|
||||
string remoteRepositoryName) => String.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"<details><summary>Test Merge Removed @ {2}:</summary>{0}{0}##### Server Instance{0}{1}{0}</details>{0}",
|
||||
Environment.NewLine, // 0
|
||||
Metadata.Name, // 1
|
||||
compileJob.Job.StartedAt); // 2
|
||||
|
||||
/// <summary>
|
||||
/// Update the deployment for a given <paramref name="compileJob"/>.
|
||||
|
||||
+141
-52
@@ -6,13 +6,13 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using GitLabApiClient;
|
||||
using GitLabApiClient.Models.MergeRequests.Responses;
|
||||
using GitLabApiClient.Models.Notes.Requests;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
using StrawberryShake;
|
||||
|
||||
using Tgstation.Server.Host.Components.Repository;
|
||||
using Tgstation.Server.Host.Models;
|
||||
using Tgstation.Server.Host.Utils.GitLab.GraphQL;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Deployment.Remote
|
||||
{
|
||||
@@ -52,49 +52,78 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote
|
||||
return Array.Empty<TestMerge>();
|
||||
}
|
||||
|
||||
var client = repositorySettings.AccessToken != null
|
||||
? new GitLabClient(GitLabRemoteFeatures.GitLabUrl, repositorySettings.AccessToken)
|
||||
: new GitLabClient(GitLabRemoteFeatures.GitLabUrl);
|
||||
var newList = revisionInformation.ActiveTestMerges.Select(x => x.TestMerge).ToList();
|
||||
|
||||
var tasks = revisionInformation
|
||||
.ActiveTestMerges
|
||||
.Select(x => client
|
||||
.MergeRequests
|
||||
.GetAsync(
|
||||
$"{repository.RemoteRepositoryOwner}/{repository.RemoteRepositoryName}",
|
||||
x.TestMerge.Number)
|
||||
.WaitAsync(cancellationToken));
|
||||
await using var client = await GraphQLGitLabClientFactory.CreateClient(repositorySettings.AccessToken);
|
||||
IOperationResult<IGetMergeRequestsResult> operationResult;
|
||||
try
|
||||
{
|
||||
await Task.WhenAll(tasks);
|
||||
operationResult = await client.GraphQL.GetMergeRequests.ExecuteAsync(
|
||||
$"{repository.RemoteRepositoryOwner}/{repository.RemoteRepositoryName}",
|
||||
revisionInformation.ActiveTestMerges.Select(revInfoTestMerge => revInfoTestMerge.TestMerge.Number.ToString(CultureInfo.InvariantCulture)).ToList(),
|
||||
cancellationToken);
|
||||
|
||||
operationResult.EnsureNoErrors();
|
||||
}
|
||||
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||
{
|
||||
Logger.LogWarning(ex, "Merge requests update check failed!");
|
||||
return newList;
|
||||
}
|
||||
|
||||
var newList = revisionInformation.ActiveTestMerges.Select(x => x.TestMerge).ToList();
|
||||
|
||||
MergeRequest? lastMerged = null;
|
||||
async ValueTask CheckRemoveMR(Task<MergeRequest> task)
|
||||
var data = operationResult.Data?.Project?.MergeRequests?.Nodes;
|
||||
if (data == null)
|
||||
{
|
||||
var mergeRequest = await task;
|
||||
Logger.LogWarning("GitLab MergeRequests check returned null!");
|
||||
return newList;
|
||||
}
|
||||
|
||||
async ValueTask CheckRemoveMR(IGetMergeRequests_Project_MergeRequests_Nodes? mergeRequest)
|
||||
{
|
||||
if (mergeRequest == null)
|
||||
{
|
||||
Logger.LogWarning("GitLab MergeRequest node was null!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (mergeRequest.State != MergeRequestState.Merged)
|
||||
return;
|
||||
|
||||
// We don't just assume, actually check the repo contains the merge commit.
|
||||
if (await repository.CommittishIsParent(mergeRequest.MergeCommitSha, cancellationToken))
|
||||
var mergeCommitSha = mergeRequest.MergeCommitSha;
|
||||
if (mergeCommitSha == null)
|
||||
{
|
||||
if (lastMerged == null || lastMerged.ClosedAt < mergeRequest.ClosedAt)
|
||||
lastMerged = mergeRequest;
|
||||
Logger.LogWarning("MergeRequest #{id} had no MergeCommitSha!", mergeRequest.Iid);
|
||||
return;
|
||||
}
|
||||
|
||||
var closedAtStr = mergeRequest.ClosedAt;
|
||||
if (closedAtStr == null)
|
||||
{
|
||||
Logger.LogWarning("MergeRequest #{id} had no ClosedAt!", mergeRequest.Iid);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!DateTimeOffset.TryParseExact(closedAtStr, "O", CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out DateTimeOffset closedAt))
|
||||
{
|
||||
Logger.LogWarning("MergeRequest #{id} had invalid ClosedAt: {closedAt}", mergeRequest.Iid, closedAtStr);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Int64.TryParse(mergeRequest.Iid, out long number))
|
||||
{
|
||||
Logger.LogWarning("MergeRequest #{id} is non-numeric!", mergeRequest.Iid);
|
||||
return;
|
||||
}
|
||||
|
||||
// We don't just assume, actually check the repo contains the merge commit.
|
||||
if (await repository.CommittishIsParent(mergeCommitSha, cancellationToken))
|
||||
newList.Remove(
|
||||
newList.First(
|
||||
potential => potential.Number == mergeRequest.Id));
|
||||
}
|
||||
potential => potential.Number == number));
|
||||
}
|
||||
|
||||
foreach (var prTask in tasks)
|
||||
await CheckRemoveMR(prTask);
|
||||
foreach (var mergeRequest in data)
|
||||
await CheckRemoveMR(mergeRequest);
|
||||
|
||||
return newList;
|
||||
}
|
||||
@@ -131,23 +160,69 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote
|
||||
int testMergeNumber,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var client = repositorySettings.AccessToken != null
|
||||
? new GitLabClient(GitLabRemoteFeatures.GitLabUrl, repositorySettings.AccessToken)
|
||||
: new GitLabClient(GitLabRemoteFeatures.GitLabUrl);
|
||||
|
||||
await using var client = await GraphQLGitLabClientFactory.CreateClient(repositorySettings.AccessToken);
|
||||
try
|
||||
{
|
||||
await client
|
||||
.MergeRequests
|
||||
.CreateNoteAsync(
|
||||
$"{remoteRepositoryOwner}/{remoteRepositoryName}",
|
||||
testMergeNumber,
|
||||
new CreateMergeRequestNoteRequest(comment))
|
||||
.WaitAsync(cancellationToken);
|
||||
string header = String.Format(CultureInfo.InvariantCulture, "{1}{0}## Test merge deployment history:{0}{0}", Environment.NewLine, DeploymentMsgHeaderStart);
|
||||
|
||||
// Try to find an existing note
|
||||
var notesQueryResult = await client.GraphQL.GetMergeRequestNotes.ExecuteAsync(
|
||||
$"{remoteRepositoryOwner}/{remoteRepositoryName}",
|
||||
testMergeNumber.ToString(CultureInfo.InvariantCulture),
|
||||
cancellationToken);
|
||||
|
||||
notesQueryResult.EnsureNoErrors();
|
||||
|
||||
var mergeRequest = notesQueryResult.Data?.Project?.MergeRequest;
|
||||
if (mergeRequest == null)
|
||||
{
|
||||
Logger.LogWarning("GitLab GetMergeRequestNotes mergeRequest returned null!");
|
||||
return;
|
||||
}
|
||||
|
||||
var comments = mergeRequest.Notes?.Nodes;
|
||||
IGetMergeRequestNotes_Project_MergeRequest_Notes_Nodes? existingComment = null;
|
||||
if (comments != null)
|
||||
{
|
||||
for (int i = comments.Count - 1; i > -1; i--)
|
||||
{
|
||||
var currentComment = comments[i];
|
||||
if (currentComment?.Author?.Username == repositorySettings.AccessUser && (currentComment?.Body?.StartsWith(DeploymentMsgHeaderStart) ?? false))
|
||||
{
|
||||
if (currentComment.Body.Length > 987856)
|
||||
{ // Limit should be 999,999 so we'll leave a 12,143 buffer
|
||||
break;
|
||||
}
|
||||
|
||||
existingComment = currentComment;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Either amend or create the note
|
||||
if (existingComment != null)
|
||||
{
|
||||
var noteModificationResult = await client.GraphQL.ModifyNote.ExecuteAsync(
|
||||
existingComment.Id,
|
||||
existingComment.Body + comment,
|
||||
cancellationToken);
|
||||
|
||||
notesQueryResult.EnsureNoErrors();
|
||||
}
|
||||
else
|
||||
{
|
||||
var noteCreationResult = await client.GraphQL.CreateNote.ExecuteAsync(
|
||||
mergeRequest.Id,
|
||||
header + comment,
|
||||
cancellationToken);
|
||||
|
||||
noteCreationResult.EnsureNoErrors();
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||
{
|
||||
Logger.LogWarning(ex, "Error posting GitHub comment!");
|
||||
Logger.LogWarning(ex, "Error posting GitLab comment!");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,26 +235,40 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote
|
||||
string remoteRepositoryName,
|
||||
bool updated) => String.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"#### Test Merge {4}{0}{0}##### Server Instance{0}{5}{1}{0}{0}##### Revision{0}Origin: {6}{0}Merge Request: {2}{0}Server: {7}{3}",
|
||||
Environment.NewLine,
|
||||
"<details><summary>Test Merge {4} @ {8}</summary>{0}{0}##### Server Instance{0}{5}{1}{0}{0}##### Revision{0}Origin: {6}{0}Merge Request: {2}{0}Server: {7}{3}</details>{0}",
|
||||
Environment.NewLine, // 0
|
||||
repositorySettings.ShowTestMergeCommitters!.Value
|
||||
? String.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"{0}{0}##### Merged By{0}{1}",
|
||||
Environment.NewLine,
|
||||
testMerge.MergedBy!.Name)
|
||||
: String.Empty,
|
||||
testMerge.TargetCommitSha,
|
||||
testMerge.Comment != null
|
||||
? String.Format(
|
||||
: String.Empty, // 1
|
||||
testMerge.TargetCommitSha, // 2
|
||||
String.IsNullOrEmpty(testMerge.Comment)
|
||||
? String.Empty
|
||||
: String.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"{0}{0}##### Comment{0}{1}",
|
||||
Environment.NewLine,
|
||||
testMerge.Comment)
|
||||
: String.Empty,
|
||||
updated ? "Updated" : "Deployed",
|
||||
Metadata.Name,
|
||||
compileJob.RevisionInformation.OriginCommitSha,
|
||||
compileJob.RevisionInformation.CommitSha);
|
||||
testMerge.Comment), // 3
|
||||
updated ? "Updated" : "Deployed", // 4
|
||||
Metadata.Name, // 5
|
||||
compileJob.RevisionInformation.OriginCommitSha, // 6
|
||||
compileJob.RevisionInformation.CommitSha, // 7
|
||||
compileJob.Job.StartedAt); // 8
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string FormatTestMergeRemoval(
|
||||
RepositorySettings repositorySettings,
|
||||
CompileJob compileJob,
|
||||
TestMerge testMerge,
|
||||
string remoteRepositoryOwner,
|
||||
string remoteRepositoryName) => String.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"<details><summary>Test Merge Removed @ {2}:</summary>{0}{0}##### Server Instance{0}{1}{0}</details>{0}",
|
||||
Environment.NewLine, // 0
|
||||
Metadata.Name, // 1
|
||||
compileJob.Job.StartedAt); // 2
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +65,15 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote
|
||||
bool updated)
|
||||
=> String.Empty;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string FormatTestMergeRemoval(
|
||||
Models.RepositorySettings repositorySettings,
|
||||
Models.CompileJob compileJob,
|
||||
TestMerge testMerge,
|
||||
string remoteRepositoryOwner,
|
||||
string remoteRepositoryName)
|
||||
=> String.Empty;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override ValueTask MarkInactiveImpl(Models.CompileJob compileJob, CancellationToken cancellationToken) => ValueTask.CompletedTask;
|
||||
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using GitLabApiClient;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
using StrawberryShake;
|
||||
|
||||
using Tgstation.Server.Api.Models;
|
||||
using Tgstation.Server.Host.Utils.GitLab.GraphQL;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Repository
|
||||
{
|
||||
@@ -45,30 +48,25 @@ namespace Tgstation.Server.Host.Components.Repository
|
||||
RepositorySettings repositorySettings,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var client = repositorySettings.AccessToken != null
|
||||
? new GitLabClient(GitLabUrl, repositorySettings.AccessToken)
|
||||
: new GitLabClient(GitLabUrl);
|
||||
|
||||
await using var client = await GraphQLGitLabClientFactory.CreateClient(repositorySettings.AccessToken);
|
||||
try
|
||||
{
|
||||
var mr = await client
|
||||
.MergeRequests
|
||||
.GetAsync($"{RemoteRepositoryOwner}/{RemoteRepositoryName}", parameters.Number)
|
||||
.WaitAsync(cancellationToken);
|
||||
var operationResult = await client.GraphQL.GetMergeRequest.ExecuteAsync(
|
||||
$"{RemoteRepositoryOwner}/{RemoteRepositoryName}",
|
||||
parameters.Number.ToString(CultureInfo.InvariantCulture),
|
||||
cancellationToken);
|
||||
|
||||
var revisionToUse = parameters.TargetCommitSha == null
|
||||
|| mr.Sha.StartsWith(parameters.TargetCommitSha, StringComparison.OrdinalIgnoreCase)
|
||||
? mr.Sha
|
||||
: parameters.TargetCommitSha;
|
||||
operationResult.EnsureNoErrors();
|
||||
var mr = operationResult.Data?.Project?.MergeRequest ?? throw new InvalidOperationException("GitLab MergeRequest check returned null!");
|
||||
|
||||
return new Models.TestMerge
|
||||
{
|
||||
Author = mr.Author.Username,
|
||||
Author = mr.Author?.Username,
|
||||
BodyAtMerge = mr.Description,
|
||||
TitleAtMerge = mr.Title,
|
||||
Comment = parameters.Comment,
|
||||
Number = parameters.Number,
|
||||
TargetCommitSha = mr.Sha,
|
||||
TargetCommitSha = mr.DiffHeadSha,
|
||||
Url = mr.WebUrl,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
using System;
|
||||
|
||||
using Microsoft.IdentityModel.JsonWebTokens;
|
||||
|
||||
namespace Tgstation.Server.Host.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// Configuration for an OpenID Connect provider.
|
||||
/// </summary>
|
||||
public sealed class OidcConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="Uri"/> containing the .well-known endpoint for the provider.
|
||||
/// </summary>
|
||||
public Uri? Authority { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The OIDC client ID.
|
||||
/// </summary>
|
||||
public string? ClientId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The OIDC client secret.
|
||||
/// </summary>
|
||||
public string? ClientSecret { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The provider's name as it should be displayed to the user.
|
||||
/// </summary>
|
||||
public string? FriendlyName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The path to return to once OIDC authentication is complete. On success the "code" and "state" query strings will be set, containing a TGS token and "oidc.(scheme key)" strings respectively. On failure the "error" query string will be set with a user readable error message.
|
||||
/// </summary>
|
||||
public string? ReturnPath { get; set; } = "/app";
|
||||
|
||||
/// <summary>
|
||||
/// Colour that should be used to theme this OIDC provider.
|
||||
/// </summary>
|
||||
public string? ThemeColour { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Image URL that should be used to theme this OIDC provider.
|
||||
/// </summary>
|
||||
public Uri? ThemeIconUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of the claim used to set the user's name.
|
||||
/// </summary>
|
||||
public string UsernameClaim { get; set; } = JwtRegisteredClaimNames.PreferredUsername;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
@@ -62,6 +63,11 @@ namespace Tgstation.Server.Host.Configuration
|
||||
/// </summary>
|
||||
public string? CustomTokenSigningKeyBase64 { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If OIDC strict mode should be enabled. This mode enforces the existence of at least one <see cref="OpenIDConnect"/>ion and allows users to register using them. Users must have the tgstation-server.group_id role set to a valid TGS group ID to login. This mode disables regular and OAuth login methods.
|
||||
/// </summary>
|
||||
public bool OidcStrictMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// OAuth provider settings.
|
||||
/// </summary>
|
||||
@@ -87,5 +93,23 @@ namespace Tgstation.Server.Host.Configuration
|
||||
/// Backing field for <see cref="OAuth"/>.
|
||||
/// </summary>
|
||||
IDictionary<OAuthProvider, OAuthConfiguration>? oAuth;
|
||||
|
||||
/// <summary>
|
||||
/// OIDC provider settings keyed by scheme name.
|
||||
/// </summary>
|
||||
public IDictionary<string, OidcConfiguration>? OpenIDConnect { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Get the <see cref="OidcProviderInfo"/>s from the <see cref="SecurityConfiguration"/>.
|
||||
/// </summary>
|
||||
/// <returns>An <see cref="IEnumerable{T}"/> of <see cref="OidcProviderInfo"/>s.</returns>
|
||||
public IEnumerable<OidcProviderInfo> OidcProviderInfos()
|
||||
=> OpenIDConnect?.Select(oidcConfig => new OidcProviderInfo
|
||||
{
|
||||
SchemeKey = oidcConfig.Key,
|
||||
FriendlyName = oidcConfig.Value.FriendlyName ?? oidcConfig.Key,
|
||||
ThemeColour = oidcConfig.Value.ThemeColour,
|
||||
ThemeIconUrl = oidcConfig.Value.ThemeIconUrl,
|
||||
}) ?? Enumerable.Empty<OidcProviderInfo>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +70,11 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// </summary>
|
||||
readonly GeneralConfiguration generalConfiguration;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="SecurityConfiguration"/> for the <see cref="ApiRootController"/>.
|
||||
/// </summary>
|
||||
readonly SecurityConfiguration securityConfiguration;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ApiRootController"/> class.
|
||||
/// </summary>
|
||||
@@ -81,6 +86,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// <param name="swarmService">The value of <see cref="swarmService"/>.</param>
|
||||
/// <param name="serverControl">The value of <see cref="serverControl"/>.</param>
|
||||
/// <param name="generalConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="generalConfiguration"/>.</param>
|
||||
/// <param name="securityConfigurationOptions">The <see cref="IOptionsSnapshot{TOptions}"/> containing the value of <see cref="securityConfiguration"/>.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="ApiController"/>.</param>
|
||||
/// <param name="apiHeadersProvider">The <see cref="IApiHeadersProvider"/> for the <see cref="ApiController"/>.</param>
|
||||
/// <param name="loginAuthority">The value of <see cref="loginAuthority"/>.</param>
|
||||
@@ -93,6 +99,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
ISwarmService swarmService,
|
||||
IServerControl serverControl,
|
||||
IOptions<GeneralConfiguration> generalConfigurationOptions,
|
||||
IOptionsSnapshot<SecurityConfiguration> securityConfigurationOptions,
|
||||
ILogger<ApiRootController> logger,
|
||||
IApiHeadersProvider apiHeadersProvider,
|
||||
IRestAuthorityInvoker<ILoginAuthority> loginAuthority)
|
||||
@@ -109,6 +116,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
this.swarmService = swarmService ?? throw new ArgumentNullException(nameof(swarmService));
|
||||
this.serverControl = serverControl ?? throw new ArgumentNullException(nameof(serverControl));
|
||||
generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
|
||||
securityConfiguration = securityConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(securityConfigurationOptions));
|
||||
this.loginAuthority = loginAuthority ?? throw new ArgumentNullException(nameof(loginAuthority));
|
||||
}
|
||||
|
||||
@@ -162,7 +170,9 @@ namespace Tgstation.Server.Host.Controllers
|
||||
?.Select(swarmServerInfo => new SwarmServerResponse(swarmServerInfo))
|
||||
.ToList(),
|
||||
OAuthProviderInfos = oAuthProviders.ProviderInfos(),
|
||||
OidcProviderInfos = securityConfiguration.OidcProviderInfos().ToList(),
|
||||
UpdateInProgress = serverControl.UpdateInProgress,
|
||||
OidcStrictMode = securityConfiguration.OidcStrictMode,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -7,12 +7,12 @@ using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using GitLabApiClient;
|
||||
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
using StrawberryShake;
|
||||
|
||||
using Tgstation.Server.Api;
|
||||
using Tgstation.Server.Api.Models;
|
||||
using Tgstation.Server.Api.Models.Request;
|
||||
@@ -27,6 +27,7 @@ using Tgstation.Server.Host.Models;
|
||||
using Tgstation.Server.Host.Security;
|
||||
using Tgstation.Server.Host.Utils;
|
||||
using Tgstation.Server.Host.Utils.GitHub;
|
||||
using Tgstation.Server.Host.Utils.GitLab.GraphQL;
|
||||
|
||||
namespace Tgstation.Server.Host.Controllers
|
||||
{
|
||||
@@ -585,70 +586,84 @@ namespace Tgstation.Server.Host.Controllers
|
||||
switch (remoteFeatures.RemoteGitProvider!.Value)
|
||||
{
|
||||
case RemoteGitProvider.GitHub:
|
||||
var gitHubClient = await gitHubClientFactory.CreateClientForRepository(
|
||||
{
|
||||
var gitHubClient = await gitHubClientFactory.CreateClientForRepository(
|
||||
model.AccessToken,
|
||||
new RepositoryIdentifier(
|
||||
remoteFeatures.RemoteRepositoryOwner!,
|
||||
remoteFeatures.RemoteRepositoryName!),
|
||||
cancellationToken);
|
||||
if (gitHubClient == null)
|
||||
{
|
||||
return this.StatusCode(HttpStatusCode.FailedDependency, new ErrorMessageResponse(ErrorCode.RemoteApiError)
|
||||
if (gitHubClient == null)
|
||||
{
|
||||
AdditionalData = "GitHub authentication failed!",
|
||||
});
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
string username;
|
||||
if (!model.AccessToken.StartsWith(Api.Models.RepositorySettings.TgsAppPrivateKeyPrefix))
|
||||
{
|
||||
var user = await gitHubClient.User.Current();
|
||||
username = user.Login;
|
||||
}
|
||||
else
|
||||
{
|
||||
// we literally need to app auth again to get the damn bot username
|
||||
var appClient = gitHubClientFactory.CreateAppClient(model.AccessToken)!;
|
||||
var app = await appClient.GitHubApps.GetCurrent();
|
||||
username = app.Name;
|
||||
return this.StatusCode(HttpStatusCode.FailedDependency, new ErrorMessageResponse(ErrorCode.RemoteApiError)
|
||||
{
|
||||
AdditionalData = "GitHub authentication failed!",
|
||||
});
|
||||
}
|
||||
|
||||
if (username != model.AccessUser)
|
||||
return Conflict(new ErrorMessageResponse(ErrorCode.RepoTokenUsernameMismatch));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return this.StatusCode(HttpStatusCode.FailedDependency, new ErrorMessageResponse(ErrorCode.RemoteApiError)
|
||||
try
|
||||
{
|
||||
AdditionalData = $"GitHub Authentication Failure: {ex.Message}",
|
||||
});
|
||||
string username;
|
||||
if (!model.AccessToken.StartsWith(Api.Models.RepositorySettings.TgsAppPrivateKeyPrefix))
|
||||
{
|
||||
var user = await gitHubClient.User.Current();
|
||||
username = user.Login;
|
||||
}
|
||||
else
|
||||
{
|
||||
// we literally need to app auth again to get the damn bot username
|
||||
var appClient = gitHubClientFactory.CreateAppClient(model.AccessToken)!;
|
||||
var app = await appClient.GitHubApps.GetCurrent();
|
||||
username = app.Name;
|
||||
}
|
||||
|
||||
if (username != model.AccessUser)
|
||||
return Conflict(new ErrorMessageResponse(ErrorCode.RepoTokenUsernameMismatch));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return this.StatusCode(HttpStatusCode.FailedDependency, new ErrorMessageResponse(ErrorCode.RemoteApiError)
|
||||
{
|
||||
AdditionalData = $"GitHub Authentication Failure: {ex.Message}",
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
case RemoteGitProvider.GitLab:
|
||||
// need to abstract this eventually
|
||||
var gitLabClient = new GitLabClient(GitLabRemoteFeatures.GitLabUrl, model.AccessToken);
|
||||
try
|
||||
{
|
||||
var user = await gitLabClient.Users.GetCurrentSessionAsync();
|
||||
if (user.Username != model.AccessUser)
|
||||
return Conflict(new ErrorMessageResponse(ErrorCode.RepoTokenUsernameMismatch));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return this.StatusCode(HttpStatusCode.FailedDependency, new ErrorMessageResponse(ErrorCode.RemoteApiError)
|
||||
// need to abstract this eventually
|
||||
await using var gitLabClient = await GraphQLGitLabClientFactory.CreateClient(model.AccessToken);
|
||||
try
|
||||
{
|
||||
AdditionalData = $"GitLab Authentication Failure: {ex.Message}",
|
||||
});
|
||||
var operationResult = await gitLabClient.GraphQL.GetCurrentUser.ExecuteAsync(cancellationToken);
|
||||
|
||||
operationResult.EnsureNoErrors();
|
||||
|
||||
var user = operationResult.Data?.CurrentUser;
|
||||
if (user == null || user.Username != model.AccessUser)
|
||||
{
|
||||
return Conflict(new ErrorMessageResponse(ErrorCode.RepoTokenUsernameMismatch));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return this.StatusCode(HttpStatusCode.FailedDependency, new ErrorMessageResponse(ErrorCode.RemoteApiError)
|
||||
{
|
||||
AdditionalData = $"GitLab Authentication Failure: {ex.Message}",
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
case RemoteGitProvider.Unknown:
|
||||
default:
|
||||
Logger.LogWarning("RemoteGitProvider is {provider}, no auth check implemented!", remoteFeatures.RemoteGitProvider.Value);
|
||||
break;
|
||||
{
|
||||
Logger.LogWarning("RemoteGitProvider is {provider}, no auth check implemented!", remoteFeatures.RemoteGitProvider.Value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Frozen;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
|
||||
using Cyberboss.AspNetCore.AsyncInitializer;
|
||||
|
||||
@@ -13,7 +14,9 @@ using HotChocolate.Subscriptions;
|
||||
using HotChocolate.Types;
|
||||
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Cors.Infrastructure;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
@@ -27,6 +30,7 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
@@ -121,6 +125,14 @@ namespace Tgstation.Server.Host.Core
|
||||
services.AddSingleton<IWatchdogFactory, TSystemWatchdogFactory>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the OIDC authentication scheme name for a given <paramref name="schemeKey"/>.
|
||||
/// </summary>
|
||||
/// <param name="schemeKey">The scheme key for the <see cref="OidcConfiguration"/>.</param>
|
||||
/// <returns>The authentication scheme name.</returns>
|
||||
static string GetOidcScheme(string schemeKey)
|
||||
=> AuthenticationContextFactory.OpenIDConnectAuthenticationSchemePrefix + schemeKey;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Application"/> class.
|
||||
/// </summary>
|
||||
@@ -239,7 +251,7 @@ namespace Tgstation.Server.Host.Core
|
||||
postSetupServices.FileLoggingConfiguration);
|
||||
|
||||
// configure authentication pipeline
|
||||
ConfigureAuthenticationPipeline(services);
|
||||
ConfigureAuthenticationPipeline(services, postSetupServices.SecurityConfiguration);
|
||||
|
||||
// add mvc, configure the json serializer settings
|
||||
var jsonVersionConverterList = new List<JsonConverter>
|
||||
@@ -316,9 +328,21 @@ namespace Tgstation.Server.Host.Core
|
||||
.AddScoped<GraphQL.Subscriptions.ITopicEventReceiver, ShutdownAwareTopicEventReceiver>()
|
||||
.AddGraphQLServer()
|
||||
.AddAuthorization(
|
||||
options => options.AddPolicy(
|
||||
TgsAuthorizeAttribute.PolicyName,
|
||||
builder => builder.RequireRole(TgsAuthorizeAttribute.UserEnabledRole)))
|
||||
options =>
|
||||
{
|
||||
options.AddPolicy(
|
||||
TgsAuthorizeAttribute.PolicyName,
|
||||
builder => builder
|
||||
.RequireAuthenticatedUser()
|
||||
.RequireRole(TgsAuthorizeAttribute.UserEnabledRole));
|
||||
options.AddPolicy(
|
||||
"testingasdf",
|
||||
builder =>
|
||||
{
|
||||
builder.RequireAuthenticatedUser();
|
||||
builder.AuthenticationSchemes.Add(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||
});
|
||||
})
|
||||
.ModifyOptions(options =>
|
||||
{
|
||||
options.EnsureAllNodesCanBeResolved = true;
|
||||
@@ -548,6 +572,7 @@ namespace Tgstation.Server.Host.Core
|
||||
/// <param name="controlPanelConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the <see cref="ControlPanelConfiguration"/> to use.</param>
|
||||
/// <param name="generalConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the <see cref="GeneralConfiguration"/> to use.</param>
|
||||
/// <param name="databaseConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the <see cref="DatabaseConfiguration"/> to use.</param>
|
||||
/// <param name="securityConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the <see cref="SecurityConfiguration"/> to use.</param>
|
||||
/// <param name="swarmConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the <see cref="SwarmConfiguration"/> to use.</param>
|
||||
/// <param name="internalConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the <see cref="InternalConfiguration"/> to use.</param>
|
||||
/// <param name="logger">The <see cref="Microsoft.Extensions.Logging.ILogger"/> for the <see cref="Application"/>.</param>
|
||||
@@ -560,6 +585,7 @@ namespace Tgstation.Server.Host.Core
|
||||
IOptions<ControlPanelConfiguration> controlPanelConfigurationOptions,
|
||||
IOptions<GeneralConfiguration> generalConfigurationOptions,
|
||||
IOptions<DatabaseConfiguration> databaseConfigurationOptions,
|
||||
IOptions<SecurityConfiguration> securityConfigurationOptions,
|
||||
IOptions<SwarmConfiguration> swarmConfigurationOptions,
|
||||
IOptions<InternalConfiguration> internalConfigurationOptions,
|
||||
ILogger<Application> logger)
|
||||
@@ -728,6 +754,20 @@ namespace Tgstation.Server.Host.Core
|
||||
logger.LogTrace("Prometheus disabled");
|
||||
|
||||
endpoints.MapHealthChecks("/health");
|
||||
|
||||
var oidcConfig = securityConfigurationOptions.Value.OpenIDConnect;
|
||||
if (oidcConfig == null)
|
||||
return;
|
||||
|
||||
foreach (var kvp in oidcConfig)
|
||||
endpoints.MapGet(
|
||||
$"/oidc/{kvp.Key}/signin",
|
||||
context => context.ChallengeAsync(
|
||||
GetOidcScheme(kvp.Key),
|
||||
new AuthenticationProperties
|
||||
{
|
||||
RedirectUri = $"/oidc/{kvp.Key}/landing",
|
||||
}));
|
||||
});
|
||||
|
||||
// 404 anything that gets this far
|
||||
@@ -748,7 +788,8 @@ namespace Tgstation.Server.Host.Core
|
||||
/// Configure the <paramref name="services"/> for the authentication pipeline.
|
||||
/// </summary>
|
||||
/// <param name="services">The <see cref="IServiceCollection"/> to configure.</param>
|
||||
void ConfigureAuthenticationPipeline(IServiceCollection services)
|
||||
/// <param name="securityConfiguration">The <see cref="SecurityConfiguration"/>.</param>
|
||||
void ConfigureAuthenticationPipeline(IServiceCollection services, SecurityConfiguration securityConfiguration)
|
||||
{
|
||||
services.AddHttpContextAccessor();
|
||||
services.AddScoped<IApiHeadersProvider, ApiHeadersProvider>();
|
||||
@@ -768,8 +809,11 @@ namespace Tgstation.Server.Host.Core
|
||||
.CurrentAuthenticationContext);
|
||||
services.AddScoped<IClaimsTransformation, AuthenticationContextClaimsTransformation>();
|
||||
|
||||
services
|
||||
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||
var authBuilder = services
|
||||
.AddAuthentication(options =>
|
||||
{
|
||||
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
})
|
||||
.AddJwtBearer(jwtBearerOptions =>
|
||||
{
|
||||
// this line isn't actually run until the first request is made
|
||||
@@ -798,13 +842,104 @@ namespace Tgstation.Server.Host.Core
|
||||
.HttpContext
|
||||
.RequestServices
|
||||
.GetRequiredService<ITokenValidator>()
|
||||
.ValidateToken(
|
||||
.ValidateTgsToken(
|
||||
context,
|
||||
context
|
||||
.HttpContext
|
||||
.RequestAborted),
|
||||
};
|
||||
});
|
||||
|
||||
var oidcConfig = securityConfiguration.OpenIDConnect;
|
||||
if (oidcConfig == null || oidcConfig.Count == 0)
|
||||
return;
|
||||
|
||||
authBuilder.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||
|
||||
foreach (var kvp in oidcConfig)
|
||||
{
|
||||
var configName = kvp.Key;
|
||||
authBuilder
|
||||
.AddOpenIdConnect(
|
||||
GetOidcScheme(configName),
|
||||
options =>
|
||||
{
|
||||
var config = kvp.Value;
|
||||
|
||||
options.Authority = config.Authority?.ToString();
|
||||
options.ClientId = config.ClientId;
|
||||
options.ClientSecret = config.ClientSecret;
|
||||
|
||||
options.Events = new OpenIdConnectEvents
|
||||
{
|
||||
OnRemoteFailure = context =>
|
||||
{
|
||||
context.HandleResponse();
|
||||
context.HttpContext.Response.Redirect($"{config.ReturnPath}?error={HttpUtility.UrlEncode(context.Failure?.Message ?? $"{options.Events.OnRemoteFailure} was called without an {nameof(Exception)}!")}&state=oidc.{HttpUtility.UrlEncode(configName)}");
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
OnTicketReceived = context =>
|
||||
{
|
||||
var services = context
|
||||
.HttpContext
|
||||
.RequestServices;
|
||||
var tokenFactory = services
|
||||
.GetRequiredService<ITokenFactory>();
|
||||
var authenticationContext = services
|
||||
.GetRequiredService<IAuthenticationContext>();
|
||||
context.HandleResponse();
|
||||
context.HttpContext.Response.Redirect($"{config.ReturnPath}?code={HttpUtility.UrlEncode(tokenFactory.CreateToken(authenticationContext.User, true))}&state=oidc.{HttpUtility.UrlEncode(configName)}");
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
};
|
||||
|
||||
Task CompleteAuth(RemoteAuthenticationContext<OpenIdConnectOptions> context)
|
||||
=> context
|
||||
.HttpContext
|
||||
.RequestServices
|
||||
.GetRequiredService<ITokenValidator>()
|
||||
.ValidateOidcToken(
|
||||
context,
|
||||
configName,
|
||||
context
|
||||
.HttpContext
|
||||
.RequestAborted);
|
||||
|
||||
if (securityConfiguration.OidcStrictMode)
|
||||
{
|
||||
options.GetClaimsFromUserInfoEndpoint = true;
|
||||
options.ClaimActions.MapUniqueJsonKey(AuthenticationContextFactory.TgsGroupIdClaimName, AuthenticationContextFactory.TgsGroupIdClaimName);
|
||||
options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
|
||||
{
|
||||
NameClaimType = config.UsernameClaim,
|
||||
RoleClaimType = "roles",
|
||||
};
|
||||
|
||||
options.Scope.Add(OpenIdConnectScope.Profile);
|
||||
options.Events.OnUserInformationReceived = CompleteAuth;
|
||||
}
|
||||
else
|
||||
options.Events.OnTokenValidated = CompleteAuth;
|
||||
|
||||
options.Scope.Add(OpenIdConnectScope.OpenId);
|
||||
options.Scope.Add(OpenIdConnectScope.OfflineAccess);
|
||||
|
||||
#if DEBUG
|
||||
options.RequireHttpsMetadata = false;
|
||||
#endif
|
||||
|
||||
options.SaveTokens = true;
|
||||
options.ResponseType = OpenIdConnectResponseType.Code;
|
||||
options.MapInboundClaims = false;
|
||||
|
||||
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
|
||||
|
||||
var basePath = $"/oidc/{configName}/";
|
||||
options.CallbackPath = new PathString(basePath + "signin-callback");
|
||||
options.SignedOutCallbackPath = new PathString(basePath + "signout-callback");
|
||||
options.RemoteSignOutPath = new PathString(basePath + "signout");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,6 +98,11 @@ namespace Tgstation.Server.Host.Database
|
||||
/// </summary>
|
||||
public DbSet<OAuthConnection> OAuthConnections { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="OidcConnection"/>s in the <see cref="DatabaseContext"/>.
|
||||
/// </summary>
|
||||
public DbSet<OidcConnection> OidcConnections { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="PermissionSet"/>s in the <see cref="DatabaseContext"/>.
|
||||
/// </summary>
|
||||
@@ -153,6 +158,9 @@ namespace Tgstation.Server.Host.Database
|
||||
/// <inheritdoc />
|
||||
IDatabaseCollection<OAuthConnection> IDatabaseContext.OAuthConnections => oAuthConnections;
|
||||
|
||||
/// <inheritdoc />
|
||||
IDatabaseCollection<OidcConnection> IDatabaseContext.OidcConnections => oidcConnections;
|
||||
|
||||
/// <inheritdoc />
|
||||
IDatabaseCollection<UserGroup> IDatabaseContext.Groups => groups;
|
||||
|
||||
@@ -239,6 +247,11 @@ namespace Tgstation.Server.Host.Database
|
||||
/// </summary>
|
||||
readonly IDatabaseCollection<OAuthConnection> oAuthConnections;
|
||||
|
||||
/// <summary>
|
||||
/// Backing field for <see cref="IDatabaseContext.OidcConnections"/>.
|
||||
/// </summary>
|
||||
readonly IDatabaseCollection<OidcConnection> oidcConnections;
|
||||
|
||||
/// <summary>
|
||||
/// Backing field for <see cref="IDatabaseContext.Groups"/>.
|
||||
/// </summary>
|
||||
@@ -288,6 +301,7 @@ namespace Tgstation.Server.Host.Database
|
||||
jobsCollection = new DatabaseCollection<Job>(Jobs!);
|
||||
reattachInformationsCollection = new DatabaseCollection<ReattachInformation>(ReattachInformations!);
|
||||
oAuthConnections = new DatabaseCollection<OAuthConnection>(OAuthConnections!);
|
||||
oidcConnections = new DatabaseCollection<OidcConnection>(OidcConnections!);
|
||||
groups = new DatabaseCollection<UserGroup>(Groups!);
|
||||
permissionSets = new DatabaseCollection<PermissionSet>(PermissionSets!);
|
||||
}
|
||||
@@ -387,8 +401,10 @@ namespace Tgstation.Server.Host.Database
|
||||
userModel.HasIndex(x => x.SystemIdentifier).IsUnique();
|
||||
userModel.HasMany(x => x.TestMerges).WithOne(x => x.MergedBy).OnDelete(DeleteBehavior.Restrict);
|
||||
userModel.HasMany(x => x.OAuthConnections).WithOne(x => x.User).OnDelete(DeleteBehavior.Cascade);
|
||||
userModel.HasMany(x => x.OidcConnections).WithOne(x => x.User).OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
modelBuilder.Entity<OAuthConnection>().HasIndex(x => new { x.Provider, x.ExternalUserId }).IsUnique();
|
||||
modelBuilder.Entity<OidcConnection>().HasIndex(x => new { x.SchemeKey, x.ExternalUserId }).IsUnique();
|
||||
|
||||
var groupsModel = modelBuilder.Entity<UserGroup>();
|
||||
groupsModel.HasIndex(x => x.Name).IsUnique();
|
||||
@@ -451,22 +467,22 @@ namespace Tgstation.Server.Host.Database
|
||||
/// <summary>
|
||||
/// Used by unit tests to remind us to setup the correct MSSQL migration downgrades.
|
||||
/// </summary>
|
||||
internal static readonly Type MSLatestMigration = typeof(MSAddAutoStartAndStop);
|
||||
internal static readonly Type MSLatestMigration = typeof(MSAddOidcConnections);
|
||||
|
||||
/// <summary>
|
||||
/// Used by unit tests to remind us to setup the correct MYSQL migration downgrades.
|
||||
/// </summary>
|
||||
internal static readonly Type MYLatestMigration = typeof(MYAddAutoStartAndStop);
|
||||
internal static readonly Type MYLatestMigration = typeof(MYAddOidcConnections);
|
||||
|
||||
/// <summary>
|
||||
/// Used by unit tests to remind us to setup the correct PostgresSQL migration downgrades.
|
||||
/// </summary>
|
||||
internal static readonly Type PGLatestMigration = typeof(PGAddAutoStartAndStop);
|
||||
internal static readonly Type PGLatestMigration = typeof(PGAddOidcConnections);
|
||||
|
||||
/// <summary>
|
||||
/// Used by unit tests to remind us to setup the correct SQLite migration downgrades.
|
||||
/// </summary>
|
||||
internal static readonly Type SLLatestMigration = typeof(SLAddAutoStartAndStop);
|
||||
internal static readonly Type SLLatestMigration = typeof(SLAddOidcConnections);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the migration to run for migrating down to a given <paramref name="targetVersion"/> for the <paramref name="currentDatabaseType"/>.
|
||||
@@ -482,6 +498,16 @@ namespace Tgstation.Server.Host.Database
|
||||
string BadDatabaseType() => throw new ArgumentException($"Invalid DatabaseType: {currentDatabaseType}", nameof(currentDatabaseType));
|
||||
|
||||
// !!! DON'T FORGET TO UPDATE THE SWARM PROTOCOL MAJOR VERSION !!!
|
||||
if (targetVersion < new Version(6, 15, 0))
|
||||
targetMigration = currentDatabaseType switch
|
||||
{
|
||||
DatabaseType.MySql => nameof(MYAddAutoStartAndStop),
|
||||
DatabaseType.PostgresSql => nameof(PGAddAutoStartAndStop),
|
||||
DatabaseType.SqlServer => nameof(MSAddAutoStartAndStop),
|
||||
DatabaseType.Sqlite => nameof(SLAddAutoStartAndStop),
|
||||
_ => BadDatabaseType(),
|
||||
};
|
||||
|
||||
if (targetVersion < new Version(6, 12, 0))
|
||||
targetMigration = currentDatabaseType switch
|
||||
{
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace Tgstation.Server.Host.Database.Design
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public MySqlDatabaseContext CreateDbContext(string[] args)
|
||||
=> new MySqlDatabaseContext(
|
||||
=> new(
|
||||
DesignTimeDbContextFactoryHelpers.CreateDatabaseContextOptions<MySqlDatabaseContext>(
|
||||
DatabaseType.MariaDB,
|
||||
"Server=127.0.0.1;User Id=root;Password=zdxfOOTlQFnklwzytzCj;Database=TGS_Design"));
|
||||
|
||||
@@ -90,6 +90,11 @@ namespace Tgstation.Server.Host.Database
|
||||
/// </summary>
|
||||
IDatabaseCollection<OAuthConnection> OAuthConnections { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="DbSet{TEntity}"/> for <see cref="OidcConnection"/>s.
|
||||
/// </summary>
|
||||
IDatabaseCollection<OidcConnection> OidcConnections { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="DbSet{TEntity}"/> for <see cref="UserGroup"/>s.
|
||||
/// </summary>
|
||||
|
||||
Generated
+1146
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,57 @@
|
||||
using System;
|
||||
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace Tgstation.Server.Host.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class MSAddOidcConnections : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(migrationBuilder);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "OidcConnections",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
UserId = table.Column<long>(type: "bigint", nullable: false),
|
||||
SchemeKey = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
|
||||
ExternalUserId = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_OidcConnections", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_OidcConnections_Users_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_OidcConnections_SchemeKey_ExternalUserId",
|
||||
table: "OidcConnections",
|
||||
columns: new[] { "SchemeKey", "ExternalUserId" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_OidcConnections_UserId",
|
||||
table: "OidcConnections",
|
||||
column: "UserId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(migrationBuilder);
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "OidcConnections");
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+1217
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,61 @@
|
||||
using System;
|
||||
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace Tgstation.Server.Host.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class MYAddOidcConnections : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(migrationBuilder);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "OidcConnections",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||
UserId = table.Column<long>(type: "bigint", nullable: false),
|
||||
SchemeKey = table.Column<string>(type: "varchar(100)", maxLength: 100, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
ExternalUserId = table.Column<string>(type: "varchar(100)", maxLength: 100, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_OidcConnections", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_OidcConnections_Users_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_OidcConnections_SchemeKey_ExternalUserId",
|
||||
table: "OidcConnections",
|
||||
columns: new[] { "SchemeKey", "ExternalUserId" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_OidcConnections_UserId",
|
||||
table: "OidcConnections",
|
||||
column: "UserId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(migrationBuilder);
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "OidcConnections");
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+1140
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,59 @@
|
||||
using System;
|
||||
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
namespace Tgstation.Server.Host.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class PGAddOidcConnections : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(migrationBuilder);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "OidcConnections",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
UserId = table.Column<long>(type: "bigint", nullable: false),
|
||||
SchemeKey = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||
ExternalUserId = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_OidcConnections", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_OidcConnections_Users_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_OidcConnections_SchemeKey_ExternalUserId",
|
||||
table: "OidcConnections",
|
||||
columns: new[] { "SchemeKey", "ExternalUserId" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_OidcConnections_UserId",
|
||||
table: "OidcConnections",
|
||||
column: "UserId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(migrationBuilder);
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "OidcConnections");
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+1111
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,57 @@
|
||||
using System;
|
||||
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace Tgstation.Server.Host.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class SLAddOidcConnections : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(migrationBuilder);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "OidcConnections",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
UserId = table.Column<long>(type: "INTEGER", nullable: false),
|
||||
SchemeKey = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
|
||||
ExternalUserId = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_OidcConnections", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_OidcConnections_Users_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_OidcConnections_SchemeKey_ExternalUserId",
|
||||
table: "OidcConnections",
|
||||
columns: new[] { "SchemeKey", "ExternalUserId" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_OidcConnections_UserId",
|
||||
table: "OidcConnections",
|
||||
column: "UserId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(migrationBuilder);
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "OidcConnections");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ namespace Tgstation.Server.Host.Database
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.10")
|
||||
.HasAnnotation("ProductVersion", "8.0.13")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 64);
|
||||
|
||||
MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
|
||||
@@ -512,6 +512,37 @@ namespace Tgstation.Server.Host.Database
|
||||
b.ToTable("OAuthConnections");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.OidcConnection", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<string>("ExternalUserId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("varchar(100)");
|
||||
|
||||
b.Property<string>("SchemeKey")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("varchar(100)");
|
||||
|
||||
b.Property<long>("UserId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.HasIndex("SchemeKey", "ExternalUserId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("OidcConnections");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b =>
|
||||
{
|
||||
b.Property<long?>("Id")
|
||||
@@ -996,6 +1027,17 @@ namespace Tgstation.Server.Host.Database
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.OidcConnection", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.User", "User")
|
||||
.WithMany("OidcConnections")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.UserGroup", "Group")
|
||||
@@ -1152,6 +1194,8 @@ namespace Tgstation.Server.Host.Database
|
||||
|
||||
b.Navigation("OAuthConnections");
|
||||
|
||||
b.Navigation("OidcConnections");
|
||||
|
||||
b.Navigation("PermissionSet");
|
||||
|
||||
b.Navigation("TestMerges");
|
||||
|
||||
+45
-1
@@ -13,7 +13,7 @@ namespace Tgstation.Server.Host.Database
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.10")
|
||||
.HasAnnotation("ProductVersion", "8.0.13")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
@@ -471,6 +471,37 @@ namespace Tgstation.Server.Host.Database
|
||||
b.ToTable("OAuthConnections");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.OidcConnection", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<string>("ExternalUserId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<string>("SchemeKey")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<long>("UserId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.HasIndex("SchemeKey", "ExternalUserId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("OidcConnections");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b =>
|
||||
{
|
||||
b.Property<long?>("Id")
|
||||
@@ -919,6 +950,17 @@ namespace Tgstation.Server.Host.Database
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.OidcConnection", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.User", "User")
|
||||
.WithMany("OidcConnections")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.UserGroup", "Group")
|
||||
@@ -1075,6 +1117,8 @@ namespace Tgstation.Server.Host.Database
|
||||
|
||||
b.Navigation("OAuthConnections");
|
||||
|
||||
b.Navigation("OidcConnections");
|
||||
|
||||
b.Navigation("PermissionSet");
|
||||
|
||||
b.Navigation("TestMerges");
|
||||
|
||||
+45
-1
@@ -13,7 +13,7 @@ namespace Tgstation.Server.Host.Database
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.10")
|
||||
.HasAnnotation("ProductVersion", "8.0.13")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
@@ -474,6 +474,37 @@ namespace Tgstation.Server.Host.Database
|
||||
b.ToTable("OAuthConnections");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.OidcConnection", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<string>("ExternalUserId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("SchemeKey")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<long>("UserId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.HasIndex("SchemeKey", "ExternalUserId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("OidcConnections");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b =>
|
||||
{
|
||||
b.Property<long?>("Id")
|
||||
@@ -925,6 +956,17 @@ namespace Tgstation.Server.Host.Database
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.OidcConnection", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.User", "User")
|
||||
.WithMany("OidcConnections")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.UserGroup", "Group")
|
||||
@@ -1081,6 +1123,8 @@ namespace Tgstation.Server.Host.Database
|
||||
|
||||
b.Navigation("OAuthConnections");
|
||||
|
||||
b.Navigation("OidcConnections");
|
||||
|
||||
b.Navigation("PermissionSet");
|
||||
|
||||
b.Navigation("TestMerges");
|
||||
|
||||
+43
-1
@@ -12,7 +12,7 @@ namespace Tgstation.Server.Host.Database
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "8.0.10");
|
||||
modelBuilder.HasAnnotation("ProductVersion", "8.0.13");
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b =>
|
||||
{
|
||||
@@ -460,6 +460,35 @@ namespace Tgstation.Server.Host.Database
|
||||
b.ToTable("OAuthConnections");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.OidcConnection", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ExternalUserId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SchemeKey")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("UserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.HasIndex("SchemeKey", "ExternalUserId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("OidcConnections");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b =>
|
||||
{
|
||||
b.Property<long?>("Id")
|
||||
@@ -892,6 +921,17 @@ namespace Tgstation.Server.Host.Database
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.OidcConnection", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.User", "User")
|
||||
.WithMany("OidcConnections")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.UserGroup", "Group")
|
||||
@@ -1048,6 +1088,8 @@ namespace Tgstation.Server.Host.Database
|
||||
|
||||
b.Navigation("OAuthConnections");
|
||||
|
||||
b.Navigation("OidcConnections");
|
||||
|
||||
b.Navigation("PermissionSet");
|
||||
|
||||
b.Navigation("TestMerges");
|
||||
|
||||
@@ -57,6 +57,7 @@ namespace Tgstation.Server.Host.Database
|
||||
serverVersion,
|
||||
mySqlOptions =>
|
||||
{
|
||||
mySqlOptions.TranslateParameterizedCollectionsToConstants();
|
||||
mySqlOptions.EnableRetryOnFailure();
|
||||
mySqlOptions.UseQuerySplittingBehavior(QuerySplittingBehavior.SingleQuery);
|
||||
});
|
||||
|
||||
@@ -41,6 +41,7 @@ namespace Tgstation.Server.Host.Database
|
||||
AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
|
||||
options.UseNpgsql(databaseConfiguration.ConnectionString, options =>
|
||||
{
|
||||
options.TranslateParameterizedCollectionsToConstants();
|
||||
options.EnableRetryOnFailure();
|
||||
options.UseQuerySplittingBehavior(QuerySplittingBehavior.SingleQuery);
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ namespace Tgstation.Server.Host.Database
|
||||
databaseConfiguration.ConnectionString,
|
||||
sqlServerOptions =>
|
||||
{
|
||||
sqlServerOptions.TranslateParameterizedCollectionsToConstants();
|
||||
sqlServerOptions.EnableRetryOnFailure();
|
||||
sqlServerOptions.UseQuerySplittingBehavior(QuerySplittingBehavior.SingleQuery);
|
||||
});
|
||||
|
||||
@@ -56,7 +56,11 @@ namespace Tgstation.Server.Host.Database
|
||||
if (databaseConfiguration.DatabaseType != DatabaseType.Sqlite)
|
||||
throw new InvalidOperationException($"Invalid DatabaseType for {nameof(SqliteDatabaseContext)}!");
|
||||
|
||||
options.UseSqlite(databaseConfiguration.ConnectionString, sqliteOptions => sqliteOptions.UseQuerySplittingBehavior(QuerySplittingBehavior.SingleQuery));
|
||||
options.UseSqlite(databaseConfiguration.ConnectionString, sqliteOptions =>
|
||||
{
|
||||
sqliteOptions.TranslateParameterizedCollectionsToConstants();
|
||||
sqliteOptions.UseQuerySplittingBehavior(QuerySplittingBehavior.SingleQuery);
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -72,19 +76,19 @@ namespace Tgstation.Server.Host.Database
|
||||
// use the DateTimeOffsetToBinaryConverter
|
||||
// Based on: https://github.com/aspnet/EntityFrameworkCore/issues/10784#issuecomment-415769754
|
||||
// This only supports millisecond precision, but should be sufficient for most use cases.
|
||||
if (!designTime)
|
||||
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
|
||||
{
|
||||
var properties = entityType
|
||||
.ClrType
|
||||
.GetProperties()
|
||||
.Where(p => p.PropertyType == typeof(DateTimeOffset) || p.PropertyType == typeof(DateTimeOffset?));
|
||||
foreach (var property in properties)
|
||||
modelBuilder
|
||||
.Entity(entityType.Name)
|
||||
.Property(property.Name)
|
||||
.HasConversion(new DateTimeOffsetToBinaryConverter());
|
||||
}
|
||||
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
|
||||
{
|
||||
var properties = entityType
|
||||
.ClrType
|
||||
.GetProperties()
|
||||
.Where(p => p.PropertyType == typeof(DateTimeOffset) || p.PropertyType == typeof(DateTimeOffset?));
|
||||
foreach (var property in properties)
|
||||
modelBuilder
|
||||
.Entity(entityType.Name)
|
||||
.Property(property.Name)
|
||||
.HasConversion(new DateTimeOffsetToBinaryConverter())
|
||||
.HasColumnType("TEXT"); // Dominion: This is for purely legacy reasons
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@ namespace Tgstation.Server.Host.Extensions
|
||||
applicationBuilder.ApplicationServices.GetRequiredService<IOptions<ControlPanelConfiguration>>(),
|
||||
applicationBuilder.ApplicationServices.GetRequiredService<IOptions<GeneralConfiguration>>(),
|
||||
applicationBuilder.ApplicationServices.GetRequiredService<IOptions<DatabaseConfiguration>>(),
|
||||
applicationBuilder.ApplicationServices.GetRequiredService<IOptions<SecurityConfiguration>>(),
|
||||
applicationBuilder.ApplicationServices.GetRequiredService<IOptions<SwarmConfiguration>>(),
|
||||
applicationBuilder.ApplicationServices.GetRequiredService<IOptions<InternalConfiguration>>(),
|
||||
applicationBuilder.ApplicationServices.GetRequiredService<ILogger<Application>>());
|
||||
|
||||
@@ -33,6 +33,7 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
|
||||
/// <param name="password">The password of the <see cref="User"/>.</param>
|
||||
/// <param name="enabled">If the <see cref="User"/> is <see cref="User.Enabled"/>.</param>
|
||||
/// <param name="oAuthConnections">The <see cref="OAuthConnection"/>s for the user.</param>
|
||||
/// <param name="oidcConnections">The <see cref="OidcConnection"/>s for the user.</param>
|
||||
/// <param name="permissionSet">The owned <see cref="PermissionSet"/> of the user.</param>
|
||||
/// <param name="userAuthority">The <see cref="IGraphQLAuthorityInvoker{TAuthority}"/> for the <see cref="IUserAuthority"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
@@ -44,6 +45,7 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
|
||||
string password,
|
||||
bool? enabled,
|
||||
IEnumerable<OAuthConnection>? oAuthConnections,
|
||||
IEnumerable<OidcConnection>? oidcConnections,
|
||||
PermissionSetInput? permissionSet,
|
||||
[Service] IGraphQLAuthorityInvoker<IUserAuthority> userAuthority,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -73,6 +75,13 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
|
||||
Provider = oAuthConnection.Provider,
|
||||
})
|
||||
.ToList(),
|
||||
OidcConnections = oidcConnections
|
||||
?.Select(oidcConnection => new Api.Models.OidcConnection
|
||||
{
|
||||
ExternalUserId = oidcConnection.ExternalUserId,
|
||||
SchemeKey = oidcConnection.SchemeKey,
|
||||
})
|
||||
.ToList(),
|
||||
},
|
||||
false,
|
||||
cancellationToken));
|
||||
@@ -85,6 +94,7 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
|
||||
/// <param name="password">The password of the <see cref="User"/>.</param>
|
||||
/// <param name="enabled">If the <see cref="User"/> is <see cref="User.Enabled"/>.</param>
|
||||
/// <param name="oAuthConnections">The <see cref="OAuthConnection"/>s for the user.</param>
|
||||
/// <param name="oidcConnections">The <see cref="OidcConnection"/>s for the user.</param>
|
||||
/// <param name="groupId">The <see cref="Entity.Id"/> of the <see cref="UserGroup"/> the <see cref="User"/> will belong to.</param>
|
||||
/// <param name="userAuthority">The <see cref="IGraphQLAuthorityInvoker{TAuthority}"/> for the <see cref="IUserAuthority"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
@@ -96,6 +106,7 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
|
||||
string password,
|
||||
bool? enabled,
|
||||
IEnumerable<OAuthConnection>? oAuthConnections,
|
||||
IEnumerable<OidcConnection>? oidcConnections,
|
||||
[ID(nameof(UserGroup))] long groupId,
|
||||
[Service] IGraphQLAuthorityInvoker<IUserAuthority> userAuthority,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -122,16 +133,24 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
|
||||
Provider = oAuthConnection.Provider,
|
||||
})
|
||||
.ToList(),
|
||||
OidcConnections = oidcConnections
|
||||
?.Select(oidcConnection => new Api.Models.OidcConnection
|
||||
{
|
||||
ExternalUserId = oidcConnection.ExternalUserId,
|
||||
SchemeKey = oidcConnection.SchemeKey,
|
||||
})
|
||||
.ToList(),
|
||||
},
|
||||
false,
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a TGS user authenticated with one or mor <see cref="OAuthConnection"/>s specifying a personal <see cref="PermissionSet"/>.
|
||||
/// Creates a TGS user authenticated with one or more <see cref="OAuthConnection"/>s or <see cref="OidcConnection"/>s specifying a personal <see cref="PermissionSet"/>.
|
||||
/// </summary>
|
||||
/// <param name="name">The <see cref="NamedEntity.Name"/> of the <see cref="User"/>.</param>
|
||||
/// <param name="oAuthConnections">The <see cref="OAuthConnection"/>s for the user.</param>
|
||||
/// <param name="oidcConnections">The <see cref="OidcConnection"/>s for the user.</param>
|
||||
/// <param name="enabled">If the <see cref="User"/> is <see cref="User.Enabled"/>.</param>
|
||||
/// <param name="permissionSet">The owned <see cref="PermissionSet"/> of the user.</param>
|
||||
/// <param name="userAuthority">The <see cref="IGraphQLAuthorityInvoker{TAuthority}"/> for the <see cref="IUserAuthority"/>.</param>
|
||||
@@ -139,9 +158,10 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
|
||||
/// <returns>The created <see cref="User"/>.</returns>
|
||||
[TgsGraphQLAuthorize<IUserAuthority>(nameof(IUserAuthority.Create))]
|
||||
[Error(typeof(ErrorMessageException))]
|
||||
public ValueTask<User> CreateUserByOAuthAndPermissionSet(
|
||||
public ValueTask<User> CreateUserByServiceConnectionAndPermissionSet(
|
||||
string name,
|
||||
IEnumerable<OAuthConnection> oAuthConnections,
|
||||
IEnumerable<OAuthConnection>? oAuthConnections,
|
||||
IEnumerable<OidcConnection>? oidcConnections,
|
||||
bool? enabled,
|
||||
PermissionSetInput? permissionSet,
|
||||
[Service] IGraphQLAuthorityInvoker<IUserAuthority> userAuthority,
|
||||
@@ -172,16 +192,24 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
|
||||
Provider = oAuthConnection.Provider,
|
||||
})
|
||||
.ToList(),
|
||||
OidcConnections = oidcConnections
|
||||
?.Select(oidcConnection => new Api.Models.OidcConnection
|
||||
{
|
||||
ExternalUserId = oidcConnection.ExternalUserId,
|
||||
SchemeKey = oidcConnection.SchemeKey,
|
||||
})
|
||||
.ToList(),
|
||||
},
|
||||
true,
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a TGS user specifying the <see cref="UserGroup"/> they will belong to.
|
||||
/// Creates a TGS user using <see cref="OAuthConnection"/>s and/or <see cref="OidcConnection"/>s specifying the <see cref="UserGroup"/> they will belong to.
|
||||
/// </summary>
|
||||
/// <param name="name">The <see cref="NamedEntity.Name"/> of the <see cref="User"/>.</param>
|
||||
/// <param name="oAuthConnections">The <see cref="OAuthConnection"/>s for the user.</param>
|
||||
/// <param name="oidcConnections">The <see cref="OidcConnection"/>s for the user.</param>
|
||||
/// <param name="groupId">The <see cref="Entity.Id"/> of the <see cref="UserGroup"/> the <see cref="User"/> will belong to.</param>
|
||||
/// <param name="enabled">If the <see cref="User"/> is <see cref="User.Enabled"/>.</param>
|
||||
/// <param name="userAuthority">The <see cref="IGraphQLAuthorityInvoker{TAuthority}"/> for the <see cref="IUserAuthority"/>.</param>
|
||||
@@ -189,9 +217,10 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
|
||||
/// <returns>The created <see cref="User"/>.</returns>
|
||||
[TgsGraphQLAuthorize<IUserAuthority>(nameof(IUserAuthority.Create))]
|
||||
[Error(typeof(ErrorMessageException))]
|
||||
public ValueTask<User> CreateUserByOAuthAndGroup(
|
||||
public ValueTask<User> CreateUserByServiceConnectionAndGroup(
|
||||
string name,
|
||||
IEnumerable<OAuthConnection> oAuthConnections,
|
||||
IEnumerable<OidcConnection> oidcConnections,
|
||||
[ID(nameof(UserGroup))] long groupId,
|
||||
bool? enabled,
|
||||
[Service] IGraphQLAuthorityInvoker<IUserAuthority> userAuthority,
|
||||
@@ -219,6 +248,13 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
|
||||
Provider = oAuthConnection.Provider,
|
||||
})
|
||||
.ToList(),
|
||||
OidcConnections = oidcConnections
|
||||
?.Select(oidcConnection => new Api.Models.OidcConnection
|
||||
{
|
||||
ExternalUserId = oidcConnection.ExternalUserId,
|
||||
SchemeKey = oidcConnection.SchemeKey,
|
||||
})
|
||||
.ToList(),
|
||||
},
|
||||
true,
|
||||
cancellationToken));
|
||||
@@ -230,6 +266,7 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
|
||||
/// <param name="systemIdentifier">The <see cref="User.SystemIdentifier"/> of the <see cref="User"/>.</param>
|
||||
/// <param name="enabled">If the <see cref="User"/> is <see cref="User.Enabled"/>.</param>
|
||||
/// <param name="oAuthConnections">The <see cref="OAuthConnection"/>s for the user.</param>
|
||||
/// <param name="oidcConnections">The <see cref="OidcConnection"/>s for the user.</param>
|
||||
/// <param name="permissionSet">The owned <see cref="PermissionSet"/> of the user.</param>
|
||||
/// <param name="userAuthority">The <see cref="IGraphQLAuthorityInvoker{TAuthority}"/> for the <see cref="IUserAuthority"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
@@ -240,6 +277,7 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
|
||||
string systemIdentifier,
|
||||
bool? enabled,
|
||||
IEnumerable<OAuthConnection>? oAuthConnections,
|
||||
IEnumerable<OidcConnection>? oidcConnections,
|
||||
PermissionSetInput permissionSet,
|
||||
[Service] IGraphQLAuthorityInvoker<IUserAuthority> userAuthority,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -267,6 +305,13 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
|
||||
Provider = oAuthConnection.Provider,
|
||||
})
|
||||
.ToList(),
|
||||
OidcConnections = oidcConnections
|
||||
?.Select(oidcConnection => new Api.Models.OidcConnection
|
||||
{
|
||||
ExternalUserId = oidcConnection.ExternalUserId,
|
||||
SchemeKey = oidcConnection.SchemeKey,
|
||||
})
|
||||
.ToList(),
|
||||
},
|
||||
false,
|
||||
cancellationToken));
|
||||
@@ -279,6 +324,7 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
|
||||
/// <param name="enabled">If the <see cref="User"/> is <see cref="User.Enabled"/>.</param>
|
||||
/// <param name="groupId">The <see cref="Entity.Id"/> of the <see cref="UserGroup"/> the <see cref="User"/> will belong to.</param>
|
||||
/// <param name="oAuthConnections">The <see cref="OAuthConnection"/>s for the user.</param>
|
||||
/// <param name="oidcConnections">The <see cref="OidcConnection"/>s for the user.</param>
|
||||
/// <param name="userAuthority">The <see cref="IGraphQLAuthorityInvoker{TAuthority}"/> for the <see cref="IUserAuthority"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>The created <see cref="User"/>.</returns>
|
||||
@@ -289,6 +335,7 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
|
||||
bool? enabled,
|
||||
[ID(nameof(UserGroup))] long groupId,
|
||||
IEnumerable<OAuthConnection>? oAuthConnections,
|
||||
IEnumerable<OidcConnection>? oidcConnections,
|
||||
[Service] IGraphQLAuthorityInvoker<IUserAuthority> userAuthority,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -312,6 +359,13 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
|
||||
Provider = oAuthConnection.Provider,
|
||||
})
|
||||
.ToList(),
|
||||
OidcConnections = oidcConnections
|
||||
?.Select(oidcConnection => new Api.Models.OidcConnection
|
||||
{
|
||||
ExternalUserId = oidcConnection.ExternalUserId,
|
||||
SchemeKey = oidcConnection.SchemeKey,
|
||||
})
|
||||
.ToList(),
|
||||
},
|
||||
false,
|
||||
cancellationToken));
|
||||
@@ -346,17 +400,19 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the current user's <see cref="OAuthConnection"/>s.
|
||||
/// Sets the current user's <see cref="OAuthConnection"/>s and <see cref="OidcConnection"/>s.
|
||||
/// </summary>
|
||||
/// <param name="newOAuthConnections">The new <see cref="OAuthConnection"/>s for the current user.</param>
|
||||
/// <param name="newOAuthConnections">Optional new <see cref="OAuthConnection"/>s for the current <see cref="User"/>.</param>
|
||||
/// <param name="newOidcConnections">Optional new <see cref="OidcConnection"/>s for the current <see cref="User"/>.</param>
|
||||
/// <param name="authenticationContext">The <see cref="IAuthenticationContext"/> to get the <see cref="Entity.Id"/> of the user.</param>
|
||||
/// <param name="userAuthority">The <see cref="IGraphQLAuthorityInvoker{TAuthority}"/> for the <see cref="IUserAuthority"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>The updated current <see cref="User"/>.</returns>
|
||||
[TgsGraphQLAuthorize(AdministrationRights.WriteUsers | AdministrationRights.EditOwnOAuthConnections)]
|
||||
[TgsGraphQLAuthorize(AdministrationRights.WriteUsers | AdministrationRights.EditOwnServiceConnections)]
|
||||
[Error(typeof(ErrorMessageException))]
|
||||
public ValueTask<User> SetCurrentOAuthConnections(
|
||||
IEnumerable<OAuthConnection> newOAuthConnections,
|
||||
public ValueTask<User> SetCurrentServiceConnections(
|
||||
IEnumerable<OAuthConnection>? newOAuthConnections,
|
||||
IEnumerable<OidcConnection>? newOidcConnections,
|
||||
[Service] IAuthenticationContext authenticationContext,
|
||||
[Service] IGraphQLAuthorityInvoker<IUserAuthority> userAuthority,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -375,6 +431,13 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
|
||||
Provider = oAuthConnection.Provider,
|
||||
})
|
||||
.ToList(),
|
||||
OidcConnections = newOidcConnections
|
||||
?.Select(oidcConnection => new Api.Models.OidcConnection
|
||||
{
|
||||
ExternalUserId = oidcConnection.ExternalUserId,
|
||||
SchemeKey = oidcConnection.SchemeKey,
|
||||
})
|
||||
.ToList(),
|
||||
},
|
||||
cancellationToken));
|
||||
}
|
||||
@@ -387,6 +450,7 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
|
||||
/// <param name="newPassword">Optional new password for the <see cref="User"/>. Only applicable to TGS users.</param>
|
||||
/// <param name="enabled">Optional new <see cref="User.Enabled"/> status for the <see cref="User"/>.</param>
|
||||
/// <param name="newOAuthConnections">Optional new <see cref="OAuthConnection"/>s for the <see cref="User"/>.</param>
|
||||
/// <param name="newOidcConnections">Optional new <see cref="OidcConnection"/>s for the <see cref="User"/>.</param>
|
||||
/// <param name="userAuthority">The <see cref="IGraphQLAuthorityInvoker{TAuthority}"/> for the <see cref="IUserAuthority"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>The updated <see cref="User"/>.</returns>
|
||||
@@ -398,6 +462,7 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
|
||||
string? newPassword,
|
||||
bool? enabled,
|
||||
IEnumerable<OAuthConnection>? newOAuthConnections,
|
||||
IEnumerable<OidcConnection>? newOidcConnections,
|
||||
[Service] IGraphQLAuthorityInvoker<IUserAuthority> userAuthority,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -410,6 +475,7 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
|
||||
null,
|
||||
null,
|
||||
newOAuthConnections,
|
||||
newOidcConnections,
|
||||
userAuthority,
|
||||
cancellationToken);
|
||||
}
|
||||
@@ -422,7 +488,8 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
|
||||
/// <param name="newPassword">Optional new password for the <see cref="User"/>. Only applicable to TGS users.</param>
|
||||
/// <param name="enabled">Optional new <see cref="User.Enabled"/> status for the <see cref="User"/>.</param>
|
||||
/// <param name="newPermissionSet">Updated owned <see cref="PermissionSet"/> for the user. Note that setting this on a <see cref="User"/> in a <see cref="UserGroup"/> will remove them from that group.</param>
|
||||
/// <param name="newOAuthConnections">The new <see cref="OAuthConnection"/>s for the <see cref="User"/>.</param>
|
||||
/// <param name="newOAuthConnections">Optional new <see cref="OAuthConnection"/>s for the <see cref="User"/>.</param>
|
||||
/// <param name="newOidcConnections">Optional new <see cref="OidcConnection"/>s for the <see cref="User"/>.</param>
|
||||
/// <param name="userAuthority">The <see cref="IGraphQLAuthorityInvoker{TAuthority}"/> for the <see cref="IUserAuthority"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>The updated <see cref="User"/>.</returns>
|
||||
@@ -435,6 +502,7 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
|
||||
bool? enabled,
|
||||
PermissionSetInput newPermissionSet,
|
||||
IEnumerable<OAuthConnection>? newOAuthConnections,
|
||||
IEnumerable<OidcConnection>? newOidcConnections,
|
||||
[Service] IGraphQLAuthorityInvoker<IUserAuthority> userAuthority,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -447,6 +515,7 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
|
||||
newPermissionSet,
|
||||
null,
|
||||
newOAuthConnections,
|
||||
newOidcConnections,
|
||||
userAuthority,
|
||||
cancellationToken);
|
||||
}
|
||||
@@ -459,7 +528,8 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
|
||||
/// <param name="newPassword">Optional new password for the <see cref="User"/>. Only applicable to TGS users.</param>
|
||||
/// <param name="enabled">Optional new <see cref="User.Enabled"/> status for the <see cref="User"/>.</param>
|
||||
/// <param name="newGroupId"><see cref="Entity.Id"/> of the <see cref="UserGroup"/> to move the <see cref="User"/> to.</param>
|
||||
/// <param name="newOAuthConnections">The new <see cref="OAuthConnection"/>s for the <see cref="User"/>.</param>
|
||||
/// <param name="newOAuthConnections">Optional new <see cref="OAuthConnection"/>s for the <see cref="User"/>.</param>
|
||||
/// <param name="newOidcConnections">Optional new <see cref="OidcConnection"/>s for the <see cref="User"/>.</param>
|
||||
/// <param name="userAuthority">The <see cref="IGraphQLAuthorityInvoker{TAuthority}"/> for the <see cref="IUserAuthority"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>The updated <see cref="User"/>.</returns>
|
||||
@@ -472,6 +542,7 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
|
||||
bool? enabled,
|
||||
[ID(nameof(UserGroup))] long newGroupId,
|
||||
IEnumerable<OAuthConnection>? newOAuthConnections,
|
||||
IEnumerable<OidcConnection>? newOidcConnections,
|
||||
[Service] IGraphQLAuthorityInvoker<IUserAuthority> userAuthority,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -484,6 +555,7 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
|
||||
null,
|
||||
newGroupId,
|
||||
newOAuthConnections,
|
||||
newOidcConnections,
|
||||
userAuthority,
|
||||
cancellationToken);
|
||||
}
|
||||
@@ -498,6 +570,7 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
|
||||
/// <param name="newPermissionSet">Optional updated new owned <see cref="PermissionSet"/> for the user. Note that setting this on a <see cref="User"/> in a <see cref="UserGroup"/> will remove them from that group.</param>
|
||||
/// <param name="newGroupId">Optional <see cref="Entity.Id"/> of the <see cref="UserGroup"/> to move the <see cref="User"/> to.</param>
|
||||
/// <param name="newOAuthConnections">Optional new <see cref="OAuthConnection"/>s for the <see cref="User"/>.</param>
|
||||
/// <param name="newOidcConnections">Optional new <see cref="OidcConnection"/>s for the <see cref="User"/>.</param>
|
||||
/// <param name="userAuthority">The <see cref="IGraphQLAuthorityInvoker{TAuthority}"/> for the <see cref="IUserAuthority"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>The updated <see cref="User"/>.</returns>
|
||||
@@ -509,6 +582,7 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
|
||||
PermissionSetInput? newPermissionSet,
|
||||
long? newGroupId,
|
||||
IEnumerable<OAuthConnection>? newOAuthConnections,
|
||||
IEnumerable<OidcConnection>? newOidcConnections,
|
||||
IGraphQLAuthorityInvoker<IUserAuthority> userAuthority,
|
||||
CancellationToken cancellationToken)
|
||||
=> userAuthority.InvokeTransformable<Models.User, User, UserGraphQLTransformer>(
|
||||
@@ -539,6 +613,13 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
|
||||
Provider = oAuthConnection.Provider,
|
||||
})
|
||||
.ToList(),
|
||||
OidcConnections = newOidcConnections
|
||||
?.Select(oidcConnection => new Api.Models.OidcConnection
|
||||
{
|
||||
ExternalUserId = oidcConnection.ExternalUserId,
|
||||
SchemeKey = oidcConnection.SchemeKey,
|
||||
})
|
||||
.ToList(),
|
||||
},
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace Tgstation.Server.Host.GraphQL.Types.OAuth
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="OAuthProvider"/> of the <see cref="OAuthConnection"/>.
|
||||
/// </summary>]
|
||||
/// </summary>
|
||||
public OAuthProvider Provider { get; }
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -20,21 +20,6 @@ namespace Tgstation.Server.Host.GraphQL.Types.OAuth
|
||||
/// </summary>
|
||||
public RedirectOAuthProviderInfo? GitHub { get; }
|
||||
|
||||
/// <summary>
|
||||
/// https://tgstation13.org.
|
||||
/// </summary>
|
||||
public RedirectOAuthProviderInfo? TGForums { get; }
|
||||
|
||||
/// <summary>
|
||||
/// https://www.keycloak.org.
|
||||
/// </summary>
|
||||
public FullOAuthProviderInfo? Keycloak { get; }
|
||||
|
||||
/// <summary>
|
||||
/// https://invisioncommunity.com.
|
||||
/// </summary>
|
||||
public FullOAuthProviderInfo? InvisionCommunity { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OAuthProviderInfos"/> class.
|
||||
/// </summary>
|
||||
@@ -58,9 +43,6 @@ namespace Tgstation.Server.Host.GraphQL.Types.OAuth
|
||||
|
||||
Discord = TryBuild(OAuthProvider.Discord, info => new BasicOAuthProviderInfo(info));
|
||||
GitHub = TryBuild(OAuthProvider.GitHub, info => new RedirectOAuthProviderInfo(info));
|
||||
TGForums = TryBuild(OAuthProvider.TGForums, info => new RedirectOAuthProviderInfo(info));
|
||||
Keycloak = TryBuild(OAuthProvider.Keycloak, info => new FullOAuthProviderInfo(info));
|
||||
InvisionCommunity = TryBuild(OAuthProvider.InvisionCommunity, info => new FullOAuthProviderInfo(info));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
|
||||
namespace Tgstation.Server.Host.GraphQL.Types.OAuth
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a valid OAuth connection.
|
||||
/// </summary>
|
||||
public sealed class OidcConnection
|
||||
{
|
||||
/// <summary>
|
||||
/// The scheme key of the <see cref="OidcConnection"/>.
|
||||
/// </summary>]
|
||||
public string SchemeKey { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the user in the OIDC provider ("sub" claim).
|
||||
/// </summary>
|
||||
public string ExternalUserId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OidcConnection"/> class.
|
||||
/// </summary>
|
||||
/// <param name="externalUserId">The value of <see cref="ExternalUserId"/>.</param>
|
||||
/// <param name="schemeKey">The value of <see cref="SchemeKey"/>.</param>
|
||||
public OidcConnection(string externalUserId, string schemeKey)
|
||||
{
|
||||
ExternalUserId = externalUserId ?? throw new ArgumentNullException(nameof(externalUserId));
|
||||
SchemeKey = schemeKey ?? throw new ArgumentNullException(nameof(schemeKey));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -105,6 +105,21 @@ namespace Tgstation.Server.Host.GraphQL.Types
|
||||
authority => authority.OAuthConnections(Id, cancellationToken));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// List of <see cref="OidcConnection"/>s associated with the user if OIDC is configured.
|
||||
/// </summary>
|
||||
/// <param name="userAuthority">The <see cref="IGraphQLAuthorityInvoker{TAuthority}"/> for the <see cref="IUserAuthority"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a new <see cref="Array"/> of <see cref="OidcConnection"/>s for the <see cref="User"/> if OAuth is configured.</returns>
|
||||
public ValueTask<OidcConnection[]> OidcConnections(
|
||||
[Service] IGraphQLAuthorityInvoker<IUserAuthority> userAuthority,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(userAuthority);
|
||||
return userAuthority.Invoke<OidcConnection[], OidcConnection[]>(
|
||||
authority => authority.OidcConnections(Id, cancellationToken));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="PermissionSet"/> associated with the <see cref="User"/>.
|
||||
/// </summary>
|
||||
|
||||
@@ -8,7 +8,10 @@ using HotChocolate.Data;
|
||||
using HotChocolate.Types;
|
||||
using HotChocolate.Types.Relay;
|
||||
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
using Tgstation.Server.Host.Authority;
|
||||
using Tgstation.Server.Host.Configuration;
|
||||
using Tgstation.Server.Host.Models.Transformers;
|
||||
using Tgstation.Server.Host.Security;
|
||||
|
||||
@@ -27,6 +30,18 @@ namespace Tgstation.Server.Host.GraphQL.Types
|
||||
/// <returns>A new <see cref="UserGroups"/>.</returns>
|
||||
public UserGroups Groups() => new();
|
||||
|
||||
/// <summary>
|
||||
/// If only OIDC logins and registration is allowed.
|
||||
/// </summary>
|
||||
/// <param name="securityConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the <see cref="SecurityConfiguration"/>.</param>
|
||||
/// <returns><see langword="true"/> if OIDC strict mode is enabled, <see langword="false"/> otherwise.</returns>
|
||||
public bool OidcStrictMode(
|
||||
[Service] IOptions<SecurityConfiguration> securityConfigurationOptions)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(securityConfigurationOptions);
|
||||
return securityConfigurationOptions.Value.OidcStrictMode;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current <see cref="User"/>.
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Tgstation.Server.Host.Models
|
||||
{
|
||||
/// <inheritdoc cref="Api.Models.OidcConnection" />
|
||||
public sealed class OidcConnection : Api.Models.OidcConnection, ILegacyApiTransformable<Api.Models.OidcConnection>
|
||||
{
|
||||
/// <summary>
|
||||
/// The row Id.
|
||||
/// </summary>
|
||||
public long Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Api.Models.EntityId.Id"/> of <see cref="User"/>.
|
||||
/// </summary>
|
||||
public long UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The owning <see cref="Models.User"/>.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public User? User { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Api.Models.OidcConnection ToApi() => new()
|
||||
{
|
||||
SchemeKey = SchemeKey,
|
||||
ExternalUserId = ExternalUserId,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -73,10 +73,15 @@ namespace Tgstation.Server.Host.Models
|
||||
public ICollection<TestMerge>? TestMerges { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="TestMerge"/>s made by the <see cref="User"/>.
|
||||
/// The <see cref="OAuthConnection"/>s for the <see cref="User"/>.
|
||||
/// </summary>
|
||||
public ICollection<OAuthConnection>? OAuthConnections { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="OidcConnection"/>s for the <see cref="User"/>.
|
||||
/// </summary>
|
||||
public ICollection<OidcConnection>? OidcConnections { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Change a <see cref="UserName.Name"/> into a <see cref="CanonicalName"/>.
|
||||
/// </summary>
|
||||
@@ -104,6 +109,9 @@ namespace Tgstation.Server.Host.Models
|
||||
result.OAuthConnections = OAuthConnections
|
||||
?.Select(x => x.ToApi())
|
||||
.ToList();
|
||||
result.OidcConnections = OidcConnections
|
||||
?.Select(x => x.ToApi())
|
||||
.ToList();
|
||||
result.Group = Group?.ToApi(false);
|
||||
result.PermissionSet = PermissionSet?.ToApi();
|
||||
return result;
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
@@ -13,8 +15,10 @@ using Microsoft.IdentityModel.JsonWebTokens;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
using Tgstation.Server.Api;
|
||||
using Tgstation.Server.Api.Rights;
|
||||
using Tgstation.Server.Host.Configuration;
|
||||
using Tgstation.Server.Host.Database;
|
||||
using Tgstation.Server.Host.Extensions;
|
||||
using Tgstation.Server.Host.Models;
|
||||
using Tgstation.Server.Host.Utils;
|
||||
|
||||
@@ -23,6 +27,16 @@ namespace Tgstation.Server.Host.Security
|
||||
/// <inheritdoc cref="IAuthenticationContext" />
|
||||
sealed class AuthenticationContextFactory : ITokenValidator, IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Internal scheme prefix for OIDC schemes.
|
||||
/// </summary>
|
||||
public const string OpenIDConnectAuthenticationSchemePrefix = $"{OpenIdConnectDefaults.AuthenticationScheme}.";
|
||||
|
||||
/// <summary>
|
||||
/// Claim name used to set user groups in OIDC strict mode.
|
||||
/// </summary>
|
||||
public const string TgsGroupIdClaimName = "tgstation-server-group-id";
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IAuthenticationContext"/> the <see cref="AuthenticationContextFactory"/> created.
|
||||
/// </summary>
|
||||
@@ -48,6 +62,11 @@ namespace Tgstation.Server.Host.Security
|
||||
/// </summary>
|
||||
readonly SwarmConfiguration swarmConfiguration;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="SecurityConfiguration"/> for the <see cref="AuthenticationContextFactory"/>.
|
||||
/// </summary>
|
||||
readonly SecurityConfiguration securityConfiguration;
|
||||
|
||||
/// <summary>
|
||||
/// Backing field for <see cref="CurrentAuthenticationContext"/>.
|
||||
/// </summary>
|
||||
@@ -63,6 +82,30 @@ namespace Tgstation.Server.Host.Security
|
||||
/// </summary>
|
||||
int initialized;
|
||||
|
||||
/// <summary>
|
||||
/// Parse a <see cref="DateTimeOffset"/> out of a <see cref="Claim"/> in a given <paramref name="principal"/>.
|
||||
/// </summary>
|
||||
/// <param name="principal">The <see cref="ClaimsPrincipal"/> containing claims.</param>
|
||||
/// <param name="key">The <see cref="Claim"/> name to parse from.</param>
|
||||
/// <returns>The parsed <see cref="DateTimeOffset"/>.</returns>
|
||||
static DateTimeOffset ParseTime(ClaimsPrincipal principal, string key)
|
||||
{
|
||||
var claim = principal.FindFirst(key);
|
||||
if (claim == default)
|
||||
throw new InvalidOperationException($"Missing '{key}' claim!");
|
||||
|
||||
try
|
||||
{
|
||||
return new DateTimeOffset(
|
||||
EpochTime.DateTime(
|
||||
Int64.Parse(claim.Value, CultureInfo.InvariantCulture)));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to parse '{key}'!", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AuthenticationContextFactory"/> class.
|
||||
/// </summary>
|
||||
@@ -70,12 +113,14 @@ namespace Tgstation.Server.Host.Security
|
||||
/// <param name="identityCache">The value of <see cref="identityCache"/>.</param>
|
||||
/// <param name="apiHeadersProvider">The <see cref="IApiHeadersProvider"/> containing the value of <see cref="apiHeaders"/>.</param>
|
||||
/// <param name="swarmConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="swarmConfiguration"/>.</param>
|
||||
/// <param name="securityConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="securityConfiguration"/>.</param>
|
||||
/// <param name="logger">The value of <see cref="logger"/>.</param>
|
||||
public AuthenticationContextFactory(
|
||||
IDatabaseContext databaseContext,
|
||||
IIdentityCache identityCache,
|
||||
IApiHeadersProvider apiHeadersProvider,
|
||||
IOptions<SwarmConfiguration> swarmConfigurationOptions,
|
||||
IOptions<SecurityConfiguration> securityConfigurationOptions,
|
||||
ILogger<AuthenticationContextFactory> logger)
|
||||
{
|
||||
this.databaseContext = databaseContext ?? throw new ArgumentNullException(nameof(databaseContext));
|
||||
@@ -84,6 +129,8 @@ namespace Tgstation.Server.Host.Security
|
||||
|
||||
apiHeaders = apiHeadersProvider.ApiHeaders;
|
||||
swarmConfiguration = swarmConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(swarmConfigurationOptions));
|
||||
securityConfiguration = securityConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(securityConfigurationOptions));
|
||||
|
||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
|
||||
currentAuthenticationContext = new AuthenticationContext();
|
||||
@@ -94,7 +141,7 @@ namespace Tgstation.Server.Host.Security
|
||||
|
||||
/// <inheritdoc />
|
||||
#pragma warning disable CA1506 // TODO: Decomplexify
|
||||
public async Task ValidateToken(TokenValidatedContext tokenValidatedContext, CancellationToken cancellationToken)
|
||||
public async Task ValidateTgsToken(Microsoft.AspNetCore.Authentication.JwtBearer.TokenValidatedContext tokenValidatedContext, CancellationToken cancellationToken)
|
||||
#pragma warning restore CA1506
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(tokenValidatedContext);
|
||||
@@ -121,26 +168,8 @@ namespace Tgstation.Server.Host.Security
|
||||
throw new InvalidOperationException("Failed to parse user ID!", e);
|
||||
}
|
||||
|
||||
DateTimeOffset ParseTime(string key)
|
||||
{
|
||||
var claim = principal.FindFirst(key);
|
||||
if (claim == default)
|
||||
throw new InvalidOperationException($"Missing '{key}' claim!");
|
||||
|
||||
try
|
||||
{
|
||||
return new DateTimeOffset(
|
||||
EpochTime.DateTime(
|
||||
Int64.Parse(claim.Value, CultureInfo.InvariantCulture)));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to parse '{key}'!", ex);
|
||||
}
|
||||
}
|
||||
|
||||
var notBefore = ParseTime(JwtRegisteredClaimNames.Nbf);
|
||||
var expires = ParseTime(JwtRegisteredClaimNames.Exp);
|
||||
var notBefore = ParseTime(principal, JwtRegisteredClaimNames.Nbf);
|
||||
var expires = ParseTime(principal, JwtRegisteredClaimNames.Exp);
|
||||
|
||||
var user = await databaseContext
|
||||
.Users
|
||||
@@ -202,5 +231,158 @@ namespace Tgstation.Server.Host.Security
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task ValidateOidcToken(RemoteAuthenticationContext<OpenIdConnectOptions> tokenValidatedContext, string schemeKey, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(tokenValidatedContext);
|
||||
|
||||
var principal = tokenValidatedContext.Principal;
|
||||
if (principal == null)
|
||||
throw new InvalidOperationException("Expected a valid principal here!");
|
||||
|
||||
var userIdClaim = principal.FindFirst(JwtRegisteredClaimNames.Sub);
|
||||
if (userIdClaim == default)
|
||||
throw new InvalidOperationException($"Missing '{JwtRegisteredClaimNames.Sub}' claim!");
|
||||
|
||||
var userId = userIdClaim.Value;
|
||||
var scheme = tokenValidatedContext.Scheme.Name;
|
||||
var deprefixedScheme = scheme.Substring(OpenIDConnectAuthenticationSchemePrefix.Length);
|
||||
var connection = await databaseContext
|
||||
.OidcConnections
|
||||
.AsQueryable()
|
||||
.Where(oidcConnection => oidcConnection.ExternalUserId == userId && oidcConnection.SchemeKey == deprefixedScheme)
|
||||
.Include(oidcConnection => oidcConnection.User)
|
||||
.ThenInclude(user => user!.Group)
|
||||
.ThenInclude(group => group!.PermissionSet)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
User user;
|
||||
if (!securityConfiguration.OidcStrictMode)
|
||||
{
|
||||
if (connection == default)
|
||||
{
|
||||
tokenValidatedContext.Fail($"Unable to find user with OidcConnection for {deprefixedScheme}/{userId}!");
|
||||
return;
|
||||
}
|
||||
|
||||
user = connection.User!;
|
||||
}
|
||||
else
|
||||
{
|
||||
var groupClaim = principal.FindFirst(TgsGroupIdClaimName);
|
||||
long? groupId;
|
||||
if (groupClaim == default)
|
||||
groupId = null;
|
||||
else if (Int64.TryParse(groupClaim.Value, out long groupIdParsed))
|
||||
groupId = groupIdParsed;
|
||||
else
|
||||
{
|
||||
tokenValidatedContext.Fail($"User has non-numeric '{TgsGroupIdClaimName}' claim!");
|
||||
return;
|
||||
}
|
||||
|
||||
UserGroup? group = groupId.HasValue
|
||||
? await databaseContext
|
||||
.Groups
|
||||
.AsQueryable()
|
||||
.Where(group => group.Id == groupId.Value)
|
||||
.Include(group => group.PermissionSet)
|
||||
.FirstOrDefaultAsync(cancellationToken)
|
||||
: null;
|
||||
|
||||
var missingClaimError = $"User missing '{TgsGroupIdClaimName}' claim!";
|
||||
if (connection == default)
|
||||
{
|
||||
var username = principal.Identity?.Name;
|
||||
if (username == null)
|
||||
{
|
||||
tokenValidatedContext.Fail("Failed to retrieve user's name from retrieved claims!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (username.Contains(':', StringComparison.Ordinal))
|
||||
{
|
||||
tokenValidatedContext.Fail("Cannot create users with the ':' in their name!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (group == null)
|
||||
{
|
||||
tokenValidatedContext.Fail(
|
||||
groupId.HasValue
|
||||
? $"'{TgsGroupIdClaimName}' does not point to a valid group!"
|
||||
: missingClaimError);
|
||||
return;
|
||||
}
|
||||
|
||||
var tgsUser = await databaseContext
|
||||
.Users
|
||||
.GetTgsUser(
|
||||
dbUser => new User
|
||||
{
|
||||
Id = dbUser.Id!.Value,
|
||||
},
|
||||
cancellationToken);
|
||||
|
||||
user = new User
|
||||
{
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
CanonicalName = User.CanonicalizeName(username),
|
||||
Name = username,
|
||||
CreatedById = tgsUser.Id,
|
||||
Enabled = true,
|
||||
GroupId = group.Id,
|
||||
OidcConnections = new List<OidcConnection>
|
||||
{
|
||||
new OidcConnection
|
||||
{
|
||||
SchemeKey = schemeKey,
|
||||
ExternalUserId = userId,
|
||||
},
|
||||
},
|
||||
PasswordHash = "_", // This can't be hashed
|
||||
};
|
||||
|
||||
databaseContext.Users.Add(user);
|
||||
}
|
||||
else
|
||||
{
|
||||
user = connection.User!;
|
||||
|
||||
// group update
|
||||
if (group == null)
|
||||
{
|
||||
user.PermissionSet = new PermissionSet
|
||||
{
|
||||
AdministrationRights = AdministrationRights.None,
|
||||
InstanceManagerRights = InstanceManagerRights.None,
|
||||
};
|
||||
user.GroupId = null;
|
||||
user.Enabled = false;
|
||||
|
||||
tokenValidatedContext.Fail(missingClaimError);
|
||||
return;
|
||||
}
|
||||
|
||||
user.Group = group;
|
||||
if (user.PermissionSet != null)
|
||||
databaseContext.PermissionSets.Remove(user.PermissionSet);
|
||||
|
||||
user.Enabled = true;
|
||||
}
|
||||
|
||||
await databaseContext.Save(cancellationToken);
|
||||
}
|
||||
|
||||
var expires = ParseTime(principal, JwtRegisteredClaimNames.Exp);
|
||||
|
||||
currentAuthenticationContext.Initialize(
|
||||
user,
|
||||
expires,
|
||||
Guid.NewGuid().ToString(),
|
||||
null,
|
||||
null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,8 +25,8 @@ namespace Tgstation.Server.Host.Security
|
||||
/// Create a <see cref="TokenResponse"/> for a given <paramref name="user"/>.
|
||||
/// </summary>
|
||||
/// <param name="user">The <see cref="Models.User"/> to create the token for. Must have the <see cref="Api.Models.EntityId.Id"/> field available.</param>
|
||||
/// <param name="oAuth">Whether or not this is an OAuth login.</param>
|
||||
/// <param name="serviceLogin">Whether or not this is an external service login.</param>
|
||||
/// <returns>A new token <see cref="string"/>.</returns>
|
||||
string CreateToken(Models.User user, bool oAuth);
|
||||
string CreateToken(Models.User user, bool serviceLogin);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,31 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
|
||||
|
||||
namespace Tgstation.Server.Host.Security
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles <see cref="TokenValidatedContext"/>s.
|
||||
/// Handles validating authentication tokens.
|
||||
/// </summary>
|
||||
public interface ITokenValidator
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles <paramref name="tokenValidatedContext"/>.
|
||||
/// Handles TGS <paramref name="tokenValidatedContext"/>s.
|
||||
/// </summary>
|
||||
/// <param name="tokenValidatedContext">The <see cref="TokenValidatedContext"/>.</param>
|
||||
/// <param name="tokenValidatedContext">The <see cref="Microsoft.AspNetCore.Authentication.JwtBearer.TokenValidatedContext"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
|
||||
Task ValidateToken(TokenValidatedContext tokenValidatedContext, CancellationToken cancellationToken);
|
||||
Task ValidateTgsToken(Microsoft.AspNetCore.Authentication.JwtBearer.TokenValidatedContext tokenValidatedContext, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Handles OIDC <paramref name="tokenValidatedContext"/>s.
|
||||
/// </summary>
|
||||
/// <param name="tokenValidatedContext">The <see cref="RemoteAuthenticationContext{TOptions}"/> for <see cref="OpenIdConnectOptions"/>.</param>
|
||||
/// <param name="schemeKey">The scheme key being used to login.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
|
||||
Task ValidateOidcToken(RemoteAuthenticationContext<OpenIdConnectOptions> tokenValidatedContext, string schemeKey, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,9 @@ namespace Tgstation.Server.Host.Security.OAuth
|
||||
sealed class InvisionCommunityOAuthValidator : GenericOAuthValidator
|
||||
{
|
||||
/// <inheritdoc />
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
public override OAuthProvider Provider => OAuthProvider.InvisionCommunity;
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Uri TokenUrl => new($"{OAuthConfiguration.ServerUrl}/oauth/token/"); // This needs the trailing slash or it doesnt get the token. Do not remove.
|
||||
|
||||
@@ -14,7 +14,9 @@ namespace Tgstation.Server.Host.Security.OAuth
|
||||
sealed class KeycloakOAuthValidator : GenericOAuthValidator
|
||||
{
|
||||
/// <inheritdoc />
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
public override OAuthProvider Provider => OAuthProvider.Keycloak;
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Uri TokenUrl => new($"{BaseProtocolPath}/token");
|
||||
|
||||
@@ -57,21 +57,18 @@ namespace Tgstation.Server.Host.Security.OAuth
|
||||
loggerFactory.CreateLogger<DiscordOAuthValidator>(),
|
||||
discordConfig));
|
||||
|
||||
if (securityConfiguration.OAuth.TryGetValue(OAuthProvider.TGForums, out var tgConfig))
|
||||
validatorsBuilder.Add(
|
||||
new TGForumsOAuthValidator(
|
||||
httpClientFactory,
|
||||
loggerFactory.CreateLogger<TGForumsOAuthValidator>(),
|
||||
tgConfig));
|
||||
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
if (securityConfiguration.OAuth.TryGetValue(OAuthProvider.Keycloak, out var keyCloakConfig))
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
validatorsBuilder.Add(
|
||||
new KeycloakOAuthValidator(
|
||||
httpClientFactory,
|
||||
loggerFactory.CreateLogger<KeycloakOAuthValidator>(),
|
||||
keyCloakConfig));
|
||||
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
if (securityConfiguration.OAuth.TryGetValue(OAuthProvider.InvisionCommunity, out var invisionConfig))
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
validatorsBuilder.Add(
|
||||
new InvisionCommunityOAuthValidator(
|
||||
httpClientFactory,
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
using System;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
using Tgstation.Server.Api.Models;
|
||||
using Tgstation.Server.Common.Http;
|
||||
using Tgstation.Server.Host.Configuration;
|
||||
|
||||
namespace Tgstation.Server.Host.Security.OAuth
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="IOAuthValidator"/> for /tg/ forums.
|
||||
/// </summary>
|
||||
sealed class TGForumsOAuthValidator : GenericOAuthValidator
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override OAuthProvider Provider => OAuthProvider.TGForums;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Uri TokenUrl => new("https://tgstation13.org/phpBB/app.php/tgapi/oauth/token");
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Uri UserInformationUrl => new("https://tgstation13.org/phpBB/app.php/tgapi/user/me");
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TGForumsOAuthValidator"/> class.
|
||||
/// </summary>
|
||||
/// <param name="httpClientFactory">The <see cref="IAbstractHttpClientFactory"/> for the <see cref="GenericOAuthValidator"/>.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="GenericOAuthValidator"/>.</param>
|
||||
/// <param name="oAuthConfiguration">The <see cref="OAuthConfiguration"/> for the <see cref="GenericOAuthValidator"/>.</param>
|
||||
public TGForumsOAuthValidator(
|
||||
IAbstractHttpClientFactory httpClientFactory,
|
||||
ILogger<TGForumsOAuthValidator> logger,
|
||||
OAuthConfiguration oAuthConfiguration)
|
||||
: base(
|
||||
httpClientFactory,
|
||||
logger,
|
||||
oAuthConfiguration)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string DecodeTokenPayload(dynamic responseJson) => responseJson.access_token;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string DecodeUserInformationPayload(dynamic responseJson) => responseJson.phpbb_username;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override OAuthTokenRequest CreateTokenRequest(string code) => new(OAuthConfiguration, code, "user");
|
||||
}
|
||||
}
|
||||
@@ -101,7 +101,7 @@ namespace Tgstation.Server.Host.Security
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string CreateToken(User user, bool oAuth)
|
||||
public string CreateToken(User user, bool serviceLogin)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(user);
|
||||
|
||||
@@ -121,7 +121,7 @@ namespace Tgstation.Server.Host.Security
|
||||
else
|
||||
notBefore = now;
|
||||
|
||||
var expiry = now.AddMinutes(oAuth
|
||||
var expiry = now.AddMinutes(serviceLogin
|
||||
? securityConfiguration.OAuthTokenExpiryMinutes
|
||||
: securityConfiguration.TokenExpiryMinutes);
|
||||
|
||||
|
||||
@@ -97,8 +97,6 @@
|
||||
<PackageReference Include="DotEnv.Core" Version="3.1.0" />
|
||||
<!-- Usage: Text formatter for Elasticsearch logging plugin -->
|
||||
<PackageReference Include="Elastic.CommonSchema.Serilog" Version="8.12.3" />
|
||||
<!-- Usage: GitLab interop -->
|
||||
<PackageReference Include="GitLabApiClient" Version="1.8.0" />
|
||||
<!-- Usage: GraphQL API Engine -->
|
||||
<PackageReference Include="HotChocolate.AspNetCore" Version="15.0.3" />
|
||||
<!-- Usage: GraphQL Authorization Plugin -->
|
||||
@@ -111,24 +109,28 @@
|
||||
<PackageReference Include="HotChocolate.Types.Scalars" Version="15.0.3" />
|
||||
<!-- Usage: git interop -->
|
||||
<PackageReference Include="LibGit2Sharp" Version="0.31.0" />
|
||||
<!-- Usage: OpenID Connect support -->
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="8.0.13" />
|
||||
<!-- Usage: Support ""legacy"" Newotonsoft.Json in HTTP pipeline. The rest of our codebase uses Newtonsoft. -->
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.13" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.14" />
|
||||
<!-- Usage: Using target JSON serializer for API -->
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="9.0.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="9.0.3" />
|
||||
<!-- Usage: Generating dumps of dotnet engine processes -->
|
||||
<PackageReference Include="Microsoft.Diagnostics.NETCore.Client" Version="0.2.607501" />
|
||||
<!-- Usage: Database ORM -->
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.13" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.3" />
|
||||
<!-- Usage: Automatic migration generation using command line -->
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.13">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.3">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<!-- Usage: Sqlite ORM plugin -->
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.13" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.3" />
|
||||
<!-- Usage: MSSQL ORM plugin -->
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.13" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.3" />
|
||||
<!-- Usage: Database connectivity health check -->
|
||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="8.0.13" />
|
||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="8.0.14" />
|
||||
<!-- Usage: OIDC support prerequisite. See https://stackoverflow.com/a/78650835/3976486 -->
|
||||
<PackageReference Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" Version="8.6.0" />
|
||||
<!-- Usage: POSIX support for syscalls, signals, and symlinks -->
|
||||
<PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" />
|
||||
<!-- Usage: Cron string parsing -->
|
||||
@@ -136,9 +138,9 @@
|
||||
<!-- Usage: YAML config plugin -->
|
||||
<PackageReference Include="NetEscapades.Configuration.Yaml" Version="3.1.0" />
|
||||
<!-- Usage: PostgresSQL ORM plugin -->
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.11" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
||||
<!-- Usage: MYSQL/MariaDB ORM plugin -->
|
||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2" />
|
||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0-preview.3.efcore.9.0.0" />
|
||||
<!-- Usage: Publishing Prometheus metrics -->
|
||||
<PackageReference Include="prometheus-net.AspNetCore.HealthChecks" Version="8.2.1" />
|
||||
<!-- Usage: Discord interop -->
|
||||
@@ -154,20 +156,21 @@
|
||||
<!-- Usage: File logging plugin -->
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
|
||||
<!-- Usage: OpenAPI spec generator -->
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.2.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.3.1" />
|
||||
<!-- Usage: Newtonsoft.Json plugin for OpenAPI spec generator -->
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Newtonsoft" Version="7.2.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Newtonsoft" Version="7.3.1" />
|
||||
<!-- Usage: Windows authentication plugin allowing searching for users by name -->
|
||||
<PackageReference Include="System.DirectoryServices.AccountManagement" Version="9.0.2" />
|
||||
<PackageReference Include="System.DirectoryServices.AccountManagement" Version="9.0.3" />
|
||||
<!-- Usage: Identifying owning user of Windows Process objects -->
|
||||
<PackageReference Include="System.Management" Version="9.0.2" />
|
||||
<PackageReference Include="System.Management" Version="9.0.3" />
|
||||
<!-- Usage: Temporary resolution to compatibility issues with EFCore 7 and .NET 8 -->
|
||||
<PackageReference Include="System.Security.Permissions" Version="9.0.2" />
|
||||
<PackageReference Include="System.Security.Permissions" Version="9.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Tgstation.Server.Api\Tgstation.Server.Api.csproj" />
|
||||
<ProjectReference Include="..\Tgstation.Server.Host.Common\Tgstation.Server.Host.Common.csproj" />
|
||||
<ProjectReference Include="..\Tgstation.Server.Host.Utils.GitLab.GraphQL\Tgstation.Server.Host.Utils.GitLab.GraphQL.csproj" />
|
||||
<ProjectReference Include="..\Tgstation.Server.Shared\Tgstation.Server.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -163,6 +163,73 @@ namespace Tgstation.Server.Host.Utils.GitHub
|
||||
.WaitAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task AppendCommentOnIssue(string repoOwner, string repoName, string comment, IssueComment issueComment, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(repoOwner);
|
||||
|
||||
ArgumentNullException.ThrowIfNull(repoName);
|
||||
|
||||
ArgumentNullException.ThrowIfNull(comment);
|
||||
|
||||
ArgumentNullException.ThrowIfNull(issueComment);
|
||||
|
||||
logger.LogTrace("AppendCommentOnIssue");
|
||||
|
||||
return gitHubClient
|
||||
.Issue
|
||||
.Comment
|
||||
.Update(
|
||||
repoOwner,
|
||||
repoName,
|
||||
issueComment.Id,
|
||||
issueComment.Body + comment)
|
||||
.WaitAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<IssueComment?> GetExistingCommentOnIssue(string repoOwner, string repoName, string header, int issueNumber, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(repoOwner);
|
||||
|
||||
ArgumentNullException.ThrowIfNull(repoName);
|
||||
|
||||
ArgumentNullException.ThrowIfNull(header);
|
||||
|
||||
logger.LogTrace("GetExistingCommentOnIssue");
|
||||
|
||||
var comments = await gitHubClient
|
||||
.Issue
|
||||
.Comment
|
||||
.GetAllForIssue(
|
||||
repoOwner,
|
||||
repoName,
|
||||
issueNumber)
|
||||
.WaitAsync(cancellationToken);
|
||||
if (comments == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
long userId = await GetCurrentUserId(cancellationToken);
|
||||
|
||||
for (int i = comments.Count - 1; i > -1; i--)
|
||||
{
|
||||
var currentComment = comments[i];
|
||||
if (currentComment.User?.Id == userId && (currentComment.Body?.StartsWith(header) ?? false))
|
||||
{
|
||||
if (currentComment.Body.Length > 250000)
|
||||
{ // Limit should be 262,143 so we'll leave a 12,143 buffer
|
||||
return null;
|
||||
}
|
||||
|
||||
return currentComment;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<long> GetRepositoryId(string repoOwner, string repoName, CancellationToken cancellationToken)
|
||||
{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user