mirror of
https://github.com/tgstation/tgstation-server.git
synced 2026-08-28 07:27:18 +01:00
@@ -189,6 +189,20 @@ We have a script to do this.
|
||||
1. Run `dotnet ef migrations add SL<NameOfYourMigration> --context SqliteDatabaseContext`.
|
||||
1. Follow the above steps.
|
||||
|
||||
## Adding OAuth Providers
|
||||
|
||||
OAuth providers are hardcoded but it is fairly easy to add new ones. The flow doesn't need to be strict OAuth either (r.e. /tg/ forums). Follow the following steps:
|
||||
|
||||
1. Add the name to the [Tgstation.Server.Api.Models.OAuthProviders](../src/Tgstation.Server.Api/Models/OAuthProviders.cs) enum (Also necessitates a minor HTTP API version bump).
|
||||
1. Create an implementation of [IOAuthValidator](../src/Tgstation.Server.Host/Security/OAuth/IOAuthValidator.cs).
|
||||
- Most providers can simply override the [GenericOAuthValidator](../src/Tgstation.Server.Host/Security/OAuth/GenericOAuthValidator.cs).
|
||||
1. Construct the implementation in the [OAuthProviders](../src/Tgstation.Server.Host/Security/OAuth/OAuthProviders.cs) class.
|
||||
1. Add a null entry to the default [appsettings.json](../src/Tgstation.Server.Host/appsettings.json).
|
||||
1. Update the main [README.md](../README.md) to indicate the new provider.
|
||||
1. Update the [API documentation](../docs/API.dox) to indicate the new provider.
|
||||
|
||||
TGS should now be able to accept authentication response tokens from your provider.
|
||||
|
||||
### Important Note About the \[Required\] Attribute.
|
||||
|
||||
We use this attribute to ensure EFCore generated tables are not nullable for specific properties. They are valid to be null in API communication. Do not use this attribute expecting the model validator to prevent null data in API request.
|
||||
|
||||
@@ -120,6 +120,14 @@ Create an `appsettings.Production.json` file next to `appsettings.json`. This wi
|
||||
|
||||
- `ControlPanel:AllowedOrigins`: Set the Access-Control-Allow-Origin headers to this list of origins for all responses (also enables all headers and methods). This is overridden by `ControlPanel:AllowAnyOrigin`
|
||||
|
||||
- `Security:<Provider Name>OAuth`: Sets the OAuth client ID and secret for a given `<Provider Name>`. The currently supported providers are `GitHub`, `Discord`, and `TGForums`. Setting these fields to `null` disables logins with the provider, but does not stop users from associating their accounts using the API. Sample Entry:
|
||||
```json
|
||||
"GitHubOAuth":{
|
||||
"ClientId": "... (Note for `TGForums`, this is the redirect_uri used)",
|
||||
"ClientSecret": "..."
|
||||
}
|
||||
```
|
||||
|
||||
### Database Configuration
|
||||
|
||||
If using a MariaDB/MySQL server, our client library [recommends you set 'utf8mb4' as your default charset](https://github.com/PomeloFoundation/Pomelo.EntityFrameworkCore.MySql#1-recommended-server-charset) disregard at your own risk.
|
||||
@@ -372,10 +380,9 @@ TGS 4 can self update without stopping your DreamDaemon servers. Any V4 release
|
||||
Here are tools for interacting with the TGS 4 web API
|
||||
|
||||
- [tgstation-server-control-panel]: Official client and included with the server (WIP). A react web app for using tgstation-server.
|
||||
- [Tgstation.Server.ControlPanel](https://github.com/tgstation/Tgstation.Server.ControlPanel): Official client. A cross platform GUI for using tgstation-server
|
||||
- [Tgstation.Server.Client](https://www.nuget.org/packages/Tgstation.Server.Client): A nuget .NET Standard 2.0 TAP based library for communicating with tgstation-server
|
||||
- [Tgstation.Server.Api](https://www.nuget.org/packages/Tgstation.Server.Api): A nuget .NET Standard 2.0 library containing API definitions for tgstation-server
|
||||
- [Postman](https://www.getpostman.com/): This repository contains [TGS.postman_collection.json](tools/TGS.postman_collection.json) which is used during development for testing. Contains example requests for all endpoints but takes some knowledge to use (Note that the pre-request script is configured to login the default admin user for every request)
|
||||
- [Tgstation.Server.ControlPanel](https://github.com/tgstation/Tgstation.Server.ControlPanel): Official client. A cross platform GUI for using tgstation-server. Feature complete but lacks OAuth login options.
|
||||
- [Tgstation.Server.Client](https://www.nuget.org/packages/Tgstation.Server.Client): A nuget .NET Standard 2.0 TAP based library for communicating with tgstation-server. Feature complete.
|
||||
- [Tgstation.Server.Api](https://www.nuget.org/packages/Tgstation.Server.Api): A nuget .NET Standard 2.0 library containing API definitions for tgstation-server. Feature complete.
|
||||
|
||||
Contact project maintainers to get your client added to this list
|
||||
|
||||
|
||||
+4
-4
@@ -3,10 +3,10 @@
|
||||
<!-- Integration tests will ensure they match across the board -->
|
||||
<Import Project="ControlPanelVersion.props" />
|
||||
<PropertyGroup>
|
||||
<TgsCoreVersion>4.6.0</TgsCoreVersion>
|
||||
<TgsConfigVersion>2.1.1</TgsConfigVersion>
|
||||
<TgsApiVersion>7.4.0</TgsApiVersion>
|
||||
<TgsClientVersion>8.4.0</TgsClientVersion>
|
||||
<TgsCoreVersion>4.7.0</TgsCoreVersion>
|
||||
<TgsConfigVersion>2.2.0</TgsConfigVersion>
|
||||
<TgsApiVersion>8.0.0</TgsApiVersion>
|
||||
<TgsClientVersion>9.0.0</TgsClientVersion>
|
||||
<TgsDmapiVersion>5.2.8</TgsDmapiVersion>
|
||||
<TgsHostWatchdogVersion>1.1.0</TgsHostWatchdogVersion>
|
||||
<TgsContainerScriptVersion>1.1.1</TgsContainerScriptVersion>
|
||||
|
||||
+46
-18
@@ -77,6 +77,24 @@ TGS will only every return the response codes listed here
|
||||
- 501: Not implemented. Functionality not available in the current server version
|
||||
- 503: Service unavailable. The server is either starting up or shutting down and isn't ready to respond to requests. You can try again soon and a response/lack thereof will indicate which of the two events it was
|
||||
|
||||
@section api_ver Server Information
|
||||
|
||||
The versions of the TGS host can be retireved with this request.
|
||||
|
||||
Note: This endpoint does not require authentication
|
||||
|
||||
GET "/" => @ref Tgstation.Server.Api.Models.ServerInformation
|
||||
|
||||
The Version model fields are based on the C# one and looks like this:
|
||||
|
||||
@code{.json}
|
||||
{
|
||||
"version": "<major>.<minor>.<patch>.<revision>"
|
||||
}
|
||||
@endcode
|
||||
|
||||
Other fields may be present in the Version model but should be ignored. See a description of these version numbers <a href="https://github.com/tgstation/tgstation-server/blob/master/.github/CONTRIBUTING.md#versioning">here</a>.
|
||||
|
||||
@section api_auth Authentication
|
||||
|
||||
Every request made to TGS requires authentication. It is provided in the form of the Authorization header.
|
||||
@@ -87,7 +105,7 @@ POST "/" => @ref Tgstation.Server.Api.Models.Token
|
||||
|
||||
Headers:
|
||||
|
||||
- Authorization:basic `<Base64 encoded credentials in the form username:password>`
|
||||
- Authorization:Basic `<Base64 encoded credentials in the form username:password>`
|
||||
|
||||
If the provided credentials are valid and your user account is enabled you will recieve a @ref Tgstation.Server.Api.Models.Token object
|
||||
@code{.json}
|
||||
@@ -100,10 +118,36 @@ If your account is disabled, you will recieve a 403 response.
|
||||
|
||||
You may recognize the bearer value as a <a href="https://jwt.io">Json Web Token</a>. This is a secure representation of your identity to the server. It expires after a set period of time or until your password changes. It must be present for requests made to all other APIs. To do so add the following header to your other requests
|
||||
|
||||
- Authorization:bearer `<your bearer token>`
|
||||
- Authorization:Bearer `<your bearer token>`
|
||||
|
||||
Continue to use this token until you begin to recieve 401 responses from the API. Then repeat the process to get a new one if your credentials are still valid
|
||||
|
||||
@subsection api_auth_o OAuth 2.0
|
||||
|
||||
TGS4 supports OAuth 2.0 with select providers for authentication.
|
||||
|
||||
The flow for this is as follows:
|
||||
|
||||
- Retrieve the @ref api_ver to find out available OAuth providers and their respective client IDs.
|
||||
- Send the user to the Authorization Request endpoint for the provider using the client ID from above. See https://tools.ietf.org/html/rfc6749#section-4.1.1. DO NOT specify a redirect URI, this should be configured in the provider.
|
||||
- Retrieve the authorization response code after successfully completing the authorize step above.
|
||||
- Perform the following request:
|
||||
|
||||
POST "/" => @ref Tgstation.Server.Api.Models.Token
|
||||
|
||||
Headers:
|
||||
|
||||
- Authorization:OAuth `OAuth Authorization Response Code`
|
||||
- OAuthProvider:<Provider Name>
|
||||
|
||||
You will be granted a bearer token as in basic auth. This will have an extended expiration to avoid repeating the entire process.
|
||||
|
||||
@subsubsection api_auth_o_providers Supported Providers
|
||||
|
||||
- ID: 0, Name: GitHub, Documentation: https://developer.github.com/apps/building-oauth-apps/authorizing-oauth-apps/
|
||||
- ID: 1, Name: Discord, Documentation: https://discord.com/developers/docs/topics/oauth2
|
||||
- ID: 2, Name: TGForums, Documentation: https://tgstation13.org/phpBB/viewtopic.php?f=45&t=9922
|
||||
|
||||
@section api_perms Permissions
|
||||
|
||||
Almost all actions available require some level of user permissions. Which are divided into two categories:
|
||||
@@ -150,22 +194,6 @@ I DELETE "/InstanceUser/{UserId}" => OK
|
||||
|
||||
Users with the permission to modify @ref Tgstation.Server.Api.Models.Instance objects can also gain user editing rights for any Instance. See @ref api_instance
|
||||
|
||||
@section api_ver Version
|
||||
|
||||
The versions of the TGS host can be retireved with this request
|
||||
|
||||
GET "/" => @ref Tgstation.Server.Api.Models.ServerInformation
|
||||
|
||||
The Version model fields are based on the C# one and looks like this:
|
||||
|
||||
@code{.json}
|
||||
{
|
||||
"version": "<major>.<minor>.<patch>.<revision>"
|
||||
}
|
||||
@endcode
|
||||
|
||||
Other fields may be present in the Version model but should be ignored. See a description of these version numbers <a href="https://github.com/tgstation/tgstation-server/blob/master/.github/CONTRIBUTING.md#versioning">here</a>.
|
||||
|
||||
@section api_admin Server-wide Administrative Actions
|
||||
|
||||
You can retrieve server update information with this
|
||||
|
||||
@@ -8,6 +8,7 @@ using System.Net.Http.Headers;
|
||||
using System.Net.Mime;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using Tgstation.Server.Api.Models;
|
||||
|
||||
namespace Tgstation.Server.Api
|
||||
{
|
||||
@@ -17,24 +18,34 @@ namespace Tgstation.Server.Api
|
||||
public sealed class ApiHeaders
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="ApiVersion"/> header key
|
||||
/// The <see cref="ApiVersion"/> header key.
|
||||
/// </summary>
|
||||
public const string ApiVersionHeader = "Api";
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="InstanceId"/> header key
|
||||
/// The <see cref="InstanceId"/> header key.
|
||||
/// </summary>
|
||||
public const string InstanceIdHeader = "Instance";
|
||||
|
||||
/// <summary>
|
||||
/// The JWT authentication header scheme
|
||||
/// The <see cref="OAuthProvider"/> header key.
|
||||
/// </summary>
|
||||
public const string JwtAuthenticationScheme = "bearer";
|
||||
public const string OAuthProviderHeader = "OAuthProvider";
|
||||
|
||||
/// <summary>
|
||||
/// The JWT authentication header scheme
|
||||
/// </summary>
|
||||
public const string BasicAuthenticationScheme = "basic";
|
||||
public const string BearerAuthenticationScheme = "Bearer";
|
||||
|
||||
/// <summary>
|
||||
/// The JWT authentication header scheme
|
||||
/// </summary>
|
||||
public const string BasicAuthenticationScheme = "Basic";
|
||||
|
||||
/// <summary>
|
||||
/// The JWT authentication header scheme
|
||||
/// </summary>
|
||||
public const string OAuthAuthenticationScheme = "OAuth";
|
||||
|
||||
/// <summary>
|
||||
/// The current <see cref="System.Reflection.AssemblyName"/>
|
||||
@@ -47,7 +58,7 @@ namespace Tgstation.Server.Api
|
||||
public static readonly Version Version = AssemblyName.Version.Semver();
|
||||
|
||||
/// <summary>
|
||||
/// The instance <see cref="Models.EntityId.Id"/> being accessed
|
||||
/// The instance <see cref="EntityId.Id"/> being accessed
|
||||
/// </summary>
|
||||
public long? InstanceId { get; set; }
|
||||
|
||||
@@ -82,9 +93,14 @@ namespace Tgstation.Server.Api
|
||||
public string? Password { get; }
|
||||
|
||||
/// <summary>
|
||||
/// If the header uses password or JWT authentication
|
||||
/// The <see cref="Models.OAuthProvider"/> the <see cref="Token"/> is for, if any.
|
||||
/// </summary>
|
||||
public bool IsTokenAuthentication => Token != null;
|
||||
public OAuthProvider? OAuthProvider { get; }
|
||||
|
||||
/// <summary>
|
||||
/// If the header uses password or TGS JWT authentication.
|
||||
/// </summary>
|
||||
public bool IsTokenAuthentication => Token != null && !OAuthProvider.HasValue;
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a given <paramref name="otherVersion"/> is compatible with our own
|
||||
@@ -98,12 +114,15 @@ namespace Tgstation.Server.Api
|
||||
/// </summary>
|
||||
/// <param name="userAgent">The value of <see cref="UserAgent"/></param>
|
||||
/// <param name="token">The value of <see cref="Token"/></param>
|
||||
public ApiHeaders(ProductHeaderValue userAgent, string token) : this(userAgent, token, null, null)
|
||||
/// <param name="oauthProvider">The value of <see cref="OAuthProvider"/>.</param>
|
||||
public ApiHeaders(ProductHeaderValue userAgent, string token, OAuthProvider? oauthProvider = null) : this(userAgent, token, null, null)
|
||||
{
|
||||
if (userAgent == null)
|
||||
throw new ArgumentNullException(nameof(userAgent));
|
||||
if (token == null)
|
||||
throw new ArgumentNullException(nameof(token));
|
||||
|
||||
OAuthProvider = oauthProvider;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -126,8 +145,10 @@ namespace Tgstation.Server.Api
|
||||
/// Construct and validates <see cref="ApiHeaders"/> from a set of <paramref name="requestHeaders"/>
|
||||
/// </summary>
|
||||
/// <param name="requestHeaders">The <see cref="RequestHeaders"/> containing the <see cref="ApiHeaders"/></param>
|
||||
/// <param name="ignoreMissingAuth">If a missing <see cref="HeaderNames.Authorization"/> should be ignored.</param>
|
||||
/// <exception cref="HeadersException">Thrown if the <paramref name="requestHeaders"/> constitue invalid <see cref="ApiHeaders"/>.</exception>
|
||||
public ApiHeaders(RequestHeaders requestHeaders)
|
||||
#pragma warning disable CA1502
|
||||
public ApiHeaders(RequestHeaders requestHeaders, bool ignoreMissingAuth = false)
|
||||
{
|
||||
if (requestHeaders == null)
|
||||
throw new ArgumentNullException(nameof(requestHeaders));
|
||||
@@ -164,7 +185,10 @@ namespace Tgstation.Server.Api
|
||||
AddError(HeaderTypes.Api, $"Malformed {ApiVersionHeader} header!");
|
||||
|
||||
if (!requestHeaders.Headers.TryGetValue(HeaderNames.Authorization, out StringValues authorization))
|
||||
AddError(HeaderTypes.Authorization, $"Missing {HeaderNames.Authorization} header!");
|
||||
{
|
||||
if (!ignoreMissingAuth)
|
||||
AddError(HeaderTypes.Authorization, $"Missing {HeaderNames.Authorization} header!");
|
||||
}
|
||||
else
|
||||
{
|
||||
var auth = authorization.First();
|
||||
@@ -187,11 +211,22 @@ namespace Tgstation.Server.Api
|
||||
InstanceId = instanceId;
|
||||
}
|
||||
|
||||
#pragma warning disable CA1308 // Normalize strings to uppercase
|
||||
switch (scheme.ToLowerInvariant())
|
||||
#pragma warning restore CA1308 // Normalize strings to uppercase
|
||||
switch (scheme)
|
||||
{
|
||||
case JwtAuthenticationScheme:
|
||||
case OAuthAuthenticationScheme:
|
||||
if (requestHeaders.Headers.TryGetValue(OAuthProviderHeader, out StringValues oauthProviderValues))
|
||||
{
|
||||
var oauthProviderString = oauthProviderValues.First();
|
||||
if (Enum.TryParse<OAuthProvider>(oauthProviderString, out var oauthProvider))
|
||||
OAuthProvider = oauthProvider;
|
||||
else
|
||||
AddError(HeaderTypes.OAuthProvider, "Invalid OAuth provider!");
|
||||
}
|
||||
else
|
||||
AddError(HeaderTypes.OAuthProvider, $"Missing {OAuthProviderHeader} header!");
|
||||
|
||||
goto case BearerAuthenticationScheme;
|
||||
case BearerAuthenticationScheme:
|
||||
Token = parameter;
|
||||
break;
|
||||
case BasicAuthenticationScheme:
|
||||
@@ -203,12 +238,12 @@ namespace Tgstation.Server.Api
|
||||
}
|
||||
catch
|
||||
{
|
||||
throw new InvalidOperationException("Invalid basic Authorization header!");
|
||||
throw new InvalidOperationException($"Invalid basic {HeaderNames.Authorization} header!");
|
||||
}
|
||||
|
||||
var basicAuthSplits = joinedString.Split(new char[] { ':' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (basicAuthSplits.Length < 2)
|
||||
throw new InvalidOperationException("Invalid basic Authorization header!");
|
||||
throw new InvalidOperationException($"Invalid basic {HeaderNames.Authorization} header!");
|
||||
|
||||
Username = basicAuthSplits.First();
|
||||
Password = String.Concat(basicAuthSplits.Skip(1));
|
||||
@@ -226,6 +261,7 @@ namespace Tgstation.Server.Api
|
||||
|
||||
ApiVersion = apiVersion!.Semver();
|
||||
}
|
||||
#pragma warning restore CA1502
|
||||
|
||||
/// <summary>
|
||||
/// Construct <see cref="ApiHeaders"/>
|
||||
@@ -253,7 +289,7 @@ namespace Tgstation.Server.Api
|
||||
/// Set <see cref="HttpRequestHeaders"/> using the <see cref="ApiHeaders"/>. This initially clears <paramref name="headers"/>
|
||||
/// </summary>
|
||||
/// <param name="headers">The <see cref="HttpRequestHeaders"/> to set</param>
|
||||
/// <param name="instanceId">The instance <see cref="Models.EntityId.Id"/> for the request</param>
|
||||
/// <param name="instanceId">The instance <see cref="EntityId.Id"/> for the request</param>
|
||||
public void SetRequestHeaders(HttpRequestHeaders headers, long? instanceId = null)
|
||||
{
|
||||
if (headers == null)
|
||||
@@ -263,12 +299,16 @@ namespace Tgstation.Server.Api
|
||||
|
||||
headers.Clear();
|
||||
headers.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json));
|
||||
if (IsTokenAuthentication)
|
||||
headers.Authorization = new AuthenticationHeaderValue(JwtAuthenticationScheme, Token);
|
||||
else
|
||||
if (!IsTokenAuthentication)
|
||||
headers.Authorization = new AuthenticationHeaderValue(
|
||||
BasicAuthenticationScheme,
|
||||
Convert.ToBase64String(Encoding.UTF8.GetBytes($"{Username}:{Password}")));
|
||||
else
|
||||
{
|
||||
headers.Authorization = new AuthenticationHeaderValue(BearerAuthenticationScheme, Token);
|
||||
if (OAuthProvider.HasValue)
|
||||
headers.Add(OAuthProviderHeader, OAuthProvider.ToString());
|
||||
}
|
||||
|
||||
headers.UserAgent.Add(new ProductInfoHeaderValue(UserAgent));
|
||||
headers.Add(ApiVersionHeader, new ProductHeaderValue(AssemblyName.Name, ApiVersion.ToString()).ToString());
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
|
||||
namespace Tgstation.Server.Api
|
||||
{
|
||||
@@ -24,13 +24,18 @@ namespace Tgstation.Server.Api
|
||||
Accept = 2,
|
||||
|
||||
/// <summary>
|
||||
/// Api header.
|
||||
/// <see cref="ApiHeaders.ApiVersionHeader"/>.
|
||||
/// </summary>
|
||||
Api = 4,
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="Microsoft.Net.Http.Headers.HeaderNames.Authorization"/>
|
||||
/// </summary>
|
||||
Authorization = 8
|
||||
Authorization = 8,
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="ApiHeaders.OAuthProviderHeader"/>.
|
||||
/// </summary>
|
||||
OAuthProvider = 16,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,10 +64,10 @@ namespace Tgstation.Server.Api.Models
|
||||
CannotChangeServerSuite,
|
||||
|
||||
/// <summary>
|
||||
/// A required GitHub API request failed.
|
||||
/// A required remote API request failed.
|
||||
/// </summary>
|
||||
[Description("A required GitHub request returned an API error!")]
|
||||
GitHubApiError,
|
||||
[Description("A required remote API request returned an error!")]
|
||||
RemoteApiError,
|
||||
|
||||
/// <summary>
|
||||
/// A server update was requested while another was in progress.
|
||||
@@ -582,5 +582,17 @@ namespace Tgstation.Server.Api.Models
|
||||
/// </summary>
|
||||
[Description("The requested port is either already in use by TGS or could not be allocated!")]
|
||||
PortNotAvailable,
|
||||
|
||||
/// <summary>
|
||||
/// Attempted to set <see cref="User.OAuthConnections"/> for the admin user.
|
||||
/// </summary>
|
||||
[Description("The admin user cannot use OAuth connections!")]
|
||||
AdminUserCannotOAuth,
|
||||
|
||||
/// <summary>
|
||||
/// Attempted to login with a disabled OAuth provider.
|
||||
/// </summary>
|
||||
[Description("The requested OAuth provider is disabled via configuration!")]
|
||||
OAuthProviderDisabled,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Tgstation.Server.Api.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a valid OAuth connection.
|
||||
/// </summary>
|
||||
public class OAuthConnection
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="OAuthProvider"/> of the <see cref="OAuthConnection"/>.
|
||||
/// </summary>]
|
||||
[EnumDataType(typeof(OAuthProvider))]
|
||||
public OAuthProvider Provider { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the user in the <see cref="Provider"/>.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[StringLength(Limits.MaximumIndexableStringLength)]
|
||||
public string? ExternalUserId { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
namespace Tgstation.Server.Api.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// List of OAuth providers supported by TGS
|
||||
/// </summary>
|
||||
public enum OAuthProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// https://github.com
|
||||
/// </summary>
|
||||
GitHub,
|
||||
|
||||
/// <summary>
|
||||
/// https://discord.com
|
||||
/// </summary>
|
||||
Discord,
|
||||
|
||||
/// <summary>
|
||||
/// https://tgstation13.org
|
||||
/// </summary>
|
||||
TGForums,
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Tgstation.Server.Api.Models
|
||||
{
|
||||
@@ -21,5 +22,10 @@ namespace Tgstation.Server.Api.Models
|
||||
/// The DMAPI version of the host.
|
||||
/// </summary>
|
||||
public Version? DMApiVersion { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Map of <see cref="OAuthProvider"/> to the server's associated client IDs for them.
|
||||
/// </summary>
|
||||
public IDictionary<OAuthProvider, string>? OAuthProviderClientIds { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
namespace Tgstation.Server.Api.Models
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Tgstation.Server.Api.Models
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public class User : Internal.User
|
||||
@@ -17,5 +19,10 @@
|
||||
/// The <see cref="User"/> who created this <see cref="User"/>
|
||||
/// </summary>
|
||||
public Internal.User? CreatedBy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// List of <see cref="OAuthConnection"/>s associated with the <see cref="User"/>.
|
||||
/// </summary>
|
||||
public ICollection<OAuthConnection>? OAuthConnections { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,5 +42,10 @@ namespace Tgstation.Server.Api.Rights
|
||||
/// User can list and download <see cref="Models.LogFile"/>s.
|
||||
/// </summary>
|
||||
DownloadLogs = 32,
|
||||
|
||||
/// <summary>
|
||||
/// User can modify their own <see cref="Models.User.OAuthConnections"/>.
|
||||
/// </summary>
|
||||
EditOwnOAuthConnections = 64,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"dotnet-ef": {
|
||||
"version": "3.1.7",
|
||||
"version": "3.1.10",
|
||||
"commands": [
|
||||
"dotnet-ef"
|
||||
]
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
using System;
|
||||
|
||||
namespace Tgstation.Server.Host.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// OAuth options.
|
||||
/// </summary>
|
||||
class OAuthConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// The client ID.
|
||||
/// </summary>
|
||||
public string ClientId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The client secret.
|
||||
/// </summary>
|
||||
public string ClientSecret { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OAuthConfiguration"/> <see langword="class"/>.
|
||||
/// </summary>
|
||||
public OAuthConfiguration() { }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OAuthConfiguration"/> <see langword="class"/>.
|
||||
/// </summary>
|
||||
/// <param name="oAuthConfiguration">The <see cref="OAuthConfiguration"/> to copy settings from.</param>
|
||||
public OAuthConfiguration(OAuthConfiguration oAuthConfiguration)
|
||||
{
|
||||
if (oAuthConfiguration == null)
|
||||
throw new ArgumentNullException(nameof(oAuthConfiguration));
|
||||
ClientId = oAuthConfiguration.ClientId;
|
||||
ClientSecret = oAuthConfiguration.ClientSecret;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using Tgstation.Server.Api.Models;
|
||||
|
||||
namespace Tgstation.Server.Host.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
@@ -15,6 +18,11 @@ namespace Tgstation.Server.Host.Configuration
|
||||
/// </summary>
|
||||
const uint DefaultTokenExpiryMinutes = 15;
|
||||
|
||||
/// <summary>
|
||||
/// Default value of <see cref="OAuthTokenExpiryMinutes"/>.
|
||||
/// </summary>
|
||||
const uint DefaultOAuthTokenExpiryMinutes = 60 * 24; // 1 day
|
||||
|
||||
/// <summary>
|
||||
/// Default value of <see cref="TokenClockSkewMinutes"/>.
|
||||
/// </summary>
|
||||
@@ -26,12 +34,12 @@ namespace Tgstation.Server.Host.Configuration
|
||||
const uint DefaultTokenSigningKeyByteAmount = 256;
|
||||
|
||||
/// <summary>
|
||||
/// Amount of minutes until generated <see cref="Api.Models.Token"/>s expire.
|
||||
/// Amount of minutes until <see cref="Token"/>s generated from passwords expire.
|
||||
/// </summary>
|
||||
public uint TokenExpiryMinutes { get; set; } = DefaultTokenExpiryMinutes;
|
||||
|
||||
/// <summary>
|
||||
/// Amount of minutes to skew the clock for <see cref="Api.Models.Token"/> validation.
|
||||
/// Amount of minutes to skew the clock for <see cref="Token"/> validation.
|
||||
/// </summary>
|
||||
public uint TokenClockSkewMinutes { get; set; } = DefaultTokenClockSkewMinutes;
|
||||
|
||||
@@ -40,9 +48,19 @@ namespace Tgstation.Server.Host.Configuration
|
||||
/// </summary>
|
||||
public uint TokenSigningKeyByteCount { get; set; } = DefaultTokenSigningKeyByteAmount;
|
||||
|
||||
/// <summary>
|
||||
/// Amount of minutes until <see cref="Token"/>s generated from OAuth logins expire.
|
||||
/// </summary>
|
||||
public uint OAuthTokenExpiryMinutes { get; set; } = DefaultOAuthTokenExpiryMinutes;
|
||||
|
||||
/// <summary>
|
||||
/// A custom token signing key. Overrides <see cref="TokenSigningKeyByteCount"/>.
|
||||
/// </summary>
|
||||
public string CustomTokenSigningKeyBase64 { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// OAuth provider settings.
|
||||
/// </summary>
|
||||
public IDictionary<OAuthProvider, OAuthConfiguration> OAuth { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Octokit;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
@@ -115,14 +113,6 @@ namespace Tgstation.Server.Host.Controllers
|
||||
fileLoggingConfiguration = fileLoggingConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(fileLoggingConfigurationOptions));
|
||||
}
|
||||
|
||||
ObjectResult RateLimit(RateLimitExceededException exception)
|
||||
{
|
||||
Logger.LogWarning(exception, "Exceeded GitHub rate limit!");
|
||||
var secondsString = Math.Ceiling((exception.Reset - DateTimeOffset.Now).TotalSeconds).ToString(CultureInfo.InvariantCulture);
|
||||
Response.Headers.Add("Retry-After", new StringValues(secondsString));
|
||||
return StatusCode(HttpStatusCode.TooManyRequests, new ErrorMessage(ErrorCode.GitHubApiRateLimit));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to download and apply an update with a given <paramref name="newVersion"/>.
|
||||
/// </summary>
|
||||
@@ -150,7 +140,10 @@ namespace Tgstation.Server.Host.Controllers
|
||||
catch (ApiException e)
|
||||
{
|
||||
Logger.LogWarning(e, OctokitException);
|
||||
return StatusCode(HttpStatusCode.FailedDependency);
|
||||
return StatusCode(HttpStatusCode.FailedDependency, new ErrorMessage(ErrorCode.RemoteApiError)
|
||||
{
|
||||
AdditionalData = e.Message
|
||||
});
|
||||
}
|
||||
|
||||
releases = releases.Where(x => x.TagName.StartsWith(updatesConfiguration.GitTagPrefix, StringComparison.InvariantCulture));
|
||||
@@ -244,7 +237,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
catch (ApiException e)
|
||||
{
|
||||
Logger.LogWarning(e, OctokitException);
|
||||
return StatusCode(HttpStatusCode.FailedDependency, new ErrorMessage(ErrorCode.GitHubApiError)
|
||||
return StatusCode(HttpStatusCode.FailedDependency, new ErrorMessage(ErrorCode.RemoteApiError)
|
||||
{
|
||||
AdditionalData = e.Message
|
||||
});
|
||||
|
||||
@@ -2,8 +2,11 @@ using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using Octokit;
|
||||
using Serilog.Context;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Mime;
|
||||
@@ -60,7 +63,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="ApiController"/></param>
|
||||
/// <param name="logger">The value of <see cref="Logger"/></param>
|
||||
/// <param name="requireHeaders">The value of <see cref="requireHeaders"/></param>
|
||||
public ApiController(
|
||||
protected ApiController(
|
||||
IDatabaseContext databaseContext,
|
||||
IAuthenticationContextFactory authenticationContextFactory,
|
||||
ILogger<ApiController> logger,
|
||||
@@ -115,6 +118,22 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// <returns>A <see cref="HttpStatusCode.Created"/> <see cref="ObjectResult"/> with the given <paramref name="payload"/>.</returns>
|
||||
protected ObjectResult Created(object payload) => StatusCode((int)HttpStatusCode.Created, payload);
|
||||
|
||||
/// <summary>
|
||||
/// 429 response for a given <paramref name="rateLimitException"/>.
|
||||
/// </summary>
|
||||
/// <param name="rateLimitException">The <see cref="RateLimitExceededException"/> that occurred.</param>
|
||||
/// <returns>A <see cref="HttpStatusCode.TooManyRequests"/> <see cref="ObjectResult"/>.</returns>
|
||||
protected ObjectResult RateLimit(RateLimitExceededException rateLimitException)
|
||||
{
|
||||
if (rateLimitException == null)
|
||||
throw new ArgumentNullException(nameof(rateLimitException));
|
||||
|
||||
Logger.LogWarning(rateLimitException, "Exceeded GitHub rate limit!");
|
||||
var secondsString = Math.Ceiling((rateLimitException.Reset - DateTimeOffset.Now).TotalSeconds).ToString(CultureInfo.InvariantCulture);
|
||||
Response.Headers.Add(HeaderNames.RetryAfter, secondsString);
|
||||
return StatusCode(HttpStatusCode.TooManyRequests, new ErrorMessage(ErrorCode.GitHubApiRateLimit));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs validation a request.
|
||||
/// </summary>
|
||||
@@ -126,13 +145,14 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// <summary>
|
||||
/// Response for missing/Invalid headers.
|
||||
/// </summary>
|
||||
/// <param name="ignoreMissingAuth">Whether or not errors due to missing <see cref="HeaderNames.Authorization"/> should be thrown.</param>
|
||||
/// <returns>The appropriate <see cref="IActionResult"/>.</returns>
|
||||
protected IActionResult HeadersIssue()
|
||||
protected IActionResult HeadersIssue(bool ignoreMissingAuth)
|
||||
{
|
||||
HeadersException headersException;
|
||||
try
|
||||
{
|
||||
var _ = new ApiHeaders(Request.GetTypedHeaders());
|
||||
var _ = new ApiHeaders(Request.GetTypedHeaders(), ignoreMissingAuth);
|
||||
throw new InvalidOperationException("Expected a header parse exception!");
|
||||
}
|
||||
catch (HeadersException ex)
|
||||
@@ -196,7 +216,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
{
|
||||
if (requireHeaders)
|
||||
{
|
||||
await HeadersIssue()
|
||||
await HeadersIssue(false)
|
||||
.ExecuteResultAsync(context)
|
||||
.ConfigureAwait(false);
|
||||
return;
|
||||
@@ -239,9 +259,19 @@ namespace Tgstation.Server.Host.Controllers
|
||||
{
|
||||
if (ApiHeaders != null)
|
||||
Logger.LogDebug(
|
||||
"Starting API Request: Version: {0}. User-Agent: {1}",
|
||||
"Starting API request: Version: {0}. {1}: {2}",
|
||||
ApiHeaders.ApiVersion.Semver(),
|
||||
HeaderNames.UserAgent,
|
||||
ApiHeaders.RawUserAgent);
|
||||
else if (Request.Headers.TryGetValue(HeaderNames.UserAgent, out var userAgents))
|
||||
Logger.LogDebug(
|
||||
"Starting unauthorized API request. {0}: {1}",
|
||||
HeaderNames.UserAgent,
|
||||
userAgents);
|
||||
else
|
||||
Logger.LogDebug(
|
||||
"Starting unauthorized API request. No {0}!",
|
||||
HeaderNames.UserAgent);
|
||||
await base.OnActionExecutionAsync(context, next).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,9 +41,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
{
|
||||
var contentTypeProvider = new FileExtensionContentTypeProvider();
|
||||
if (!contentTypeProvider.TryGetContentType(fileInfo.Name, out var contentType))
|
||||
{
|
||||
contentType = "application/octet-stream";
|
||||
}
|
||||
contentType = MediaTypeNames.Application.Octet;
|
||||
|
||||
return File(appRoute, contentType);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using Octokit;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Tgstation.Server.Api;
|
||||
@@ -17,6 +20,7 @@ using Tgstation.Server.Host.Core;
|
||||
using Tgstation.Server.Host.Database;
|
||||
using Tgstation.Server.Host.Models;
|
||||
using Tgstation.Server.Host.Security;
|
||||
using Tgstation.Server.Host.Security.OAuth;
|
||||
using Tgstation.Server.Host.System;
|
||||
using Wangkanai.Detection;
|
||||
|
||||
@@ -53,6 +57,11 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// </summary>
|
||||
readonly IIdentityCache identityCache;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IOAuthProviders"/> for the <see cref="HomeController"/>.
|
||||
/// </summary>
|
||||
readonly IOAuthProviders oAuthProviders;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IBrowserResolver"/> for the <see cref="HomeController"/>
|
||||
/// </summary>
|
||||
@@ -78,6 +87,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// <param name="cryptographySuite">The value of <see cref="cryptographySuite"/></param>
|
||||
/// <param name="assemblyInformationProvider">The value of <see cref="assemblyInformationProvider"/></param>
|
||||
/// <param name="identityCache">The value of <see cref="identityCache"/></param>
|
||||
/// <param name="oAuthProviders">The value of <see cref="oAuthProviders"/>.</param>
|
||||
/// <param name="browserResolver">The value of <see cref="browserResolver"/></param>
|
||||
/// <param name="generalConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="generalConfiguration"/>.</param>
|
||||
/// <param name="controlPanelConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="controlPanelConfiguration"/></param>
|
||||
@@ -90,6 +100,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
ICryptographySuite cryptographySuite,
|
||||
IAssemblyInformationProvider assemblyInformationProvider,
|
||||
IIdentityCache identityCache,
|
||||
IOAuthProviders oAuthProviders,
|
||||
IBrowserResolver browserResolver,
|
||||
IOptions<GeneralConfiguration> generalConfigurationOptions,
|
||||
IOptions<ControlPanelConfiguration> controlPanelConfigurationOptions,
|
||||
@@ -106,6 +117,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite));
|
||||
this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider));
|
||||
this.identityCache = identityCache ?? throw new ArgumentNullException(nameof(identityCache));
|
||||
this.oAuthProviders = oAuthProviders ?? throw new ArgumentNullException(nameof(oAuthProviders));
|
||||
this.browserResolver = browserResolver;
|
||||
generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
|
||||
controlPanelConfiguration = controlPanelConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(controlPanelConfigurationOptions));
|
||||
@@ -114,36 +126,49 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// <summary>
|
||||
/// Main page of the <see cref="Application"/>
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>
|
||||
/// The <see cref="ServerInformation"/> of the <see cref="Application"/> if a properly authenticated API request, the web control panel if on a browser and enabled, <see cref="UnauthorizedResult"/> otherwise.
|
||||
/// A <see cref="Task{TResult}"/> resuting in the <see cref="JsonResult"/> containing <see cref="ServerInformation"/> of the <see cref="Application"/> if a properly authenticated API request, the web control panel if on a browser and enabled, <see cref="UnauthorizedResult"/> otherwise.
|
||||
/// </returns>
|
||||
/// <response code="200"><see cref="ServerInformation"/> retrieved successfully.</response>
|
||||
[HttpGet]
|
||||
[TgsAuthorize]
|
||||
[AllowAnonymous]
|
||||
[ProducesResponseType(typeof(ServerInformation), 200)]
|
||||
public IActionResult Home()
|
||||
public async Task<IActionResult> Home(CancellationToken cancellationToken)
|
||||
{
|
||||
if (AuthenticationContext != null)
|
||||
return Json(new ServerInformation
|
||||
{
|
||||
Version = assemblyInformationProvider.Version,
|
||||
ApiVersion = ApiHeaders.Version,
|
||||
DMApiVersion = DMApiConstants.Version,
|
||||
MinimumPasswordLength = generalConfiguration.MinimumPasswordLength,
|
||||
InstanceLimit = generalConfiguration.InstanceLimit,
|
||||
UserLimit = generalConfiguration.UserLimit,
|
||||
ValidInstancePaths = generalConfiguration.ValidInstancePaths
|
||||
});
|
||||
|
||||
// if we are using a browser and the control panel, soft redirect to the app page
|
||||
if (controlPanelConfiguration.Enable && browserResolver.Browser.Type != BrowserType.Generic)
|
||||
{
|
||||
Logger.LogDebug("Unauthorized browser request (User-Agent: \"{0}\"), redirecting to control panel...", browserResolver.UserAgent);
|
||||
return Redirect(Application.ControlPanelRoute);
|
||||
return Redirect(Core.Application.ControlPanelRoute);
|
||||
}
|
||||
|
||||
return ApiHeaders == null ? HeadersIssue() : Unauthorized();
|
||||
// we only allow authorization header issues
|
||||
if (ApiHeaders == null)
|
||||
try
|
||||
{
|
||||
var headers = new ApiHeaders(Request.GetTypedHeaders(), true);
|
||||
if (!headers.Compatible())
|
||||
return StatusCode(
|
||||
HttpStatusCode.UpgradeRequired,
|
||||
new ErrorMessage(ErrorCode.ApiMismatch));
|
||||
}
|
||||
catch (HeadersException)
|
||||
{
|
||||
return HeadersIssue(true);
|
||||
}
|
||||
|
||||
return Json(new ServerInformation
|
||||
{
|
||||
Version = assemblyInformationProvider.Version,
|
||||
ApiVersion = ApiHeaders.Version,
|
||||
DMApiVersion = DMApiConstants.Version,
|
||||
MinimumPasswordLength = generalConfiguration.MinimumPasswordLength,
|
||||
InstanceLimit = generalConfiguration.InstanceLimit,
|
||||
UserLimit = generalConfiguration.UserLimit,
|
||||
ValidInstancePaths = generalConfiguration.ValidInstancePaths,
|
||||
OAuthProviderClientIds = await oAuthProviders.ClientIds(cancellationToken).ConfigureAwait(false)
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -154,40 +179,77 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// <response code="200">User logged in and <see cref="Token"/> generated successfully.</response>
|
||||
/// <response code="401">User authentication failed.</response>
|
||||
/// <response code="403">User authenticated but is disabled by an administrator.</response>
|
||||
/// <response code="429">OAuth authentication failed due to rate limiting.</response>
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(Token), 200)]
|
||||
#pragma warning disable CA1506 // TODO: Decomplexify
|
||||
[ProducesResponseType(typeof(ErrorMessage), 429)]
|
||||
#pragma warning disable CA1506 // TODO: Decomplexify
|
||||
public async Task<IActionResult> CreateToken(CancellationToken cancellationToken)
|
||||
{
|
||||
if (ApiHeaders == null)
|
||||
{
|
||||
Response.Headers.Add(HeaderNames.WWWAuthenticate, new StringValues("basic realm=\"Create TGS4 bearer token\""));
|
||||
return HeadersIssue();
|
||||
return HeadersIssue(false);
|
||||
}
|
||||
|
||||
if (ApiHeaders.IsTokenAuthentication)
|
||||
return BadRequest(new ErrorMessage(ErrorCode.TokenWithToken));
|
||||
|
||||
ISystemIdentity systemIdentity;
|
||||
try
|
||||
{
|
||||
// trust the system over the database because a user's name can change while still having the same SID
|
||||
systemIdentity = await systemIdentityFactory.CreateSystemIdentity(ApiHeaders.Username, ApiHeaders.Password, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (NotImplementedException)
|
||||
{
|
||||
systemIdentity = null;
|
||||
}
|
||||
var oAuthLogin = ApiHeaders.OAuthProvider.HasValue;
|
||||
|
||||
ISystemIdentity systemIdentity = null;
|
||||
if (!oAuthLogin)
|
||||
try
|
||||
{
|
||||
// trust the system over the database because a user's name can change while still having the same SID
|
||||
systemIdentity = await systemIdentityFactory.CreateSystemIdentity(ApiHeaders.Username, ApiHeaders.Password, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (NotImplementedException ex)
|
||||
{
|
||||
Logger.LogTrace(ex, "System identities not implemented!");
|
||||
}
|
||||
|
||||
using (systemIdentity)
|
||||
{
|
||||
// Get the user from the database
|
||||
IQueryable<Models.User> query = DatabaseContext.Users.AsQueryable();
|
||||
string canonicalName = Models.User.CanonicalizeName(ApiHeaders.Username);
|
||||
if (systemIdentity == null)
|
||||
query = query.Where(x => x.CanonicalName == canonicalName);
|
||||
if (oAuthLogin)
|
||||
{
|
||||
string externalUserId;
|
||||
try
|
||||
{
|
||||
var validator = oAuthProviders
|
||||
.GetValidator(ApiHeaders.OAuthProvider.Value);
|
||||
|
||||
if (validator == null)
|
||||
return BadRequest(new ErrorMessage(ErrorCode.OAuthProviderDisabled));
|
||||
|
||||
externalUserId = await validator
|
||||
.ValidateResponseCode(ApiHeaders.Token, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
catch (RateLimitExceededException ex)
|
||||
{
|
||||
return RateLimit(ex);
|
||||
}
|
||||
|
||||
if (externalUserId == null)
|
||||
return Unauthorized();
|
||||
|
||||
query = query.Where(
|
||||
x => x.OAuthConnections.Any(
|
||||
y => y.Provider == ApiHeaders.OAuthProvider.Value
|
||||
&& y.ExternalUserId == externalUserId));
|
||||
}
|
||||
else
|
||||
query = query.Where(x => x.CanonicalName == canonicalName || x.SystemIdentifier == systemIdentity.Uid);
|
||||
{
|
||||
string canonicalName = Models.User.CanonicalizeName(ApiHeaders.Username);
|
||||
if (systemIdentity == null)
|
||||
query = query.Where(x => x.CanonicalName == canonicalName);
|
||||
else
|
||||
query = query.Where(x => x.CanonicalName == canonicalName || x.SystemIdentifier == systemIdentity.Uid);
|
||||
}
|
||||
|
||||
var users = await query.Select(x => new Models.User
|
||||
{
|
||||
Id = x.Id,
|
||||
@@ -213,32 +275,33 @@ namespace Tgstation.Server.Host.Controllers
|
||||
var originalHash = user.PasswordHash;
|
||||
var isDbUser = originalHash != null;
|
||||
bool usingSystemIdentity = systemIdentity != null && !isDbUser;
|
||||
if (!usingSystemIdentity)
|
||||
{
|
||||
// DB User password check and update
|
||||
if (!cryptographySuite.CheckUserPassword(user, ApiHeaders.Password))
|
||||
return Unauthorized();
|
||||
if (user.PasswordHash != originalHash)
|
||||
if (!oAuthLogin)
|
||||
if (!usingSystemIdentity)
|
||||
{
|
||||
Logger.LogDebug("User ID {0}'s password hash needs a refresh, updating database.", user.Id);
|
||||
var updatedUser = new Models.User
|
||||
// DB User password check and update
|
||||
if (!cryptographySuite.CheckUserPassword(user, ApiHeaders.Password))
|
||||
return Unauthorized();
|
||||
if (user.PasswordHash != originalHash)
|
||||
{
|
||||
Id = user.Id
|
||||
};
|
||||
DatabaseContext.Users.Attach(updatedUser);
|
||||
updatedUser.PasswordHash = user.PasswordHash;
|
||||
Logger.LogDebug("User ID {0}'s password hash needs a refresh, updating database.", user.Id);
|
||||
var updatedUser = new Models.User
|
||||
{
|
||||
Id = user.Id
|
||||
};
|
||||
DatabaseContext.Users.Attach(updatedUser);
|
||||
updatedUser.PasswordHash = user.PasswordHash;
|
||||
await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
else if (systemIdentity.Username != user.Name)
|
||||
{
|
||||
// System identity username change update
|
||||
Logger.LogDebug("User ID {0}'s system identity needs a refresh, updating database.", user.Id);
|
||||
DatabaseContext.Users.Attach(user);
|
||||
user.Name = systemIdentity.Username;
|
||||
user.CanonicalName = Models.User.CanonicalizeName(user.Name);
|
||||
await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
else if (systemIdentity.Username != user.Name)
|
||||
{
|
||||
// System identity username change update
|
||||
Logger.LogDebug("User ID {0}'s system identity needs a refresh, updating database.", user.Id);
|
||||
DatabaseContext.Users.Attach(user);
|
||||
user.Name = systemIdentity.Username;
|
||||
user.CanonicalName = Models.User.CanonicalizeName(user.Name);
|
||||
await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Now that the bookeeping is done, tell them to fuck off if necessary
|
||||
if (!user.Enabled.Value)
|
||||
@@ -247,7 +310,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
return Forbid();
|
||||
}
|
||||
|
||||
var token = await tokenFactory.CreateToken(user, cancellationToken).ConfigureAwait(false);
|
||||
var token = await tokenFactory.CreateToken(user, oAuthLogin, cancellationToken).ConfigureAwait(false);
|
||||
if (usingSystemIdentity)
|
||||
{
|
||||
// expire the identity slightly after the auth token in case of lag
|
||||
@@ -262,6 +325,6 @@ namespace Tgstation.Server.Host.Controllers
|
||||
return Json(token);
|
||||
}
|
||||
}
|
||||
#pragma warning restore CA1506
|
||||
#pragma warning restore CA1506
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,7 +281,9 @@ namespace Tgstation.Server.Host.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
#pragma warning disable CA1508 // Avoid dead conditional code
|
||||
if (earlyOut != null)
|
||||
#pragma warning restore CA1508 // Avoid dead conditional code
|
||||
return earlyOut;
|
||||
|
||||
// Last test, ensure it's in the list of valid paths
|
||||
@@ -420,9 +422,9 @@ namespace Tgstation.Server.Host.Controllers
|
||||
|
||||
var moveJob = await InstanceQuery()
|
||||
.SelectMany(x => x.Jobs).
|
||||
#pragma warning disable CA1307 // Specify StringComparison
|
||||
#pragma warning disable CA1310 // Specify StringComparison
|
||||
Where(x => !x.StoppedAt.HasValue && x.Description.StartsWith(MoveInstanceJobPrefix))
|
||||
#pragma warning restore CA1307 // Specify StringComparison
|
||||
#pragma warning restore CA1310 // Specify StringComparison
|
||||
.Select(x => new Models.Job
|
||||
{
|
||||
Id = x.Id
|
||||
@@ -614,9 +616,9 @@ namespace Tgstation.Server.Host.Controllers
|
||||
|
||||
var moveJobs = await GetBaseQuery()
|
||||
.SelectMany(x => x.Jobs)
|
||||
#pragma warning disable CA1307 // Specify StringComparison
|
||||
#pragma warning disable CA1310 // Specify StringComparison
|
||||
.Where(x => !x.StoppedAt.HasValue && x.Description.StartsWith(MoveInstanceJobPrefix))
|
||||
#pragma warning restore CA1307 // Specify StringComparison
|
||||
#pragma warning restore CA1310 // Specify StringComparison
|
||||
.Include(x => x.StartedBy).ThenInclude(x => x.CreatedBy)
|
||||
.Include(x => x.Instance)
|
||||
.ToListAsync(cancellationToken)
|
||||
@@ -687,9 +689,9 @@ namespace Tgstation.Server.Host.Controllers
|
||||
|
||||
var moveJob = await QueryForUser()
|
||||
.SelectMany(x => x.Jobs)
|
||||
#pragma warning disable CA1307 // Specify StringComparison
|
||||
#pragma warning disable CA1310 // Specify StringComparison
|
||||
.Where(x => !x.StoppedAt.HasValue && x.Description.StartsWith(MoveInstanceJobPrefix))
|
||||
#pragma warning restore CA1307 // Specify StringComparison
|
||||
#pragma warning restore CA1310 // Specify StringComparison
|
||||
.Include(x => x.StartedBy).ThenInclude(x => x.CreatedBy)
|
||||
.FirstOrDefaultAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
@@ -426,7 +426,9 @@ namespace Tgstation.Server.Host.Controllers
|
||||
|| (model.UpdateFromOrigin == true && !userRights.HasFlag(RepositoryRights.UpdateBranch)))
|
||||
return Forbid();
|
||||
|
||||
if (currentModel.AccessToken?.Length == 0 && currentModel.AccessUser?.Length == 0)
|
||||
#pragma warning disable CA1508 // Avoid dead conditional code
|
||||
if (model.AccessToken?.Length == 0 && model.AccessUser?.Length == 0)
|
||||
#pragma warning restore CA1508 // Avoid dead conditional code
|
||||
{
|
||||
// setting an empty string clears everything
|
||||
currentModel.AccessUser = null;
|
||||
|
||||
@@ -74,7 +74,9 @@ namespace Tgstation.Server.Host.Controllers
|
||||
BadRequestObjectResult CheckValidName(UserUpdate model, bool newUser)
|
||||
{
|
||||
var userInvalidWithNullName = newUser && model.Name == null && model.SystemIdentifier == null;
|
||||
#pragma warning disable CA1508 // https://github.com/dotnet/roslyn-analyzers/issues/3685
|
||||
if (userInvalidWithNullName || (model.Name != null && String.IsNullOrWhiteSpace(model.Name)))
|
||||
#pragma warning restore CA1508
|
||||
return BadRequest(new ErrorMessage(ErrorCode.UserMissingName));
|
||||
|
||||
model.Name = model.Name?.Trim();
|
||||
@@ -118,6 +120,9 @@ namespace Tgstation.Server.Host.Controllers
|
||||
if (model == null)
|
||||
throw new ArgumentNullException(nameof(model));
|
||||
|
||||
if (model.OAuthConnections?.Any(x => x == null) == true)
|
||||
return BadRequest(new ErrorMessage(ErrorCode.ModelValidationFailure));
|
||||
|
||||
if (!(model.Password == null ^ model.SystemIdentifier == null))
|
||||
return BadRequest(new ErrorMessage(ErrorCode.UserMismatchPasswordSid));
|
||||
|
||||
@@ -132,17 +137,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
if (fail != null)
|
||||
return fail;
|
||||
|
||||
var dbUser = new Models.User
|
||||
{
|
||||
AdministrationRights = RightsHelper.Clamp(model.AdministrationRights ?? AdministrationRights.None),
|
||||
CreatedAt = DateTimeOffset.Now,
|
||||
CreatedBy = AuthenticationContext.User,
|
||||
Enabled = model.Enabled ?? false,
|
||||
InstanceManagerRights = RightsHelper.Clamp(model.InstanceManagerRights ?? InstanceManagerRights.None),
|
||||
Name = model.Name,
|
||||
SystemIdentifier = model.SystemIdentifier,
|
||||
InstanceUsers = new List<Models.InstanceUser>()
|
||||
};
|
||||
var dbUser = CreateNewUserFromModel(model);
|
||||
|
||||
if (model.SystemIdentifier != null)
|
||||
try
|
||||
@@ -157,7 +152,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
{
|
||||
return RequiresPosixSystemIdentity();
|
||||
}
|
||||
else
|
||||
else if (!(model.Password?.Length == 0 && model.OAuthConnections.Count != 0))
|
||||
{
|
||||
var result = TrySetPassword(dbUser, model.Password, true);
|
||||
if (result != null)
|
||||
@@ -170,6 +165,8 @@ namespace Tgstation.Server.Host.Controllers
|
||||
|
||||
await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
Logger.LogInformation("Created new user {0} ({1})", dbUser.Name, dbUser.Id);
|
||||
|
||||
return Created(dbUser.ToApi(true));
|
||||
}
|
||||
|
||||
@@ -182,7 +179,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// <response code="200"><see cref="Api.Models.User"/> updated successfully.</response>
|
||||
/// <response code="404">Requested <see cref="Api.Models.Internal.User.Id"/> does not exist.</response>
|
||||
[HttpPost]
|
||||
[TgsAuthorize(AdministrationRights.WriteUsers | AdministrationRights.EditOwnPassword)]
|
||||
[TgsAuthorize(AdministrationRights.WriteUsers | AdministrationRights.EditOwnPassword | AdministrationRights.EditOwnOAuthConnections)]
|
||||
[ProducesResponseType(typeof(Api.Models.User), 200)]
|
||||
[ProducesResponseType(typeof(ErrorMessage), 404)]
|
||||
#pragma warning disable CA1502 // TODO: Decomplexify
|
||||
@@ -192,19 +189,22 @@ namespace Tgstation.Server.Host.Controllers
|
||||
if (model == null)
|
||||
throw new ArgumentNullException(nameof(model));
|
||||
|
||||
if (!model.Id.HasValue)
|
||||
if (!model.Id.HasValue || model.OAuthConnections?.Any(x => x == null) == true)
|
||||
return BadRequest(new ErrorMessage(ErrorCode.ModelValidationFailure));
|
||||
|
||||
var callerAdministrationRights = (AdministrationRights)AuthenticationContext.GetRight(RightsType.Administration);
|
||||
var passwordEditOnly = !callerAdministrationRights.HasFlag(AdministrationRights.WriteUsers);
|
||||
var canEditAllUsers = callerAdministrationRights.HasFlag(AdministrationRights.WriteUsers);
|
||||
var passwordEdit = canEditAllUsers || callerAdministrationRights.HasFlag(AdministrationRights.EditOwnPassword);
|
||||
var oAuthEdit = canEditAllUsers || callerAdministrationRights.HasFlag(AdministrationRights.EditOwnOAuthConnections);
|
||||
|
||||
var originalUser = passwordEditOnly
|
||||
var originalUser = !canEditAllUsers
|
||||
? AuthenticationContext.User
|
||||
: await DatabaseContext
|
||||
.Users
|
||||
.AsQueryable()
|
||||
.Where(x => x.Id == model.Id)
|
||||
.Include(x => x.CreatedBy)
|
||||
.Include(x => x.OAuthConnections)
|
||||
.FirstOrDefaultAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
@@ -214,13 +214,15 @@ namespace Tgstation.Server.Host.Controllers
|
||||
if (originalUser.CanonicalName == Models.User.CanonicalizeName(Models.User.TgsSystemUserName))
|
||||
return Forbid();
|
||||
|
||||
// Ensure they are only trying to edit password (system identity change will trigger a bad request)
|
||||
if (passwordEditOnly
|
||||
// Ensure they are only trying to edit things they have perms for (system identity change will trigger a bad request)
|
||||
if ((!canEditAllUsers
|
||||
&& (model.Id != originalUser.Id
|
||||
|| model.InstanceManagerRights.HasValue
|
||||
|| model.AdministrationRights.HasValue
|
||||
|| model.Enabled.HasValue
|
||||
|| model.Name != null))
|
||||
|| (!passwordEdit && model.Password != null)
|
||||
|| (!oAuthEdit && model.OAuthConnections != null))
|
||||
return Forbid();
|
||||
|
||||
if (model.SystemIdentifier != null && model.SystemIdentifier != originalUser.SystemIdentifier)
|
||||
@@ -246,6 +248,22 @@ namespace Tgstation.Server.Host.Controllers
|
||||
originalUser.Enabled = model.Enabled.Value;
|
||||
}
|
||||
|
||||
if (model.OAuthConnections != null
|
||||
&& (model.OAuthConnections.Count != originalUser.OAuthConnections.Count
|
||||
|| !model.OAuthConnections.All(x => originalUser.OAuthConnections.Any(y => y.Provider == x.Provider && y.ExternalUserId == x.ExternalUserId))))
|
||||
{
|
||||
if (originalUser.CanonicalName == Models.User.CanonicalizeName(Api.Models.User.AdminName))
|
||||
return BadRequest(new ErrorMessage(ErrorCode.AdminUserCannotOAuth));
|
||||
|
||||
originalUser.OAuthConnections.Clear();
|
||||
foreach (var updatedConnection in model.OAuthConnections)
|
||||
originalUser.OAuthConnections.Add(new Models.OAuthConnection
|
||||
{
|
||||
Provider = updatedConnection.Provider,
|
||||
ExternalUserId = updatedConnection.ExternalUserId
|
||||
});
|
||||
}
|
||||
|
||||
var fail = CheckValidName(model, false);
|
||||
if (fail != null)
|
||||
return fail;
|
||||
@@ -254,6 +272,8 @@ namespace Tgstation.Server.Host.Controllers
|
||||
|
||||
await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
Logger.LogInformation("Updated user {0} ({1})", originalUser.Name, originalUser.Id);
|
||||
|
||||
// return id only if not a self update and cannot read users
|
||||
return Json(
|
||||
model.Id == originalUser.Id
|
||||
@@ -293,6 +313,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
.AsQueryable()
|
||||
.Where(x => x.CanonicalName != Models.User.CanonicalizeName(Models.User.TgsSystemUserName))
|
||||
.Include(x => x.CreatedBy)
|
||||
.Include(x => x.OAuthConnections)
|
||||
.ToListAsync(cancellationToken).ConfigureAwait(false);
|
||||
return Json(users.Select(x => x.ToApi(true)));
|
||||
}
|
||||
@@ -321,6 +342,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
.AsQueryable()
|
||||
.Where(x => x.Id == id)
|
||||
.Include(x => x.CreatedBy)
|
||||
.Include(x => x.OAuthConnections)
|
||||
.FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (user == default)
|
||||
return NotFound();
|
||||
@@ -330,5 +352,30 @@ namespace Tgstation.Server.Host.Controllers
|
||||
|
||||
return Json(user.ToApi(true));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="User"/> from a given <paramref name="model"/>.
|
||||
/// </summary>
|
||||
/// <param name="model">The <see cref="Api.Models.User"/> to use as a template.</param>
|
||||
/// <returns>A new <see cref="User"/>.</returns>
|
||||
Models.User CreateNewUserFromModel(Api.Models.User model) => new Models.User
|
||||
{
|
||||
AdministrationRights = RightsHelper.Clamp(model.AdministrationRights ?? AdministrationRights.None),
|
||||
CreatedAt = DateTimeOffset.Now,
|
||||
CreatedBy = AuthenticationContext.User,
|
||||
Enabled = model.Enabled ?? false,
|
||||
InstanceManagerRights = RightsHelper.Clamp(model.InstanceManagerRights ?? InstanceManagerRights.None),
|
||||
Name = model.Name,
|
||||
SystemIdentifier = model.SystemIdentifier,
|
||||
InstanceUsers = new List<Models.InstanceUser>(),
|
||||
OAuthConnections = model
|
||||
.OAuthConnections
|
||||
?.Select(x => new Models.OAuthConnection
|
||||
{
|
||||
Provider = x.Provider,
|
||||
ExternalUserId = x.ExternalUserId
|
||||
})
|
||||
.ToList()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ using Tgstation.Server.Host.IO;
|
||||
using Tgstation.Server.Host.Jobs;
|
||||
using Tgstation.Server.Host.Properties;
|
||||
using Tgstation.Server.Host.Security;
|
||||
using Tgstation.Server.Host.Security.OAuth;
|
||||
using Tgstation.Server.Host.Setup;
|
||||
using Tgstation.Server.Host.System;
|
||||
|
||||
@@ -100,7 +101,6 @@ namespace Tgstation.Server.Host.Core
|
||||
// configure configuration
|
||||
services.UseStandardConfig<UpdatesConfiguration>(Configuration);
|
||||
services.UseStandardConfig<ControlPanelConfiguration>(Configuration);
|
||||
services.UseStandardConfig<SecurityConfiguration>(Configuration);
|
||||
|
||||
// enable options which give us config reloading
|
||||
services.AddOptions();
|
||||
@@ -127,7 +127,10 @@ namespace Tgstation.Server.Host.Core
|
||||
config =>
|
||||
{
|
||||
if (microsoftEventLevel.HasValue)
|
||||
{
|
||||
config.MinimumLevel.Override("Microsoft", microsoftEventLevel.Value);
|
||||
config.MinimumLevel.Override("System.Net.Http.HttpClient", microsoftEventLevel.Value);
|
||||
}
|
||||
},
|
||||
sinkConfig =>
|
||||
{
|
||||
@@ -159,24 +162,26 @@ namespace Tgstation.Server.Host.Core
|
||||
});
|
||||
|
||||
// configure bearer token validation
|
||||
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(jwtBearerOptions =>
|
||||
{
|
||||
// this line isn't actually run until the first request is made
|
||||
// at that point tokenFactory will be populated
|
||||
jwtBearerOptions.TokenValidationParameters = tokenFactory.ValidationParameters;
|
||||
jwtBearerOptions.Events = new JwtBearerEvents
|
||||
services
|
||||
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||
.AddJwtBearer(jwtBearerOptions =>
|
||||
{
|
||||
// Application is our composition root so this monstrosity of a line is okay
|
||||
// At least, that's what I tell myself to sleep at night
|
||||
OnTokenValidated = ctx => ctx
|
||||
.HttpContext
|
||||
.RequestServices
|
||||
.GetRequiredService<IClaimsInjector>()
|
||||
.InjectClaimsIntoContext(
|
||||
ctx,
|
||||
ctx.HttpContext.RequestAborted)
|
||||
};
|
||||
});
|
||||
// this line isn't actually run until the first request is made
|
||||
// at that point tokenFactory will be populated
|
||||
jwtBearerOptions.TokenValidationParameters = tokenFactory.ValidationParameters;
|
||||
jwtBearerOptions.Events = new JwtBearerEvents
|
||||
{
|
||||
// Application is our composition root so this monstrosity of a line is okay
|
||||
// At least, that's what I tell myself to sleep at night
|
||||
OnTokenValidated = ctx => ctx
|
||||
.HttpContext
|
||||
.RequestServices
|
||||
.GetRequiredService<IClaimsInjector>()
|
||||
.InjectClaimsIntoContext(
|
||||
ctx,
|
||||
ctx.HttpContext.RequestAborted)
|
||||
};
|
||||
});
|
||||
|
||||
// WARNING: STATIC CODE
|
||||
// fucking prevents converting 'sub' to M$ bs
|
||||
@@ -216,6 +221,9 @@ namespace Tgstation.Server.Host.Core
|
||||
// CORS conditionally enabled later
|
||||
services.AddCors();
|
||||
|
||||
// Enable managed HTTP clients
|
||||
services.AddHttpClient();
|
||||
|
||||
void AddTypedContext<TContext>() where TContext : DatabaseContext
|
||||
{
|
||||
var configureAction = DatabaseContext.GetConfigureAction<TContext>();
|
||||
@@ -260,6 +268,7 @@ namespace Tgstation.Server.Host.Core
|
||||
// configure security services
|
||||
services.AddScoped<IAuthenticationContextFactory, AuthenticationContextFactory>();
|
||||
services.AddScoped<IClaimsInjector, ClaimsInjector>();
|
||||
services.AddSingleton<IOAuthProviders, OAuthProviders>();
|
||||
services.AddSingleton<IIdentityCache, IdentityCache>();
|
||||
services.AddSingleton<ICryptographySuite, CryptographySuite>();
|
||||
services.AddSingleton<ITokenFactory, TokenFactory>();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using Octokit;
|
||||
using Tgstation.Server.Host.System;
|
||||
|
||||
@@ -25,7 +25,10 @@ namespace Tgstation.Server.Host.Core
|
||||
/// Create a <see cref="GitHubClient"/>
|
||||
/// </summary>
|
||||
/// <returns>A new <see cref="GitHubClient"/></returns>
|
||||
GitHubClient CreateBaseClient() => new GitHubClient(new ProductHeaderValue(assemblyInformationProvider.VersionPrefix, assemblyInformationProvider.Version.ToString()));
|
||||
GitHubClient CreateBaseClient() => new GitHubClient(
|
||||
new ProductHeaderValue(
|
||||
assemblyInformationProvider.ProductInfoHeaderValue.Product.Name,
|
||||
assemblyInformationProvider.ProductInfoHeaderValue.Product.Version));
|
||||
|
||||
/// <inheritdoc />
|
||||
public IGitHubClient CreateClient() => CreateBaseClient();
|
||||
|
||||
@@ -176,7 +176,7 @@ namespace Tgstation.Server.Host.Core
|
||||
In = ParameterLocation.Header,
|
||||
Type = SecuritySchemeType.Http,
|
||||
Name = HeaderNames.Authorization,
|
||||
Scheme = ApiHeaders.JwtAuthenticationScheme
|
||||
Scheme = ApiHeaders.BearerAuthenticationScheme
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -87,10 +87,15 @@ namespace Tgstation.Server.Host.Database
|
||||
public DbSet<TestMerge> TestMerges { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="RevInfoTestMerge"/>s om the <see cref="DatabaseContext"/>
|
||||
/// The <see cref="RevInfoTestMerge"/>s in the <see cref="DatabaseContext"/>
|
||||
/// </summary>
|
||||
public DbSet<RevInfoTestMerge> RevInfoTestMerges { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="OAuthConnection"/>s in the <see cref="DatabaseContext"/>
|
||||
/// </summary>
|
||||
public DbSet<OAuthConnection> OAuthConnections { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="DeleteBehavior"/> for the <see cref="CompileJob"/>/<see cref="RevisionInformation"/> foreign key.
|
||||
/// </summary>
|
||||
@@ -132,6 +137,9 @@ namespace Tgstation.Server.Host.Database
|
||||
/// <inheritdoc />
|
||||
IDatabaseCollection<ReattachInformation> IDatabaseContext.ReattachInformations => reattachInformationsCollection;
|
||||
|
||||
/// <inheritdoc />
|
||||
IDatabaseCollection<OAuthConnection> IDatabaseContext.OAuthConnections => oAuthConnections;
|
||||
|
||||
/// <summary>
|
||||
/// Backing field for <see cref="IDatabaseContext.Users"/>.
|
||||
/// </summary>
|
||||
@@ -192,6 +200,11 @@ namespace Tgstation.Server.Host.Database
|
||||
/// </summary>
|
||||
readonly IDatabaseCollection<ReattachInformation> reattachInformationsCollection;
|
||||
|
||||
/// <summary>
|
||||
/// Backing field for <see cref="IDatabaseContext.OAuthConnections"/>.
|
||||
/// </summary>
|
||||
readonly IDatabaseCollection<OAuthConnection> oAuthConnections;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the configure action for a given <typeparamref name="TDatabaseContext"/>.
|
||||
/// </summary>
|
||||
@@ -216,7 +229,7 @@ namespace Tgstation.Server.Host.Database
|
||||
/// Construct a <see cref="DatabaseContext"/>
|
||||
/// </summary>
|
||||
/// <param name="dbContextOptions">The <see cref="DbContextOptions"/> for the <see cref="DatabaseContext"/>.</param>
|
||||
public DatabaseContext(DbContextOptions dbContextOptions) : base(dbContextOptions)
|
||||
protected DatabaseContext(DbContextOptions dbContextOptions) : base(dbContextOptions)
|
||||
{
|
||||
usersCollection = new DatabaseCollection<User>(Users);
|
||||
instancesCollection = new DatabaseCollection<Instance>(Instances);
|
||||
@@ -230,6 +243,7 @@ namespace Tgstation.Server.Host.Database
|
||||
revisionInformationsCollection = new DatabaseCollection<RevisionInformation>(RevisionInformations);
|
||||
jobsCollection = new DatabaseCollection<Job>(Jobs);
|
||||
reattachInformationsCollection = new DatabaseCollection<ReattachInformation>(ReattachInformations);
|
||||
oAuthConnections = new DatabaseCollection<OAuthConnection>(OAuthConnections);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -244,6 +258,9 @@ namespace Tgstation.Server.Host.Database
|
||||
userModel.HasIndex(x => x.CanonicalName).IsUnique();
|
||||
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);
|
||||
|
||||
modelBuilder.Entity<OAuthConnection>().HasIndex(x => new { x.Provider, x.ExternalUserId }).IsUnique();
|
||||
|
||||
modelBuilder.Entity<InstanceUser>().HasIndex(x => new { x.UserId, x.InstanceId }).IsUnique();
|
||||
|
||||
@@ -316,6 +333,7 @@ namespace Tgstation.Server.Host.Database
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
#pragma warning disable CA1502 // Cyclomatic complexity
|
||||
public async Task SchemaDowngradeForServerVersion(
|
||||
ILogger<DatabaseContext> logger,
|
||||
Version version,
|
||||
@@ -338,7 +356,43 @@ namespace Tgstation.Server.Host.Database
|
||||
if (version < new Version(4, 1, 0))
|
||||
throw new NotSupportedException("Cannot migrate below version 4.1.0!");
|
||||
|
||||
if(version < new Version(4, 4, 0))
|
||||
if (version < new Version(4, 6, 0))
|
||||
switch (currentDatabaseType)
|
||||
{
|
||||
case DatabaseType.MariaDB:
|
||||
case DatabaseType.MySql:
|
||||
targetMigration = nameof(MYAddAdditionalDDParameters);
|
||||
break;
|
||||
case DatabaseType.PostgresSql:
|
||||
targetMigration = nameof(PGAddAdditionalDDParameters);
|
||||
break;
|
||||
case DatabaseType.SqlServer:
|
||||
case DatabaseType.Sqlite:
|
||||
targetMigration = nameof(MSAddAdditionalDDParameters);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException($"Invalid DatabaseType: {currentDatabaseType}", nameof(currentDatabaseType));
|
||||
}
|
||||
|
||||
if (version < new Version(4, 5, 0))
|
||||
switch (currentDatabaseType)
|
||||
{
|
||||
case DatabaseType.MariaDB:
|
||||
case DatabaseType.MySql:
|
||||
targetMigration = nameof(MYAddDeploymentColumns);
|
||||
break;
|
||||
case DatabaseType.PostgresSql:
|
||||
targetMigration = nameof(PGAddDeploymentColumns);
|
||||
break;
|
||||
case DatabaseType.SqlServer:
|
||||
case DatabaseType.Sqlite:
|
||||
targetMigration = nameof(MSAddDeploymentColumns);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException($"Invalid DatabaseType: {currentDatabaseType}", nameof(currentDatabaseType));
|
||||
}
|
||||
|
||||
if (version < new Version(4, 4, 0))
|
||||
switch (currentDatabaseType)
|
||||
{
|
||||
case DatabaseType.MariaDB:
|
||||
@@ -403,5 +457,6 @@ namespace Tgstation.Server.Host.Database
|
||||
logger.LogCritical(e, "Failed to migrate!");
|
||||
}
|
||||
}
|
||||
#pragma warning restore CA1502 // Cyclomatic complexity
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Threading;
|
||||
@@ -14,65 +14,70 @@ namespace Tgstation.Server.Host.Database
|
||||
public interface IDatabaseContext
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="User"/>s in the <see cref="IDatabaseContext"/>
|
||||
/// The <see cref="User"/>s in the <see cref="IDatabaseContext"/>.
|
||||
/// </summary>
|
||||
IDatabaseCollection<User> Users { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Instance"/>s in the <see cref="IDatabaseContext"/>
|
||||
/// The <see cref="Instance"/>s in the <see cref="IDatabaseContext"/>.
|
||||
/// </summary>
|
||||
IDatabaseCollection<Instance> Instances { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="InstanceUser"/>s in the <see cref="IDatabaseContext"/>
|
||||
/// The <see cref="InstanceUser"/>s in the <see cref="IDatabaseContext"/>.
|
||||
/// </summary>
|
||||
IDatabaseCollection<InstanceUser> InstanceUsers { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Job"/>s in the <see cref="IDatabaseContext"/>
|
||||
/// The <see cref="Job"/>s in the <see cref="IDatabaseContext"/>.
|
||||
/// </summary>
|
||||
IDatabaseCollection<Job> Jobs { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="CompileJob"/>s in the <see cref="IDatabaseContext"/>
|
||||
/// The <see cref="CompileJob"/>s in the <see cref="IDatabaseContext"/>.
|
||||
/// </summary>
|
||||
IDatabaseCollection<CompileJob> CompileJobs { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="RevisionInformation"/>s in the <see cref="IDatabaseContext"/>
|
||||
/// The <see cref="RevisionInformation"/>s in the <see cref="IDatabaseContext"/>.
|
||||
/// </summary>
|
||||
IDatabaseCollection<RevisionInformation> RevisionInformations { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Models.DreamMakerSettings"/> in the <see cref="IDatabaseContext"/>
|
||||
/// The <see cref="Models.DreamMakerSettings"/> in the <see cref="IDatabaseContext"/>.
|
||||
/// </summary>
|
||||
IDatabaseCollection<DreamMakerSettings> DreamMakerSettings { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Models.DreamDaemonSettings"/> in the <see cref="IDatabaseContext"/>
|
||||
/// The <see cref="Models.DreamDaemonSettings"/> in the <see cref="IDatabaseContext"/>.
|
||||
/// </summary>
|
||||
IDatabaseCollection<DreamDaemonSettings> DreamDaemonSettings { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ChatBot"/>s in the <see cref="IDatabaseContext"/>
|
||||
/// The <see cref="ChatBot"/>s in the <see cref="IDatabaseContext"/>.
|
||||
/// </summary>
|
||||
IDatabaseCollection<ChatBot> ChatBots { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ChatChannel"/> in the <see cref="IDatabaseContext"/>
|
||||
/// The <see cref="ChatChannel"/> in the <see cref="IDatabaseContext"/>.
|
||||
/// </summary>
|
||||
IDatabaseCollection<ChatChannel> ChatChannels { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Models.RepositorySettings"/> in the <see cref="IDatabaseContext"/>
|
||||
/// The <see cref="Models.RepositorySettings"/> in the <see cref="IDatabaseContext"/>.
|
||||
/// </summary>
|
||||
IDatabaseCollection<RepositorySettings> RepositorySettings { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="DbSet{TEntity}"/> for <see cref="ReattachInformation"/>s
|
||||
/// The <see cref="DbSet{TEntity}"/> for <see cref="ReattachInformation"/>s.
|
||||
/// </summary>
|
||||
IDatabaseCollection<ReattachInformation> ReattachInformations { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="DbSet{TEntity}"/> for <see cref="OAuthConnection"/>s.
|
||||
/// </summary>
|
||||
IDatabaseCollection<OAuthConnection> OAuthConnections { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Saves changes made to the <see cref="IDatabaseContext"/>
|
||||
/// </summary>
|
||||
|
||||
Generated
+822
@@ -0,0 +1,822 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace Tgstation.Server.Host.Database.Migrations
|
||||
{
|
||||
[DbContext(typeof(SqlServerDatabaseContext))]
|
||||
[Migration("20201122231219_MSAddOAuthConnections")]
|
||||
partial class MSAddOAuthConnections
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "3.1.10")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128)
|
||||
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint")
|
||||
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
|
||||
|
||||
b.Property<int>("ChannelLimit")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("ConnectionString")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)")
|
||||
.HasMaxLength(10000);
|
||||
|
||||
b.Property<bool?>("Enabled")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<long>("InstanceId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(100)")
|
||||
.HasMaxLength(100);
|
||||
|
||||
b.Property<int>("Provider")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<long>("ReconnectionInterval")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InstanceId", "Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("ChatBots");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint")
|
||||
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
|
||||
|
||||
b.Property<long>("ChatSettingsId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<decimal?>("DiscordChannelId")
|
||||
.HasColumnType("decimal(20,0)");
|
||||
|
||||
b.Property<string>("IrcChannel")
|
||||
.HasColumnType("nvarchar(100)")
|
||||
.HasMaxLength(100);
|
||||
|
||||
b.Property<bool?>("IsAdminChannel")
|
||||
.IsRequired()
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<bool?>("IsUpdatesChannel")
|
||||
.IsRequired()
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<bool?>("IsWatchdogChannel")
|
||||
.IsRequired()
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("Tag")
|
||||
.HasColumnType("nvarchar(max)")
|
||||
.HasMaxLength(10000);
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ChatSettingsId", "DiscordChannelId")
|
||||
.IsUnique()
|
||||
.HasFilter("[DiscordChannelId] IS NOT NULL");
|
||||
|
||||
b.HasIndex("ChatSettingsId", "IrcChannel")
|
||||
.IsUnique()
|
||||
.HasFilter("[IrcChannel] IS NOT NULL");
|
||||
|
||||
b.ToTable("ChatChannels");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint")
|
||||
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
|
||||
|
||||
b.Property<string>("ByondVersion")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int?>("DMApiMajorVersion")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int?>("DMApiMinorVersion")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int?>("DMApiPatchVersion")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<Guid?>("DirectoryName")
|
||||
.IsRequired()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("DmeName")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int?>("GitHubDeploymentId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<long?>("GitHubRepoId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long>("JobId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<int?>("MinimumSecurityLevel")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Output")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<long>("RevisionInformationId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DirectoryName");
|
||||
|
||||
b.HasIndex("JobId")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("RevisionInformationId");
|
||||
|
||||
b.ToTable("CompileJobs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint")
|
||||
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
|
||||
|
||||
b.Property<string>("AdditionalParameters")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)")
|
||||
.HasMaxLength(10000);
|
||||
|
||||
b.Property<bool?>("AllowWebClient")
|
||||
.IsRequired()
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<bool?>("AutoStart")
|
||||
.IsRequired()
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<long>("HeartbeatSeconds")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long>("InstanceId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<int>("Port")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("SecurityLevel")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<long>("StartupTimeout")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long>("TopicRequestTimeout")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InstanceId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("DreamDaemonSettings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint")
|
||||
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
|
||||
|
||||
b.Property<int>("ApiValidationPort")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("ApiValidationSecurityLevel")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<long>("InstanceId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("ProjectName")
|
||||
.HasColumnType("nvarchar(max)")
|
||||
.HasMaxLength(10000);
|
||||
|
||||
b.Property<bool?>("RequireDMApiValidation")
|
||||
.IsRequired()
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InstanceId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("DreamMakerSettings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint")
|
||||
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
|
||||
|
||||
b.Property<long>("AutoUpdateInterval")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<int>("ChatBotLimit")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("ConfigurationType")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)")
|
||||
.HasMaxLength(10000);
|
||||
|
||||
b.Property<bool?>("Online")
|
||||
.IsRequired()
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Path")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Instances");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint")
|
||||
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
|
||||
|
||||
b.Property<decimal>("ByondRights")
|
||||
.HasColumnType("decimal(20,0)");
|
||||
|
||||
b.Property<decimal>("ChatBotRights")
|
||||
.HasColumnType("decimal(20,0)");
|
||||
|
||||
b.Property<decimal>("ConfigurationRights")
|
||||
.HasColumnType("decimal(20,0)");
|
||||
|
||||
b.Property<decimal>("DreamDaemonRights")
|
||||
.HasColumnType("decimal(20,0)");
|
||||
|
||||
b.Property<decimal>("DreamMakerRights")
|
||||
.HasColumnType("decimal(20,0)");
|
||||
|
||||
b.Property<long>("InstanceId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<decimal>("InstanceUserRights")
|
||||
.HasColumnType("decimal(20,0)");
|
||||
|
||||
b.Property<decimal>("RepositoryRights")
|
||||
.HasColumnType("decimal(20,0)");
|
||||
|
||||
b.Property<long>("UserId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InstanceId");
|
||||
|
||||
b.HasIndex("UserId", "InstanceId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("InstanceUsers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint")
|
||||
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
|
||||
|
||||
b.Property<decimal?>("CancelRight")
|
||||
.HasColumnType("decimal(20,0)");
|
||||
|
||||
b.Property<decimal?>("CancelRightsType")
|
||||
.HasColumnType("decimal(20,0)");
|
||||
|
||||
b.Property<bool?>("Cancelled")
|
||||
.IsRequired()
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<long?>("CancelledById")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<long?>("ErrorCode")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("ExceptionDetails")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<long>("InstanceId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTimeOffset?>("StartedAt")
|
||||
.IsRequired()
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<long>("StartedById")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTimeOffset?>("StoppedAt")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CancelledById");
|
||||
|
||||
b.HasIndex("InstanceId");
|
||||
|
||||
b.HasIndex("StartedById");
|
||||
|
||||
b.ToTable("Jobs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint")
|
||||
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
|
||||
|
||||
b.Property<string>("ExternalUserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(100)")
|
||||
.HasMaxLength(100);
|
||||
|
||||
b.Property<int>("Provider")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<long?>("UserId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.HasIndex("Provider", "ExternalUserId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("OAuthConnections");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint")
|
||||
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
|
||||
|
||||
b.Property<string>("AccessIdentifier")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<long>("CompileJobId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<int>("LaunchSecurityLevel")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("Port")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("ProcessId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("RebootState")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CompileJobId");
|
||||
|
||||
b.ToTable("ReattachInformations");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint")
|
||||
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
|
||||
|
||||
b.Property<string>("AccessToken")
|
||||
.HasColumnType("nvarchar(max)")
|
||||
.HasMaxLength(10000);
|
||||
|
||||
b.Property<string>("AccessUser")
|
||||
.HasColumnType("nvarchar(max)")
|
||||
.HasMaxLength(10000);
|
||||
|
||||
b.Property<bool?>("AutoUpdatesKeepTestMerges")
|
||||
.IsRequired()
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<bool?>("AutoUpdatesSynchronize")
|
||||
.IsRequired()
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("CommitterEmail")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)")
|
||||
.HasMaxLength(10000);
|
||||
|
||||
b.Property<string>("CommitterName")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)")
|
||||
.HasMaxLength(10000);
|
||||
|
||||
b.Property<bool?>("CreateGitHubDeployments")
|
||||
.IsRequired()
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<long>("InstanceId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<bool?>("PostTestMergeComment")
|
||||
.IsRequired()
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<bool?>("PushTestMergeCommits")
|
||||
.IsRequired()
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<bool?>("ShowTestMergeCommitters")
|
||||
.IsRequired()
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InstanceId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("RepositorySettings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint")
|
||||
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
|
||||
|
||||
b.Property<long>("RevisionInformationId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long>("TestMergeId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RevisionInformationId");
|
||||
|
||||
b.HasIndex("TestMergeId");
|
||||
|
||||
b.ToTable("RevInfoTestMerges");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint")
|
||||
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
|
||||
|
||||
b.Property<string>("CommitSha")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(40)")
|
||||
.HasMaxLength(40);
|
||||
|
||||
b.Property<long>("InstanceId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("OriginCommitSha")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(40)")
|
||||
.HasMaxLength(40);
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InstanceId", "CommitSha")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("RevisionInformations");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint")
|
||||
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
|
||||
|
||||
b.Property<string>("Author")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("BodyAtMerge")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Comment")
|
||||
.HasColumnType("nvarchar(max)")
|
||||
.HasMaxLength(10000);
|
||||
|
||||
b.Property<DateTimeOffset>("MergedAt")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<long>("MergedById")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<int>("Number")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<long?>("PrimaryRevisionInformationId")
|
||||
.IsRequired()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("PullRequestRevision")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(40)")
|
||||
.HasMaxLength(40);
|
||||
|
||||
b.Property<string>("TitleAtMerge")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Url")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("MergedById");
|
||||
|
||||
b.HasIndex("PrimaryRevisionInformationId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("TestMerges");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.User", b =>
|
||||
{
|
||||
b.Property<long?>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint")
|
||||
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
|
||||
|
||||
b.Property<decimal>("AdministrationRights")
|
||||
.HasColumnType("decimal(20,0)");
|
||||
|
||||
b.Property<string>("CanonicalName")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<DateTimeOffset?>("CreatedAt")
|
||||
.IsRequired()
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<long?>("CreatedById")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<bool?>("Enabled")
|
||||
.IsRequired()
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<decimal>("InstanceManagerRights")
|
||||
.HasColumnType("decimal(20,0)");
|
||||
|
||||
b.Property<DateTimeOffset?>("LastPasswordUpdate")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)")
|
||||
.HasMaxLength(10000);
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("SystemIdentifier")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CanonicalName")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("CreatedById");
|
||||
|
||||
b.HasIndex("SystemIdentifier")
|
||||
.IsUnique()
|
||||
.HasFilter("[SystemIdentifier] IS NOT NULL");
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance")
|
||||
.WithMany("ChatSettings")
|
||||
.HasForeignKey("InstanceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.ChatBot", "ChatSettings")
|
||||
.WithMany("Channels")
|
||||
.HasForeignKey("ChatSettingsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.Job", "Job")
|
||||
.WithOne()
|
||||
.HasForeignKey("Tgstation.Server.Host.Models.CompileJob", "JobId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation")
|
||||
.WithMany("CompileJobs")
|
||||
.HasForeignKey("RevisionInformationId")
|
||||
.OnDelete(DeleteBehavior.ClientNoAction)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance")
|
||||
.WithOne("DreamDaemonSettings")
|
||||
.HasForeignKey("Tgstation.Server.Host.Models.DreamDaemonSettings", "InstanceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance")
|
||||
.WithOne("DreamMakerSettings")
|
||||
.HasForeignKey("Tgstation.Server.Host.Models.DreamMakerSettings", "InstanceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance")
|
||||
.WithMany("InstanceUsers")
|
||||
.HasForeignKey("InstanceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Tgstation.Server.Host.Models.User", null)
|
||||
.WithMany("InstanceUsers")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.User", "CancelledBy")
|
||||
.WithMany()
|
||||
.HasForeignKey("CancelledById");
|
||||
|
||||
b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance")
|
||||
.WithMany("Jobs")
|
||||
.HasForeignKey("InstanceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Tgstation.Server.Host.Models.User", "StartedBy")
|
||||
.WithMany()
|
||||
.HasForeignKey("StartedById")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.User", "User")
|
||||
.WithMany("OAuthConnections")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob")
|
||||
.WithMany()
|
||||
.HasForeignKey("CompileJobId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance")
|
||||
.WithOne("RepositorySettings")
|
||||
.HasForeignKey("Tgstation.Server.Host.Models.RepositorySettings", "InstanceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation")
|
||||
.WithMany("ActiveTestMerges")
|
||||
.HasForeignKey("RevisionInformationId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Tgstation.Server.Host.Models.TestMerge", "TestMerge")
|
||||
.WithMany("RevisonInformations")
|
||||
.HasForeignKey("TestMergeId")
|
||||
.OnDelete(DeleteBehavior.ClientNoAction)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance")
|
||||
.WithMany("RevisionInformations")
|
||||
.HasForeignKey("InstanceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.User", "MergedBy")
|
||||
.WithMany("TestMerges")
|
||||
.HasForeignKey("MergedById")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "PrimaryRevisionInformation")
|
||||
.WithOne("PrimaryTestMerge")
|
||||
.HasForeignKey("Tgstation.Server.Host.Models.TestMerge", "PrimaryRevisionInformationId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.User", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.User", "CreatedBy")
|
||||
.WithMany("CreatedUsers")
|
||||
.HasForeignKey("CreatedById");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using System;
|
||||
|
||||
namespace Tgstation.Server.Host.Database.Migrations
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds the OAuthConnections table for MSSQL.
|
||||
/// </summary>
|
||||
public partial class MSAddOAuthConnections : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
if (migrationBuilder == null)
|
||||
throw new ArgumentNullException(nameof(migrationBuilder));
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "OAuthConnections",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
Provider = table.Column<int>(nullable: false),
|
||||
ExternalUserId = table.Column<string>(nullable: false, maxLength: 100),
|
||||
UserId = table.Column<long>(nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_OAuthConnections", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_OAuthConnections_Users_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_OAuthConnections_UserId",
|
||||
table: "OAuthConnections",
|
||||
column: "UserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_OAuthConnections_Provider_ExternalUserId",
|
||||
table: "OAuthConnections",
|
||||
columns: new[] { "Provider", "ExternalUserId" },
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
if (migrationBuilder == null)
|
||||
throw new ArgumentNullException(nameof(migrationBuilder));
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "OAuthConnections");
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+811
@@ -0,0 +1,811 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace Tgstation.Server.Host.Database.Migrations
|
||||
{
|
||||
[DbContext(typeof(MySqlDatabaseContext))]
|
||||
[Migration("20201122231327_MYAddOAuthConnections")]
|
||||
partial class MYAddOAuthConnections
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "3.1.10")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 64);
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<ushort?>("ChannelLimit")
|
||||
.IsRequired()
|
||||
.HasColumnType("smallint unsigned");
|
||||
|
||||
b.Property<string>("ConnectionString")
|
||||
.IsRequired()
|
||||
.HasColumnType("longtext CHARACTER SET utf8mb4")
|
||||
.HasMaxLength(10000);
|
||||
|
||||
b.Property<bool?>("Enabled")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<long>("InstanceId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("varchar(100) CHARACTER SET utf8mb4")
|
||||
.HasMaxLength(100);
|
||||
|
||||
b.Property<int>("Provider")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<uint?>("ReconnectionInterval")
|
||||
.IsRequired()
|
||||
.HasColumnType("int unsigned");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InstanceId", "Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("ChatBots");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long>("ChatSettingsId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<ulong?>("DiscordChannelId")
|
||||
.HasColumnType("bigint unsigned");
|
||||
|
||||
b.Property<string>("IrcChannel")
|
||||
.HasColumnType("varchar(100) CHARACTER SET utf8mb4")
|
||||
.HasMaxLength(100);
|
||||
|
||||
b.Property<bool?>("IsAdminChannel")
|
||||
.IsRequired()
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<bool?>("IsUpdatesChannel")
|
||||
.IsRequired()
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<bool?>("IsWatchdogChannel")
|
||||
.IsRequired()
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<string>("Tag")
|
||||
.HasColumnType("longtext CHARACTER SET utf8mb4")
|
||||
.HasMaxLength(10000);
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ChatSettingsId", "DiscordChannelId")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("ChatSettingsId", "IrcChannel")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("ChatChannels");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("ByondVersion")
|
||||
.IsRequired()
|
||||
.HasColumnType("longtext CHARACTER SET utf8mb4");
|
||||
|
||||
b.Property<int?>("DMApiMajorVersion")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int?>("DMApiMinorVersion")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int?>("DMApiPatchVersion")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<Guid?>("DirectoryName")
|
||||
.IsRequired()
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<string>("DmeName")
|
||||
.IsRequired()
|
||||
.HasColumnType("longtext CHARACTER SET utf8mb4");
|
||||
|
||||
b.Property<int?>("GitHubDeploymentId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<long?>("GitHubRepoId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long>("JobId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<int?>("MinimumSecurityLevel")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Output")
|
||||
.IsRequired()
|
||||
.HasColumnType("longtext CHARACTER SET utf8mb4");
|
||||
|
||||
b.Property<long>("RevisionInformationId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DirectoryName");
|
||||
|
||||
b.HasIndex("JobId")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("RevisionInformationId");
|
||||
|
||||
b.ToTable("CompileJobs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("AdditionalParameters")
|
||||
.IsRequired()
|
||||
.HasColumnType("longtext CHARACTER SET utf8mb4")
|
||||
.HasMaxLength(10000);
|
||||
|
||||
b.Property<bool?>("AllowWebClient")
|
||||
.IsRequired()
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<bool?>("AutoStart")
|
||||
.IsRequired()
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<uint?>("HeartbeatSeconds")
|
||||
.IsRequired()
|
||||
.HasColumnType("int unsigned");
|
||||
|
||||
b.Property<long>("InstanceId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<ushort?>("Port")
|
||||
.IsRequired()
|
||||
.HasColumnType("smallint unsigned");
|
||||
|
||||
b.Property<int>("SecurityLevel")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<uint?>("StartupTimeout")
|
||||
.IsRequired()
|
||||
.HasColumnType("int unsigned");
|
||||
|
||||
b.Property<uint?>("TopicRequestTimeout")
|
||||
.IsRequired()
|
||||
.HasColumnType("int unsigned");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InstanceId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("DreamDaemonSettings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<ushort?>("ApiValidationPort")
|
||||
.IsRequired()
|
||||
.HasColumnType("smallint unsigned");
|
||||
|
||||
b.Property<int>("ApiValidationSecurityLevel")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<long>("InstanceId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("ProjectName")
|
||||
.HasColumnType("longtext CHARACTER SET utf8mb4")
|
||||
.HasMaxLength(10000);
|
||||
|
||||
b.Property<bool?>("RequireDMApiValidation")
|
||||
.IsRequired()
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InstanceId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("DreamMakerSettings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<uint?>("AutoUpdateInterval")
|
||||
.IsRequired()
|
||||
.HasColumnType("int unsigned");
|
||||
|
||||
b.Property<ushort?>("ChatBotLimit")
|
||||
.IsRequired()
|
||||
.HasColumnType("smallint unsigned");
|
||||
|
||||
b.Property<int>("ConfigurationType")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("longtext CHARACTER SET utf8mb4")
|
||||
.HasMaxLength(10000);
|
||||
|
||||
b.Property<bool?>("Online")
|
||||
.IsRequired()
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasColumnType("varchar(255) CHARACTER SET utf8mb4");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Path")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Instances");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<ulong>("ByondRights")
|
||||
.HasColumnType("bigint unsigned");
|
||||
|
||||
b.Property<ulong>("ChatBotRights")
|
||||
.HasColumnType("bigint unsigned");
|
||||
|
||||
b.Property<ulong>("ConfigurationRights")
|
||||
.HasColumnType("bigint unsigned");
|
||||
|
||||
b.Property<ulong>("DreamDaemonRights")
|
||||
.HasColumnType("bigint unsigned");
|
||||
|
||||
b.Property<ulong>("DreamMakerRights")
|
||||
.HasColumnType("bigint unsigned");
|
||||
|
||||
b.Property<long>("InstanceId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<ulong>("InstanceUserRights")
|
||||
.HasColumnType("bigint unsigned");
|
||||
|
||||
b.Property<ulong>("RepositoryRights")
|
||||
.HasColumnType("bigint unsigned");
|
||||
|
||||
b.Property<long>("UserId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InstanceId");
|
||||
|
||||
b.HasIndex("UserId", "InstanceId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("InstanceUsers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<ulong?>("CancelRight")
|
||||
.HasColumnType("bigint unsigned");
|
||||
|
||||
b.Property<ulong?>("CancelRightsType")
|
||||
.HasColumnType("bigint unsigned");
|
||||
|
||||
b.Property<bool?>("Cancelled")
|
||||
.IsRequired()
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<long?>("CancelledById")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasColumnType("longtext CHARACTER SET utf8mb4");
|
||||
|
||||
b.Property<uint?>("ErrorCode")
|
||||
.HasColumnType("int unsigned");
|
||||
|
||||
b.Property<string>("ExceptionDetails")
|
||||
.HasColumnType("longtext CHARACTER SET utf8mb4");
|
||||
|
||||
b.Property<long>("InstanceId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTimeOffset?>("StartedAt")
|
||||
.IsRequired()
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<long>("StartedById")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTimeOffset?>("StoppedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CancelledById");
|
||||
|
||||
b.HasIndex("InstanceId");
|
||||
|
||||
b.HasIndex("StartedById");
|
||||
|
||||
b.ToTable("Jobs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("ExternalUserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("varchar(100) CHARACTER SET utf8mb4")
|
||||
.HasMaxLength(100);
|
||||
|
||||
b.Property<int>("Provider")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<long?>("UserId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.HasIndex("Provider", "ExternalUserId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("OAuthConnections");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("AccessIdentifier")
|
||||
.IsRequired()
|
||||
.HasColumnType("longtext CHARACTER SET utf8mb4");
|
||||
|
||||
b.Property<long>("CompileJobId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<int>("LaunchSecurityLevel")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<ushort>("Port")
|
||||
.HasColumnType("smallint unsigned");
|
||||
|
||||
b.Property<int>("ProcessId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("RebootState")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CompileJobId");
|
||||
|
||||
b.ToTable("ReattachInformations");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("AccessToken")
|
||||
.HasColumnType("longtext CHARACTER SET utf8mb4")
|
||||
.HasMaxLength(10000);
|
||||
|
||||
b.Property<string>("AccessUser")
|
||||
.HasColumnType("longtext CHARACTER SET utf8mb4")
|
||||
.HasMaxLength(10000);
|
||||
|
||||
b.Property<bool?>("AutoUpdatesKeepTestMerges")
|
||||
.IsRequired()
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<bool?>("AutoUpdatesSynchronize")
|
||||
.IsRequired()
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<string>("CommitterEmail")
|
||||
.IsRequired()
|
||||
.HasColumnType("longtext CHARACTER SET utf8mb4")
|
||||
.HasMaxLength(10000);
|
||||
|
||||
b.Property<string>("CommitterName")
|
||||
.IsRequired()
|
||||
.HasColumnType("longtext CHARACTER SET utf8mb4")
|
||||
.HasMaxLength(10000);
|
||||
|
||||
b.Property<bool?>("CreateGitHubDeployments")
|
||||
.IsRequired()
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<long>("InstanceId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<bool?>("PostTestMergeComment")
|
||||
.IsRequired()
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<bool?>("PushTestMergeCommits")
|
||||
.IsRequired()
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<bool?>("ShowTestMergeCommitters")
|
||||
.IsRequired()
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InstanceId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("RepositorySettings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long>("RevisionInformationId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long>("TestMergeId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RevisionInformationId");
|
||||
|
||||
b.HasIndex("TestMergeId");
|
||||
|
||||
b.ToTable("RevInfoTestMerges");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("CommitSha")
|
||||
.IsRequired()
|
||||
.HasColumnType("varchar(40) CHARACTER SET utf8mb4")
|
||||
.HasMaxLength(40);
|
||||
|
||||
b.Property<long>("InstanceId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("OriginCommitSha")
|
||||
.IsRequired()
|
||||
.HasColumnType("varchar(40) CHARACTER SET utf8mb4")
|
||||
.HasMaxLength(40);
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InstanceId", "CommitSha")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("RevisionInformations");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("Author")
|
||||
.IsRequired()
|
||||
.HasColumnType("longtext CHARACTER SET utf8mb4");
|
||||
|
||||
b.Property<string>("BodyAtMerge")
|
||||
.IsRequired()
|
||||
.HasColumnType("longtext CHARACTER SET utf8mb4");
|
||||
|
||||
b.Property<string>("Comment")
|
||||
.HasColumnType("longtext CHARACTER SET utf8mb4")
|
||||
.HasMaxLength(10000);
|
||||
|
||||
b.Property<DateTimeOffset>("MergedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<long>("MergedById")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<int>("Number")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<long?>("PrimaryRevisionInformationId")
|
||||
.IsRequired()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("PullRequestRevision")
|
||||
.IsRequired()
|
||||
.HasColumnType("varchar(40) CHARACTER SET utf8mb4")
|
||||
.HasMaxLength(40);
|
||||
|
||||
b.Property<string>("TitleAtMerge")
|
||||
.IsRequired()
|
||||
.HasColumnType("longtext CHARACTER SET utf8mb4");
|
||||
|
||||
b.Property<string>("Url")
|
||||
.IsRequired()
|
||||
.HasColumnType("longtext CHARACTER SET utf8mb4");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("MergedById");
|
||||
|
||||
b.HasIndex("PrimaryRevisionInformationId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("TestMerges");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.User", b =>
|
||||
{
|
||||
b.Property<long?>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<ulong>("AdministrationRights")
|
||||
.HasColumnType("bigint unsigned");
|
||||
|
||||
b.Property<string>("CanonicalName")
|
||||
.IsRequired()
|
||||
.HasColumnType("varchar(255) CHARACTER SET utf8mb4");
|
||||
|
||||
b.Property<DateTimeOffset?>("CreatedAt")
|
||||
.IsRequired()
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<long?>("CreatedById")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<bool?>("Enabled")
|
||||
.IsRequired()
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<ulong>("InstanceManagerRights")
|
||||
.HasColumnType("bigint unsigned");
|
||||
|
||||
b.Property<DateTimeOffset?>("LastPasswordUpdate")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("longtext CHARACTER SET utf8mb4")
|
||||
.HasMaxLength(10000);
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.HasColumnType("longtext CHARACTER SET utf8mb4");
|
||||
|
||||
b.Property<string>("SystemIdentifier")
|
||||
.HasColumnType("varchar(255) CHARACTER SET utf8mb4");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CanonicalName")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("CreatedById");
|
||||
|
||||
b.HasIndex("SystemIdentifier")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance")
|
||||
.WithMany("ChatSettings")
|
||||
.HasForeignKey("InstanceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.ChatBot", "ChatSettings")
|
||||
.WithMany("Channels")
|
||||
.HasForeignKey("ChatSettingsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.Job", "Job")
|
||||
.WithOne()
|
||||
.HasForeignKey("Tgstation.Server.Host.Models.CompileJob", "JobId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation")
|
||||
.WithMany("CompileJobs")
|
||||
.HasForeignKey("RevisionInformationId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance")
|
||||
.WithOne("DreamDaemonSettings")
|
||||
.HasForeignKey("Tgstation.Server.Host.Models.DreamDaemonSettings", "InstanceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance")
|
||||
.WithOne("DreamMakerSettings")
|
||||
.HasForeignKey("Tgstation.Server.Host.Models.DreamMakerSettings", "InstanceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance")
|
||||
.WithMany("InstanceUsers")
|
||||
.HasForeignKey("InstanceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Tgstation.Server.Host.Models.User", null)
|
||||
.WithMany("InstanceUsers")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.User", "CancelledBy")
|
||||
.WithMany()
|
||||
.HasForeignKey("CancelledById");
|
||||
|
||||
b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance")
|
||||
.WithMany("Jobs")
|
||||
.HasForeignKey("InstanceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Tgstation.Server.Host.Models.User", "StartedBy")
|
||||
.WithMany()
|
||||
.HasForeignKey("StartedById")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.User", "User")
|
||||
.WithMany("OAuthConnections")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob")
|
||||
.WithMany()
|
||||
.HasForeignKey("CompileJobId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance")
|
||||
.WithOne("RepositorySettings")
|
||||
.HasForeignKey("Tgstation.Server.Host.Models.RepositorySettings", "InstanceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation")
|
||||
.WithMany("ActiveTestMerges")
|
||||
.HasForeignKey("RevisionInformationId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Tgstation.Server.Host.Models.TestMerge", "TestMerge")
|
||||
.WithMany("RevisonInformations")
|
||||
.HasForeignKey("TestMergeId")
|
||||
.OnDelete(DeleteBehavior.ClientNoAction)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance")
|
||||
.WithMany("RevisionInformations")
|
||||
.HasForeignKey("InstanceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.User", "MergedBy")
|
||||
.WithMany("TestMerges")
|
||||
.HasForeignKey("MergedById")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "PrimaryRevisionInformation")
|
||||
.WithOne("PrimaryTestMerge")
|
||||
.HasForeignKey("Tgstation.Server.Host.Models.TestMerge", "PrimaryRevisionInformationId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.User", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.User", "CreatedBy")
|
||||
.WithMany("CreatedUsers")
|
||||
.HasForeignKey("CreatedById");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using System;
|
||||
|
||||
namespace Tgstation.Server.Host.Database.Migrations
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds the OAuthConnections table for MYSQL.
|
||||
/// </summary>
|
||||
public partial class MYAddOAuthConnections : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
if (migrationBuilder == null)
|
||||
throw new ArgumentNullException(nameof(migrationBuilder));
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "OAuthConnections",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(nullable: false)
|
||||
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||
Provider = table.Column<int>(nullable: false),
|
||||
ExternalUserId = table.Column<string>(nullable: false, maxLength: 100),
|
||||
UserId = table.Column<long>(nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_OAuthConnections", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_OAuthConnections_Users_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_OAuthConnections_UserId",
|
||||
table: "OAuthConnections",
|
||||
column: "UserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_OAuthConnections_Provider_ExternalUserId",
|
||||
table: "OAuthConnections",
|
||||
columns: new[] { "Provider", "ExternalUserId" },
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
if (migrationBuilder == null)
|
||||
throw new ArgumentNullException(nameof(migrationBuilder));
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "OAuthConnections");
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+819
@@ -0,0 +1,819 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
namespace Tgstation.Server.Host.Database.Migrations
|
||||
{
|
||||
[DbContext(typeof(PostgresSqlDatabaseContext))]
|
||||
[Migration("20201122231443_PGAddOAuthConnections")]
|
||||
partial class PGAddOAuthConnections
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn)
|
||||
.HasAnnotation("ProductVersion", "3.1.10")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint")
|
||||
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
|
||||
b.Property<int>("ChannelLimit")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("ConnectionString")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(10000)")
|
||||
.HasMaxLength(10000);
|
||||
|
||||
b.Property<bool?>("Enabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<long>("InstanceId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasMaxLength(100);
|
||||
|
||||
b.Property<int>("Provider")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<long>("ReconnectionInterval")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InstanceId", "Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("ChatBots");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint")
|
||||
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
|
||||
b.Property<long>("ChatSettingsId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<decimal?>("DiscordChannelId")
|
||||
.HasColumnType("numeric(20,0)");
|
||||
|
||||
b.Property<string>("IrcChannel")
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasMaxLength(100);
|
||||
|
||||
b.Property<bool?>("IsAdminChannel")
|
||||
.IsRequired()
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool?>("IsUpdatesChannel")
|
||||
.IsRequired()
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool?>("IsWatchdogChannel")
|
||||
.IsRequired()
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Tag")
|
||||
.HasColumnType("character varying(10000)")
|
||||
.HasMaxLength(10000);
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ChatSettingsId", "DiscordChannelId")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("ChatSettingsId", "IrcChannel")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("ChatChannels");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint")
|
||||
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
|
||||
b.Property<string>("ByondVersion")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int?>("DMApiMajorVersion")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int?>("DMApiMinorVersion")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int?>("DMApiPatchVersion")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<Guid?>("DirectoryName")
|
||||
.IsRequired()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("DmeName")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int?>("GitHubDeploymentId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<long?>("GitHubRepoId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long>("JobId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<int?>("MinimumSecurityLevel")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Output")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<long>("RevisionInformationId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DirectoryName");
|
||||
|
||||
b.HasIndex("JobId")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("RevisionInformationId");
|
||||
|
||||
b.ToTable("CompileJobs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint")
|
||||
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
|
||||
b.Property<string>("AdditionalParameters")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(10000)")
|
||||
.HasMaxLength(10000);
|
||||
|
||||
b.Property<bool?>("AllowWebClient")
|
||||
.IsRequired()
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool?>("AutoStart")
|
||||
.IsRequired()
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<long>("HeartbeatSeconds")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long>("InstanceId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<int>("Port")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("SecurityLevel")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<long>("StartupTimeout")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long>("TopicRequestTimeout")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InstanceId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("DreamDaemonSettings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint")
|
||||
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
|
||||
b.Property<int>("ApiValidationPort")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ApiValidationSecurityLevel")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<long>("InstanceId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("ProjectName")
|
||||
.HasColumnType("character varying(10000)")
|
||||
.HasMaxLength(10000);
|
||||
|
||||
b.Property<bool?>("RequireDMApiValidation")
|
||||
.IsRequired()
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InstanceId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("DreamMakerSettings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint")
|
||||
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
|
||||
b.Property<long>("AutoUpdateInterval")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<int>("ChatBotLimit")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ConfigurationType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(10000)")
|
||||
.HasMaxLength(10000);
|
||||
|
||||
b.Property<bool?>("Online")
|
||||
.IsRequired()
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Path")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Instances");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint")
|
||||
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
|
||||
b.Property<decimal>("ByondRights")
|
||||
.HasColumnType("numeric(20,0)");
|
||||
|
||||
b.Property<decimal>("ChatBotRights")
|
||||
.HasColumnType("numeric(20,0)");
|
||||
|
||||
b.Property<decimal>("ConfigurationRights")
|
||||
.HasColumnType("numeric(20,0)");
|
||||
|
||||
b.Property<decimal>("DreamDaemonRights")
|
||||
.HasColumnType("numeric(20,0)");
|
||||
|
||||
b.Property<decimal>("DreamMakerRights")
|
||||
.HasColumnType("numeric(20,0)");
|
||||
|
||||
b.Property<long>("InstanceId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<decimal>("InstanceUserRights")
|
||||
.HasColumnType("numeric(20,0)");
|
||||
|
||||
b.Property<decimal>("RepositoryRights")
|
||||
.HasColumnType("numeric(20,0)");
|
||||
|
||||
b.Property<long>("UserId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InstanceId");
|
||||
|
||||
b.HasIndex("UserId", "InstanceId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("InstanceUsers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint")
|
||||
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
|
||||
b.Property<decimal?>("CancelRight")
|
||||
.HasColumnType("numeric(20,0)");
|
||||
|
||||
b.Property<decimal?>("CancelRightsType")
|
||||
.HasColumnType("numeric(20,0)");
|
||||
|
||||
b.Property<bool?>("Cancelled")
|
||||
.IsRequired()
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<long?>("CancelledById")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<long?>("ErrorCode")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("ExceptionDetails")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<long>("InstanceId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTimeOffset?>("StartedAt")
|
||||
.IsRequired()
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<long>("StartedById")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTimeOffset?>("StoppedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CancelledById");
|
||||
|
||||
b.HasIndex("InstanceId");
|
||||
|
||||
b.HasIndex("StartedById");
|
||||
|
||||
b.ToTable("Jobs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint")
|
||||
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
|
||||
b.Property<string>("ExternalUserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasMaxLength(100);
|
||||
|
||||
b.Property<int>("Provider")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<long?>("UserId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.HasIndex("Provider", "ExternalUserId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("OAuthConnections");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint")
|
||||
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
|
||||
b.Property<string>("AccessIdentifier")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<long>("CompileJobId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<int>("LaunchSecurityLevel")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Port")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ProcessId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("RebootState")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CompileJobId");
|
||||
|
||||
b.ToTable("ReattachInformations");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint")
|
||||
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
|
||||
b.Property<string>("AccessToken")
|
||||
.HasColumnType("character varying(10000)")
|
||||
.HasMaxLength(10000);
|
||||
|
||||
b.Property<string>("AccessUser")
|
||||
.HasColumnType("character varying(10000)")
|
||||
.HasMaxLength(10000);
|
||||
|
||||
b.Property<bool?>("AutoUpdatesKeepTestMerges")
|
||||
.IsRequired()
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool?>("AutoUpdatesSynchronize")
|
||||
.IsRequired()
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("CommitterEmail")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(10000)")
|
||||
.HasMaxLength(10000);
|
||||
|
||||
b.Property<string>("CommitterName")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(10000)")
|
||||
.HasMaxLength(10000);
|
||||
|
||||
b.Property<bool?>("CreateGitHubDeployments")
|
||||
.IsRequired()
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<long>("InstanceId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<bool?>("PostTestMergeComment")
|
||||
.IsRequired()
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool?>("PushTestMergeCommits")
|
||||
.IsRequired()
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool?>("ShowTestMergeCommitters")
|
||||
.IsRequired()
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InstanceId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("RepositorySettings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint")
|
||||
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
|
||||
b.Property<long>("RevisionInformationId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long>("TestMergeId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RevisionInformationId");
|
||||
|
||||
b.HasIndex("TestMergeId");
|
||||
|
||||
b.ToTable("RevInfoTestMerges");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint")
|
||||
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
|
||||
b.Property<string>("CommitSha")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(40)")
|
||||
.HasMaxLength(40);
|
||||
|
||||
b.Property<long>("InstanceId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("OriginCommitSha")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(40)")
|
||||
.HasMaxLength(40);
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InstanceId", "CommitSha")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("RevisionInformations");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint")
|
||||
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
|
||||
b.Property<string>("Author")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("BodyAtMerge")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Comment")
|
||||
.HasColumnType("character varying(10000)")
|
||||
.HasMaxLength(10000);
|
||||
|
||||
b.Property<DateTimeOffset>("MergedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<long>("MergedById")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<int>("Number")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<long?>("PrimaryRevisionInformationId")
|
||||
.IsRequired()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("PullRequestRevision")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(40)")
|
||||
.HasMaxLength(40);
|
||||
|
||||
b.Property<string>("TitleAtMerge")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Url")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("MergedById");
|
||||
|
||||
b.HasIndex("PrimaryRevisionInformationId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("TestMerges");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.User", b =>
|
||||
{
|
||||
b.Property<long?>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint")
|
||||
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
|
||||
b.Property<decimal>("AdministrationRights")
|
||||
.HasColumnType("numeric(20,0)");
|
||||
|
||||
b.Property<string>("CanonicalName")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTimeOffset?>("CreatedAt")
|
||||
.IsRequired()
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<long?>("CreatedById")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<bool?>("Enabled")
|
||||
.IsRequired()
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<decimal>("InstanceManagerRights")
|
||||
.HasColumnType("numeric(20,0)");
|
||||
|
||||
b.Property<DateTimeOffset?>("LastPasswordUpdate")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(10000)")
|
||||
.HasMaxLength(10000);
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("SystemIdentifier")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CanonicalName")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("CreatedById");
|
||||
|
||||
b.HasIndex("SystemIdentifier")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance")
|
||||
.WithMany("ChatSettings")
|
||||
.HasForeignKey("InstanceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.ChatBot", "ChatSettings")
|
||||
.WithMany("Channels")
|
||||
.HasForeignKey("ChatSettingsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.Job", "Job")
|
||||
.WithOne()
|
||||
.HasForeignKey("Tgstation.Server.Host.Models.CompileJob", "JobId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation")
|
||||
.WithMany("CompileJobs")
|
||||
.HasForeignKey("RevisionInformationId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance")
|
||||
.WithOne("DreamDaemonSettings")
|
||||
.HasForeignKey("Tgstation.Server.Host.Models.DreamDaemonSettings", "InstanceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance")
|
||||
.WithOne("DreamMakerSettings")
|
||||
.HasForeignKey("Tgstation.Server.Host.Models.DreamMakerSettings", "InstanceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance")
|
||||
.WithMany("InstanceUsers")
|
||||
.HasForeignKey("InstanceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Tgstation.Server.Host.Models.User", null)
|
||||
.WithMany("InstanceUsers")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.User", "CancelledBy")
|
||||
.WithMany()
|
||||
.HasForeignKey("CancelledById");
|
||||
|
||||
b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance")
|
||||
.WithMany("Jobs")
|
||||
.HasForeignKey("InstanceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Tgstation.Server.Host.Models.User", "StartedBy")
|
||||
.WithMany()
|
||||
.HasForeignKey("StartedById")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.User", "User")
|
||||
.WithMany("OAuthConnections")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob")
|
||||
.WithMany()
|
||||
.HasForeignKey("CompileJobId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance")
|
||||
.WithOne("RepositorySettings")
|
||||
.HasForeignKey("Tgstation.Server.Host.Models.RepositorySettings", "InstanceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation")
|
||||
.WithMany("ActiveTestMerges")
|
||||
.HasForeignKey("RevisionInformationId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Tgstation.Server.Host.Models.TestMerge", "TestMerge")
|
||||
.WithMany("RevisonInformations")
|
||||
.HasForeignKey("TestMergeId")
|
||||
.OnDelete(DeleteBehavior.ClientNoAction)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance")
|
||||
.WithMany("RevisionInformations")
|
||||
.HasForeignKey("InstanceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.User", "MergedBy")
|
||||
.WithMany("TestMerges")
|
||||
.HasForeignKey("MergedById")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "PrimaryRevisionInformation")
|
||||
.WithOne("PrimaryTestMerge")
|
||||
.HasForeignKey("Tgstation.Server.Host.Models.TestMerge", "PrimaryRevisionInformationId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.User", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.User", "CreatedBy")
|
||||
.WithMany("CreatedUsers")
|
||||
.HasForeignKey("CreatedById");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using System;
|
||||
|
||||
namespace Tgstation.Server.Host.Database.Migrations
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds the OAuthConnections table for PostgresSQL.
|
||||
/// </summary>
|
||||
public partial class PGAddOAuthConnections : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
if (migrationBuilder == null)
|
||||
throw new ArgumentNullException(nameof(migrationBuilder));
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "OAuthConnections",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
Provider = table.Column<int>(nullable: false),
|
||||
ExternalUserId = table.Column<string>(nullable: false, maxLength: 100),
|
||||
UserId = table.Column<long>(nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_OAuthConnections", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_OAuthConnections_Users_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_OAuthConnections_UserId",
|
||||
table: "OAuthConnections",
|
||||
column: "UserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_OAuthConnections_Provider_ExternalUserId",
|
||||
table: "OAuthConnections",
|
||||
columns: new[] { "Provider", "ExternalUserId" },
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
if (migrationBuilder == null)
|
||||
throw new ArgumentNullException(nameof(migrationBuilder));
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "OAuthConnections");
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+810
@@ -0,0 +1,810 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace Tgstation.Server.Host.Database.Migrations
|
||||
{
|
||||
[DbContext(typeof(SqliteDatabaseContext))]
|
||||
[Migration("20201122231546_SLAddOAuthConnections")]
|
||||
partial class SLAddOAuthConnections
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "3.1.10");
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<ushort?>("ChannelLimit")
|
||||
.IsRequired()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ConnectionString")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasMaxLength(10000);
|
||||
|
||||
b.Property<bool?>("Enabled")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("InstanceId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasMaxLength(100);
|
||||
|
||||
b.Property<int>("Provider")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<uint?>("ReconnectionInterval")
|
||||
.IsRequired()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InstanceId", "Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("ChatBots");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("ChatSettingsId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<ulong?>("DiscordChannelId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("IrcChannel")
|
||||
.HasColumnType("TEXT")
|
||||
.HasMaxLength(100);
|
||||
|
||||
b.Property<bool?>("IsAdminChannel")
|
||||
.IsRequired()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool?>("IsUpdatesChannel")
|
||||
.IsRequired()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool?>("IsWatchdogChannel")
|
||||
.IsRequired()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Tag")
|
||||
.HasColumnType("TEXT")
|
||||
.HasMaxLength(10000);
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ChatSettingsId", "DiscordChannelId")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("ChatSettingsId", "IrcChannel")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("ChatChannels");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ByondVersion")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("DMApiMajorVersion")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("DMApiMinorVersion")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("DMApiPatchVersion")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid?>("DirectoryName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DmeName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("GitHubDeploymentId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long?>("GitHubRepoId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("JobId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("MinimumSecurityLevel")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Output")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("RevisionInformationId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DirectoryName");
|
||||
|
||||
b.HasIndex("JobId")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("RevisionInformationId");
|
||||
|
||||
b.ToTable("CompileJobs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AdditionalParameters")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasMaxLength(10000);
|
||||
|
||||
b.Property<bool?>("AllowWebClient")
|
||||
.IsRequired()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool?>("AutoStart")
|
||||
.IsRequired()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<uint?>("HeartbeatSeconds")
|
||||
.IsRequired()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("InstanceId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<ushort?>("Port")
|
||||
.IsRequired()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SecurityLevel")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<uint?>("StartupTimeout")
|
||||
.IsRequired()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<uint?>("TopicRequestTimeout")
|
||||
.IsRequired()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InstanceId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("DreamDaemonSettings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<ushort?>("ApiValidationPort")
|
||||
.IsRequired()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ApiValidationSecurityLevel")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("InstanceId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ProjectName")
|
||||
.HasColumnType("TEXT")
|
||||
.HasMaxLength(10000);
|
||||
|
||||
b.Property<bool?>("RequireDMApiValidation")
|
||||
.IsRequired()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InstanceId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("DreamMakerSettings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<uint?>("AutoUpdateInterval")
|
||||
.IsRequired()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<ushort?>("ChatBotLimit")
|
||||
.IsRequired()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ConfigurationType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasMaxLength(10000);
|
||||
|
||||
b.Property<bool?>("Online")
|
||||
.IsRequired()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Path")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Instances");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<ulong>("ByondRights")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<ulong>("ChatBotRights")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<ulong>("ConfigurationRights")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<ulong>("DreamDaemonRights")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<ulong>("DreamMakerRights")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("InstanceId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<ulong>("InstanceUserRights")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<ulong>("RepositoryRights")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("UserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InstanceId");
|
||||
|
||||
b.HasIndex("UserId", "InstanceId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("InstanceUsers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<ulong?>("CancelRight")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<ulong?>("CancelRightsType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool?>("Cancelled")
|
||||
.IsRequired()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long?>("CancelledById")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<uint?>("ErrorCode")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ExceptionDetails")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("InstanceId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTimeOffset?>("StartedAt")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("StartedById")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTimeOffset?>("StoppedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CancelledById");
|
||||
|
||||
b.HasIndex("InstanceId");
|
||||
|
||||
b.HasIndex("StartedById");
|
||||
|
||||
b.ToTable("Jobs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ExternalUserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasMaxLength(100);
|
||||
|
||||
b.Property<int>("Provider")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long?>("UserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.HasIndex("Provider", "ExternalUserId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("OAuthConnections");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AccessIdentifier")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("CompileJobId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("LaunchSecurityLevel")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<ushort>("Port")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ProcessId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("RebootState")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CompileJobId");
|
||||
|
||||
b.ToTable("ReattachInformations");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AccessToken")
|
||||
.HasColumnType("TEXT")
|
||||
.HasMaxLength(10000);
|
||||
|
||||
b.Property<string>("AccessUser")
|
||||
.HasColumnType("TEXT")
|
||||
.HasMaxLength(10000);
|
||||
|
||||
b.Property<bool?>("AutoUpdatesKeepTestMerges")
|
||||
.IsRequired()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool?>("AutoUpdatesSynchronize")
|
||||
.IsRequired()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("CommitterEmail")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasMaxLength(10000);
|
||||
|
||||
b.Property<string>("CommitterName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasMaxLength(10000);
|
||||
|
||||
b.Property<bool?>("CreateGitHubDeployments")
|
||||
.IsRequired()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("InstanceId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool?>("PostTestMergeComment")
|
||||
.IsRequired()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool?>("PushTestMergeCommits")
|
||||
.IsRequired()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool?>("ShowTestMergeCommitters")
|
||||
.IsRequired()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InstanceId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("RepositorySettings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("RevisionInformationId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("TestMergeId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RevisionInformationId");
|
||||
|
||||
b.HasIndex("TestMergeId");
|
||||
|
||||
b.ToTable("RevInfoTestMerges");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("CommitSha")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasMaxLength(40);
|
||||
|
||||
b.Property<long>("InstanceId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("OriginCommitSha")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasMaxLength(40);
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InstanceId", "CommitSha")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("RevisionInformations");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Author")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("BodyAtMerge")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Comment")
|
||||
.HasColumnType("TEXT")
|
||||
.HasMaxLength(10000);
|
||||
|
||||
b.Property<DateTimeOffset>("MergedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("MergedById")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Number")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long?>("PrimaryRevisionInformationId")
|
||||
.IsRequired()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("PullRequestRevision")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasMaxLength(40);
|
||||
|
||||
b.Property<string>("TitleAtMerge")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Url")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("MergedById");
|
||||
|
||||
b.HasIndex("PrimaryRevisionInformationId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("TestMerges");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.User", b =>
|
||||
{
|
||||
b.Property<long?>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<ulong>("AdministrationRights")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("CanonicalName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset?>("CreatedAt")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long?>("CreatedById")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool?>("Enabled")
|
||||
.IsRequired()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<ulong>("InstanceManagerRights")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTimeOffset?>("LastPasswordUpdate")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasMaxLength(10000);
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SystemIdentifier")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CanonicalName")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("CreatedById");
|
||||
|
||||
b.HasIndex("SystemIdentifier")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance")
|
||||
.WithMany("ChatSettings")
|
||||
.HasForeignKey("InstanceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.ChatBot", "ChatSettings")
|
||||
.WithMany("Channels")
|
||||
.HasForeignKey("ChatSettingsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.Job", "Job")
|
||||
.WithOne()
|
||||
.HasForeignKey("Tgstation.Server.Host.Models.CompileJob", "JobId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation")
|
||||
.WithMany("CompileJobs")
|
||||
.HasForeignKey("RevisionInformationId")
|
||||
.OnDelete(DeleteBehavior.ClientNoAction)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance")
|
||||
.WithOne("DreamDaemonSettings")
|
||||
.HasForeignKey("Tgstation.Server.Host.Models.DreamDaemonSettings", "InstanceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance")
|
||||
.WithOne("DreamMakerSettings")
|
||||
.HasForeignKey("Tgstation.Server.Host.Models.DreamMakerSettings", "InstanceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance")
|
||||
.WithMany("InstanceUsers")
|
||||
.HasForeignKey("InstanceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Tgstation.Server.Host.Models.User", null)
|
||||
.WithMany("InstanceUsers")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.User", "CancelledBy")
|
||||
.WithMany()
|
||||
.HasForeignKey("CancelledById");
|
||||
|
||||
b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance")
|
||||
.WithMany("Jobs")
|
||||
.HasForeignKey("InstanceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Tgstation.Server.Host.Models.User", "StartedBy")
|
||||
.WithMany()
|
||||
.HasForeignKey("StartedById")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.User", "User")
|
||||
.WithMany("OAuthConnections")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob")
|
||||
.WithMany()
|
||||
.HasForeignKey("CompileJobId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance")
|
||||
.WithOne("RepositorySettings")
|
||||
.HasForeignKey("Tgstation.Server.Host.Models.RepositorySettings", "InstanceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation")
|
||||
.WithMany("ActiveTestMerges")
|
||||
.HasForeignKey("RevisionInformationId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Tgstation.Server.Host.Models.TestMerge", "TestMerge")
|
||||
.WithMany("RevisonInformations")
|
||||
.HasForeignKey("TestMergeId")
|
||||
.OnDelete(DeleteBehavior.ClientNoAction)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance")
|
||||
.WithMany("RevisionInformations")
|
||||
.HasForeignKey("InstanceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.User", "MergedBy")
|
||||
.WithMany("TestMerges")
|
||||
.HasForeignKey("MergedById")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "PrimaryRevisionInformation")
|
||||
.WithOne("PrimaryTestMerge")
|
||||
.HasForeignKey("Tgstation.Server.Host.Models.TestMerge", "PrimaryRevisionInformationId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.User", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.User", "CreatedBy")
|
||||
.WithMany("CreatedUsers")
|
||||
.HasForeignKey("CreatedById");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using System;
|
||||
|
||||
namespace Tgstation.Server.Host.Database.Migrations
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds the OAuthConnections table for SQLite.
|
||||
/// </summary>
|
||||
public partial class SLAddOAuthConnections : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
if (migrationBuilder == null)
|
||||
throw new ArgumentNullException(nameof(migrationBuilder));
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "OAuthConnections",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Provider = table.Column<int>(nullable: false),
|
||||
ExternalUserId = table.Column<string>(nullable: false, maxLength: 100),
|
||||
UserId = table.Column<long>(nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_OAuthConnections", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_OAuthConnections_Users_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_OAuthConnections_UserId",
|
||||
table: "OAuthConnections",
|
||||
column: "UserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_OAuthConnections_Provider_ExternalUserId",
|
||||
table: "OAuthConnections",
|
||||
columns: new[] { "Provider", "ExternalUserId" },
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
if (migrationBuilder == null)
|
||||
throw new ArgumentNullException(nameof(migrationBuilder));
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "OAuthConnections");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ namespace Tgstation.Server.Host.Database.Migrations
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "3.1.7")
|
||||
.HasAnnotation("ProductVersion", "3.1.10")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 64);
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b =>
|
||||
@@ -373,6 +373,33 @@ namespace Tgstation.Server.Host.Database.Migrations
|
||||
b.ToTable("Jobs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("ExternalUserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("varchar(100) CHARACTER SET utf8mb4")
|
||||
.HasMaxLength(100);
|
||||
|
||||
b.Property<int>("Provider")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<long?>("UserId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.HasIndex("Provider", "ExternalUserId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("OAuthConnections");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
@@ -705,6 +732,14 @@ namespace Tgstation.Server.Host.Database.Migrations
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.User", "User")
|
||||
.WithMany("OAuthConnections")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob")
|
||||
|
||||
+37
-1
@@ -15,7 +15,7 @@ namespace Tgstation.Server.Host.Database.Migrations
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn)
|
||||
.HasAnnotation("ProductVersion", "3.1.7")
|
||||
.HasAnnotation("ProductVersion", "3.1.10")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b =>
|
||||
@@ -374,6 +374,34 @@ namespace Tgstation.Server.Host.Database.Migrations
|
||||
b.ToTable("Jobs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint")
|
||||
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
|
||||
b.Property<string>("ExternalUserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasMaxLength(100);
|
||||
|
||||
b.Property<int>("Provider")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<long?>("UserId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.HasIndex("Provider", "ExternalUserId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("OAuthConnections");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
@@ -712,6 +740,14 @@ namespace Tgstation.Server.Host.Database.Migrations
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.User", "User")
|
||||
.WithMany("OAuthConnections")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob")
|
||||
|
||||
+37
-1
@@ -14,7 +14,7 @@ namespace Tgstation.Server.Host.Database.Migrations
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "3.1.7")
|
||||
.HasAnnotation("ProductVersion", "3.1.10")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128)
|
||||
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
|
||||
|
||||
@@ -376,6 +376,34 @@ namespace Tgstation.Server.Host.Database.Migrations
|
||||
b.ToTable("Jobs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint")
|
||||
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
|
||||
|
||||
b.Property<string>("ExternalUserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(100)")
|
||||
.HasMaxLength(100);
|
||||
|
||||
b.Property<int>("Provider")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<long?>("UserId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.HasIndex("Provider", "ExternalUserId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("OAuthConnections");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
@@ -715,6 +743,14 @@ namespace Tgstation.Server.Host.Database.Migrations
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.User", "User")
|
||||
.WithMany("OAuthConnections")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob")
|
||||
|
||||
+36
-1
@@ -13,7 +13,7 @@ namespace Tgstation.Server.Host.Database.Migrations
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "3.1.7");
|
||||
.HasAnnotation("ProductVersion", "3.1.10");
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b =>
|
||||
{
|
||||
@@ -372,6 +372,33 @@ namespace Tgstation.Server.Host.Database.Migrations
|
||||
b.ToTable("Jobs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ExternalUserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasMaxLength(100);
|
||||
|
||||
b.Property<int>("Provider")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long?>("UserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.HasIndex("Provider", "ExternalUserId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("OAuthConnections");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
@@ -704,6 +731,14 @@ namespace Tgstation.Server.Host.Database.Migrations
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.User", "User")
|
||||
.WithMany("OAuthConnections")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b =>
|
||||
{
|
||||
b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob")
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
namespace Tgstation.Server.Host.Models
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public sealed class OAuthConnection : Api.Models.OAuthConnection
|
||||
{
|
||||
/// <summary>
|
||||
/// The row Id.
|
||||
/// </summary>
|
||||
public long Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The owning <see cref="Models.User"/>.
|
||||
/// </summary>
|
||||
public User User { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Convert the <see cref="OAuthConnection"/> to it's API form.
|
||||
/// </summary>
|
||||
/// <returns>A new <see cref="Api.Models.OAuthConnection"/>.</returns>
|
||||
public Api.Models.OAuthConnection ToApi() => new Api.Models.OAuthConnection
|
||||
{
|
||||
Provider = Provider,
|
||||
ExternalUserId = ExternalUserId
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
@@ -48,6 +48,11 @@ namespace Tgstation.Server.Host.Models
|
||||
/// </summary>
|
||||
public ICollection<TestMerge> TestMerges { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="TestMerge"/>s made by the <see cref="User"/>
|
||||
/// </summary>
|
||||
public ICollection<OAuthConnection> OAuthConnections { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Change a <see cref="Api.Models.Internal.User.Name"/> into a <see cref="CanonicalName"/>.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Tgstation.Server.Api.Models;
|
||||
@@ -19,8 +19,9 @@ namespace Tgstation.Server.Host.Security
|
||||
/// Create a <see cref="Token"/> 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.Internal.User.Id"/> field available</param>
|
||||
/// <param name="oAuth">Whether or not this is an OAuth login.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in a new <see cref="Token"/></returns>
|
||||
Task<Token> CreateToken(Models.User user, CancellationToken cancellationToken);
|
||||
Task<Token> CreateToken(Models.User user, bool oAuth, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Mime;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Tgstation.Server.Api.Models;
|
||||
using Tgstation.Server.Host.Configuration;
|
||||
using Tgstation.Server.Host.System;
|
||||
|
||||
namespace Tgstation.Server.Host.Security.OAuth
|
||||
{
|
||||
/// <summary>
|
||||
/// Base <see langword="class"/> for <see cref="IOAuthValidator"/>s.
|
||||
/// </summary>
|
||||
abstract class BaseOAuthValidator : IOAuthValidator
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public abstract OAuthProvider Provider { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ILogger"/> for the <see cref="BaseOAuthValidator"/>.
|
||||
/// </summary>
|
||||
protected ILogger<BaseOAuthValidator> Logger { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="OAuthConfiguration"/> for the <see cref="BaseOAuthValidator"/>.
|
||||
/// </summary>
|
||||
protected OAuthConfiguration OAuthConfiguration { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IHttpClientFactory"/> for the <see cref="BaseOAuthValidator"/>.
|
||||
/// </summary>
|
||||
readonly IHttpClientFactory httpClientFactory;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IAssemblyInformationProvider"/> for the <see cref="BaseOAuthValidator"/>.
|
||||
/// </summary>
|
||||
readonly IAssemblyInformationProvider assemblyInformationProvider;
|
||||
|
||||
/// <summary>
|
||||
/// Gets <see cref="JsonSerializerSettings"/> that should be used.
|
||||
/// </summary>
|
||||
/// <returns>A new <see cref="JsonSerializerSettings"/> <see cref="object"/>.</returns>
|
||||
protected static JsonSerializerSettings SerializerSettings() => new JsonSerializerSettings
|
||||
{
|
||||
ContractResolver = new DefaultContractResolver
|
||||
{
|
||||
NamingStrategy = new SnakeCaseNamingStrategy()
|
||||
}
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GenericOAuthValidator"/> <see langword="class"/>.
|
||||
/// </summary>
|
||||
/// <param name="httpClientFactory">The value of <see cref="httpClientFactory"/>.</param>
|
||||
/// <param name="assemblyInformationProvider">The value of <see cref="assemblyInformationProvider"/>.</param>
|
||||
/// <param name="logger">The value of <see cref="Logger"/>.</param>
|
||||
/// <param name="oAuthConfiguration">The value of <see cref="OAuthConfiguration"/>.</param>
|
||||
public BaseOAuthValidator(
|
||||
IHttpClientFactory httpClientFactory,
|
||||
IAssemblyInformationProvider assemblyInformationProvider,
|
||||
ILogger<BaseOAuthValidator> logger,
|
||||
OAuthConfiguration oAuthConfiguration)
|
||||
{
|
||||
this.httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory));
|
||||
this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider));
|
||||
Logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
OAuthConfiguration = oAuthConfiguration ?? throw new ArgumentNullException(nameof(oAuthConfiguration));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract Task<string> GetClientId(CancellationToken cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract Task<string> ValidateResponseCode(string code, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Create a new configured <see cref="HttpClient"/>.
|
||||
/// </summary>
|
||||
/// <returns>A new configured <see cref="HttpClient"/>.</returns>
|
||||
protected HttpClient CreateHttpClient()
|
||||
{
|
||||
var httpClient = httpClientFactory.CreateClient();
|
||||
try
|
||||
{
|
||||
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json));
|
||||
httpClient.DefaultRequestHeaders.UserAgent.Add(assemblyInformationProvider.ProductInfoHeaderValue);
|
||||
return httpClient;
|
||||
}
|
||||
catch
|
||||
{
|
||||
httpClient.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using Tgstation.Server.Api.Models;
|
||||
using Tgstation.Server.Host.Configuration;
|
||||
using Tgstation.Server.Host.System;
|
||||
|
||||
namespace Tgstation.Server.Host.Security.OAuth
|
||||
{
|
||||
/// <summary>
|
||||
/// OAuth validator for Discord.
|
||||
/// </summary>
|
||||
sealed class DiscordOAuthValidator : GenericOAuthValidator
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DiscordOAuthValidator"/> <see langword="class"/>.
|
||||
/// </summary>
|
||||
/// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/> for the <see cref="GenericOAuthValidator"/>.</param>
|
||||
/// <param name="assemblyInformationProvider">The <see cref="IAssemblyInformationProvider"/> 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 DiscordOAuthValidator(
|
||||
IHttpClientFactory httpClientFactory,
|
||||
IAssemblyInformationProvider assemblyInformationProvider,
|
||||
ILogger<DiscordOAuthValidator> logger,
|
||||
OAuthConfiguration oAuthConfiguration)
|
||||
: base(httpClientFactory, assemblyInformationProvider, logger, oAuthConfiguration)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override OAuthProvider Provider => OAuthProvider.Discord;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Uri TokenUrl => new Uri("https://discord.com/api/oauth2/token");
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Uri UserInformationUrl => new Uri("https://discord.com/api/users/@me");
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override OAuthTokenRequest CreateTokenRequest(string code) => new DiscordTokenRequest(OAuthConfiguration, code);
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string DecodeTokenPayload(dynamic responseJson) => responseJson.access_token;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string DecodeUserInformationPayload(dynamic responseJson) => responseJson.id;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using Tgstation.Server.Host.Configuration;
|
||||
|
||||
namespace Tgstation.Server.Host.Security.OAuth
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="OAuthTokenRequest"/> for Discord.
|
||||
/// </summary>
|
||||
/// <remarks>See https://discord.com/developers/docs/topics/oauth2</remarks>
|
||||
sealed class DiscordTokenRequest : OAuthTokenRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// The 'grant_type' field.
|
||||
/// </summary>
|
||||
public string GrantType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The 'scope' field.
|
||||
/// </summary>
|
||||
public string Scope { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DiscordTokenRequest"/> <see langword="class"/>.
|
||||
/// </summary>
|
||||
/// <param name="oAuthConfiguration">The <see cref="OAuthConfiguration"/> for the <see cref="OAuthTokenRequest"/>.</param>
|
||||
/// <param name="code">The OAuth code for the <see cref="OAuthTokenRequest"/>.</param>
|
||||
public DiscordTokenRequest(OAuthConfiguration oAuthConfiguration, string code)
|
||||
: base(oAuthConfiguration, code)
|
||||
{
|
||||
GrantType = "authorization_code";
|
||||
Scope = "identify";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Tgstation.Server.Api;
|
||||
using Tgstation.Server.Host.Configuration;
|
||||
using Tgstation.Server.Host.System;
|
||||
|
||||
namespace Tgstation.Server.Host.Security.OAuth
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="IOAuthValidator"/> for generic OAuth2 endpoints.
|
||||
/// </summary>
|
||||
abstract class GenericOAuthValidator : BaseOAuthValidator
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="Uri"/> to <see cref="HttpMethod.Post"/> to to get the access token.
|
||||
/// </summary>
|
||||
protected abstract Uri TokenUrl { get; }
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="Uri"/> to <see cref="HttpMethod.Get"/> the user information payload from.
|
||||
/// </summary>
|
||||
protected abstract Uri UserInformationUrl { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GenericOAuthValidator"/> <see langword="class"/>.
|
||||
/// </summary>
|
||||
/// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/> for the <see cref="BaseOAuthValidator"/></param>
|
||||
/// <param name="assemblyInformationProvider">The <see cref="IAssemblyInformationProvider"/> for the <see cref="BaseOAuthValidator"/></param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="BaseOAuthValidator"/></param>
|
||||
/// <param name="oAuthConfiguration">The <see cref="OAuthConfiguration"/> for the <see cref="BaseOAuthValidator"/>.</param>
|
||||
public GenericOAuthValidator(
|
||||
IHttpClientFactory httpClientFactory,
|
||||
IAssemblyInformationProvider assemblyInformationProvider,
|
||||
ILogger<GenericOAuthValidator> logger,
|
||||
OAuthConfiguration oAuthConfiguration)
|
||||
: base(
|
||||
httpClientFactory,
|
||||
assemblyInformationProvider,
|
||||
logger,
|
||||
oAuthConfiguration)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decode the token payload <paramref name="responseJson"/>.
|
||||
/// </summary>
|
||||
/// <param name="responseJson">The token payload <see cref="JObject"/>.</param>
|
||||
/// <returns>The OAuth2 bearer access token on success, <see langword="null"/> otherwise.</returns>
|
||||
protected abstract string DecodeTokenPayload(dynamic responseJson);
|
||||
|
||||
/// <summary>
|
||||
/// Decode the user information payload <paramref name="responseJson"/>.
|
||||
/// </summary>
|
||||
/// <param name="responseJson">The user information payload <see cref="JObject"/>.</param>
|
||||
/// <returns>The user ID on success, <see langword="null"/> otherwise.</returns>
|
||||
protected abstract string DecodeUserInformationPayload(dynamic responseJson);
|
||||
|
||||
/// <summary>
|
||||
/// Create the <see cref="OAuthTokenRequest"/> for a given <paramref name="code"/>
|
||||
/// </summary>
|
||||
/// <param name="code">The OAuth code from the browser.</param>
|
||||
/// <returns>The <see cref="OAuthTokenRequest"/> to send to <see cref="TokenUrl"/>.</returns>
|
||||
protected abstract OAuthTokenRequest CreateTokenRequest(string code);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<string> ValidateResponseCode(string code, CancellationToken cancellationToken)
|
||||
{
|
||||
using var httpClient = CreateHttpClient();
|
||||
try
|
||||
{
|
||||
Logger.LogTrace("Validating response code...");
|
||||
using var tokenRequest = new HttpRequestMessage(HttpMethod.Post, TokenUrl);
|
||||
|
||||
var tokenRequestPayload = CreateTokenRequest(code);
|
||||
|
||||
// roundabout but it works
|
||||
var tokenRequestJson = JsonConvert.SerializeObject(
|
||||
tokenRequestPayload,
|
||||
SerializerSettings());
|
||||
|
||||
var tokenRequestDictionary = JsonConvert.DeserializeObject<Dictionary<string, string>>(tokenRequestJson);
|
||||
tokenRequest.Content = new FormUrlEncodedContent(tokenRequestDictionary);
|
||||
|
||||
var tokenResponse = await httpClient.SendAsync(tokenRequest, cancellationToken).ConfigureAwait(false);
|
||||
tokenResponse.EnsureSuccessStatusCode();
|
||||
var tokenResponsePayload = await tokenResponse.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
var tokenResponseJson = JObject.Parse(tokenResponsePayload);
|
||||
|
||||
var accessToken = DecodeTokenPayload(tokenResponseJson);
|
||||
if (accessToken == null)
|
||||
return null;
|
||||
|
||||
Logger.LogTrace("Getting user details...");
|
||||
using var userInformationRequest = new HttpRequestMessage(HttpMethod.Get, UserInformationUrl);
|
||||
userInformationRequest.Headers.Authorization = new AuthenticationHeaderValue(
|
||||
ApiHeaders.BearerAuthenticationScheme,
|
||||
accessToken);
|
||||
|
||||
var userInformationResponse = await httpClient.SendAsync(userInformationRequest, cancellationToken).ConfigureAwait(false);
|
||||
userInformationResponse.EnsureSuccessStatusCode();
|
||||
|
||||
var userInformationPayload = await userInformationResponse.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
var userInformationJson = JObject.Parse(userInformationPayload);
|
||||
|
||||
return DecodeUserInformationPayload(userInformationJson);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Error while completing OAuth handshake!");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<string> GetClientId(CancellationToken cancellationToken) => Task.FromResult(OAuthConfiguration.ClientId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Octokit;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Tgstation.Server.Api.Models;
|
||||
using Tgstation.Server.Host.Configuration;
|
||||
using Tgstation.Server.Host.Core;
|
||||
|
||||
namespace Tgstation.Server.Host.Security.OAuth
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="IOAuthValidator"/> for GitHub.
|
||||
/// </summary>
|
||||
sealed class GitHubOAuthValidator : IOAuthValidator
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public OAuthProvider Provider => OAuthProvider.GitHub;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IGitHubClientFactory"/> for the <see cref="GitHubOAuthValidator"/>.
|
||||
/// </summary>
|
||||
readonly IGitHubClientFactory gitHubClientFactory;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ILogger"/> for the <see cref="GitHubOAuthValidator"/>.
|
||||
/// </summary>
|
||||
readonly ILogger<GitHubOAuthValidator> logger;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="OAuthConfiguration"/> for the <see cref="GitHubOAuthValidator"/>.
|
||||
/// </summary>
|
||||
readonly OAuthConfiguration oAuthConfiguration;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GitHubOAuthValidator"/> <see langword="class"/>.
|
||||
/// </summary>
|
||||
/// <param name="gitHubClientFactory">The value of <see cref="gitHubClientFactory"/>.</param>
|
||||
/// <param name="logger">The value of <see cref="logger"/>.</param>
|
||||
/// <param name="oAuthConfiguration">The value of <see cref="oAuthConfiguration"/>.</param>
|
||||
public GitHubOAuthValidator(
|
||||
IGitHubClientFactory gitHubClientFactory,
|
||||
ILogger<GitHubOAuthValidator> logger,
|
||||
OAuthConfiguration oAuthConfiguration)
|
||||
{
|
||||
this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory));
|
||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
this.oAuthConfiguration = oAuthConfiguration ?? throw new ArgumentNullException(nameof(oAuthConfiguration));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<string> ValidateResponseCode(string code, CancellationToken cancellationToken)
|
||||
{
|
||||
if (code == null)
|
||||
throw new ArgumentNullException(nameof(code));
|
||||
|
||||
var client = gitHubClientFactory.CreateClient();
|
||||
try
|
||||
{
|
||||
logger.LogTrace("Validating response code...");
|
||||
var response = await client
|
||||
.Oauth
|
||||
.CreateAccessToken(
|
||||
new OauthTokenRequest(
|
||||
oAuthConfiguration.ClientId,
|
||||
oAuthConfiguration.ClientSecret,
|
||||
code))
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var token = response.AccessToken;
|
||||
if (token == null)
|
||||
return null;
|
||||
|
||||
var authenticatedClient = gitHubClientFactory.CreateClient(token);
|
||||
|
||||
logger.LogTrace("Getting user details...");
|
||||
var userDetails = await authenticatedClient
|
||||
.User
|
||||
.Current()
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return userDetails.Id.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
catch (RateLimitExceededException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
logger.LogWarning(ex, "API error while completing OAuth handshake!");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<string> GetClientId(CancellationToken cancellationToken) => Task.FromResult(oAuthConfiguration.ClientId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Tgstation.Server.Api.Models;
|
||||
|
||||
namespace Tgstation.Server.Host.Security.OAuth
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains <see cref="IOAuthValidator"/>s
|
||||
/// </summary>
|
||||
public interface IOAuthProviders
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the <see cref="IOAuthValidator"/> for a given <paramref name="oAuthProvider"/>.
|
||||
/// </summary>
|
||||
/// <param name="oAuthProvider">The <see cref="OAuthProvider"/> to get the validator for.</param>
|
||||
/// <returns>The <see cref="IOAuthValidator"/> for <paramref name="oAuthProvider"/>.</returns>
|
||||
IOAuthValidator GetValidator(OAuthProvider oAuthProvider);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="Dictionary{TKey, TValue}"/> of the provider client IDs.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in a anew <see cref="Dictionary{TKey, TValue}"/> of the active provider client IDs.</returns>
|
||||
Task<Dictionary<OAuthProvider, string>> ClientIds(CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Tgstation.Server.Api.Models;
|
||||
|
||||
namespace Tgstation.Server.Host.Security.OAuth
|
||||
{
|
||||
/// <summary>
|
||||
/// Validates OAuth responses for a given <see cref="Provider"/>.
|
||||
/// </summary>
|
||||
public interface IOAuthValidator
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="OAuthProvider"/> this validator is for.
|
||||
/// </summary>
|
||||
OAuthProvider Provider { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the OAuth client ID of validator.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the client ID of the validator on success, <see langword="null"/> on failure.</returns>
|
||||
Task<string> GetClientId(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Validate a given OAuth response <paramref name="code"/>.
|
||||
/// </summary>
|
||||
/// <param name="code">The OAuth response string from web application.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in <see langword="null"/> if authentication failed, <see cref="global::System.UInt64.MaxValue"/> if a rate limit occurred, and the validated <see cref="OAuthConnection.ExternalUserId"/> otherwise.</returns>
|
||||
Task<string> ValidateResponseCode(string code, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Tgstation.Server.Api.Models;
|
||||
using Tgstation.Server.Host.Configuration;
|
||||
using Tgstation.Server.Host.Core;
|
||||
using Tgstation.Server.Host.System;
|
||||
|
||||
namespace Tgstation.Server.Host.Security.OAuth
|
||||
{
|
||||
/// <inheritdoc />
|
||||
sealed class OAuthProviders : IOAuthProviders
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="IReadOnlyCollection{T}"/> of <see cref="IOAuthValidator"/>s.
|
||||
/// </summary>
|
||||
readonly IReadOnlyCollection<IOAuthValidator> validators;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OAuthProviders"/> <see langword="class"/>.
|
||||
/// </summary>
|
||||
/// <param name="gitHubClientFactory">The <see cref="IGitHubClientFactory"/> to use.</param>
|
||||
/// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/> to use.</param>
|
||||
/// <param name="assemblyInformationProvider">The <see cref="IAssemblyInformationProvider"/> to use.</param>
|
||||
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/> to use.</param>
|
||||
/// <param name="securityConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the <see cref="SecurityConfiguration"/> to use.</param>
|
||||
public OAuthProviders(
|
||||
IGitHubClientFactory gitHubClientFactory,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
IAssemblyInformationProvider assemblyInformationProvider,
|
||||
ILoggerFactory loggerFactory,
|
||||
IOptions<SecurityConfiguration> securityConfigurationOptions)
|
||||
{
|
||||
if (loggerFactory == null)
|
||||
throw new ArgumentNullException(nameof(loggerFactory));
|
||||
|
||||
var securityConfiguration = securityConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(securityConfigurationOptions));
|
||||
|
||||
var validatorsBuilder = new List<IOAuthValidator>();
|
||||
|
||||
if (securityConfiguration.OAuth.TryGetValue(OAuthProvider.GitHub, out var gitHubConfig))
|
||||
validatorsBuilder.Add(
|
||||
new GitHubOAuthValidator(
|
||||
gitHubClientFactory,
|
||||
loggerFactory.CreateLogger<GitHubOAuthValidator>(),
|
||||
gitHubConfig));
|
||||
|
||||
if (securityConfiguration.OAuth.TryGetValue(OAuthProvider.Discord, out var discordConfig))
|
||||
validatorsBuilder.Add(
|
||||
new DiscordOAuthValidator(
|
||||
httpClientFactory,
|
||||
assemblyInformationProvider,
|
||||
loggerFactory.CreateLogger<DiscordOAuthValidator>(),
|
||||
discordConfig));
|
||||
|
||||
if(securityConfiguration.OAuth.TryGetValue(OAuthProvider.TGForums, out var tgConfig))
|
||||
validatorsBuilder.Add(
|
||||
new TGForumsOAuthValidator(
|
||||
httpClientFactory,
|
||||
assemblyInformationProvider,
|
||||
loggerFactory.CreateLogger<TGForumsOAuthValidator>(),
|
||||
tgConfig));
|
||||
|
||||
validators = validatorsBuilder;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IOAuthValidator GetValidator(OAuthProvider oAuthProvider) => validators.FirstOrDefault(x => x.Provider == oAuthProvider);
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Dictionary<OAuthProvider, string>> ClientIds(CancellationToken cancellationToken)
|
||||
{
|
||||
var providersAndTasks = validators.ToDictionary(
|
||||
x => x.Provider,
|
||||
x => x.GetClientId(cancellationToken));
|
||||
|
||||
await Task.WhenAll(providersAndTasks.Values).ConfigureAwait(false);
|
||||
|
||||
return providersAndTasks
|
||||
.Where(x => x.Value.Result != null)
|
||||
.ToDictionary(
|
||||
x => x.Key,
|
||||
x => x.Value.Result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using Tgstation.Server.Host.Configuration;
|
||||
|
||||
namespace Tgstation.Server.Host.Security.OAuth
|
||||
{
|
||||
/// <summary>
|
||||
/// Generic OAuth token request.
|
||||
/// </summary>
|
||||
class OAuthTokenRequest : OAuthConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// The OAuth code.
|
||||
/// </summary>
|
||||
public string Code { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OAuthTokenRequest"/>
|
||||
/// </summary>
|
||||
/// <param name="oAuthConfiguration">The <see cref="OAuthConfiguration"/> to build from.</param>
|
||||
/// <param name="code">The OAuth code received from the browser.</param>
|
||||
public OAuthTokenRequest(OAuthConfiguration oAuthConfiguration, string code)
|
||||
: base(oAuthConfiguration)
|
||||
{
|
||||
Code = code ?? throw new ArgumentNullException(nameof(code));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
namespace Tgstation.Server.Host.Security.OAuth
|
||||
{
|
||||
/// <summary>
|
||||
/// Base <see langword="class"/> for tgstation forum responses.
|
||||
/// </summary>
|
||||
abstract class TGBaseResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Expected value of <see cref="Status"/>.
|
||||
/// </summary>
|
||||
public const string OkStatus = "OK";
|
||||
|
||||
/// <summary>
|
||||
/// The response status.
|
||||
/// </summary>
|
||||
public string Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The response error, if any.
|
||||
/// </summary>
|
||||
public string Error { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace Tgstation.Server.Host.Security.OAuth
|
||||
{
|
||||
/// <summary>
|
||||
/// Response when creating a tgstation forums session.
|
||||
/// </summary>
|
||||
sealed class TGCreateSessionResponse : TGBaseResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// The session's private token. Similar to OAuth authorization response code.
|
||||
/// </summary>
|
||||
public string SessionPrivateToken { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The session's public token. Barely similar to OAuth client ID.
|
||||
/// </summary>
|
||||
public string SessionPublicToken { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
using Tgstation.Server.Api.Models;
|
||||
using Tgstation.Server.Host.Configuration;
|
||||
using Tgstation.Server.Host.System;
|
||||
|
||||
namespace Tgstation.Server.Host.Security.OAuth
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="IOAuthValidator"/> for /tg/ forums.
|
||||
/// </summary>
|
||||
sealed class TGForumsOAuthValidator : BaseOAuthValidator
|
||||
{
|
||||
/// <summary>
|
||||
/// Amount of minutes until unused sessions that were created are forgotten.
|
||||
/// </summary>
|
||||
const uint SessionRetentionMinutes = 10;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override OAuthProvider Provider => OAuthProvider.TGForums;
|
||||
|
||||
/// <summary>
|
||||
/// The active session.
|
||||
/// </summary>
|
||||
readonly List<Tuple<TGCreateSessionResponse, DateTimeOffset>> sessions;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TGForumsOAuthValidator"/> <see langword="class"/>.
|
||||
/// </summary>
|
||||
/// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/> for the <see cref="BaseOAuthValidator"/></param>
|
||||
/// <param name="assemblyInformationProvider">The <see cref="IAssemblyInformationProvider"/> for the <see cref="BaseOAuthValidator"/></param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="BaseOAuthValidator"/></param>
|
||||
/// <param name="oAuthConfiguration">The <see cref="OAuthConfiguration"/> for the <see cref="BaseOAuthValidator"/>.</param>
|
||||
public TGForumsOAuthValidator(
|
||||
IHttpClientFactory httpClientFactory,
|
||||
IAssemblyInformationProvider assemblyInformationProvider,
|
||||
ILogger<TGForumsOAuthValidator> logger,
|
||||
OAuthConfiguration oAuthConfiguration)
|
||||
: base(
|
||||
httpClientFactory,
|
||||
assemblyInformationProvider,
|
||||
logger,
|
||||
oAuthConfiguration)
|
||||
{
|
||||
sessions = new List<Tuple<TGCreateSessionResponse, DateTimeOffset>>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<string> GetClientId(CancellationToken cancellationToken)
|
||||
{
|
||||
var expiredSessions = sessions.RemoveAll(x => x.Item2.AddMinutes(SessionRetentionMinutes) < DateTimeOffset.Now);
|
||||
if (expiredSessions > 0)
|
||||
Logger.LogTrace("Expired {0} sessions", expiredSessions);
|
||||
|
||||
Logger.LogTrace("Creating new session...");
|
||||
try
|
||||
{
|
||||
UriBuilder builder = new UriBuilder("https://tgstation13.org/phpBB/oauth_create_session.php")
|
||||
{
|
||||
Query = $"site_private_token={HttpUtility.UrlEncode(Convert.ToBase64String(Encoding.UTF8.GetBytes(OAuthConfiguration.ClientSecret)))}&return_uri={HttpUtility.UrlEncode(OAuthConfiguration.ClientId)}"
|
||||
};
|
||||
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, builder.Uri);
|
||||
using var httpClient = CreateHttpClient();
|
||||
|
||||
using var response = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
var newSession = JsonConvert.DeserializeObject<TGCreateSessionResponse>(json, SerializerSettings());
|
||||
|
||||
if (newSession.Status != TGBaseResponse.OkStatus)
|
||||
{
|
||||
Logger.LogWarning("Invalid status from /tg/ API! Status: {0}, Error: {1}", newSession.Status, newSession.Error);
|
||||
return null;
|
||||
}
|
||||
|
||||
sessions.Add(Tuple.Create(newSession, DateTimeOffset.Now));
|
||||
return newSession.SessionPublicToken;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Failed to create TG Forums session!");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<string> ValidateResponseCode(string code, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var sessionTuple = sessions.FirstOrDefault(x => x.Item1.SessionPublicToken == code);
|
||||
if(sessionTuple == null)
|
||||
{
|
||||
Logger.LogWarning("No known session with this code active!");
|
||||
return null;
|
||||
}
|
||||
|
||||
Logger.LogTrace("Validating session...");
|
||||
|
||||
UriBuilder builder = new UriBuilder("https://tgstation13.org/phpBB/oauth_get_session_info.php")
|
||||
{
|
||||
Query = $"site_private_token={HttpUtility.UrlEncode(Convert.ToBase64String(Encoding.UTF8.GetBytes(OAuthConfiguration.ClientSecret)))}&session_private_token={HttpUtility.UrlEncode(sessionTuple.Item1.SessionPrivateToken)}"
|
||||
};
|
||||
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, builder.Uri);
|
||||
using var httpClient = CreateHttpClient();
|
||||
|
||||
using var response = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
var sessionInfo = JsonConvert.DeserializeObject<TGGetSessionInfoResponse>(json, SerializerSettings());
|
||||
|
||||
if (sessionInfo.Status != TGBaseResponse.OkStatus)
|
||||
{
|
||||
Logger.LogWarning("Invalid status from /tg/ API! Status: {0}, Error: {1}", sessionInfo.Status, sessionInfo.Error);
|
||||
return null;
|
||||
}
|
||||
|
||||
sessions.Remove(sessionTuple);
|
||||
return sessionInfo.PhpbbUsername;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Failed to create TG Forums session!");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace Tgstation.Server.Host.Security.OAuth
|
||||
{
|
||||
/// <summary>
|
||||
/// Response when getting tgstation forum user's info.
|
||||
/// </summary>
|
||||
sealed class TGGetSessionInfoResponse : TGBaseResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// The user's forum account name.
|
||||
/// </summary>
|
||||
public string PhpbbUsername { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -7,4 +7,6 @@
|
||||
- [IIdentityCache](./IIdentityCache.cs) and [implementation](./IdentityCache.cs) is used to store `ISystemIdentity`s for the duration of their associated tokens as [IdentityCacheObject](./IdentityCacheObject.cs)s.
|
||||
- [ITokenFactory](./ITokenFactory.cs) and [implementation](./TokenFactory.cs) is used to generate the Json Web Token for a session after a user successfully authenticates.
|
||||
- [ISystemIdentity](./ISystemIdentity.cs)s represent a logon session with the operating system for a given user. It contains a method to run code under the security context of said user.
|
||||
- [ISystemIdentityFactory](./ISystemIdentityFactory.cs) is used to create `ISystemIdentity`s by attempting to log the user in with the OS with a given username and password.
|
||||
- [ISystemIdentityFactory](./ISystemIdentityFactory.cs) is used to create `ISystemIdentity`s by attempting to log the user in with the OS with a given username and password.
|
||||
|
||||
- [OAuth](./OAuth) contains classes related to OAuth 2.0 authentication
|
||||
|
||||
@@ -76,7 +76,7 @@ namespace Tgstation.Server.Host.Security
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Token> CreateToken(Models.User user, CancellationToken cancellationToken)
|
||||
public async Task<Token> CreateToken(Models.User user, bool oAuth, CancellationToken cancellationToken)
|
||||
{
|
||||
if (user == null)
|
||||
throw new ArgumentNullException(nameof(user));
|
||||
@@ -93,7 +93,9 @@ namespace Tgstation.Server.Host.Security
|
||||
if (nowUnix == lpuUnix)
|
||||
await asyncDelayer.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var expiry = now.AddMinutes(securityConfiguration.TokenExpiryMinutes);
|
||||
var expiry = now.AddMinutes(oAuth
|
||||
? securityConfiguration.OAuthTokenExpiryMinutes
|
||||
: securityConfiguration.TokenExpiryMinutes);
|
||||
var claims = new Claim[]
|
||||
{
|
||||
new Claim(JwtRegisteredClaimNames.Sub, user.Id.Value.ToString(CultureInfo.InvariantCulture)),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Tgstation.Server.Host.Configuration;
|
||||
using Tgstation.Server.Host.Configuration;
|
||||
using Tgstation.Server.Host.System;
|
||||
|
||||
namespace Tgstation.Server.Host.Setup
|
||||
@@ -18,6 +18,11 @@ namespace Tgstation.Server.Host.Setup
|
||||
/// </summary>
|
||||
DatabaseConfiguration DatabaseConfiguration { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Configuration.SecurityConfiguration"/>.
|
||||
/// </summary>
|
||||
SecurityConfiguration SecurityConfiguration { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Configuration.FileLoggingConfiguration"/>.
|
||||
/// </summary>
|
||||
|
||||
@@ -18,6 +18,9 @@ namespace Tgstation.Server.Host.Setup
|
||||
/// <inheritdoc />
|
||||
public DatabaseConfiguration DatabaseConfiguration => databaseConfigurationOptions.Value;
|
||||
|
||||
/// <inheritdoc />
|
||||
public SecurityConfiguration SecurityConfiguration => securityConfigurationOptions.Value;
|
||||
|
||||
/// <inheritdoc />
|
||||
public FileLoggingConfiguration FileLoggingConfiguration => fileLoggingConfigurationOptions.Value;
|
||||
|
||||
@@ -34,6 +37,11 @@ namespace Tgstation.Server.Host.Setup
|
||||
/// </summary>
|
||||
readonly IOptions<DatabaseConfiguration> databaseConfigurationOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Backing <see cref="IOptions{TOptions}"/> for <see cref="SecurityConfiguration"/>.
|
||||
/// </summary>
|
||||
readonly IOptions<SecurityConfiguration> securityConfigurationOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Backing <see cref="IOptions{TOptions}"/> for <see cref="FileLoggingConfiguration"/>.
|
||||
/// </summary>
|
||||
@@ -46,12 +54,14 @@ namespace Tgstation.Server.Host.Setup
|
||||
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/> used to create <see cref="Logger"/>.</param>
|
||||
/// <param name="generalConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="GeneralConfiguration"/>.</param>
|
||||
/// <param name="databaseConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="DatabaseConfiguration"/>.</param>
|
||||
/// <param name="securityConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="SecurityConfiguration"/>.</param>
|
||||
/// <param name="fileLoggingConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="FileLoggingConfiguration"/>.</param>
|
||||
public PostSetupServices(
|
||||
IPlatformIdentifier platformIdentifier,
|
||||
ILoggerFactory loggerFactory,
|
||||
IOptions<GeneralConfiguration> generalConfigurationOptions,
|
||||
IOptions<DatabaseConfiguration> databaseConfigurationOptions,
|
||||
IOptions<SecurityConfiguration> securityConfigurationOptions,
|
||||
IOptions<FileLoggingConfiguration> fileLoggingConfigurationOptions)
|
||||
{
|
||||
PlatformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier));
|
||||
@@ -61,6 +71,7 @@ namespace Tgstation.Server.Host.Setup
|
||||
Logger = loggerFactory.CreateLogger<TLoggerType>();
|
||||
this.generalConfigurationOptions = generalConfigurationOptions ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
|
||||
this.databaseConfigurationOptions = databaseConfigurationOptions ?? throw new ArgumentNullException(nameof(databaseConfigurationOptions));
|
||||
this.securityConfigurationOptions = securityConfigurationOptions ?? throw new ArgumentNullException(nameof(securityConfigurationOptions));
|
||||
this.fileLoggingConfigurationOptions = fileLoggingConfigurationOptions ?? throw new ArgumentNullException(nameof(fileLoggingConfigurationOptions));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ namespace Tgstation.Server.Host.Setup
|
||||
|
||||
services.UseStandardConfig<GeneralConfiguration>(Configuration);
|
||||
services.UseStandardConfig<DatabaseConfiguration>(Configuration);
|
||||
services.UseStandardConfig<SecurityConfiguration>(Configuration);
|
||||
services.UseStandardConfig<FileLoggingConfiguration>(Configuration);
|
||||
|
||||
ConfigureHostedService(services);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Reflection;
|
||||
using Tgstation.Server.Api;
|
||||
|
||||
@@ -22,6 +23,11 @@ namespace Tgstation.Server.Host.System
|
||||
/// <inheritdoc />
|
||||
public string VersionString { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public ProductInfoHeaderValue ProductInfoHeaderValue => new ProductInfoHeaderValue(
|
||||
VersionPrefix,
|
||||
Version.ToString());
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AssemblyInformationProvider"/> <see langword="class"/>.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Tgstation.Server.Host.System
|
||||
@@ -32,5 +33,10 @@ namespace Tgstation.Server.Host.System
|
||||
/// The version of the assembly.
|
||||
/// </summary>
|
||||
Version Version { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ProductInfoHeaderValue"/> for the assembly.
|
||||
/// </summary>
|
||||
ProductInfoHeaderValue ProductInfoHeaderValue { get; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<Import Project="../../build/Version.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
@@ -66,20 +66,20 @@
|
||||
<PackageReference Include="Cyberboss.SmartIrc4net.Standard" Version="0.4.6" />
|
||||
<PackageReference Include="Discord.Net.WebSocket" Version="2.2.0" />
|
||||
<PackageReference Include="LibGit2Sharp" Version="0.27.0-preview-0034" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="3.1.7" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.7" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="3.3.0">
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="3.1.10" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.10" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="3.3.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.1.7" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.7">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.1.10" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.10">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.7" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.7" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.7">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.10" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.10" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.10">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
@@ -87,7 +87,7 @@
|
||||
<PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="3.1.4" />
|
||||
<PackageReference Include="Octokit" Version="0.48.0" />
|
||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="3.1.2" />
|
||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="3.2.4" />
|
||||
<!-- If this is updated, be sure to update the reference in the README.md -->
|
||||
<PackageReference Include="Serilog.Extensions.Logging" Version="3.0.1" />
|
||||
<PackageReference Include="Serilog.Sinks.Async" Version="1.4.0" />
|
||||
@@ -97,13 +97,13 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.5.1" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Newtonsoft" Version="5.5.1" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.6.3" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Newtonsoft" Version="5.6.3" />
|
||||
<PackageReference Include="System.Data.SqlClient" Version="4.8.2" />
|
||||
<PackageReference Include="System.DirectoryServices.AccountManagement" Version="4.7.0" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.7.1" />
|
||||
<PackageReference Include="System.Management" Version="4.7.0" />
|
||||
<PackageReference Include="Wangkanai.Detection.Browser" Version="2.0.0" />
|
||||
<PackageReference Include="System.DirectoryServices.AccountManagement" Version="5.0.0" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.8.0" />
|
||||
<PackageReference Include="System.Management" Version="5.0.0" />
|
||||
<PackageReference Include="Wangkanai.Detection.Browser" Version="2.0.1" />
|
||||
<PackageReference Include="Z.EntityFramework.Plus.EFCore" Version="3.0.57" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -54,6 +54,11 @@
|
||||
"TokenExpiryMinutes": 15,
|
||||
"TokenClockSkewMinutes": 1,
|
||||
"TokenSigningKeyByteAmount": 256,
|
||||
"CustomTokenSigningKeyBase64": null
|
||||
"CustomTokenSigningKeyBase64": null,
|
||||
"OAuth": {
|
||||
"GitHub": null,
|
||||
"Discord": null,
|
||||
"TGForums": null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Headers;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using System;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Mime;
|
||||
using Tgstation.Server.Api.Models;
|
||||
|
||||
namespace Tgstation.Server.Api.Tests
|
||||
{
|
||||
@@ -21,6 +22,7 @@ namespace Tgstation.Server.Api.Tests
|
||||
Assert.ThrowsException<ArgumentNullException>(() => new ApiHeaders(null, null));
|
||||
Assert.ThrowsException<ArgumentNullException>(() => new ApiHeaders(productHeaderValue, null));
|
||||
var headers = new ApiHeaders(productHeaderValue, String.Empty);
|
||||
headers = new ApiHeaders(productHeaderValue, String.Empty, OAuthProvider.GitHub);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Moq;
|
||||
using Octokit;
|
||||
using System;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Threading.Tasks;
|
||||
using Tgstation.Server.Host.System;
|
||||
|
||||
@@ -17,8 +18,7 @@ namespace Tgstation.Server.Host.Core.Tests
|
||||
public async Task TestCreateBasicClient()
|
||||
{
|
||||
var mockApp = new Mock<IAssemblyInformationProvider>();
|
||||
mockApp.SetupGet(x => x.Version).Returns(new Version()).Verifiable();
|
||||
mockApp.SetupGet(x => x.VersionPrefix).Returns("TGSTests").Verifiable();
|
||||
mockApp.SetupGet(x => x.ProductInfoHeaderValue).Returns(new ProductInfoHeaderValue("TGSTests", "1.2.3")).Verifiable();
|
||||
|
||||
var factory = new GitHubClientFactory(mockApp.Object);
|
||||
|
||||
@@ -36,8 +36,7 @@ namespace Tgstation.Server.Host.Core.Tests
|
||||
public async Task TestCreateTokenClient()
|
||||
{
|
||||
var mockApp = new Mock<IAssemblyInformationProvider>();
|
||||
mockApp.SetupGet(x => x.Version).Returns(new Version()).Verifiable();
|
||||
mockApp.SetupGet(x => x.VersionPrefix).Returns("TGSTests").Verifiable();
|
||||
mockApp.SetupGet(x => x.ProductInfoHeaderValue).Returns(new ProductInfoHeaderValue("TGSTests", "1.2.3")).Verifiable();
|
||||
|
||||
var factory = new GitHubClientFactory(mockApp.Object);
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Moq;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
@@ -12,6 +13,7 @@ using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Mime;
|
||||
using System.Net.Sockets;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
@@ -37,9 +39,9 @@ namespace Tgstation.Server.Tests
|
||||
readonly IServerClientFactory clientFactory = new ServerClientFactory(new ProductHeaderValue(Assembly.GetExecutingAssembly().GetName().Name, Assembly.GetExecutingAssembly().GetName().Version.ToString()));
|
||||
|
||||
[TestMethod]
|
||||
public async Task TestUpdateProtocol()
|
||||
public async Task TestUpdateProtocolAndDisabledOAuth()
|
||||
{
|
||||
using var server = new TestingServer();
|
||||
using var server = new TestingServer(false);
|
||||
using var serverCts = new CancellationTokenSource();
|
||||
var cancellationToken = serverCts.Token;
|
||||
var serverTask = server.Run(cancellationToken);
|
||||
@@ -47,11 +49,30 @@ namespace Tgstation.Server.Tests
|
||||
{
|
||||
var testUpdateVersion = new Version(4, 3, 0);
|
||||
using (var adminClient = await CreateAdminClient(server.Url, cancellationToken))
|
||||
{
|
||||
// Disabled OAuth test
|
||||
using (var httpClient = new HttpClient())
|
||||
using (var request = new HttpRequestMessage(HttpMethod.Post, server.Url.ToString()))
|
||||
{
|
||||
request.Headers.Accept.Clear();
|
||||
request.Headers.UserAgent.Add(new ProductInfoHeaderValue("RootTest", "1.0.0"));
|
||||
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json));
|
||||
request.Headers.Add(ApiHeaders.ApiVersionHeader, "Tgstation.Server.Api/" + ApiHeaders.Version);
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue(ApiHeaders.OAuthAuthenticationScheme, adminClient.Token.Bearer);
|
||||
request.Headers.Add(ApiHeaders.OAuthProviderHeader, OAuthProvider.GitHub.ToString());
|
||||
using var response = await httpClient.SendAsync(request, cancellationToken);
|
||||
Assert.AreEqual(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
var message = JsonConvert.DeserializeObject<ErrorMessage>(content);
|
||||
Assert.AreEqual(ErrorCode.OAuthProviderDisabled, message.ErrorCode);
|
||||
}
|
||||
|
||||
//attempt to update to stable
|
||||
await adminClient.Administration.Update(new Administration
|
||||
{
|
||||
NewVersion = testUpdateVersion
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
//wait up to 3 minutes for the dl and install
|
||||
await Task.WhenAny(serverTask, Task.Delay(TimeSpan.FromMinutes(3), cancellationToken)).ConfigureAwait(false);
|
||||
@@ -207,7 +228,7 @@ namespace Tgstation.Server.Tests
|
||||
Assert.Inconclusive("Cannot run server test because DreamDaemon will not start headless while the BYOND pager is running!");
|
||||
}
|
||||
|
||||
using var server = new TestingServer();
|
||||
using var server = new TestingServer(true);
|
||||
|
||||
const int MaximumTestMinutes = 20;
|
||||
using var hardTimeoutCancellationTokenSource = new CancellationTokenSource(TimeSpan.FromMinutes(MaximumTestMinutes));
|
||||
@@ -450,7 +471,7 @@ namespace Tgstation.Server.Tests
|
||||
[TestMethod]
|
||||
public async Task TestRepoParentLookup()
|
||||
{
|
||||
using var testingServer = new TestingServer();
|
||||
using var testingServer = new TestingServer(false);
|
||||
LibGit2Sharp.Repository.Clone("https://github.com/Cyberboss/test", testingServer.Directory);
|
||||
var libGit2Repo = new LibGit2Sharp.Repository(testingServer.Directory);
|
||||
using var repo = new Host.Components.Repository.Repository(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
@@ -56,12 +57,12 @@ namespace Tgstation.Server.Tests
|
||||
request.Headers.Accept.Clear();
|
||||
request.Headers.UserAgent.Add(new ProductInfoHeaderValue("RootTest", "1.0.0"));
|
||||
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json));
|
||||
request.Headers.Add(ApiHeaders.ApiVersionHeader, "Tgstation.Server.Api/6.0.0");
|
||||
request.Headers.Add(ApiHeaders.ApiVersionHeader, "Tgstation.Server.Api/" + ApiHeaders.Version);
|
||||
using var response = await httpClient.SendAsync(request, cancellationToken);
|
||||
Assert.AreEqual(HttpStatusCode.Unauthorized, response.StatusCode);
|
||||
Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
var message = JsonConvert.DeserializeObject<ErrorMessage>(content);
|
||||
Assert.AreEqual(ErrorCode.BadHeaders, message.ErrorCode);
|
||||
var message = JsonConvert.DeserializeObject<ServerInformation>(content);
|
||||
Assert.AreEqual(ApiHeaders.Version, message.ApiVersion);
|
||||
}
|
||||
|
||||
using (var request = new HttpRequestMessage(HttpMethod.Get, url.ToString()))
|
||||
@@ -70,7 +71,21 @@ namespace Tgstation.Server.Tests
|
||||
request.Headers.UserAgent.Add(new ProductInfoHeaderValue("RootTest", "1.0.0"));
|
||||
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("*/*"));
|
||||
request.Headers.Add(ApiHeaders.ApiVersionHeader, "Tgstation.Server.Api/6.0.0");
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("bearer", token);
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue(ApiHeaders.BearerAuthenticationScheme, token);
|
||||
using var response = await httpClient.SendAsync(request, cancellationToken);
|
||||
Assert.AreEqual(HttpStatusCode.UpgradeRequired, response.StatusCode);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
var message = JsonConvert.DeserializeObject<ErrorMessage>(content);
|
||||
Assert.AreEqual(ErrorCode.ApiMismatch, message.ErrorCode);
|
||||
}
|
||||
|
||||
using (var request = new HttpRequestMessage(HttpMethod.Get, url.ToString() + Routes.Administration.Substring(1)))
|
||||
{
|
||||
request.Headers.Accept.Clear();
|
||||
request.Headers.UserAgent.Add(new ProductInfoHeaderValue("RootTest", "1.0.0"));
|
||||
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("*/*"));
|
||||
request.Headers.Add(ApiHeaders.ApiVersionHeader, "Tgstation.Server.Api/6.0.0");
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue(ApiHeaders.BearerAuthenticationScheme, token);
|
||||
using var response = await httpClient.SendAsync(request, cancellationToken);
|
||||
Assert.AreEqual(HttpStatusCode.UpgradeRequired, response.StatusCode);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
@@ -83,8 +98,8 @@ namespace Tgstation.Server.Tests
|
||||
request.Headers.Accept.Clear();
|
||||
request.Headers.UserAgent.Add(new ProductInfoHeaderValue("RootTest", "1.0.0"));
|
||||
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json));
|
||||
request.Headers.Add(ApiHeaders.ApiVersionHeader, "Tgstation.Server.Api/7.0.0");
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("bearer", token);
|
||||
request.Headers.Add(ApiHeaders.ApiVersionHeader, "Tgstation.Server.Api/" + ApiHeaders.Version);
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue(ApiHeaders.BearerAuthenticationScheme, token);
|
||||
request.Content = new StringContent(
|
||||
"{ newVersion: 1234 }",
|
||||
Encoding.UTF8,
|
||||
@@ -101,7 +116,7 @@ namespace Tgstation.Server.Tests
|
||||
request.Headers.Accept.Clear();
|
||||
request.Headers.UserAgent.Add(new ProductInfoHeaderValue("RootTest", "1.0.0"));
|
||||
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json));
|
||||
request.Headers.Add(ApiHeaders.ApiVersionHeader, "Tgstation.Server.Api/7.0.0");
|
||||
request.Headers.Add(ApiHeaders.ApiVersionHeader, "Tgstation.Server.Api/" + ApiHeaders.Version);
|
||||
using var response = await httpClient.SendAsync(request, cancellationToken);
|
||||
Assert.AreEqual(HttpStatusCode.Unauthorized, response.StatusCode);
|
||||
}
|
||||
@@ -111,14 +126,29 @@ namespace Tgstation.Server.Tests
|
||||
request.Headers.Accept.Clear();
|
||||
request.Headers.UserAgent.Add(new ProductInfoHeaderValue("RootTest", "1.0.0"));
|
||||
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json));
|
||||
request.Headers.Add(ApiHeaders.ApiVersionHeader, "Tgstation.Server.Api/7.0.0");
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("bearer", token);
|
||||
request.Headers.Add(ApiHeaders.ApiVersionHeader, "Tgstation.Server.Api/" + ApiHeaders.Version);
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue(ApiHeaders.BearerAuthenticationScheme, token);
|
||||
using var response = await httpClient.SendAsync(request, cancellationToken);
|
||||
Assert.AreEqual(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
var message = JsonConvert.DeserializeObject<ErrorMessage>(content);
|
||||
Assert.AreEqual(ErrorCode.InstanceHeaderRequired, message.ErrorCode);
|
||||
}
|
||||
|
||||
using (var request = new HttpRequestMessage(HttpMethod.Post, url.ToString()))
|
||||
{
|
||||
request.Headers.Accept.Clear();
|
||||
request.Headers.UserAgent.Add(new ProductInfoHeaderValue("RootTest", "1.0.0"));
|
||||
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json));
|
||||
request.Headers.Add(ApiHeaders.ApiVersionHeader, "Tgstation.Server.Api/" + ApiHeaders.Version);
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue(ApiHeaders.OAuthAuthenticationScheme, token);
|
||||
request.Headers.Add(ApiHeaders.OAuthProviderHeader, "FakeProvider");
|
||||
using var response = await httpClient.SendAsync(request, cancellationToken);
|
||||
Assert.AreEqual(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
var message = JsonConvert.DeserializeObject<ErrorMessage>(content);
|
||||
Assert.AreEqual(ErrorCode.BadHeaders, message.ErrorCode);
|
||||
}
|
||||
}
|
||||
|
||||
async Task TestServerInformation(IServerClientFactory clientFactory, IServerClient serverClient, CancellationToken cancellationToken)
|
||||
@@ -140,12 +170,35 @@ namespace Tgstation.Server.Tests
|
||||
};
|
||||
|
||||
var badClient = clientFactory.CreateFromToken(serverClient.Url, newToken);
|
||||
await Assert.ThrowsExceptionAsync<UnauthorizedException>(() => badClient.Version(cancellationToken)).ConfigureAwait(false);
|
||||
await Assert.ThrowsExceptionAsync<UnauthorizedException>(() => badClient.Administration.Read(cancellationToken)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
async Task TestOAuthFails(IServerClient serverClient, CancellationToken cancellationToken)
|
||||
{
|
||||
var url = serverClient.Url;
|
||||
var token = serverClient.Token.Bearer;
|
||||
// check that 400s are returned appropriately
|
||||
using var httpClient = new HttpClient();
|
||||
|
||||
// just hitting each type of oauth provider for coverage
|
||||
foreach (var I in Enum.GetValues(typeof(OAuthProvider)))
|
||||
using (var request = new HttpRequestMessage(HttpMethod.Post, url.ToString()))
|
||||
{
|
||||
request.Headers.Accept.Clear();
|
||||
request.Headers.UserAgent.Add(new ProductInfoHeaderValue("RootTest", "1.0.0"));
|
||||
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json));
|
||||
request.Headers.Add(ApiHeaders.ApiVersionHeader, "Tgstation.Server.Api/" + ApiHeaders.Version);
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue(ApiHeaders.OAuthAuthenticationScheme, token);
|
||||
request.Headers.Add(ApiHeaders.OAuthProviderHeader, I.ToString());
|
||||
using var response = await httpClient.SendAsync(request, cancellationToken);
|
||||
Assert.AreEqual(HttpStatusCode.Unauthorized, response.StatusCode);
|
||||
}
|
||||
}
|
||||
|
||||
public Task Run(IServerClientFactory clientFactory, IServerClient serverClient, CancellationToken cancellationToken)
|
||||
=> Task.WhenAll(
|
||||
TestRequestValidation(serverClient, cancellationToken),
|
||||
TestOAuthFails(serverClient, cancellationToken),
|
||||
TestServerInformation(clientFactory, serverClient, cancellationToken));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Tgstation.Server.Api.Models;
|
||||
using Tgstation.Server.Host;
|
||||
using Tgstation.Server.Host.Configuration;
|
||||
using Tgstation.Server.Host.Core;
|
||||
@@ -31,7 +32,7 @@ namespace Tgstation.Server.Tests
|
||||
|
||||
IServer realServer;
|
||||
|
||||
public TestingServer()
|
||||
public TestingServer(bool enableOAuth)
|
||||
{
|
||||
Directory = Environment.GetEnvironmentVariable("TGS4_TEST_TEMP_DIRECTORY");
|
||||
if (String.IsNullOrWhiteSpace(Directory))
|
||||
@@ -86,6 +87,17 @@ namespace Tgstation.Server.Tests
|
||||
"General:ByondTopicTimeout=3000"
|
||||
};
|
||||
|
||||
// enable all oauth providers
|
||||
if (enableOAuth)
|
||||
foreach (var I in Enum.GetValues(typeof(OAuthProvider)))
|
||||
{
|
||||
args.Add($"Security:OAuth:{I}:ClientId=Fake");
|
||||
args.Add($"Security:OAuth:{I}:ClientSecret=Faker");
|
||||
}
|
||||
|
||||
// SPECIFICALLY DELETE THE DEV APPSETTINGS, WE DON'T WANT IT IN THE WAY
|
||||
File.Delete("appsettings.Development.json");
|
||||
|
||||
if (!String.IsNullOrEmpty(gitHubAccessToken))
|
||||
args.Add(String.Format(CultureInfo.InvariantCulture, "General:GitHubAccessToken={0}", gitHubAccessToken));
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -29,7 +29,7 @@ namespace Tgstation.Server.Tests
|
||||
|
||||
async Task BasicTests(CancellationToken cancellationToken)
|
||||
{
|
||||
var user = await this.client.Read(cancellationToken).ConfigureAwait(false);
|
||||
var user = await client.Read(cancellationToken).ConfigureAwait(false);
|
||||
Assert.IsNotNull(user);
|
||||
Assert.AreEqual("Admin", user.Name);
|
||||
Assert.IsNull(user.SystemIdentifier);
|
||||
|
||||
Reference in New Issue
Block a user