OIDC First Implementation

This commit is contained in:
Jordan Dominion
2025-03-09 17:53:43 -04:00
parent ff8473b73c
commit 2e75125860
56 changed files with 5750 additions and 142 deletions
+6 -6
View File
@@ -3,13 +3,13 @@
<!-- Integration tests will ensure they match across the board -->
<Import Project="WebpanelVersion.props" />
<PropertyGroup>
<TgsCoreVersion>6.14.1</TgsCoreVersion>
<TgsConfigVersion>5.5.0</TgsConfigVersion>
<TgsRestVersion>10.12.1</TgsRestVersion>
<TgsGraphQLVersion>0.5.0</TgsGraphQLVersion>
<TgsCoreVersion>6.15.0</TgsCoreVersion>
<TgsConfigVersion>5.6.0</TgsConfigVersion>
<TgsRestVersion>10.13.0</TgsRestVersion>
<TgsGraphQLVersion>0.6.0</TgsGraphQLVersion>
<TgsCommonLibraryVersion>7.0.0</TgsCommonLibraryVersion>
<TgsApiLibraryVersion>17.0.1</TgsApiLibraryVersion>
<TgsClientVersion>20.0.0</TgsClientVersion>
<TgsApiLibraryVersion>18.0.0</TgsApiLibraryVersion>
<TgsClientVersion>21.0.0</TgsClientVersion>
<TgsDmapiVersion>7.3.1</TgsDmapiVersion>
<TgsInteropVersion>5.10.0</TgsInteropVersion>
<TgsHostWatchdogVersion>1.6.0</TgsHostWatchdogVersion>
+13 -1
View File
@@ -53,6 +53,11 @@ namespace Tgstation.Server.Api
/// </summary>
public const string OAuthAuthenticationScheme = "OAuth";
/// <summary>
/// The OIDC authentication header scheme.
/// </summary>
public const string OpenIDConnectAuthenticationSchemePrefix = "OpenIdConnect.";
/// <summary>
/// Added to <see cref="MediaTypeNames.Application"/> in netstandard2.1. Can't use because of lack of .NET Framework support.
/// </summary>
@@ -103,6 +108,11 @@ namespace Tgstation.Server.Api
/// </summary>
public TokenResponse? Token { get; }
/// <summary>
/// The parsed <see cref="AuthenticationHeaderValue"/> if any.
/// </summary>
public AuthenticationHeaderValue? AuthenticationHeader { get; }
/// <summary>
/// The client's username.
/// </summary>
@@ -252,6 +262,7 @@ namespace Tgstation.Server.Api
{
splits.RemoveAt(0);
var parameter = String.Concat(splits);
AuthenticationHeader = new AuthenticationHeaderValue(scheme, parameter);
if (String.IsNullOrEmpty(parameter))
AddError(HeaderErrorTypes.AuthorizationInvalid, "Missing authentication parameter!");
else
@@ -320,7 +331,8 @@ namespace Tgstation.Server.Api
Password = String.Concat(basicAuthSplits.Skip(1));
break;
default:
AddError(HeaderErrorTypes.AuthorizationInvalid, "Invalid authentication scheme!");
if (!scheme.StartsWith(OpenIDConnectAuthenticationSchemePrefix))
AddError(HeaderErrorTypes.AuthorizationInvalid, "Invalid authentication scheme!");
break;
}
}
+1 -1
View File
@@ -584,7 +584,7 @@ namespace Tgstation.Server.Api.Models
/// Attempted to set <see cref="Internal.UserApiBase.OAuthConnections"/> for the admin user.
/// </summary>
[Description("The admin user cannot use OAuth connections!")]
AdminUserCannotOAuth,
AdminUserCannotHaveServiceConnection,
/// <summary>
/// Attempted to login with a disabled OAuth provider.
@@ -10,6 +10,11 @@ namespace Tgstation.Server.Api.Models.Internal
/// </summary>
public ICollection<OAuthConnection>? OAuthConnections { get; set; }
/// <summary>
/// List of <see cref="OidcConnection"/>s associated with the user.
/// </summary>
public ICollection<OidcConnection>? OidcConnections { get; set; }
/// <summary>
/// The <see cref="Models.PermissionSet"/> directly associated with the user.
/// </summary>
@@ -6,7 +6,7 @@ using Newtonsoft.Json.Converters;
namespace Tgstation.Server.Api.Models
{
/// <summary>
/// List of OAuth providers supported by TGS.
/// List of OAuth2.0 providers supported by TGS that do not support OIDC.
/// </summary>
[JsonConverter(typeof(StringEnumConverter))]
public enum OAuthProvider
@@ -24,12 +24,13 @@ namespace Tgstation.Server.Api.Models
/// <summary>
/// Pre-hostening https://tgstation13.org. No longer supported.
/// </summary>
[Obsolete("tgstation13.org no longer has a custom OAuth solution", true)]
[Obsolete("tgstation13.org no longer has a custom OAuth solution. This option will be removed in a future TGS version.", true)]
TGForums,
/// <summary>
/// https://www.keycloak.org.
/// </summary>
[Obsolete("This should now be implemented as an OIDC provider. This option will be removed in a future TGS version.")]
Keycloak,
/// <summary>
@@ -0,0 +1,28 @@
using System.ComponentModel.DataAnnotations;
using Newtonsoft.Json;
namespace Tgstation.Server.Api.Models
{
/// <summary>
/// Represents a valid OIDC connection.
/// </summary>
public class OidcConnection
{
/// <summary>
/// The <see cref="OidcProviderInfo.SchemeKey"/> of the <see cref="OidcConnection"/>.
/// </summary>]
[Required]
[RequestOptions(FieldPresence.Required)]
[StringLength(Limits.MaximumIndexableStringLength, MinimumLength = 1)]
public string? SchemeKey { get; set; }
/// <summary>
/// The ID of the user in the OIDC proivder ("sub" claim).
/// </summary>
[Required]
[RequestOptions(FieldPresence.Required)]
[StringLength(Limits.MaximumIndexableStringLength, MinimumLength = 1)]
public string? ExternalUserId { get; set; }
}
}
@@ -0,0 +1,30 @@
using System;
namespace Tgstation.Server.Api.Models
{
/// <summary>
/// Represents a configured OIDC provider.
/// </summary>
public sealed class OidcProviderInfo
{
/// <summary>
/// The scheme key used to reference the <see cref="OidcProviderInfo"/>. Unique amongst providers.
/// </summary>
public string? SchemeKey { get; set; }
/// <summary>
/// The provider's name as it should be displayed to the user.
/// </summary>
public string? FriendlyName { get; set; }
/// <summary>
/// Colour that should be used to theme this OIDC provider.
/// </summary>
public string? ThemeColour { get; set; }
/// <summary>
/// Image URL that should be used to theme this OIDC provider.
/// </summary>
public Uri? ThemeIconUrl { get; set; }
}
}
@@ -36,6 +36,11 @@ namespace Tgstation.Server.Api.Models.Response
/// </summary>
public Dictionary<OAuthProvider, OAuthProviderInfo>? OAuthProviderInfos { get; set; }
/// <summary>
/// List of configured <see cref="OidcProviderInfo"/>s.
/// </summary>
public List<OidcProviderInfo>? OidcProviderInfos { get; set; }
/// <summary>
/// If there is a server update in progress.
/// </summary>
@@ -44,9 +44,9 @@ namespace Tgstation.Server.Api.Rights
DownloadLogs = 1 << 5,
/// <summary>
/// User can modify their own <see cref="Models.Internal.UserApiBase.OAuthConnections"/>.
/// User can modify their own <see cref="Models.Internal.UserApiBase.OAuthConnections"/> and <see cref="Models.Internal.UserApiBase.OidcConnections"/>.
/// </summary>
EditOwnOAuthConnections = 1 << 6,
EditOwnServiceConnections = 1 << 6,
/// <summary>
/// User can upgrade/downgrade TGS using file uploads through the API.
@@ -1,5 +1,5 @@
mutation CreateUserFromOAuthConnection($name: String!, $oAuthConnections: [OAuthConnectionInput!]!) {
createUserByOAuthAndPermissionSet(input: { name: $name, oAuthConnections: $oAuthConnections }) {
createUserByServiceConnectionAndPermissionSet(input: { name: $name, oAuthConnections: $oAuthConnections }) {
errors {
... on ErrorMessageError {
additionalData
@@ -7,7 +7,7 @@ mutation CreateUserGroup($name: String!) {
administrationRights {
canChangeVersion
canDownloadLogs
canEditOwnOAuthConnections
canEditOwnServiceConnections
canEditOwnPassword
canReadUsers
canRestartHost
@@ -7,7 +7,7 @@ mutation CreateUserGroupWithInstanceListPerm($name: String!) {
administrationRights {
canChangeVersion
canDownloadLogs
canEditOwnOAuthConnections
canEditOwnServiceConnections
canEditOwnPassword
canReadUsers
canRestartHost
@@ -6,7 +6,7 @@ mutation SetFullPermsOnUserGroup($id: ID!) {
administrationRights: {
canChangeVersion: true
canDownloadLogs: true
canEditOwnOAuthConnections: true
canEditOwnServiceConnections: true
canEditOwnPassword: true
canReadUsers: true
canWriteUsers: true
@@ -36,7 +36,7 @@ mutation SetFullPermsOnUserGroup($id: ID!) {
administrationRights {
canChangeVersion
canDownloadLogs
canEditOwnOAuthConnections
canEditOwnServiceConnections
canEditOwnPassword
canReadUsers
canRestartHost
@@ -10,27 +10,27 @@ mutation SetUserGroup($id: ID!, $newGroupId: ID!) {
user {
ownedPermissionSet {
instanceManagerRights {
canCreate
canDelete
canGrantPermissions
canList
canRead
canRelocate
canRename
canSetAutoUpdate
canSetChatBotLimit
canSetConfiguration
canSetOnline
canCreate
canDelete
canGrantPermissions
canList
canRead
canRelocate
canRename
canSetAutoUpdate
canSetChatBotLimit
canSetConfiguration
canSetOnline
}
administrationRights {
canChangeVersion
canDownloadLogs
canEditOwnOAuthConnections
canEditOwnPassword
canReadUsers
canRestartHost
canUploadVersion
canWriteUsers
canChangeVersion
canDownloadLogs
canEditOwnServiceConnections
canEditOwnPassword
canReadUsers
canRestartHost
canUploadVersion
canWriteUsers
}
}
group {
@@ -12,7 +12,7 @@ mutation SetUserPermissionSet($id: ID!, $permissionSet: PermissionSetInput!) {
administrationRights {
canChangeVersion
canDownloadLogs
canEditOwnOAuthConnections
canEditOwnServiceConnections
canEditOwnPassword
canReadUsers
canRestartHost
@@ -37,7 +37,7 @@ mutation SetUserPermissionSet($id: ID!, $permissionSet: PermissionSetInput!) {
administrationRights {
canChangeVersion
canDownloadLogs
canEditOwnOAuthConnections
canEditOwnServiceConnections
canEditOwnPassword
canReadUsers
canRestartHost
@@ -20,7 +20,7 @@ query GetSomeGroupInfo($id: ID!) {
administrationRights {
canChangeVersion
canDownloadLogs
canEditOwnOAuthConnections
canEditOwnServiceConnections
canEditOwnPassword
canReadUsers
canRestartHost
@@ -20,7 +20,7 @@ query ReadCurrentUser {
administrationRights {
canChangeVersion
canDownloadLogs
canEditOwnOAuthConnections
canEditOwnServiceConnections
canEditOwnPassword
canReadUsers
canRestartHost
@@ -43,6 +43,14 @@ namespace Tgstation.Server.Host.Authority
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in an <see cref="global::System.Array"/> of <see cref="GraphQL.Types.OAuth.OAuthConnection"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
ValueTask<AuthorityResponse<GraphQL.Types.OAuth.OAuthConnection[]>> OAuthConnections(long userId, CancellationToken cancellationToken);
/// <summary>
/// Gets the <see cref="GraphQL.Types.OAuth.OidcConnection"/>s for the <see cref="User"/> with a given <paramref name="userId"/>.
/// </summary>
/// <param name="userId">The <see cref="EntityId.Id"/> of the <see cref="User"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in an <see cref="global::System.Array"/> of <see cref="GraphQL.Types.OAuth.OidcConnection"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
ValueTask<AuthorityResponse<GraphQL.Types.OAuth.OidcConnection[]>> OidcConnections(long userId, CancellationToken cancellationToken);
/// <summary>
/// Gets all registered <see cref="User"/>s.
/// </summary>
@@ -70,7 +78,7 @@ namespace Tgstation.Server.Host.Authority
/// <param name="updateRequest">The <see cref="UserUpdateRequest"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in am <see cref="AuthorityResponse{TResult}"/> for the created <see cref="User"/>.</returns>
[TgsAuthorize(AdministrationRights.WriteUsers | AdministrationRights.EditOwnPassword | AdministrationRights.EditOwnOAuthConnections)]
[TgsAuthorize(AdministrationRights.WriteUsers | AdministrationRights.EditOwnPassword | AdministrationRights.EditOwnServiceConnections)]
ValueTask<AuthorityResponse<User>> Update(UserUpdateRequest updateRequest, CancellationToken cancellationToken);
}
}
@@ -41,6 +41,11 @@ namespace Tgstation.Server.Host.Authority
/// </summary>
readonly IOAuthConnectionsDataLoader oAuthConnectionsDataLoader;
/// <summary>
/// The <see cref="IOidcConnectionsDataLoader"/> for the <see cref="UserAuthority"/>.
/// </summary>
readonly IOidcConnectionsDataLoader oidcConnectionsDataLoader;
/// <summary>
/// The <see cref="ISystemIdentityFactory"/> for the <see cref="UserAuthority"/>.
/// </summary>
@@ -95,7 +100,7 @@ namespace Tgstation.Server.Host.Authority
}
/// <summary>
/// Implements the <see cref="usersDataLoader"/>.
/// Implements the <see cref="oAuthConnectionsDataLoader"/>.
/// </summary>
/// <param name="userIds">The <see cref="IReadOnlyCollection{T}"/> of <see cref="User"/> <see cref="EntityId.Id"/>s to load the OAuthConnections for.</param>
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> to load from.</param>
@@ -117,10 +122,37 @@ namespace Tgstation.Server.Host.Authority
.ToListAsync(cancellationToken);
return list.ToLookup(
oauthConnection => oauthConnection.UserId,
oAuthConnection => oAuthConnection.UserId,
x => new GraphQL.Types.OAuth.OAuthConnection(x.ExternalUserId!, x.Provider));
}
/// <summary>
/// Implements the <see cref="oidcConnectionsDataLoader"/>.
/// </summary>
/// <param name="userIds">The <see cref="IReadOnlyCollection{T}"/> of <see cref="User"/> <see cref="EntityId.Id"/>s to load the OidcConnections for.</param>
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> to load from.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a <see cref="Dictionary{TKey, TValue}"/> of the requested <see cref="User"/>s.</returns>
[DataLoader]
public static async ValueTask<ILookup<long, GraphQL.Types.OAuth.OidcConnection>> GetOidcConnections(
IReadOnlyList<long> userIds,
IDatabaseContext databaseContext,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(userIds);
ArgumentNullException.ThrowIfNull(databaseContext);
var list = await databaseContext
.OidcConnections
.AsQueryable()
.Where(x => userIds.Contains(x.User!.Id!.Value))
.ToListAsync(cancellationToken);
return list.ToLookup(
oidcConnection => oidcConnection.UserId,
x => new GraphQL.Types.OAuth.OidcConnection(x.ExternalUserId!, x.SchemeKey!));
}
/// <summary>
/// Check if a given <paramref name="model"/> has a valid <see cref="UserName.Name"/> specified.
/// </summary>
@@ -147,6 +179,7 @@ namespace Tgstation.Server.Host.Authority
/// <param name="logger">The <see cref="ILogger"/> to use.</param>
/// <param name="usersDataLoader">The value of <see cref="usersDataLoader"/>.</param>
/// <param name="oAuthConnectionsDataLoader">The value of <see cref="oAuthConnectionsDataLoader"/>.</param>
/// <param name="oidcConnectionsDataLoader">The value of <see cref="oidcConnectionsDataLoader"/>.</param>
/// <param name="systemIdentityFactory">The value of <see cref="systemIdentityFactory"/>.</param>
/// <param name="permissionsUpdateNotifyee">The value of <see cref="permissionsUpdateNotifyee"/>.</param>
/// <param name="cryptographySuite">The value of <see cref="cryptographySuite"/>.</param>
@@ -159,6 +192,7 @@ namespace Tgstation.Server.Host.Authority
ILogger<UserAuthority> logger,
IUsersDataLoader usersDataLoader,
IOAuthConnectionsDataLoader oAuthConnectionsDataLoader,
IOidcConnectionsDataLoader oidcConnectionsDataLoader,
ISystemIdentityFactory systemIdentityFactory,
IPermissionsUpdateNotifyee permissionsUpdateNotifyee,
ICryptographySuite cryptographySuite,
@@ -172,6 +206,7 @@ namespace Tgstation.Server.Host.Authority
{
this.usersDataLoader = usersDataLoader ?? throw new ArgumentNullException(nameof(usersDataLoader));
this.oAuthConnectionsDataLoader = oAuthConnectionsDataLoader ?? throw new ArgumentNullException(nameof(oAuthConnectionsDataLoader));
this.oidcConnectionsDataLoader = oidcConnectionsDataLoader ?? throw new ArgumentNullException(nameof(oidcConnectionsDataLoader));
this.systemIdentityFactory = systemIdentityFactory ?? throw new ArgumentNullException(nameof(systemIdentityFactory));
this.permissionsUpdateNotifyee = permissionsUpdateNotifyee ?? throw new ArgumentNullException(nameof(permissionsUpdateNotifyee));
this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite));
@@ -289,6 +324,11 @@ namespace Tgstation.Server.Host.Authority
=> new AuthorityResponse<GraphQL.Types.OAuth.OAuthConnection[]>(
await oAuthConnectionsDataLoader.LoadRequiredAsync(userId, cancellationToken));
/// <inheritdoc />
public async ValueTask<AuthorityResponse<GraphQL.Types.OAuth.OidcConnection[]>> OidcConnections(long userId, CancellationToken cancellationToken)
=> new AuthorityResponse<GraphQL.Types.OAuth.OidcConnection[]>(
await oidcConnectionsDataLoader.LoadRequiredAsync(userId, cancellationToken));
/// <inheritdoc />
public async ValueTask<AuthorityResponse<User>> Create(
UserCreateRequest createRequest,
@@ -372,7 +412,7 @@ namespace Tgstation.Server.Host.Authority
var callerAdministrationRights = (AdministrationRights)AuthenticationContext.GetRight(RightsType.Administration);
var canEditAllUsers = callerAdministrationRights.HasFlag(AdministrationRights.WriteUsers);
var passwordEdit = canEditAllUsers || callerAdministrationRights.HasFlag(AdministrationRights.EditOwnPassword);
var oAuthEdit = canEditAllUsers || callerAdministrationRights.HasFlag(AdministrationRights.EditOwnOAuthConnections);
var oAuthEdit = canEditAllUsers || callerAdministrationRights.HasFlag(AdministrationRights.EditOwnServiceConnections);
var originalUser = !canEditAllUsers
? AuthenticationContext.User
@@ -382,6 +422,7 @@ namespace Tgstation.Server.Host.Authority
.Where(x => x.Id == model.Id)
.Include(x => x.CreatedBy)
.Include(x => x.OAuthConnections)
.Include(x => x.OidcConnections)
.Include(x => x.Group!)
.ThenInclude(x => x.PermissionSet)
.Include(x => x.PermissionSet)
@@ -438,7 +479,7 @@ namespace Tgstation.Server.Host.Authority
|| !model.OAuthConnections.All(x => originalUser.OAuthConnections.Any(y => y.Provider == x.Provider && y.ExternalUserId == x.ExternalUserId))))
{
if (originalUser.CanonicalName == User.CanonicalizeName(DefaultCredentials.AdminUserName))
return BadRequest<User>(ErrorCode.AdminUserCannotOAuth);
return BadRequest<User>(ErrorCode.AdminUserCannotHaveServiceConnection);
if (model.OAuthConnections.Count == 0 && originalUser.PasswordHash == null && originalUser.SystemIdentifier == null)
return BadRequest<User>(ErrorCode.CannotRemoveLastAuthenticationOption);
@@ -452,6 +493,25 @@ namespace Tgstation.Server.Host.Authority
});
}
if (model.OidcConnections != null
&& (model.OidcConnections.Count != originalUser.OidcConnections!.Count
|| !model.OidcConnections.All(x => originalUser.OidcConnections.Any(y => y.SchemeKey == x.SchemeKey && y.ExternalUserId == x.ExternalUserId))))
{
if (originalUser.CanonicalName == User.CanonicalizeName(DefaultCredentials.AdminUserName))
return BadRequest<User>(ErrorCode.AdminUserCannotHaveServiceConnection);
if (model.OidcConnections.Count == 0 && originalUser.PasswordHash == null && originalUser.SystemIdentifier == null)
return BadRequest<User>(ErrorCode.CannotRemoveLastAuthenticationOption);
originalUser.OidcConnections.Clear();
foreach (var updatedConnection in model.OidcConnections)
originalUser.OidcConnections.Add(new Models.OidcConnection
{
SchemeKey = updatedConnection.SchemeKey,
ExternalUserId = updatedConnection.ExternalUserId,
});
}
if (model.Group != null)
{
originalUser.Group = await DatabaseContext
@@ -554,6 +614,7 @@ namespace Tgstation.Server.Host.Authority
queryable = queryable
.Include(x => x.CreatedBy)
.Include(x => x.OAuthConnections)
.Include(x => x.OidcConnections)
.Include(x => x.Group!)
.ThenInclude(x => x.PermissionSet)
.Include(x => x.PermissionSet);
@@ -603,6 +664,15 @@ namespace Tgstation.Server.Host.Authority
})
.ToList()
?? new List<Models.OAuthConnection>(),
OidcConnections = model
.OidcConnections
?.Select(x => new Models.OidcConnection
{
SchemeKey = x.SchemeKey,
ExternalUserId = x.ExternalUserId,
})
.ToList()
?? new List<Models.OidcConnection>(),
};
}
@@ -0,0 +1,45 @@
using System;
namespace Tgstation.Server.Host.Configuration
{
/// <summary>
/// Configuration for an OpenID Connect provider.
/// </summary>
public sealed class OidcConfiguration
{
/// <summary>
/// The <see cref="Uri"/> containing the .well-known endpoint for the provider.
/// </summary>
public Uri? Authority { get; set; }
/// <summary>
/// The OIDC client ID.
/// </summary>
public string? ClientId { get; set; }
/// <summary>
/// The OIDC client secret.
/// </summary>
public string? ClientSecret { get; set; }
/// <summary>
/// The provider's name as it should be displayed to the user.
/// </summary>
public string? FriendlyName { get; set; }
/// <summary>
/// The path to return to once OIDC authentication is complete. On success the "code" and "state" query strings will be set, containing a TGS token and "oidc.(scheme key)" strings respectively. On failure the "error" query string will be set with a user readable error message.
/// </summary>
public string? ReturnPath { get; set; } = "/app";
/// <summary>
/// Colour that should be used to theme this OIDC provider.
/// </summary>
public string? ThemeColour { get; set; }
/// <summary>
/// Image URL that should be used to theme this OIDC provider.
/// </summary>
public Uri? ThemeIconUrl { get; set; }
}
}
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using Swashbuckle.AspNetCore.SwaggerGen;
@@ -87,5 +88,23 @@ namespace Tgstation.Server.Host.Configuration
/// Backing field for <see cref="OAuth"/>.
/// </summary>
IDictionary<OAuthProvider, OAuthConfiguration>? oAuth;
/// <summary>
/// OIDC provider settings keyed by scheme name.
/// </summary>
public IDictionary<string, OidcConfiguration>? OpenIDConnect { get; set; }
/// <summary>
/// Get the <see cref="OidcProviderInfo"/>s from the <see cref="SecurityConfiguration"/>.
/// </summary>
/// <returns>An <see cref="IEnumerable{T}"/> of <see cref="OidcProviderInfo"/>s.</returns>
public IEnumerable<OidcProviderInfo> OidcProviderInfos()
=> OpenIDConnect?.Select(oidcConfig => new OidcProviderInfo
{
SchemeKey = oidcConfig.Key,
FriendlyName = oidcConfig.Value.FriendlyName ?? oidcConfig.Key,
ThemeColour = oidcConfig.Value.ThemeColour,
ThemeIconUrl = oidcConfig.Value.ThemeIconUrl,
}) ?? Enumerable.Empty<OidcProviderInfo>();
}
}
@@ -70,6 +70,11 @@ namespace Tgstation.Server.Host.Controllers
/// </summary>
readonly GeneralConfiguration generalConfiguration;
/// <summary>
/// The <see cref="SecurityConfiguration"/> for the <see cref="ApiRootController"/>.
/// </summary>
readonly SecurityConfiguration securityConfiguration;
/// <summary>
/// Initializes a new instance of the <see cref="ApiRootController"/> class.
/// </summary>
@@ -81,6 +86,7 @@ namespace Tgstation.Server.Host.Controllers
/// <param name="swarmService">The value of <see cref="swarmService"/>.</param>
/// <param name="serverControl">The value of <see cref="serverControl"/>.</param>
/// <param name="generalConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="generalConfiguration"/>.</param>
/// <param name="securityConfigurationOptions">The <see cref="IOptionsSnapshot{TOptions}"/> containing the value of <see cref="securityConfiguration"/>.</param>
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="ApiController"/>.</param>
/// <param name="apiHeadersProvider">The <see cref="IApiHeadersProvider"/> for the <see cref="ApiController"/>.</param>
/// <param name="loginAuthority">The value of <see cref="loginAuthority"/>.</param>
@@ -93,6 +99,7 @@ namespace Tgstation.Server.Host.Controllers
ISwarmService swarmService,
IServerControl serverControl,
IOptions<GeneralConfiguration> generalConfigurationOptions,
IOptionsSnapshot<SecurityConfiguration> securityConfigurationOptions,
ILogger<ApiRootController> logger,
IApiHeadersProvider apiHeadersProvider,
IRestAuthorityInvoker<ILoginAuthority> loginAuthority)
@@ -109,6 +116,7 @@ namespace Tgstation.Server.Host.Controllers
this.swarmService = swarmService ?? throw new ArgumentNullException(nameof(swarmService));
this.serverControl = serverControl ?? throw new ArgumentNullException(nameof(serverControl));
generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
securityConfiguration = securityConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(securityConfigurationOptions));
this.loginAuthority = loginAuthority ?? throw new ArgumentNullException(nameof(loginAuthority));
}
@@ -162,6 +170,7 @@ namespace Tgstation.Server.Host.Controllers
?.Select(swarmServerInfo => new SwarmServerResponse(swarmServerInfo))
.ToList(),
OAuthProviderInfos = oAuthProviders.ProviderInfos(),
OidcProviderInfos = securityConfiguration.OidcProviderInfos().ToList(),
UpdateInProgress = serverControl.UpdateInProgress,
});
}
+124 -8
View File
@@ -3,6 +3,7 @@ using System.Collections.Frozen;
using System.Collections.Generic;
using System.Globalization;
using System.Threading.Tasks;
using System.Web;
using Cyberboss.AspNetCore.AsyncInitializer;
@@ -13,7 +14,9 @@ using HotChocolate.Subscriptions;
using HotChocolate.Types;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Cors.Infrastructure;
using Microsoft.AspNetCore.Hosting;
@@ -27,6 +30,7 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Newtonsoft.Json;
@@ -121,6 +125,14 @@ namespace Tgstation.Server.Host.Core
services.AddSingleton<IWatchdogFactory, TSystemWatchdogFactory>();
}
/// <summary>
/// Get the OIDC authentication scheme name for a given <paramref name="schemeKey"/>.
/// </summary>
/// <param name="schemeKey">The scheme key for the <see cref="OidcConfiguration"/>.</param>
/// <returns>The authentication scheme name.</returns>
static string GetOidcScheme(string schemeKey)
=> ApiHeaders.OpenIDConnectAuthenticationSchemePrefix + schemeKey;
/// <summary>
/// Initializes a new instance of the <see cref="Application"/> class.
/// </summary>
@@ -239,7 +251,7 @@ namespace Tgstation.Server.Host.Core
postSetupServices.FileLoggingConfiguration);
// configure authentication pipeline
ConfigureAuthenticationPipeline(services);
ConfigureAuthenticationPipeline(services, postSetupServices.SecurityConfiguration);
// add mvc, configure the json serializer settings
var jsonVersionConverterList = new List<JsonConverter>
@@ -316,9 +328,21 @@ namespace Tgstation.Server.Host.Core
.AddScoped<GraphQL.Subscriptions.ITopicEventReceiver, ShutdownAwareTopicEventReceiver>()
.AddGraphQLServer()
.AddAuthorization(
options => options.AddPolicy(
TgsAuthorizeAttribute.PolicyName,
builder => builder.RequireRole(TgsAuthorizeAttribute.UserEnabledRole)))
options =>
{
options.AddPolicy(
TgsAuthorizeAttribute.PolicyName,
builder => builder
.RequireAuthenticatedUser()
.RequireRole(TgsAuthorizeAttribute.UserEnabledRole));
options.AddPolicy(
"testingasdf",
builder =>
{
builder.RequireAuthenticatedUser();
builder.AuthenticationSchemes.Add(CookieAuthenticationDefaults.AuthenticationScheme);
});
})
.ModifyOptions(options =>
{
options.EnsureAllNodesCanBeResolved = true;
@@ -548,6 +572,7 @@ namespace Tgstation.Server.Host.Core
/// <param name="controlPanelConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the <see cref="ControlPanelConfiguration"/> to use.</param>
/// <param name="generalConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the <see cref="GeneralConfiguration"/> to use.</param>
/// <param name="databaseConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the <see cref="DatabaseConfiguration"/> to use.</param>
/// <param name="securityConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the <see cref="SecurityConfiguration"/> to use.</param>
/// <param name="swarmConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the <see cref="SwarmConfiguration"/> to use.</param>
/// <param name="internalConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the <see cref="InternalConfiguration"/> to use.</param>
/// <param name="logger">The <see cref="Microsoft.Extensions.Logging.ILogger"/> for the <see cref="Application"/>.</param>
@@ -560,6 +585,7 @@ namespace Tgstation.Server.Host.Core
IOptions<ControlPanelConfiguration> controlPanelConfigurationOptions,
IOptions<GeneralConfiguration> generalConfigurationOptions,
IOptions<DatabaseConfiguration> databaseConfigurationOptions,
IOptions<SecurityConfiguration> securityConfigurationOptions,
IOptions<SwarmConfiguration> swarmConfigurationOptions,
IOptions<InternalConfiguration> internalConfigurationOptions,
ILogger<Application> logger)
@@ -728,6 +754,20 @@ namespace Tgstation.Server.Host.Core
logger.LogTrace("Prometheus disabled");
endpoints.MapHealthChecks("/health");
var oidcConfig = securityConfigurationOptions.Value.OpenIDConnect;
if (oidcConfig == null)
return;
foreach (var kvp in oidcConfig)
endpoints.MapGet(
$"/oidc/{kvp.Key}/signin",
context => context.ChallengeAsync(
GetOidcScheme(kvp.Key),
new AuthenticationProperties
{
RedirectUri = $"/oidc/{kvp.Key}/landing",
}));
});
// 404 anything that gets this far
@@ -748,7 +788,8 @@ namespace Tgstation.Server.Host.Core
/// Configure the <paramref name="services"/> for the authentication pipeline.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> to configure.</param>
void ConfigureAuthenticationPipeline(IServiceCollection services)
/// <param name="securityConfiguration">The <see cref="SecurityConfiguration"/>.</param>
void ConfigureAuthenticationPipeline(IServiceCollection services, SecurityConfiguration securityConfiguration)
{
services.AddHttpContextAccessor();
services.AddScoped<IApiHeadersProvider, ApiHeadersProvider>();
@@ -768,8 +809,11 @@ namespace Tgstation.Server.Host.Core
.CurrentAuthenticationContext);
services.AddScoped<IClaimsTransformation, AuthenticationContextClaimsTransformation>();
services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
var authBuilder = services
.AddAuthentication(options =>
{
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(jwtBearerOptions =>
{
// this line isn't actually run until the first request is made
@@ -798,13 +842,85 @@ namespace Tgstation.Server.Host.Core
.HttpContext
.RequestServices
.GetRequiredService<ITokenValidator>()
.ValidateToken(
.ValidateTgsToken(
context,
context
.HttpContext
.RequestAborted),
};
});
var oidcConfig = securityConfiguration.OpenIDConnect;
if (oidcConfig == null || oidcConfig.Count == 0)
return;
authBuilder.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme);
foreach (var kvp in oidcConfig)
{
var configName = kvp.Key;
authBuilder
.AddOpenIdConnect(
GetOidcScheme(configName),
options =>
{
var config = kvp.Value;
options.Authority = config.Authority?.ToString();
options.ClientId = config.ClientId;
options.ClientSecret = config.ClientSecret;
options.Scope.Add(OpenIdConnectScope.OpenId);
options.Scope.Add(OpenIdConnectScope.OfflineAccess);
#if DEBUG
options.RequireHttpsMetadata = false;
#endif
options.SaveTokens = true;
options.ResponseType = OpenIdConnectResponseType.Code;
options.MapInboundClaims = false;
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
var basePath = $"/oidc/{configName}/";
options.CallbackPath = new PathString(basePath + "signin-callback");
options.SignedOutCallbackPath = new PathString(basePath + "signout-callback");
options.RemoteSignOutPath = new PathString(basePath + "signout");
options.Events = new OpenIdConnectEvents
{
OnTokenValidated = context => context
.HttpContext
.RequestServices
.GetRequiredService<ITokenValidator>()
.ValidateOidcToken(
context,
context
.HttpContext
.RequestAborted),
OnRemoteFailure = context =>
{
context.HandleResponse();
context.HttpContext.Response.Redirect($"{config.ReturnPath}?error={HttpUtility.UrlEncode(context.Failure?.Message ?? $"{options.Events.OnRemoteFailure} was called without an {nameof(Exception)}!")}&state=oidc.{HttpUtility.UrlEncode(configName)}");
return Task.CompletedTask;
},
OnTicketReceived = context =>
{
var services = context
.HttpContext
.RequestServices;
var tokenFactory = services
.GetRequiredService<ITokenFactory>();
var authenticationContext = services
.GetRequiredService<IAuthenticationContext>();
context.HandleResponse();
context.HttpContext.Response.Redirect($"{config.ReturnPath}?code={HttpUtility.UrlEncode(tokenFactory.CreateToken(authenticationContext.User, true))}&state=oidc.{HttpUtility.UrlEncode(configName)}");
return Task.CompletedTask;
},
};
});
}
}
}
}
@@ -98,6 +98,11 @@ namespace Tgstation.Server.Host.Database
/// </summary>
public DbSet<OAuthConnection> OAuthConnections { get; set; }
/// <summary>
/// The <see cref="OidcConnection"/>s in the <see cref="DatabaseContext"/>.
/// </summary>
public DbSet<OidcConnection> OidcConnections { get; set; }
/// <summary>
/// The <see cref="PermissionSet"/>s in the <see cref="DatabaseContext"/>.
/// </summary>
@@ -153,6 +158,9 @@ namespace Tgstation.Server.Host.Database
/// <inheritdoc />
IDatabaseCollection<OAuthConnection> IDatabaseContext.OAuthConnections => oAuthConnections;
/// <inheritdoc />
IDatabaseCollection<OidcConnection> IDatabaseContext.OidcConnections => oidcConnections;
/// <inheritdoc />
IDatabaseCollection<UserGroup> IDatabaseContext.Groups => groups;
@@ -239,6 +247,11 @@ namespace Tgstation.Server.Host.Database
/// </summary>
readonly IDatabaseCollection<OAuthConnection> oAuthConnections;
/// <summary>
/// Backing field for <see cref="IDatabaseContext.OidcConnections"/>.
/// </summary>
readonly IDatabaseCollection<OidcConnection> oidcConnections;
/// <summary>
/// Backing field for <see cref="IDatabaseContext.Groups"/>.
/// </summary>
@@ -288,6 +301,7 @@ namespace Tgstation.Server.Host.Database
jobsCollection = new DatabaseCollection<Job>(Jobs!);
reattachInformationsCollection = new DatabaseCollection<ReattachInformation>(ReattachInformations!);
oAuthConnections = new DatabaseCollection<OAuthConnection>(OAuthConnections!);
oidcConnections = new DatabaseCollection<OidcConnection>(OidcConnections!);
groups = new DatabaseCollection<UserGroup>(Groups!);
permissionSets = new DatabaseCollection<PermissionSet>(PermissionSets!);
}
@@ -387,8 +401,10 @@ namespace Tgstation.Server.Host.Database
userModel.HasIndex(x => x.SystemIdentifier).IsUnique();
userModel.HasMany(x => x.TestMerges).WithOne(x => x.MergedBy).OnDelete(DeleteBehavior.Restrict);
userModel.HasMany(x => x.OAuthConnections).WithOne(x => x.User).OnDelete(DeleteBehavior.Cascade);
userModel.HasMany(x => x.OidcConnections).WithOne(x => x.User).OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<OAuthConnection>().HasIndex(x => new { x.Provider, x.ExternalUserId }).IsUnique();
modelBuilder.Entity<OidcConnection>().HasIndex(x => new { x.SchemeKey, x.ExternalUserId }).IsUnique();
var groupsModel = modelBuilder.Entity<UserGroup>();
groupsModel.HasIndex(x => x.Name).IsUnique();
@@ -90,6 +90,11 @@ namespace Tgstation.Server.Host.Database
/// </summary>
IDatabaseCollection<OAuthConnection> OAuthConnections { get; }
/// <summary>
/// The <see cref="DbSet{TEntity}"/> for <see cref="OidcConnection"/>s.
/// </summary>
IDatabaseCollection<OidcConnection> OidcConnections { get; }
/// <summary>
/// The <see cref="DbSet{TEntity}"/> for <see cref="UserGroup"/>s.
/// </summary>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,57 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
namespace Tgstation.Server.Host.Database.Migrations
{
/// <inheritdoc />
public partial class MSAddOidcConnections : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
ArgumentNullException.ThrowIfNull(migrationBuilder);
migrationBuilder.CreateTable(
name: "OidcConnections",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
UserId = table.Column<long>(type: "bigint", nullable: false),
SchemeKey = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
ExternalUserId = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
},
constraints: table =>
{
table.PrimaryKey("PK_OidcConnections", x => x.Id);
table.ForeignKey(
name: "FK_OidcConnections_Users_UserId",
column: x => x.UserId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_OidcConnections_SchemeKey_ExternalUserId",
table: "OidcConnections",
columns: new[] { "SchemeKey", "ExternalUserId" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_OidcConnections_UserId",
table: "OidcConnections",
column: "UserId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
ArgumentNullException.ThrowIfNull(migrationBuilder);
migrationBuilder.DropTable(
name: "OidcConnections");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,61 @@
using System;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
namespace Tgstation.Server.Host.Database.Migrations
{
/// <inheritdoc />
public partial class MYAddOidcConnections : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
ArgumentNullException.ThrowIfNull(migrationBuilder);
migrationBuilder.CreateTable(
name: "OidcConnections",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
UserId = table.Column<long>(type: "bigint", nullable: false),
SchemeKey = table.Column<string>(type: "varchar(100)", maxLength: 100, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
ExternalUserId = table.Column<string>(type: "varchar(100)", maxLength: 100, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
},
constraints: table =>
{
table.PrimaryKey("PK_OidcConnections", x => x.Id);
table.ForeignKey(
name: "FK_OidcConnections_Users_UserId",
column: x => x.UserId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateIndex(
name: "IX_OidcConnections_SchemeKey_ExternalUserId",
table: "OidcConnections",
columns: new[] { "SchemeKey", "ExternalUserId" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_OidcConnections_UserId",
table: "OidcConnections",
column: "UserId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
ArgumentNullException.ThrowIfNull(migrationBuilder);
migrationBuilder.DropTable(
name: "OidcConnections");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,59 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace Tgstation.Server.Host.Database.Migrations
{
/// <inheritdoc />
public partial class PGAddOidcConnections : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
ArgumentNullException.ThrowIfNull(migrationBuilder);
migrationBuilder.CreateTable(
name: "OidcConnections",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
UserId = table.Column<long>(type: "bigint", nullable: false),
SchemeKey = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
ExternalUserId = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
},
constraints: table =>
{
table.PrimaryKey("PK_OidcConnections", x => x.Id);
table.ForeignKey(
name: "FK_OidcConnections_Users_UserId",
column: x => x.UserId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_OidcConnections_SchemeKey_ExternalUserId",
table: "OidcConnections",
columns: new[] { "SchemeKey", "ExternalUserId" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_OidcConnections_UserId",
table: "OidcConnections",
column: "UserId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
ArgumentNullException.ThrowIfNull(migrationBuilder);
migrationBuilder.DropTable(
name: "OidcConnections");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,57 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
namespace Tgstation.Server.Host.Database.Migrations
{
/// <inheritdoc />
public partial class SLAddOidcConnections : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
ArgumentNullException.ThrowIfNull(migrationBuilder);
migrationBuilder.CreateTable(
name: "OidcConnections",
columns: table => new
{
Id = table.Column<long>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
UserId = table.Column<long>(type: "INTEGER", nullable: false),
SchemeKey = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
ExternalUserId = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
},
constraints: table =>
{
table.PrimaryKey("PK_OidcConnections", x => x.Id);
table.ForeignKey(
name: "FK_OidcConnections_Users_UserId",
column: x => x.UserId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_OidcConnections_SchemeKey_ExternalUserId",
table: "OidcConnections",
columns: new[] { "SchemeKey", "ExternalUserId" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_OidcConnections_UserId",
table: "OidcConnections",
column: "UserId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
ArgumentNullException.ThrowIfNull(migrationBuilder);
migrationBuilder.DropTable(
name: "OidcConnections");
}
}
}
@@ -13,7 +13,7 @@ namespace Tgstation.Server.Host.Database
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.10")
.HasAnnotation("ProductVersion", "8.0.13")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
@@ -512,6 +512,37 @@ namespace Tgstation.Server.Host.Database
b.ToTable("OAuthConnections");
});
modelBuilder.Entity("Tgstation.Server.Host.Models.OidcConnection", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
b.Property<string>("ExternalUserId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.Property<string>("SchemeKey")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.Property<long>("UserId")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("UserId");
b.HasIndex("SchemeKey", "ExternalUserId")
.IsUnique();
b.ToTable("OidcConnections");
});
modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b =>
{
b.Property<long?>("Id")
@@ -996,6 +1027,17 @@ namespace Tgstation.Server.Host.Database
b.Navigation("User");
});
modelBuilder.Entity("Tgstation.Server.Host.Models.OidcConnection", b =>
{
b.HasOne("Tgstation.Server.Host.Models.User", "User")
.WithMany("OidcConnections")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b =>
{
b.HasOne("Tgstation.Server.Host.Models.UserGroup", "Group")
@@ -1152,6 +1194,8 @@ namespace Tgstation.Server.Host.Database
b.Navigation("OAuthConnections");
b.Navigation("OidcConnections");
b.Navigation("PermissionSet");
b.Navigation("TestMerges");
@@ -13,7 +13,7 @@ namespace Tgstation.Server.Host.Database
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.10")
.HasAnnotation("ProductVersion", "8.0.13")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
@@ -471,6 +471,37 @@ namespace Tgstation.Server.Host.Database
b.ToTable("OAuthConnections");
});
modelBuilder.Entity("Tgstation.Server.Host.Models.OidcConnection", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<string>("ExternalUserId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("SchemeKey")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<long>("UserId")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("UserId");
b.HasIndex("SchemeKey", "ExternalUserId")
.IsUnique();
b.ToTable("OidcConnections");
});
modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b =>
{
b.Property<long?>("Id")
@@ -919,6 +950,17 @@ namespace Tgstation.Server.Host.Database
b.Navigation("User");
});
modelBuilder.Entity("Tgstation.Server.Host.Models.OidcConnection", b =>
{
b.HasOne("Tgstation.Server.Host.Models.User", "User")
.WithMany("OidcConnections")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b =>
{
b.HasOne("Tgstation.Server.Host.Models.UserGroup", "Group")
@@ -1075,6 +1117,8 @@ namespace Tgstation.Server.Host.Database
b.Navigation("OAuthConnections");
b.Navigation("OidcConnections");
b.Navigation("PermissionSet");
b.Navigation("TestMerges");
@@ -13,7 +13,7 @@ namespace Tgstation.Server.Host.Database
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.10")
.HasAnnotation("ProductVersion", "8.0.13")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
@@ -474,6 +474,37 @@ namespace Tgstation.Server.Host.Database
b.ToTable("OAuthConnections");
});
modelBuilder.Entity("Tgstation.Server.Host.Models.OidcConnection", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
b.Property<string>("ExternalUserId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("SchemeKey")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<long>("UserId")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("UserId");
b.HasIndex("SchemeKey", "ExternalUserId")
.IsUnique();
b.ToTable("OidcConnections");
});
modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b =>
{
b.Property<long?>("Id")
@@ -925,6 +956,17 @@ namespace Tgstation.Server.Host.Database
b.Navigation("User");
});
modelBuilder.Entity("Tgstation.Server.Host.Models.OidcConnection", b =>
{
b.HasOne("Tgstation.Server.Host.Models.User", "User")
.WithMany("OidcConnections")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b =>
{
b.HasOne("Tgstation.Server.Host.Models.UserGroup", "Group")
@@ -1081,6 +1123,8 @@ namespace Tgstation.Server.Host.Database
b.Navigation("OAuthConnections");
b.Navigation("OidcConnections");
b.Navigation("PermissionSet");
b.Navigation("TestMerges");
@@ -12,7 +12,7 @@ namespace Tgstation.Server.Host.Database
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "8.0.10");
modelBuilder.HasAnnotation("ProductVersion", "8.0.13");
modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b =>
{
@@ -460,6 +460,35 @@ namespace Tgstation.Server.Host.Database
b.ToTable("OAuthConnections");
});
modelBuilder.Entity("Tgstation.Server.Host.Models.OidcConnection", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ExternalUserId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("SchemeKey")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<long>("UserId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("UserId");
b.HasIndex("SchemeKey", "ExternalUserId")
.IsUnique();
b.ToTable("OidcConnections");
});
modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b =>
{
b.Property<long?>("Id")
@@ -892,6 +921,17 @@ namespace Tgstation.Server.Host.Database
b.Navigation("User");
});
modelBuilder.Entity("Tgstation.Server.Host.Models.OidcConnection", b =>
{
b.HasOne("Tgstation.Server.Host.Models.User", "User")
.WithMany("OidcConnections")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b =>
{
b.HasOne("Tgstation.Server.Host.Models.UserGroup", "Group")
@@ -1048,6 +1088,8 @@ namespace Tgstation.Server.Host.Database
b.Navigation("OAuthConnections");
b.Navigation("OidcConnections");
b.Navigation("PermissionSet");
b.Navigation("TestMerges");
@@ -66,6 +66,7 @@ namespace Tgstation.Server.Host.Extensions
applicationBuilder.ApplicationServices.GetRequiredService<IOptions<ControlPanelConfiguration>>(),
applicationBuilder.ApplicationServices.GetRequiredService<IOptions<GeneralConfiguration>>(),
applicationBuilder.ApplicationServices.GetRequiredService<IOptions<DatabaseConfiguration>>(),
applicationBuilder.ApplicationServices.GetRequiredService<IOptions<SecurityConfiguration>>(),
applicationBuilder.ApplicationServices.GetRequiredService<IOptions<SwarmConfiguration>>(),
applicationBuilder.ApplicationServices.GetRequiredService<IOptions<InternalConfiguration>>(),
applicationBuilder.ApplicationServices.GetRequiredService<ILogger<Application>>());
@@ -33,6 +33,7 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
/// <param name="password">The password of the <see cref="User"/>.</param>
/// <param name="enabled">If the <see cref="User"/> is <see cref="User.Enabled"/>.</param>
/// <param name="oAuthConnections">The <see cref="OAuthConnection"/>s for the user.</param>
/// <param name="oidcConnections">The <see cref="OidcConnection"/>s for the user.</param>
/// <param name="permissionSet">The owned <see cref="PermissionSet"/> of the user.</param>
/// <param name="userAuthority">The <see cref="IGraphQLAuthorityInvoker{TAuthority}"/> for the <see cref="IUserAuthority"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
@@ -44,6 +45,7 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
string password,
bool? enabled,
IEnumerable<OAuthConnection>? oAuthConnections,
IEnumerable<OidcConnection>? oidcConnections,
PermissionSetInput? permissionSet,
[Service] IGraphQLAuthorityInvoker<IUserAuthority> userAuthority,
CancellationToken cancellationToken)
@@ -73,6 +75,13 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
Provider = oAuthConnection.Provider,
})
.ToList(),
OidcConnections = oidcConnections
?.Select(oidcConnection => new Api.Models.OidcConnection
{
ExternalUserId = oidcConnection.ExternalUserId,
SchemeKey = oidcConnection.SchemeKey,
})
.ToList(),
},
false,
cancellationToken));
@@ -85,6 +94,7 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
/// <param name="password">The password of the <see cref="User"/>.</param>
/// <param name="enabled">If the <see cref="User"/> is <see cref="User.Enabled"/>.</param>
/// <param name="oAuthConnections">The <see cref="OAuthConnection"/>s for the user.</param>
/// <param name="oidcConnections">The <see cref="OidcConnection"/>s for the user.</param>
/// <param name="groupId">The <see cref="Entity.Id"/> of the <see cref="UserGroup"/> the <see cref="User"/> will belong to.</param>
/// <param name="userAuthority">The <see cref="IGraphQLAuthorityInvoker{TAuthority}"/> for the <see cref="IUserAuthority"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
@@ -96,6 +106,7 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
string password,
bool? enabled,
IEnumerable<OAuthConnection>? oAuthConnections,
IEnumerable<OidcConnection>? oidcConnections,
[ID(nameof(UserGroup))] long groupId,
[Service] IGraphQLAuthorityInvoker<IUserAuthority> userAuthority,
CancellationToken cancellationToken)
@@ -122,16 +133,24 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
Provider = oAuthConnection.Provider,
})
.ToList(),
OidcConnections = oidcConnections
?.Select(oidcConnection => new Api.Models.OidcConnection
{
ExternalUserId = oidcConnection.ExternalUserId,
SchemeKey = oidcConnection.SchemeKey,
})
.ToList(),
},
false,
cancellationToken));
}
/// <summary>
/// Creates a TGS user authenticated with one or mor <see cref="OAuthConnection"/>s specifying a personal <see cref="PermissionSet"/>.
/// Creates a TGS user authenticated with one or more <see cref="OAuthConnection"/>s or <see cref="OidcConnection"/>s specifying a personal <see cref="PermissionSet"/>.
/// </summary>
/// <param name="name">The <see cref="NamedEntity.Name"/> of the <see cref="User"/>.</param>
/// <param name="oAuthConnections">The <see cref="OAuthConnection"/>s for the user.</param>
/// <param name="oidcConnections">The <see cref="OidcConnection"/>s for the user.</param>
/// <param name="enabled">If the <see cref="User"/> is <see cref="User.Enabled"/>.</param>
/// <param name="permissionSet">The owned <see cref="PermissionSet"/> of the user.</param>
/// <param name="userAuthority">The <see cref="IGraphQLAuthorityInvoker{TAuthority}"/> for the <see cref="IUserAuthority"/>.</param>
@@ -139,9 +158,10 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
/// <returns>The created <see cref="User"/>.</returns>
[TgsGraphQLAuthorize<IUserAuthority>(nameof(IUserAuthority.Create))]
[Error(typeof(ErrorMessageException))]
public ValueTask<User> CreateUserByOAuthAndPermissionSet(
public ValueTask<User> CreateUserByServiceConnectionAndPermissionSet(
string name,
IEnumerable<OAuthConnection> oAuthConnections,
IEnumerable<OAuthConnection>? oAuthConnections,
IEnumerable<OidcConnection>? oidcConnections,
bool? enabled,
PermissionSetInput? permissionSet,
[Service] IGraphQLAuthorityInvoker<IUserAuthority> userAuthority,
@@ -172,16 +192,24 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
Provider = oAuthConnection.Provider,
})
.ToList(),
OidcConnections = oidcConnections
?.Select(oidcConnection => new Api.Models.OidcConnection
{
ExternalUserId = oidcConnection.ExternalUserId,
SchemeKey = oidcConnection.SchemeKey,
})
.ToList(),
},
true,
cancellationToken));
}
/// <summary>
/// Creates a TGS user specifying the <see cref="UserGroup"/> they will belong to.
/// Creates a TGS user using <see cref="OAuthConnection"/>s and/or <see cref="OidcConnection"/>s specifying the <see cref="UserGroup"/> they will belong to.
/// </summary>
/// <param name="name">The <see cref="NamedEntity.Name"/> of the <see cref="User"/>.</param>
/// <param name="oAuthConnections">The <see cref="OAuthConnection"/>s for the user.</param>
/// <param name="oidcConnections">The <see cref="OidcConnection"/>s for the user.</param>
/// <param name="groupId">The <see cref="Entity.Id"/> of the <see cref="UserGroup"/> the <see cref="User"/> will belong to.</param>
/// <param name="enabled">If the <see cref="User"/> is <see cref="User.Enabled"/>.</param>
/// <param name="userAuthority">The <see cref="IGraphQLAuthorityInvoker{TAuthority}"/> for the <see cref="IUserAuthority"/>.</param>
@@ -189,9 +217,10 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
/// <returns>The created <see cref="User"/>.</returns>
[TgsGraphQLAuthorize<IUserAuthority>(nameof(IUserAuthority.Create))]
[Error(typeof(ErrorMessageException))]
public ValueTask<User> CreateUserByOAuthAndGroup(
public ValueTask<User> CreateUserByServiceConnectionAndGroup(
string name,
IEnumerable<OAuthConnection> oAuthConnections,
IEnumerable<OidcConnection> oidcConnections,
[ID(nameof(UserGroup))] long groupId,
bool? enabled,
[Service] IGraphQLAuthorityInvoker<IUserAuthority> userAuthority,
@@ -219,6 +248,13 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
Provider = oAuthConnection.Provider,
})
.ToList(),
OidcConnections = oidcConnections
?.Select(oidcConnection => new Api.Models.OidcConnection
{
ExternalUserId = oidcConnection.ExternalUserId,
SchemeKey = oidcConnection.SchemeKey,
})
.ToList(),
},
true,
cancellationToken));
@@ -230,6 +266,7 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
/// <param name="systemIdentifier">The <see cref="User.SystemIdentifier"/> of the <see cref="User"/>.</param>
/// <param name="enabled">If the <see cref="User"/> is <see cref="User.Enabled"/>.</param>
/// <param name="oAuthConnections">The <see cref="OAuthConnection"/>s for the user.</param>
/// <param name="oidcConnections">The <see cref="OidcConnection"/>s for the user.</param>
/// <param name="permissionSet">The owned <see cref="PermissionSet"/> of the user.</param>
/// <param name="userAuthority">The <see cref="IGraphQLAuthorityInvoker{TAuthority}"/> for the <see cref="IUserAuthority"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
@@ -240,6 +277,7 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
string systemIdentifier,
bool? enabled,
IEnumerable<OAuthConnection>? oAuthConnections,
IEnumerable<OidcConnection>? oidcConnections,
PermissionSetInput permissionSet,
[Service] IGraphQLAuthorityInvoker<IUserAuthority> userAuthority,
CancellationToken cancellationToken)
@@ -267,6 +305,13 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
Provider = oAuthConnection.Provider,
})
.ToList(),
OidcConnections = oidcConnections
?.Select(oidcConnection => new Api.Models.OidcConnection
{
ExternalUserId = oidcConnection.ExternalUserId,
SchemeKey = oidcConnection.SchemeKey,
})
.ToList(),
},
false,
cancellationToken));
@@ -279,6 +324,7 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
/// <param name="enabled">If the <see cref="User"/> is <see cref="User.Enabled"/>.</param>
/// <param name="groupId">The <see cref="Entity.Id"/> of the <see cref="UserGroup"/> the <see cref="User"/> will belong to.</param>
/// <param name="oAuthConnections">The <see cref="OAuthConnection"/>s for the user.</param>
/// <param name="oidcConnections">The <see cref="OidcConnection"/>s for the user.</param>
/// <param name="userAuthority">The <see cref="IGraphQLAuthorityInvoker{TAuthority}"/> for the <see cref="IUserAuthority"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>The created <see cref="User"/>.</returns>
@@ -289,6 +335,7 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
bool? enabled,
[ID(nameof(UserGroup))] long groupId,
IEnumerable<OAuthConnection>? oAuthConnections,
IEnumerable<OidcConnection>? oidcConnections,
[Service] IGraphQLAuthorityInvoker<IUserAuthority> userAuthority,
CancellationToken cancellationToken)
{
@@ -312,6 +359,13 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
Provider = oAuthConnection.Provider,
})
.ToList(),
OidcConnections = oidcConnections
?.Select(oidcConnection => new Api.Models.OidcConnection
{
ExternalUserId = oidcConnection.ExternalUserId,
SchemeKey = oidcConnection.SchemeKey,
})
.ToList(),
},
false,
cancellationToken));
@@ -346,17 +400,19 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
}
/// <summary>
/// Sets the current user's <see cref="OAuthConnection"/>s.
/// Sets the current user's <see cref="OAuthConnection"/>s and <see cref="OidcConnection"/>s.
/// </summary>
/// <param name="newOAuthConnections">The new <see cref="OAuthConnection"/>s for the current user.</param>
/// <param name="newOAuthConnections">Optional new <see cref="OAuthConnection"/>s for the current <see cref="User"/>.</param>
/// <param name="newOidcConnections">Optional new <see cref="OidcConnection"/>s for the current <see cref="User"/>.</param>
/// <param name="authenticationContext">The <see cref="IAuthenticationContext"/> to get the <see cref="Entity.Id"/> of the user.</param>
/// <param name="userAuthority">The <see cref="IGraphQLAuthorityInvoker{TAuthority}"/> for the <see cref="IUserAuthority"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>The updated current <see cref="User"/>.</returns>
[TgsGraphQLAuthorize(AdministrationRights.WriteUsers | AdministrationRights.EditOwnOAuthConnections)]
[TgsGraphQLAuthorize(AdministrationRights.WriteUsers | AdministrationRights.EditOwnServiceConnections)]
[Error(typeof(ErrorMessageException))]
public ValueTask<User> SetCurrentOAuthConnections(
IEnumerable<OAuthConnection> newOAuthConnections,
public ValueTask<User> SetCurrentServiceConnections(
IEnumerable<OAuthConnection>? newOAuthConnections,
IEnumerable<OidcConnection>? newOidcConnections,
[Service] IAuthenticationContext authenticationContext,
[Service] IGraphQLAuthorityInvoker<IUserAuthority> userAuthority,
CancellationToken cancellationToken)
@@ -375,6 +431,13 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
Provider = oAuthConnection.Provider,
})
.ToList(),
OidcConnections = newOidcConnections
?.Select(oidcConnection => new Api.Models.OidcConnection
{
ExternalUserId = oidcConnection.ExternalUserId,
SchemeKey = oidcConnection.SchemeKey,
})
.ToList(),
},
cancellationToken));
}
@@ -387,6 +450,7 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
/// <param name="newPassword">Optional new password for the <see cref="User"/>. Only applicable to TGS users.</param>
/// <param name="enabled">Optional new <see cref="User.Enabled"/> status for the <see cref="User"/>.</param>
/// <param name="newOAuthConnections">Optional new <see cref="OAuthConnection"/>s for the <see cref="User"/>.</param>
/// <param name="newOidcConnections">Optional new <see cref="OidcConnection"/>s for the <see cref="User"/>.</param>
/// <param name="userAuthority">The <see cref="IGraphQLAuthorityInvoker{TAuthority}"/> for the <see cref="IUserAuthority"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>The updated <see cref="User"/>.</returns>
@@ -398,6 +462,7 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
string? newPassword,
bool? enabled,
IEnumerable<OAuthConnection>? newOAuthConnections,
IEnumerable<OidcConnection>? newOidcConnections,
[Service] IGraphQLAuthorityInvoker<IUserAuthority> userAuthority,
CancellationToken cancellationToken)
{
@@ -410,6 +475,7 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
null,
null,
newOAuthConnections,
newOidcConnections,
userAuthority,
cancellationToken);
}
@@ -422,7 +488,8 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
/// <param name="newPassword">Optional new password for the <see cref="User"/>. Only applicable to TGS users.</param>
/// <param name="enabled">Optional new <see cref="User.Enabled"/> status for the <see cref="User"/>.</param>
/// <param name="newPermissionSet">Updated owned <see cref="PermissionSet"/> for the user. Note that setting this on a <see cref="User"/> in a <see cref="UserGroup"/> will remove them from that group.</param>
/// <param name="newOAuthConnections">The new <see cref="OAuthConnection"/>s for the <see cref="User"/>.</param>
/// <param name="newOAuthConnections">Optional new <see cref="OAuthConnection"/>s for the <see cref="User"/>.</param>
/// <param name="newOidcConnections">Optional new <see cref="OidcConnection"/>s for the <see cref="User"/>.</param>
/// <param name="userAuthority">The <see cref="IGraphQLAuthorityInvoker{TAuthority}"/> for the <see cref="IUserAuthority"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>The updated <see cref="User"/>.</returns>
@@ -435,6 +502,7 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
bool? enabled,
PermissionSetInput newPermissionSet,
IEnumerable<OAuthConnection>? newOAuthConnections,
IEnumerable<OidcConnection>? newOidcConnections,
[Service] IGraphQLAuthorityInvoker<IUserAuthority> userAuthority,
CancellationToken cancellationToken)
{
@@ -447,6 +515,7 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
newPermissionSet,
null,
newOAuthConnections,
newOidcConnections,
userAuthority,
cancellationToken);
}
@@ -459,7 +528,8 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
/// <param name="newPassword">Optional new password for the <see cref="User"/>. Only applicable to TGS users.</param>
/// <param name="enabled">Optional new <see cref="User.Enabled"/> status for the <see cref="User"/>.</param>
/// <param name="newGroupId"><see cref="Entity.Id"/> of the <see cref="UserGroup"/> to move the <see cref="User"/> to.</param>
/// <param name="newOAuthConnections">The new <see cref="OAuthConnection"/>s for the <see cref="User"/>.</param>
/// <param name="newOAuthConnections">Optional new <see cref="OAuthConnection"/>s for the <see cref="User"/>.</param>
/// <param name="newOidcConnections">Optional new <see cref="OidcConnection"/>s for the <see cref="User"/>.</param>
/// <param name="userAuthority">The <see cref="IGraphQLAuthorityInvoker{TAuthority}"/> for the <see cref="IUserAuthority"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>The updated <see cref="User"/>.</returns>
@@ -472,6 +542,7 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
bool? enabled,
[ID(nameof(UserGroup))] long newGroupId,
IEnumerable<OAuthConnection>? newOAuthConnections,
IEnumerable<OidcConnection>? newOidcConnections,
[Service] IGraphQLAuthorityInvoker<IUserAuthority> userAuthority,
CancellationToken cancellationToken)
{
@@ -484,6 +555,7 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
null,
newGroupId,
newOAuthConnections,
newOidcConnections,
userAuthority,
cancellationToken);
}
@@ -498,6 +570,7 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
/// <param name="newPermissionSet">Optional updated new owned <see cref="PermissionSet"/> for the user. Note that setting this on a <see cref="User"/> in a <see cref="UserGroup"/> will remove them from that group.</param>
/// <param name="newGroupId">Optional <see cref="Entity.Id"/> of the <see cref="UserGroup"/> to move the <see cref="User"/> to.</param>
/// <param name="newOAuthConnections">Optional new <see cref="OAuthConnection"/>s for the <see cref="User"/>.</param>
/// <param name="newOidcConnections">Optional new <see cref="OidcConnection"/>s for the <see cref="User"/>.</param>
/// <param name="userAuthority">The <see cref="IGraphQLAuthorityInvoker{TAuthority}"/> for the <see cref="IUserAuthority"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>The updated <see cref="User"/>.</returns>
@@ -509,6 +582,7 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
PermissionSetInput? newPermissionSet,
long? newGroupId,
IEnumerable<OAuthConnection>? newOAuthConnections,
IEnumerable<OidcConnection>? newOidcConnections,
IGraphQLAuthorityInvoker<IUserAuthority> userAuthority,
CancellationToken cancellationToken)
=> userAuthority.InvokeTransformable<Models.User, User, UserGraphQLTransformer>(
@@ -539,6 +613,13 @@ namespace Tgstation.Server.Host.GraphQL.Mutations
Provider = oAuthConnection.Provider,
})
.ToList(),
OidcConnections = newOidcConnections
?.Select(oidcConnection => new Api.Models.OidcConnection
{
ExternalUserId = oidcConnection.ExternalUserId,
SchemeKey = oidcConnection.SchemeKey,
})
.ToList(),
},
cancellationToken));
}
@@ -11,7 +11,7 @@ namespace Tgstation.Server.Host.GraphQL.Types.OAuth
{
/// <summary>
/// The <see cref="OAuthProvider"/> of the <see cref="OAuthConnection"/>.
/// </summary>]
/// </summary>
public OAuthProvider Provider { get; }
/// <summary>
@@ -20,21 +20,6 @@ namespace Tgstation.Server.Host.GraphQL.Types.OAuth
/// </summary>
public RedirectOAuthProviderInfo? GitHub { get; }
/// <summary>
/// https://tgstation13.org.
/// </summary>
public RedirectOAuthProviderInfo? TGForums { get; }
/// <summary>
/// https://www.keycloak.org.
/// </summary>
public FullOAuthProviderInfo? Keycloak { get; }
/// <summary>
/// https://invisioncommunity.com.
/// </summary>
public FullOAuthProviderInfo? InvisionCommunity { get; }
/// <summary>
/// Initializes a new instance of the <see cref="OAuthProviderInfos"/> class.
/// </summary>
@@ -58,8 +43,6 @@ namespace Tgstation.Server.Host.GraphQL.Types.OAuth
Discord = TryBuild(OAuthProvider.Discord, info => new BasicOAuthProviderInfo(info));
GitHub = TryBuild(OAuthProvider.GitHub, info => new RedirectOAuthProviderInfo(info));
Keycloak = TryBuild(OAuthProvider.Keycloak, info => new FullOAuthProviderInfo(info));
InvisionCommunity = TryBuild(OAuthProvider.InvisionCommunity, info => new FullOAuthProviderInfo(info));
}
}
}
@@ -0,0 +1,31 @@
using System;
namespace Tgstation.Server.Host.GraphQL.Types.OAuth
{
/// <summary>
/// Represents a valid OAuth connection.
/// </summary>
public sealed class OidcConnection
{
/// <summary>
/// The scheme key of the <see cref="OidcConnection"/>.
/// </summary>]
public string SchemeKey { get; }
/// <summary>
/// The ID of the user in the OIDC provider ("sub" claim).
/// </summary>
public string ExternalUserId { get; }
/// <summary>
/// Initializes a new instance of the <see cref="OidcConnection"/> class.
/// </summary>
/// <param name="externalUserId">The value of <see cref="ExternalUserId"/>.</param>
/// <param name="schemeKey">The value of <see cref="SchemeKey"/>.</param>
public OidcConnection(string externalUserId, string schemeKey)
{
ExternalUserId = externalUserId ?? throw new ArgumentNullException(nameof(externalUserId));
SchemeKey = schemeKey ?? throw new ArgumentNullException(nameof(schemeKey));
}
}
}
@@ -105,6 +105,21 @@ namespace Tgstation.Server.Host.GraphQL.Types
authority => authority.OAuthConnections(Id, cancellationToken));
}
/// <summary>
/// List of <see cref="OidcConnection"/>s associated with the user if OIDC is configured.
/// </summary>
/// <param name="userAuthority">The <see cref="IGraphQLAuthorityInvoker{TAuthority}"/> for the <see cref="IUserAuthority"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a new <see cref="Array"/> of <see cref="OidcConnection"/>s for the <see cref="User"/> if OAuth is configured.</returns>
public ValueTask<OidcConnection[]> OidcConnections(
[Service] IGraphQLAuthorityInvoker<IUserAuthority> userAuthority,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(userAuthority);
return userAuthority.Invoke<OidcConnection[], OidcConnection[]>(
authority => authority.OidcConnections(Id, cancellationToken));
}
/// <summary>
/// The <see cref="PermissionSet"/> associated with the <see cref="User"/>.
/// </summary>
@@ -0,0 +1,31 @@
using System.ComponentModel.DataAnnotations;
namespace Tgstation.Server.Host.Models
{
/// <inheritdoc cref="Api.Models.OidcConnection" />
public sealed class OidcConnection : Api.Models.OidcConnection, ILegacyApiTransformable<Api.Models.OidcConnection>
{
/// <summary>
/// The row Id.
/// </summary>
public long Id { get; set; }
/// <summary>
/// The <see cref="Api.Models.EntityId.Id"/> of <see cref="User"/>.
/// </summary>
public long UserId { get; set; }
/// <summary>
/// The owning <see cref="Models.User"/>.
/// </summary>
[Required]
public User? User { get; set; }
/// <inheritdoc />
public Api.Models.OidcConnection ToApi() => new()
{
SchemeKey = SchemeKey,
ExternalUserId = ExternalUserId,
};
}
}
+9 -1
View File
@@ -73,10 +73,15 @@ namespace Tgstation.Server.Host.Models
public ICollection<TestMerge>? TestMerges { get; set; }
/// <summary>
/// The <see cref="TestMerge"/>s made by the <see cref="User"/>.
/// The <see cref="OAuthConnection"/>s for the <see cref="User"/>.
/// </summary>
public ICollection<OAuthConnection>? OAuthConnections { get; set; }
/// <summary>
/// The <see cref="OidcConnection"/>s for the <see cref="User"/>.
/// </summary>
public ICollection<OidcConnection>? OidcConnections { get; set; }
/// <summary>
/// Change a <see cref="UserName.Name"/> into a <see cref="CanonicalName"/>.
/// </summary>
@@ -104,6 +109,9 @@ namespace Tgstation.Server.Host.Models
result.OAuthConnections = OAuthConnections
?.Select(x => x.ToApi())
.ToList();
result.OidcConnections = OidcConnections
?.Select(x => x.ToApi())
.ToList();
result.Group = Group?.ToApi(false);
result.PermissionSet = PermissionSet?.ToApi();
return result;
@@ -5,7 +5,6 @@ using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@@ -63,6 +62,30 @@ namespace Tgstation.Server.Host.Security
/// </summary>
int initialized;
/// <summary>
/// Parse a <see cref="DateTimeOffset"/> out of a <see cref="Claim"/> in a given <paramref name="principal"/>.
/// </summary>
/// <param name="principal">The <see cref="ClaimsPrincipal"/> containing claims.</param>
/// <param name="key">The <see cref="Claim"/> name to parse from.</param>
/// <returns>The parsed <see cref="DateTimeOffset"/>.</returns>
static DateTimeOffset ParseTime(ClaimsPrincipal principal, string key)
{
var claim = principal.FindFirst(key);
if (claim == default)
throw new InvalidOperationException($"Missing '{key}' claim!");
try
{
return new DateTimeOffset(
EpochTime.DateTime(
Int64.Parse(claim.Value, CultureInfo.InvariantCulture)));
}
catch (Exception ex)
{
throw new InvalidOperationException($"Failed to parse '{key}'!", ex);
}
}
/// <summary>
/// Initializes a new instance of the <see cref="AuthenticationContextFactory"/> class.
/// </summary>
@@ -94,7 +117,7 @@ namespace Tgstation.Server.Host.Security
/// <inheritdoc />
#pragma warning disable CA1506 // TODO: Decomplexify
public async Task ValidateToken(TokenValidatedContext tokenValidatedContext, CancellationToken cancellationToken)
public async Task ValidateTgsToken(Microsoft.AspNetCore.Authentication.JwtBearer.TokenValidatedContext tokenValidatedContext, CancellationToken cancellationToken)
#pragma warning restore CA1506
{
ArgumentNullException.ThrowIfNull(tokenValidatedContext);
@@ -121,26 +144,8 @@ namespace Tgstation.Server.Host.Security
throw new InvalidOperationException("Failed to parse user ID!", e);
}
DateTimeOffset ParseTime(string key)
{
var claim = principal.FindFirst(key);
if (claim == default)
throw new InvalidOperationException($"Missing '{key}' claim!");
try
{
return new DateTimeOffset(
EpochTime.DateTime(
Int64.Parse(claim.Value, CultureInfo.InvariantCulture)));
}
catch (Exception ex)
{
throw new InvalidOperationException($"Failed to parse '{key}'!", ex);
}
}
var notBefore = ParseTime(JwtRegisteredClaimNames.Nbf);
var expires = ParseTime(JwtRegisteredClaimNames.Exp);
var notBefore = ParseTime(principal, JwtRegisteredClaimNames.Nbf);
var expires = ParseTime(principal, JwtRegisteredClaimNames.Exp);
var user = await databaseContext
.Users
@@ -202,5 +207,42 @@ namespace Tgstation.Server.Host.Security
throw;
}
}
/// <inheritdoc />
public async Task ValidateOidcToken(Microsoft.AspNetCore.Authentication.OpenIdConnect.TokenValidatedContext tokenValidatedContext, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(tokenValidatedContext);
var principal = new ClaimsPrincipal(new ClaimsIdentity(tokenValidatedContext.SecurityToken.Claims));
var userIdClaim = principal.FindFirst(JwtRegisteredClaimNames.Sub);
if (userIdClaim == default)
throw new InvalidOperationException($"Missing '{JwtRegisteredClaimNames.Sub}' claim!");
var userId = userIdClaim.Value;
var scheme = tokenValidatedContext.Scheme.Name;
var deprefixedScheme = scheme.Substring(ApiHeaders.OpenIDConnectAuthenticationSchemePrefix.Length);
var connection = await databaseContext
.OidcConnections
.AsQueryable()
.Where(oidcConnection => oidcConnection.ExternalUserId == userId && oidcConnection.SchemeKey == deprefixedScheme)
.Include(oidcConnection => oidcConnection.User)
.ThenInclude(user => user!.PermissionSet)
.FirstOrDefaultAsync(cancellationToken);
if (connection == default)
{
tokenValidatedContext.Fail($"Unable to find user with OidcConnection for {deprefixedScheme}/{userId}!");
return;
}
var expires = ParseTime(principal, JwtRegisteredClaimNames.Exp);
currentAuthenticationContext.Initialize(
connection.User!,
expires,
Guid.NewGuid().ToString(),
null,
null);
}
}
}
@@ -25,8 +25,8 @@ namespace Tgstation.Server.Host.Security
/// Create a <see cref="TokenResponse"/> for a given <paramref name="user"/>.
/// </summary>
/// <param name="user">The <see cref="Models.User"/> to create the token for. Must have the <see cref="Api.Models.EntityId.Id"/> field available.</param>
/// <param name="oAuth">Whether or not this is an OAuth login.</param>
/// <param name="serviceLogin">Whether or not this is an external service login.</param>
/// <returns>A new token <see cref="string"/>.</returns>
string CreateToken(Models.User user, bool oAuth);
string CreateToken(Models.User user, bool serviceLogin);
}
}
@@ -1,21 +1,27 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication.JwtBearer;
namespace Tgstation.Server.Host.Security
{
/// <summary>
/// Handles <see cref="TokenValidatedContext"/>s.
/// Handles validating authentication tokens.
/// </summary>
public interface ITokenValidator
{
/// <summary>
/// Handles <paramref name="tokenValidatedContext"/>.
/// Handles TGS <paramref name="tokenValidatedContext"/>s.
/// </summary>
/// <param name="tokenValidatedContext">The <see cref="TokenValidatedContext"/>.</param>
/// <param name="tokenValidatedContext">The <see cref="Microsoft.AspNetCore.Authentication.JwtBearer.TokenValidatedContext"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
Task ValidateToken(TokenValidatedContext tokenValidatedContext, CancellationToken cancellationToken);
Task ValidateTgsToken(Microsoft.AspNetCore.Authentication.JwtBearer.TokenValidatedContext tokenValidatedContext, CancellationToken cancellationToken);
/// <summary>
/// Handles OIDC <paramref name="tokenValidatedContext"/>s.
/// </summary>
/// <param name="tokenValidatedContext">The <see cref="Microsoft.AspNetCore.Authentication.OpenIdConnect.TokenValidatedContext"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
Task ValidateOidcToken(Microsoft.AspNetCore.Authentication.OpenIdConnect.TokenValidatedContext tokenValidatedContext, CancellationToken cancellationToken);
}
}
@@ -14,7 +14,9 @@ namespace Tgstation.Server.Host.Security.OAuth
sealed class InvisionCommunityOAuthValidator : GenericOAuthValidator
{
/// <inheritdoc />
#pragma warning disable CS0618 // Type or member is obsolete
public override OAuthProvider Provider => OAuthProvider.InvisionCommunity;
#pragma warning restore CS0618 // Type or member is obsolete
/// <inheritdoc />
protected override Uri TokenUrl => new($"{OAuthConfiguration.ServerUrl}/oauth/token/"); // This needs the trailing slash or it doesnt get the token. Do not remove.
@@ -14,7 +14,9 @@ namespace Tgstation.Server.Host.Security.OAuth
sealed class KeycloakOAuthValidator : GenericOAuthValidator
{
/// <inheritdoc />
#pragma warning disable CS0618 // Type or member is obsolete
public override OAuthProvider Provider => OAuthProvider.Keycloak;
#pragma warning restore CS0618 // Type or member is obsolete
/// <inheritdoc />
protected override Uri TokenUrl => new($"{BaseProtocolPath}/token");
@@ -57,14 +57,18 @@ namespace Tgstation.Server.Host.Security.OAuth
loggerFactory.CreateLogger<DiscordOAuthValidator>(),
discordConfig));
#pragma warning disable CS0618 // Type or member is obsolete
if (securityConfiguration.OAuth.TryGetValue(OAuthProvider.Keycloak, out var keyCloakConfig))
#pragma warning restore CS0618 // Type or member is obsolete
validatorsBuilder.Add(
new KeycloakOAuthValidator(
httpClientFactory,
loggerFactory.CreateLogger<KeycloakOAuthValidator>(),
keyCloakConfig));
#pragma warning disable CS0618 // Type or member is obsolete
if (securityConfiguration.OAuth.TryGetValue(OAuthProvider.InvisionCommunity, out var invisionConfig))
#pragma warning restore CS0618 // Type or member is obsolete
validatorsBuilder.Add(
new InvisionCommunityOAuthValidator(
httpClientFactory,
@@ -101,7 +101,7 @@ namespace Tgstation.Server.Host.Security
}
/// <inheritdoc />
public string CreateToken(User user, bool oAuth)
public string CreateToken(User user, bool serviceLogin)
{
ArgumentNullException.ThrowIfNull(user);
@@ -121,7 +121,7 @@ namespace Tgstation.Server.Host.Security
else
notBefore = now;
var expiry = now.AddMinutes(oAuth
var expiry = now.AddMinutes(serviceLogin
? securityConfiguration.OAuthTokenExpiryMinutes
: securityConfiguration.TokenExpiryMinutes);
@@ -111,6 +111,8 @@
<PackageReference Include="HotChocolate.Types.Scalars" Version="15.0.3" />
<!-- Usage: git interop -->
<PackageReference Include="LibGit2Sharp" Version="0.31.0" />
<!-- Usage: OpenID Connect support -->
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="8.0.13" />
<!-- Usage: Support ""legacy"" Newotonsoft.Json in HTTP pipeline. The rest of our codebase uses Newtonsoft. -->
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.13" />
<!-- Usage: Using target JSON serializer for API -->
@@ -129,6 +131,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.13" />
<!-- Usage: Database connectivity health check -->
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="8.0.13" />
<PackageReference Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" Version="8.6.0" />
<!-- Usage: POSIX support for syscalls, signals, and symlinks -->
<PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" />
<!-- Usage: Cron string parsing -->
@@ -1,6 +1,6 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
@@ -10,7 +10,7 @@ using Moq;
using System;
using System.Threading;
using Tgstation.Server.Host.Components;
using Tgstation.Server.Api;
using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.Security;
using Tgstation.Server.Host.System;
@@ -23,6 +23,10 @@ namespace Tgstation.Server.Host.Core.Tests
[TestClass]
public sealed class TestApplication
{
[TestMethod]
public void TestApiDefinedOidcSchemeMatchesMicrosofts()
=> Assert.AreEqual($"{OpenIdConnectDefaults.AuthenticationScheme}.", ApiHeaders.OpenIDConnectAuthenticationSchemePrefix);
[TestMethod]
public void TestMethodThrows()
{
@@ -40,43 +44,47 @@ namespace Tgstation.Server.Host.Core.Tests
Assert.ThrowsException<ArgumentNullException>(() => app.ConfigureServices(mockServiceCollection, null, null));
Assert.ThrowsException<ArgumentNullException>(() => app.Configure(null, null, null, null, null, null, null, null, null, null, null));
Assert.ThrowsException<ArgumentNullException>(() => app.Configure(null, null, null, null, null, null, null, null, null, null, null, null));
var mockAppBuilder = new Mock<IApplicationBuilder>();
Assert.ThrowsException<ArgumentNullException>(() => app.Configure(mockAppBuilder.Object, null, null, null, null, null, null, null, null, null, null));
Assert.ThrowsException<ArgumentNullException>(() => app.Configure(mockAppBuilder.Object, null, null, null, null, null, null, null, null, null, null, null));
var mockServerControl = new Mock<IServerControl>();
Assert.ThrowsException<ArgumentNullException>(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, null, null, null, null, null, null, null, null, null));
Assert.ThrowsException<ArgumentNullException>(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, null, null, null, null, null, null, null, null, null, null));
var mockTokenFactory = new Mock<ITokenFactory>();
Assert.ThrowsException<ArgumentNullException>(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, null, null, null, null, null, null, null, null));
Assert.ThrowsException<ArgumentNullException>(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, null, null, null, null, null, null, null, null, null));
var mockServerPortProvider = new Mock<IServerPortProvider>();
mockServerPortProvider.SetupGet(x => x.HttpApiPort).Returns(5345);
Assert.ThrowsException<ArgumentNullException>(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, mockServerPortProvider.Object, null, null, null, null, null, null, null));
Assert.ThrowsException<ArgumentNullException>(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, mockServerPortProvider.Object, null, null, null, null, null, null, null, null));
var mockAssemblyInformationProvider = Mock.Of<IAssemblyInformationProvider>();
Assert.ThrowsException<ArgumentNullException>(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, mockServerPortProvider.Object, mockAssemblyInformationProvider, null, null, null, null, null, null));
Assert.ThrowsException<ArgumentNullException>(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, mockServerPortProvider.Object, mockAssemblyInformationProvider, null, null, null, null, null, null, null));
var mockControlPanelOptions = new Mock<IOptions<ControlPanelConfiguration>>();
mockControlPanelOptions.SetupGet(x => x.Value).Returns(new ControlPanelConfiguration()).Verifiable();
Assert.ThrowsException<ArgumentNullException>(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, mockServerPortProvider.Object, mockAssemblyInformationProvider, mockControlPanelOptions.Object, null, null, null, null, null));
Assert.ThrowsException<ArgumentNullException>(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, mockServerPortProvider.Object, mockAssemblyInformationProvider, mockControlPanelOptions.Object, null, null, null, null, null, null));
var mockGeneralOptions = new Mock<IOptions<GeneralConfiguration>>();
mockGeneralOptions.SetupGet(x => x.Value).Returns(new GeneralConfiguration()).Verifiable();
Assert.ThrowsException<ArgumentNullException>(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, mockServerPortProvider.Object, mockAssemblyInformationProvider, mockControlPanelOptions.Object, mockGeneralOptions.Object, null, null, null, null));
Assert.ThrowsException<ArgumentNullException>(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, mockServerPortProvider.Object, mockAssemblyInformationProvider, mockControlPanelOptions.Object, mockGeneralOptions.Object, null, null, null, null, null));
var mockDatabaseOptions = new Mock<IOptions<DatabaseConfiguration>>();
mockDatabaseOptions.SetupGet(x => x.Value).Returns(new DatabaseConfiguration()).Verifiable();
Assert.ThrowsException<ArgumentNullException>(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, mockServerPortProvider.Object, mockAssemblyInformationProvider, mockControlPanelOptions.Object, mockGeneralOptions.Object, mockDatabaseOptions.Object, null, null, null));
Assert.ThrowsException<ArgumentNullException>(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, mockServerPortProvider.Object, mockAssemblyInformationProvider, mockControlPanelOptions.Object, mockGeneralOptions.Object, mockDatabaseOptions.Object, null, null, null, null));
var mockSecurityOptions = new Mock<IOptions<SecurityConfiguration>>();
mockDatabaseOptions.SetupGet(x => x.Value).Returns(new DatabaseConfiguration()).Verifiable();
Assert.ThrowsException<ArgumentNullException>(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, mockServerPortProvider.Object, mockAssemblyInformationProvider, mockControlPanelOptions.Object, mockGeneralOptions.Object, mockDatabaseOptions.Object, mockSecurityOptions.Object, null, null, null));
var mockSwarmOptions = new Mock<IOptions<SwarmConfiguration>>();
mockSwarmOptions.SetupGet(x => x.Value).Returns(new SwarmConfiguration()).Verifiable();
Assert.ThrowsException<ArgumentNullException>(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, mockServerPortProvider.Object, mockAssemblyInformationProvider, mockControlPanelOptions.Object, mockGeneralOptions.Object, mockDatabaseOptions.Object, mockSwarmOptions.Object, null, null));
Assert.ThrowsException<ArgumentNullException>(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, mockServerPortProvider.Object, mockAssemblyInformationProvider, mockControlPanelOptions.Object, mockGeneralOptions.Object, mockDatabaseOptions.Object, mockSecurityOptions.Object, mockSwarmOptions.Object, null, null));
var mockInternalOptions = new Mock<IOptions<InternalConfiguration>>();
mockInternalOptions.SetupGet(x => x.Value).Returns(new InternalConfiguration()).Verifiable();
Assert.ThrowsException<ArgumentNullException>(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, mockServerPortProvider.Object, mockAssemblyInformationProvider, mockControlPanelOptions.Object, mockGeneralOptions.Object, mockDatabaseOptions.Object, mockSwarmOptions.Object, mockInternalOptions.Object, null));
Assert.ThrowsException<ArgumentNullException>(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, mockServerPortProvider.Object, mockAssemblyInformationProvider, mockControlPanelOptions.Object, mockGeneralOptions.Object, mockDatabaseOptions.Object, mockSecurityOptions.Object, mockSwarmOptions.Object, mockInternalOptions.Object, null));
mockControlPanelOptions.VerifyAll();
mockInternalOptions.VerifyAll();
@@ -89,7 +89,7 @@ namespace Tgstation.Server.Host.Swarm.Tests
public TokenValidationParameters ValidationParameters => throw new NotSupportedException();
public string CreateToken(User user, bool oAuth)
public string CreateToken(User user, bool serviceLogin)
{
throw new NotSupportedException();
}
@@ -127,7 +127,7 @@ namespace Tgstation.Server.Tests.Live
{
Id = restUser.Id,
OAuthConnections = sampleOAuthConnections
}, cancellationToken), Api.Models.ErrorCode.AdminUserCannotOAuth);
}, cancellationToken), Api.Models.ErrorCode.AdminUserCannotHaveServiceConnection);
var testUser = await client.Users.Create(
new UserCreateRequest
@@ -334,7 +334,7 @@ namespace Tgstation.Server.Tests.Live
Assert.IsFalse(group.PermissionSet.AdministrationRights.CanChangeVersion);
Assert.IsFalse(group.PermissionSet.AdministrationRights.CanDownloadLogs);
Assert.IsFalse(group.PermissionSet.AdministrationRights.CanEditOwnOAuthConnections);
Assert.IsFalse(group.PermissionSet.AdministrationRights.CanEditOwnServiceConnections);
Assert.IsFalse(group.PermissionSet.AdministrationRights.CanEditOwnPassword);
Assert.IsFalse(group.PermissionSet.AdministrationRights.CanReadUsers);
Assert.IsFalse(group.PermissionSet.AdministrationRights.CanRestartHost);
@@ -363,7 +363,7 @@ namespace Tgstation.Server.Tests.Live
Assert.IsFalse(group2.PermissionSet.AdministrationRights.CanChangeVersion);
Assert.IsFalse(group2.PermissionSet.AdministrationRights.CanDownloadLogs);
Assert.IsFalse(group2.PermissionSet.AdministrationRights.CanEditOwnOAuthConnections);
Assert.IsFalse(group2.PermissionSet.AdministrationRights.CanEditOwnServiceConnections);
Assert.IsFalse(group2.PermissionSet.AdministrationRights.CanEditOwnPassword);
Assert.IsFalse(group2.PermissionSet.AdministrationRights.CanReadUsers);
Assert.IsFalse(group2.PermissionSet.AdministrationRights.CanRestartHost);
@@ -416,7 +416,7 @@ namespace Tgstation.Server.Tests.Live
Assert.IsTrue(group3.PermissionSet.AdministrationRights.CanChangeVersion);
Assert.IsTrue(group3.PermissionSet.AdministrationRights.CanDownloadLogs);
Assert.IsTrue(group3.PermissionSet.AdministrationRights.CanEditOwnOAuthConnections);
Assert.IsTrue(group3.PermissionSet.AdministrationRights.CanEditOwnServiceConnections);
Assert.IsTrue(group3.PermissionSet.AdministrationRights.CanEditOwnPassword);
Assert.IsTrue(group3.PermissionSet.AdministrationRights.CanReadUsers);
Assert.IsTrue(group3.PermissionSet.AdministrationRights.CanRestartHost);
@@ -448,7 +448,7 @@ namespace Tgstation.Server.Tests.Live
}
],
cancellationToken),
data => data.CreateUserByOAuthAndPermissionSet,
data => data.CreateUserByServiceConnectionAndPermissionSet,
cancellationToken);
var testUser2 = oAuthCreateResult.User;
@@ -483,7 +483,7 @@ namespace Tgstation.Server.Tests.Live
{
CanChangeVersion = true,
CanDownloadLogs = true,
CanEditOwnOAuthConnections = true,
CanEditOwnServiceConnections = true,
CanEditOwnPassword = true,
CanReadUsers = true,
CanRestartHost = true,