Merge pull request #496 from Cyberboss/OkayButWhenAreYouActuallyGoingToWriteTheServer

Authentication Framework
This commit is contained in:
Jordan Brown
2018-04-19 10:20:36 -04:00
committed by GitHub
110 changed files with 2922 additions and 576 deletions
+2
View File
@@ -5,5 +5,7 @@ docker-compose.yml
.vs
.vscode
legacy
packages
build
*/bin
*/obj
+1
View File
@@ -11,3 +11,4 @@ artifacts/
*DS_Store
*.sln.ide
/TestResults
/src/Tgstation.Server.Host/appsettings.Development.json
+8 -2
View File
@@ -1,4 +1,4 @@
FROM microsoft/dotnet:2.1-sdk AS build
FROM microsoft/dotnet:2.0-sdk AS build
WORKDIR /src
COPY tgstation-server.sln ./
@@ -36,8 +36,14 @@ RUN dotnet test -c Release
FROM build AS publish
RUN dotnet publish -c Release -o /app
FROM microsoft/dotnet:2.1-runtime
FROM microsoft/dotnet:2.0-runtime
EXPOSE 80
WORKDIR /app
COPY --from=publish /app .
RUN mkdir /config_data && mv appsettings.Docker.json /config_data/appsettings.Production.json
VOLUME ["/config_data"]
ENTRYPOINT ["dotnet", "Tgstation.Server.Host.Console.dll"]
+222
View File
@@ -0,0 +1,222 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http.Headers;
using System.Reflection;
using Microsoft.AspNetCore.Http.Headers;
using Microsoft.Net.Http.Headers;
using Microsoft.Extensions.Primitives;
namespace Tgstation.Server.Api
{
/// <summary>
/// Represents the header that must be present for every server request
/// </summary>
public sealed class ApiHeaders
{
/// <summary>
/// TODO: Remove this when https://github.com/dotnet/corefx/pull/26701 makes it into the sdk
/// </summary>
public const string ApplicationJson = "application/json";
/// <summary>
/// The <see cref="Username"/> header key
/// </summary>
const string usernameHeader = "Username";
/// <summary>
/// The <see cref="InstanceId"/> header key
/// </summary>
const string instanceIdHeader = "InstanceId";
/// <summary>
/// The JWT authentication header scheme
/// </summary>
const string jwtAuthenticationScheme = "Bearer";
/// <summary>
/// The password authentication header scheme
/// </summary>
const string passwordAuthenticationScheme = "Password";
/// <summary>
/// The current <see cref="AssemblyName"/>
/// </summary>
static readonly AssemblyName assemblyName = Assembly.GetExecutingAssembly().GetName();
/// <summary>
/// The <see cref="Models.Instance.Id"/> being accessed
/// </summary>
public long? InstanceId { get; set; }
/// <summary>
/// The client's user agent
/// </summary>
public ProductHeaderValue UserAgent { get; }
/// <summary>
/// The client's API version
/// </summary>
public Version ApiVersion { get; }
/// <summary>
/// The client's JWT
/// </summary>
public string Token { get; }
/// <summary>
/// The client's username
/// </summary>
public string Username { get; }
/// <summary>
/// The client's password
/// </summary>
public string Password { get; }
/// <summary>
/// If the header uses password or JWT authentication
/// </summary>
public bool IsTokenAuthentication => Token != null;
/// <summary>
/// Construct <see cref="ApiHeaders"/> for JWT authentication
/// </summary>
/// <param name="userAgent">The value of <see cref="UserAgent"/></param>
/// <param name="token">The value of <see cref="Token"/></param>
public ApiHeaders(ProductHeaderValue userAgent, string token) : this(userAgent, token, null, null)
{
if (userAgent == null)
throw new ArgumentNullException(nameof(userAgent));
if (token == null)
throw new ArgumentNullException(nameof(token));
}
/// <summary>
/// Construct <see cref="ApiHeaders"/> for password authentication
/// </summary>
/// <param name="userAgent">The value of <see cref="UserAgent"/></param>
/// <param name="username">The value of <see cref="Username"/></param>
/// <param name="password">The value of <see cref="Password"/></param>
public ApiHeaders(ProductHeaderValue userAgent, string username, string password) : this(userAgent, null, username, password)
{
if (userAgent == null)
throw new ArgumentNullException(nameof(userAgent));
if (username == null)
throw new ArgumentNullException(nameof(username));
if (password == null)
throw new ArgumentNullException(nameof(password));
}
/// <summary>
/// Construct and validates <see cref="ApiHeaders"/> from a set of <paramref name="requestHeaders"/>
/// </summary>
/// <param name="requestHeaders">The <see cref="RequestHeaders"/> containing the <see cref="ApiHeaders"/></param>
public ApiHeaders(RequestHeaders requestHeaders)
{
var jsonAccept = new Microsoft.Net.Http.Headers.MediaTypeHeaderValue(ApplicationJson);
if (!requestHeaders.Accept.Any(x => x.MediaType == jsonAccept.MediaType))
throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Client does not accept {0}!", ApplicationJson));
if (!requestHeaders.Headers.TryGetValue(HeaderNames.UserAgent, out StringValues userAgentValues) || !ProductInfoHeaderValue.TryParse(userAgentValues.FirstOrDefault(), out ProductInfoHeaderValue clientUserAgent))
throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Missing {0} headers!", HeaderNames.UserAgent));
//assure the client user agent has a name and version
if (String.IsNullOrWhiteSpace(clientUserAgent.Product.Name) || !Version.TryParse(clientUserAgent.Product.Version, out Version clientVersion))
throw new InvalidOperationException("Malformed client user agent!");
var apiUserAgentHeader = userAgentValues[1];
//make sure the api header matches ours
if (!ProductInfoHeaderValue.TryParse(apiUserAgentHeader, out ProductInfoHeaderValue apiUserAgent) || apiUserAgent.Product.Name != assemblyName.Name)
throw new InvalidOperationException("Missing API user agent!");
if (!Version.TryParse(apiUserAgent.Product.Version, out Version ApiVersion))
throw new InvalidOperationException("Malformed API version!");
//check api version compatibility
var ourVersion = assemblyName.Version;
if (ourVersion.Major != ApiVersion.Major || ourVersion.Minor != ApiVersion.Minor || ourVersion.Build > ApiVersion.Build)
throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Given API version is incompatible with version {0}!", ourVersion));
if (!requestHeaders.Headers.TryGetValue(HeaderNames.Authorization, out StringValues authorization))
throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Missing {0} header!", HeaderNames.Authorization));
var auth = authorization.First();
var splits = new List<string>(auth.Split(' '));
var scheme = splits.First();
if (String.IsNullOrWhiteSpace(scheme))
throw new InvalidOperationException("Missing authentication scheme!");
splits.RemoveAt(0);
var parameter = String.Concat(splits);
if (String.IsNullOrEmpty(parameter))
throw new InvalidOperationException("Missing authentication parameter!");
if(requestHeaders.Headers.TryGetValue(instanceIdHeader, out StringValues instanceIdValues))
{
var instanceIdString = instanceIdValues.FirstOrDefault();
if (instanceIdString != default && Int64.TryParse(instanceIdString, out long instanceId))
InstanceId = instanceId;
}
switch (scheme)
{
case jwtAuthenticationScheme:
Token = parameter;
break;
case passwordAuthenticationScheme:
Password = parameter;
var fail = !requestHeaders.Headers.TryGetValue(usernameHeader, out StringValues values);
if (!fail)
{
Username = values.FirstOrDefault();
fail = String.IsNullOrWhiteSpace(Username);
}
if (fail)
throw new InvalidOperationException("Missing Username header!");
break;
default:
throw new InvalidOperationException("Invalid authentication scheme!");
}
}
/// <summary>
/// Construct <see cref="ApiHeaders"/>
/// </summary>
/// <param name="userAgent">The value of <see cref="UserAgent"/></param>
/// <param name="token">The value of <see cref="Token"/></param>
/// <param name="username">The value of <see cref="Username"/></param>
/// <param name="password">The value of <see cref="Password"/></param>
ApiHeaders(ProductHeaderValue userAgent, string token, string username, string password)
{
UserAgent = userAgent;
Token = token;
Username = username;
Password = password;
}
/// <summary>
/// Set <see cref="HttpRequestHeaders"/> using the <see cref="ApiHeaders"/>. This initially clears <paramref name="headers"/>
/// </summary>
/// <param name="headers">The <see cref="HttpRequestHeaders"/> to set</param>
public void SetRequestHeaders(HttpRequestHeaders headers)
{
if (headers == null)
throw new ArgumentNullException(nameof(headers));
headers.Clear();
headers.Accept.Add(new MediaTypeWithQualityHeaderValue(ApplicationJson));
if (IsTokenAuthentication)
headers.Authorization = new AuthenticationHeaderValue(jwtAuthenticationScheme, Token);
else
{
headers.Authorization = new AuthenticationHeaderValue(passwordAuthenticationScheme, Password);
headers.Add(usernameHeader, Username);
}
headers.UserAgent.Add(new ProductInfoHeaderValue(UserAgent));
headers.UserAgent.Add(new ProductInfoHeaderValue(new ProductHeaderValue(assemblyName.Name, assemblyName.Version.ToString())));
if(InstanceId.HasValue)
headers.Add(instanceIdHeader, InstanceId.ToString());
}
}
}
+1 -1
View File
@@ -6,7 +6,7 @@ namespace Tgstation.Server.Api
/// <summary>
/// Indicates the rights <see cref="Enum"/> for a model
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
public sealed class ModelAttribute : Attribute
{
/// <summary>
@@ -4,36 +4,15 @@ using Tgstation.Server.Api.Rights;
namespace Tgstation.Server.Api.Models
{
/// <summary>
/// Metadata about an installation
/// </summary>
[Model(RightsType.Administration)]
public sealed class Administration
/// <inheritdoc />
public sealed class Administration : Internal.ServerSettings
{
/// <summary>
/// Use the specified Windows/UNIX authentication to authorize users. Setting this to <see langword="null"/> enables full administrative anonymous access
/// </summary>
[Permissions(ReadRight = AdministrationRights.ChangeAuthenticationGroup, WriteRight = AdministrationRights.ChangeAuthenticationGroup)]
public string SystemAuthenticationGroup { get; set; }
/// <summary>
/// Automatically send unhandled exception data to a public collection service. This will be limited to system information, path data, and game code compilation information.
/// </summary>
[Permissions(ReadRight = AdministrationRights.ChangeTelemetry, WriteRight = AdministrationRights.ChangeTelemetry)]
public bool EnableTelemetry { get; set; }
/// <summary>
/// If the <see cref="DreamDaemon"/> instances will not be stopped when the server exits. Resets to <see langword="false"/> when the server restarts
/// </summary>
[Permissions(ReadRight = AdministrationRights.SoftStop, WriteRight = AdministrationRights.SoftStop)]
public bool SoftStop { get; set; }
/// <summary>
/// The git repository to recieve updates to Tgstation.Server.Host from, must include credentials if necessary. If set to <see langword="null"/> upstream pulls will be disabled entirely
/// </summary>
[Permissions(ReadRight = AdministrationRights.SetUpstreamRepository, WriteRight = AdministrationRights.SetUpstreamRepository)]
public string UpstreamRepository { get; set; }
/// <summary>
/// The latest available version of the Tgstation.Server.Host assembly from the upstream repository. If <see cref="Version.Minor"/> is higher than <see cref="CurrentVersion"/>'s the update cannot be applied due to API changes
/// </summary>
@@ -47,7 +26,7 @@ namespace Tgstation.Server.Api.Models
public Version CurrentVersion { get; set; }
/// <summary>
/// Users in the <see cref="SystemAuthenticationGroup"/>
/// Users in the <see cref="Internal.ServerSettings.SystemAuthenticationGroup"/>
/// </summary>
[Permissions(DenyWrite = true)]
public IReadOnlyList<User> Users { get; set; }
-71
View File
@@ -1,71 +0,0 @@
using System.Collections.Generic;
using Tgstation.Server.Api.Rights;
namespace Tgstation.Server.Api.Models
{
/// <summary>
/// Manage the server chat bots
/// </summary>
[Model(RightsType.Chat, RequiresInstance = true)]
public sealed class Chat
{
/// <summary>
/// If the IRC client is enabled
/// </summary>
[Permissions(WriteRight = ChatRights.SetIrcEnabled)]
public bool IrcEnabled { get; set; }
/// <summary>
/// The IRC server name
/// </summary>
[Permissions(ReadRight = ChatRights.SetIrcSettings, WriteRight = ChatRights.SetIrcSettings)]
public string IrcHost { get; set; }
/// <summary>
/// The IRC server port
/// </summary>
[Permissions(ReadRight = ChatRights.SetIrcSettings, WriteRight = ChatRights.SetIrcSettings)]
public ushort IrcPort { get; set; }
/// <summary>
/// The IRC server NickServ password
/// </summary>
[Permissions(ReadRight = ChatRights.SetIrcSettings, WriteRight = ChatRights.SetIrcSettings)]
public string IrcNickServPassword { get; set; }
/// <summary>
/// Channels the IRC client should join/listen/announce in and allow admin commands
/// </summary>
[Permissions(WriteRight = ChatRights.SetIrcChannels)]
public List<string> IrcAdminChannels { get; set; }
/// <summary>
/// Channels the IRC client should join/listen/announce in
/// </summary>
[Permissions(WriteRight = ChatRights.SetIrcChannels)]
public List<string> IrcGeneralChannels { get; set; }
/// <summary>
/// If the Discord bot is enabled
/// </summary>
[Permissions(WriteRight = ChatRights.SetDiscordEnabled)]
public bool DiscordEnabled { get; set; }
/// <summary>
/// The Discord bot token
/// </summary>
[Permissions(ReadRight = ChatRights.SetDiscordSettings, WriteRight = ChatRights.SetDiscordSettings)]
public string DiscordBotToken { get; set; }
/// <summary>
/// Channels the Discord bot should listen/announce in and allow admin commands
/// </summary>
[Permissions(WriteRight = ChatRights.SetDiscordChannels)]
public List<long> DiscordAdminChannels { get; set; }
/// <summary>
/// Channels the Discord bot should listen/announce in
/// </summary>
[Permissions(WriteRight = ChatRights.SetDiscordChannels)]
public List<long> DiscordGeneralChannels { get; set; }
}
}
@@ -0,0 +1,23 @@
namespace Tgstation.Server.Api.Models
{
/// <summary>
/// Indicates a chat channel
/// </summary>
public class ChatChannel
{
/// <summary>
/// The IRC channel name
/// </summary>
public string IrcChannel { get; set; }
/// <summary>
/// The Discord channel ID
/// </summary>
public long DiscordChannelId { get; set; }
/// <summary>
/// If the <see cref="ChatChannel"/> is an admin channel
/// </summary>
public bool IsAdminChannel { get; set; }
}
}
@@ -0,0 +1,15 @@
using System.Collections.Generic;
using Tgstation.Server.Api.Rights;
namespace Tgstation.Server.Api.Models
{
/// <inheritdoc />
public sealed class ChatSettings : Internal.ChatSettings
{
/// <summary>
/// Channels the Discord bot should listen/announce in
/// </summary>
[Permissions(WriteRight = ChatSettingsRights.SetChannels)]
public List<ChatChannel> Channels { get; set; }
}
}
@@ -0,0 +1,16 @@
namespace Tgstation.Server.Api.Models
{
/// <inheritdoc />
public sealed class CompileJob : Internal.CompileJob
{
/// <summary>
/// The <see cref="User"/> that triggered the job
/// </summary>
public User TriggeredBy { get; set; }
/// <summary>
/// Git revision the compiler ran on. Not modifiable
/// </summary>
public RevisionInformation RevisionInformation { get; set; }
}
}
@@ -30,9 +30,7 @@ namespace Tgstation.Server.Api.Models
/// <summary>
/// The content of the <see cref="Configuration"/> file. Will be <see langword="null"/> if <see cref="ReadDenied"/> is <see langword="true"/> or during listing operations
/// </summary>
#pragma warning disable CA1819 // Properties should not return arrays
public byte[] Content { get; set; }
#pragma warning restore CA1819 // Properties should not return arrays
/// <summary>
/// The MD5 hash of the file when last read by the user. Will be <see langword="null"/> if <see cref="ReadDenied"/> is <see langword="true"/>. If this doesn't match during update actions, the write will be denied with error code 409
+7 -63
View File
@@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using Tgstation.Server.Api.Models.Internal;
using Tgstation.Server.Api.Rights;
namespace Tgstation.Server.Api.Models
@@ -7,38 +6,13 @@ namespace Tgstation.Server.Api.Models
/// <summary>
/// Represents an instance of BYOND's DreamDaemon game server. Create action starts the server. Delete action shuts down the server
/// </summary>
[Model(RightsType.DreamDaemon, CanCrud = true, RequiresInstance = true)]
public sealed class DreamDaemon
public sealed class DreamDaemon : DreamDaemonSettings
{
/// <summary>
/// The pull requests merged on the live game version
/// </summary>
[Permissions(DenyWrite = true, ReadRight = DreamDaemonRights.ReadRevision)]
public IReadOnlyDictionary<int, string> PullRequests { get; set; }
/// <summary>
/// The git sha the live game version was compiled at
/// </summary>
[Permissions(DenyWrite = true, ReadRight = DreamDaemonRights.ReadRevision)]
public string Sha { get; set; }
/// <summary>
/// The git sha of the origin branch the live game version was compiled at
/// </summary>
[Permissions(DenyWrite = true, ReadRight = DreamDaemonRights.ReadRevision)]
public string OriginSha { get; set; }
/// <summary>
/// When the live revision was compiled
/// </summary>
[Permissions(DenyWrite = true, ReadRight = DreamDaemonRights.ReadRevision)]
public DateTimeOffset CompiledAt { get; set; }
/// <summary>
/// If <see cref="DreamDaemon"/> starts when it's <see cref="Instance"/> starts
/// </summary>
[Permissions(ReadRight = DreamDaemonRights.ReadMetadata, WriteRight = DreamDaemonRights.SetAutoStart)]
public bool? AutoStart { get; set; }
public CompileJob CompileJob { get; set; }
/// <summary>
/// The current status of <see cref="DreamDaemon"/>
@@ -47,45 +21,15 @@ namespace Tgstation.Server.Api.Models
public DreamDaemonStatus? Status { get; set; }
/// <summary>
/// If the BYOND web client can be used to connect to the game server
/// The current <see cref="DreamDaemonSecurity"/> of <see cref="DreamDaemon"/>
/// </summary>
[Permissions(ReadRight = DreamDaemonRights.ReadMetadata, WriteRight = DreamDaemonRights.SetWebClient)]
public bool? AllowWebClient { get; set; }
/// <summary>
/// If the server is undergoing a soft reset. This may be automatically set by changes to other fields
/// </summary>
[Permissions(ReadRight = DreamDaemonRights.ReadMetadata, WriteRight = DreamDaemonRights.SoftRestart)]
public bool? SoftRestart { get; set; }
/// <summary>
/// If the server is undergoing a soft shutdown
/// </summary>
[Permissions(ReadRight = DreamDaemonRights.ReadMetadata, WriteRight = DreamDaemonRights.SoftShutdown)]
public bool? SoftShutdown { get; set; }
/// <summary>
/// The <see cref="DreamDaemonSecurity"/> level of <see cref="DreamDaemon"/>
/// </summary>
[Permissions(ReadRight = DreamDaemonRights.ReadMetadata, WriteRight = DreamDaemonRights.SetSecurity)]
public DreamDaemonSecurity? SecurirtyLevel { get; set; }
/// <summary>
/// The first port <see cref="DreamDaemon"/> uses. This should be the publically advertised port
/// </summary>
[Permissions(ReadRight = DreamDaemonRights.ReadMetadata, WriteRight = DreamDaemonRights.SetPorts)]
public ushort? PrimaryPort { get; set; }
/// <summary>
/// The second port <see cref="DreamDaemon"/> uses
/// </summary>
[Permissions(ReadRight = DreamDaemonRights.ReadMetadata, WriteRight = DreamDaemonRights.SetPorts)]
public ushort? SecondaryPort { get; set; }
[Permissions(DenyWrite = true, ReadRight = DreamDaemonRights.ReadMetadata)]
public DreamDaemonSecurity? CurrentSecurity { get; set; }
/// <summary>
/// The port the running <see cref="DreamDaemon"/> instance is set to
/// </summary>
[Permissions(DenyWrite = true, ReadRight = DreamDaemonRights.ReadMetadata)]
public bool CurrentPort { get; set; }
public ushort? CurrentPort { get; set; }
}
}
@@ -8,7 +8,7 @@
/// <summary>
/// Server is unrestricted in terms of file access and shell commands
/// </summary>
Trusted = 0,
Trusted,
/// <summary>
/// Server will not be able to run shell commands or access files outside it's working directory
/// </summary>
+5 -47
View File
@@ -1,4 +1,4 @@
using System;
using Tgstation.Server.Api.Models.Internal;
using Tgstation.Server.Api.Rights;
namespace Tgstation.Server.Api.Models
@@ -6,60 +6,18 @@ namespace Tgstation.Server.Api.Models
/// <summary>
/// Represents the state of the DreamMaker compiler. Create action starts a new compile. Delete action cancels the current compile
/// </summary>
[Model(RightsType.DreamMaker, ReadRight = DreamMakerRights.Read, CanCrud = true, RequiresInstance = true)]
public sealed class DreamMaker
public sealed class DreamMaker : DreamMakerSettings
{
/// <summary>
/// When the compilation started
/// The last <see cref="CompileJob"/> ran
/// </summary>
[Permissions(DenyWrite = true)]
public DateTimeOffset StartedAt { get; set; }
/// <summary>
/// When the compilation finished
/// </summary>
[Permissions(DenyWrite = true)]
public DateTimeOffset FinishedAt { get; set; }
public CompileJob LastJob { get; set; }
/// <summary>
/// The <see cref="CompilerStatus"/> of the compiler
/// </summary>
[Permissions(DenyWrite = true)]
public CompilerStatus Status { get; set; }
/// <summary>
/// If the compiler targeted the primary directory
/// </summary>
[Permissions(DenyWrite = true)]
public bool? TargetedPrimaryDirectory { get; set; }
/// <summary>
/// Textual output of DM
/// </summary>
[Permissions(DenyWrite = true)]
public string Output { get; set; }
/// <summary>
/// Exit code of DM
/// </summary>
[Permissions(DenyWrite = true)]
public int? ExitCode { get; set; }
/// <summary>
/// Git revision the compiler ran on. Not modifiable
/// </summary>
[Permissions(DenyWrite = true)]
public string Revision { get; set; }
/// <summary>
/// Git revision of the origin branch the compiler ran on. Not modifiable
/// </summary>
[Permissions(DenyWrite = true)]
public string OriginRevision { get; set; }
/// <summary>
/// How often the <see cref="DreamMaker"/> automatically compiles in minutes
/// </summary>
[Permissions(WriteRight = DreamMakerRights.SetAutoCompile)]
public int? AutoCompileInterval { get; set; }
/// <summary>
/// The .dme file <see cref="DreamMaker"/> tries to compile with
/// </summary>
[Permissions(WriteRight = DreamMakerRights.SetDme)]
public string TargetDme { get; set; }
}
}
+12 -3
View File
@@ -1,4 +1,5 @@
using Tgstation.Server.Api.Rights;
using System.ComponentModel.DataAnnotations;
using Tgstation.Server.Api.Rights;
namespace Tgstation.Server.Api.Models
{
@@ -6,7 +7,7 @@ namespace Tgstation.Server.Api.Models
/// Metadata about a server instance
/// </summary>
[Model(RightsType.InstanceManager, CanCrud = true, CanList = true)]
public sealed class Instance
public class Instance
{
/// <summary>
/// The id of the <see cref="Instance"/>. Not modifiable
@@ -18,12 +19,14 @@ namespace Tgstation.Server.Api.Models
/// The name of the <see cref="Instance"/>
/// </summary>
[Permissions(WriteRight = InstanceManagerRights.Rename)]
[Required]
public string Name { get; set; }
/// <summary>
/// The path to where the <see cref="Instance"/> is located
/// The path to where the <see cref="Instance"/> is located. Changing this will temporarily offline the <see cref="Instance"/> while it moves
/// </summary>
[Permissions(WriteRight = InstanceManagerRights.Relocate)]
[Required]
public string Path { get; set; }
/// <summary>
@@ -31,5 +34,11 @@ namespace Tgstation.Server.Api.Models
/// </summary>
[Permissions(WriteRight = InstanceManagerRights.SetOnline)]
public bool Online { get; set; }
/// <summary>
/// If <see cref="Configuration"/> can be used on the <see cref="Instance"/>
/// </summary>
[Permissions(WriteRight = InstanceManagerRights.SetConfiguration)]
public bool ConfigurationAllowed { get; set; }
}
}
@@ -6,14 +6,8 @@ namespace Tgstation.Server.Api.Models
/// Represents a <see cref="User"/>s permissions in an <see cref="Instance"/>
/// </summary>
[Model(RightsType.InstanceUser, WriteRight = InstanceUserRights.WriteUsers, CanList = true, RequiresInstance = true)]
public sealed class InstanceUser
public class InstanceUser
{
/// <summary>
/// See definition in <see cref="User.Id"/>
/// </summary>
[Permissions(DenyWrite = true)]
public long Id { get; set; }
/// <summary>
/// The <see cref="Rights.ByondRights"/> of the <see cref="InstanceUser"/>
/// </summary>
@@ -35,9 +29,9 @@ namespace Tgstation.Server.Api.Models
public RepositoryRights RepositoryRights { get; set; }
/// <summary>
/// The <see cref="Rights.ChatRights"/> of the <see cref="InstanceUser"/>
/// The <see cref="Rights.ChatSettingsRights"/> of the <see cref="InstanceUser"/>
/// </summary>
public ChatRights ChatRights { get; set; }
public ChatSettingsRights ChatSettingsRights { get; set; }
/// <summary>
/// The <see cref="Rights.ConfigurationRights"/> of the <see cref="InstanceUser"/>
@@ -0,0 +1,51 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Tgstation.Server.Api.Rights;
namespace Tgstation.Server.Api.Models.Internal
{
/// <summary>
/// Manage the server chat bots
/// </summary>
[Model(RightsType.ChatSettings, RequiresInstance = true)]
public class ChatSettings
{
/// <summary>
/// If the IRC client is enabled
/// </summary>
[Permissions(WriteRight = ChatSettingsRights.SetIrcEnabled)]
public bool IrcEnabled { get; set; }
/// <summary>
/// The IRC server name
/// </summary>
[Permissions(ReadRight = ChatSettingsRights.SetIrcSettings, WriteRight = ChatSettingsRights.SetIrcSettings)]
[Required]
public string IrcHost { get; set; }
/// <summary>
/// The IRC server port
/// </summary>
[Permissions(ReadRight = ChatSettingsRights.SetIrcSettings, WriteRight = ChatSettingsRights.SetIrcSettings)]
public ushort IrcPort { get; set; }
/// <summary>
/// The IRC server NickServ password
/// </summary>
[Permissions(ReadRight = ChatSettingsRights.SetIrcSettings, WriteRight = ChatSettingsRights.SetIrcSettings)]
public string IrcNickServPassword { get; set; }
/// <summary>
/// If the Discord bot is enabled
/// </summary>
[Permissions(WriteRight = ChatSettingsRights.SetDiscordEnabled)]
public bool DiscordEnabled { get; set; }
/// <summary>
/// The Discord bot token
/// </summary>
[Permissions(ReadRight = ChatSettingsRights.SetDiscordSettings, WriteRight = ChatSettingsRights.SetDiscordSettings)]
public string DiscordBotToken { get; set; }
}
}
@@ -0,0 +1,42 @@
using System;
using System.ComponentModel.DataAnnotations;
namespace Tgstation.Server.Api.Models.Internal
{
/// <summary>
/// Represents a run of <see cref="DreamMaker"/>
/// </summary>
public class CompileJob
{
/// <summary>
/// The ID of the job
/// </summary>
public long Id { get; set; }
/// <summary>
/// When the compilation started
/// </summary>
[Required]
public DateTimeOffset StartedAt { get; set; }
/// <summary>
/// When the compilation finished
/// </summary>
public DateTimeOffset FinishedAt { get; set; }
/// <summary>
/// If the compiler targeted the primary directory
/// </summary>
public bool? TargetedPrimaryDirectory { get; set; }
/// <summary>
/// Textual output of DM
/// </summary>
public string Output { get; set; }
/// <summary>
/// Exit code of DM. If <see langword="null"/>, the job was cancelled
/// </summary>
public int? ExitCode { get; set; }
}
}
@@ -0,0 +1,53 @@
using Tgstation.Server.Api.Rights;
namespace Tgstation.Server.Api.Models.Internal
{
/// <summary>
/// Configurable settings for <see cref="DreamDaemon"/>
/// </summary>
[Model(RightsType.DreamDaemon, CanCrud = true, RequiresInstance = true)]
public class DreamDaemonSettings
{
/// <summary>
/// If <see cref="DreamDaemon"/> starts when it's <see cref="Instance"/> starts
/// </summary>
[Permissions(ReadRight = DreamDaemonRights.ReadMetadata, WriteRight = DreamDaemonRights.SetAutoStart)]
public bool AutoStart { get; set; }
/// <summary>
/// If the BYOND web client can be used to connect to the game server
/// </summary>
[Permissions(ReadRight = DreamDaemonRights.ReadMetadata, WriteRight = DreamDaemonRights.SetWebClient)]
public bool AllowWebClient { get; set; }
/// <summary>
/// If the server is undergoing a soft reset. This may be automatically set by changes to other fields
/// </summary>
[Permissions(ReadRight = DreamDaemonRights.ReadMetadata, WriteRight = DreamDaemonRights.SoftRestart)]
public bool SoftRestart { get; set; }
/// <summary>
/// If the server is undergoing a soft shutdown
/// </summary>
[Permissions(ReadRight = DreamDaemonRights.ReadMetadata, WriteRight = DreamDaemonRights.SoftShutdown)]
public bool SoftShutdown { get; set; }
/// <summary>
/// The <see cref="DreamDaemonSecurity"/> level of <see cref="DreamDaemon"/>
/// </summary>
[Permissions(ReadRight = DreamDaemonRights.ReadMetadata, WriteRight = DreamDaemonRights.SetSecurity)]
public DreamDaemonSecurity SecurityLevel { get; set; }
/// <summary>
/// The first port <see cref="DreamDaemon"/> uses. This should be the publically advertised port
/// </summary>
[Permissions(ReadRight = DreamDaemonRights.ReadMetadata, WriteRight = DreamDaemonRights.SetPorts)]
public ushort PrimaryPort { get; set; }
/// <summary>
/// The second port <see cref="DreamDaemon"/> uses
/// </summary>
[Permissions(ReadRight = DreamDaemonRights.ReadMetadata, WriteRight = DreamDaemonRights.SetPorts)]
public ushort SecondaryPort { get; set; }
}
}
@@ -0,0 +1,23 @@
using Tgstation.Server.Api.Rights;
namespace Tgstation.Server.Api.Models.Internal
{
/// <summary>
/// Configurable settings for <see cref="DreamMaker"/>
/// </summary>
[Model(RightsType.DreamMaker, ReadRight = DreamMakerRights.Read, CanCrud = true, RequiresInstance = true)]
public class DreamMakerSettings
{
/// <summary>
/// How often the <see cref="DreamMakerSettings"/> automatically compiles in minutes
/// </summary>
[Permissions(WriteRight = DreamMakerRights.SetAutoCompile)]
public int? AutoCompileInterval { get; set; }
/// <summary>
/// The .dme file <see cref="DreamMakerSettings"/> tries to compile with
/// </summary>
[Permissions(WriteRight = DreamMakerRights.SetDme)]
public string TargetDme { get; set; }
}
}
@@ -0,0 +1,44 @@
using System;
using System.ComponentModel.DataAnnotations;
namespace Tgstation.Server.Api.Models.Internal
{
/// <summary>
/// Represents a long running job
/// </summary>
[Model(RequiresInstance = true)]
public class Job
{
/// <summary>
/// The <see cref="Job"/> ID
/// </summary>
[Permissions(DenyWrite = true)]
public long Id { get; set; }
/// <summary>
/// English description of the <see cref="Job"/>
/// </summary>
[Permissions(DenyWrite = true)]
[Required]
public string Description { get; set; }
/// <summary>
/// When the <see cref="Job"/> was started
/// </summary>
[Permissions(DenyWrite = true)]
[Required]
public DateTimeOffset StartedAt { get; set; }
/// <summary>
/// When the <see cref="Job"/> stopped
/// </summary>
[Permissions(DenyWrite = true)]
public DateTimeOffset StoppedAt { get; set; }
/// <summary>
/// If the <see cref="Job"/> was cancelled
/// </summary>
[Permissions(DenyWrite = true)]
public bool Cancelled { get; set; }
}
}
@@ -0,0 +1,50 @@
using System.ComponentModel.DataAnnotations;
using Tgstation.Server.Api.Rights;
namespace Tgstation.Server.Api.Models.Internal
{
/// <summary>
/// Represents configurable settings for a <see cref="Repository"/>
/// </summary>
[Model(RightsType.Repository, ReadRight = RepositoryRights.Read, RequiresInstance = true)]
public class RepositorySettings
{
/// <summary>
/// The name of the committer
/// </summary>
[Permissions(WriteRight = RepositoryRights.ChangeCommitter)]
[Required]
public string CommitterName { get; set; }
/// <summary>
/// The e-mail of the committer
/// </summary>
[Permissions(WriteRight = RepositoryRights.ChangeCommitter)]
[Required]
public string CommitterEmail { get; set; }
/// <summary>
/// The username to access the git repository with
/// </summary>
[Permissions(ReadRight = RepositoryRights.ChangeCredentials, WriteRight = RepositoryRights.ChangeCredentials)]
public string AccessUser { get; set; }
/// <summary>
/// The token/password to access the git repository with
/// </summary>
[Permissions(ReadRight = RepositoryRights.ChangeCredentials, WriteRight = RepositoryRights.ChangeCredentials)]
public string AccessToken { get; set; }
/// <summary>
/// If commits created from testmerges are pushed to the remote
/// </summary>
[Permissions(WriteRight = RepositoryRights.ChangeTestMergeCommits)]
public bool PushTestMergeCommits { get; set; }
/// <summary>
/// How often the <see cref="Repository"/> automatically updates in minutes
/// </summary>
[Permissions(WriteRight = RepositoryRights.ChangeAutoUpdate)]
public int? AutoUpdateInterval { get; set; }
}
}
@@ -0,0 +1,24 @@
using System.ComponentModel.DataAnnotations;
namespace Tgstation.Server.Api.Models.Internal
{
/// <summary>
/// Represents information about a current git revison
/// </summary>
public class RevisionInformation
{
/// <summary>
/// The revision sha
/// </summary>
[Permissions(DenyWrite = true)]
[Required, StringLength(40)]
public string Revision { get; set; }
/// <summary>
/// The sha of the most recent remote commit
/// </summary>
[Permissions(DenyWrite = true)]
[Required, StringLength(40)]
public string OriginRevision { get; set; }
}
}
@@ -0,0 +1,29 @@
using Tgstation.Server.Api.Rights;
namespace Tgstation.Server.Api.Models.Internal
{
/// <summary>
/// Metadata about an installation
/// </summary>
[Model(RightsType.Administration)]
public class ServerSettings
{
/// <summary>
/// Use the specified Windows/POSIX authentication group to authorize users. Changing this may enable or disable <see cref="User"/>s depending on how they were configured. Setting this to <see langword="null"/> changes the authentication mode to database.
/// </summary>
[Permissions(ReadRight = AdministrationRights.ChangeAuthenticationGroup, WriteRight = AdministrationRights.ChangeAuthenticationGroup)]
public string SystemAuthenticationGroup { get; set; }
/// <summary>
/// Automatically send unhandled exception data to a public collection service. This will be limited to system information, path data, and game code compilation information.
/// </summary>
[Permissions(ReadRight = AdministrationRights.ChangeTelemetry, WriteRight = AdministrationRights.ChangeTelemetry)]
public bool EnableTelemetry { get; set; }
/// <summary>
/// The git repository to recieve updates to Tgstation.Server.Host from, must include credentials if necessary. If set to <see langword="null"/> upstream pulls will be disabled entirely
/// </summary>
[Permissions(ReadRight = AdministrationRights.SetUpstreamRepository, WriteRight = AdministrationRights.SetUpstreamRepository)]
public string UpstreamRepository { get; set; }
}
}
@@ -0,0 +1,40 @@
using System;
using System.ComponentModel.DataAnnotations;
namespace Tgstation.Server.Api.Models.Internal
{
/// <summary>
/// Represents a merge of a GitHub pull request
/// </summary>
public class TestMerge : TestMergeParameters
{
/// <summary>
/// The ID of the <see cref="TestMerge"/>
/// </summary>
public long Id { get; set; }
/// <summary>
/// When the <see cref="TestMerge"/> was created
/// </summary>
[Required]
public DateTimeOffset MergedAt { get; set; }
/// <summary>
/// The title of the pull request
/// </summary>
[Required]
public string TitleAtMerge { get; set; }
/// <summary>
/// The body of the pull request
/// </summary>
[Required]
public string BodyAtMerge { get; set; }
/// <summary>
/// The author of the pull request
/// </summary>
[Required]
public string Author { get; set; }
}
}
@@ -0,0 +1,57 @@
using System;
using System.ComponentModel.DataAnnotations;
using Tgstation.Server.Api.Rights;
namespace Tgstation.Server.Api.Models.Internal
{
/// <summary>
/// Represents a server <see cref="User"/>
/// </summary>
[Model(RightsType.Administration, WriteRight = AdministrationRights.EditUsers, CanCrud = true)]
public class User
{
/// <summary>
/// The ID of the <see cref="User"/>
/// </summary>
[Permissions(DenyWrite = true)]
public long Id { get; set; }
/// <summary>
/// If the <see cref="User"/> is enabled since users cannot be deleted. System users cannot be disabled
/// </summary>
[Permissions(WriteRight = AdministrationRights.EditUsers)]
public bool Enabled { get; set; }
/// <summary>
/// When the <see cref="User"/> was created
/// </summary>
[Permissions(DenyWrite = true)]
[Required]
public DateTimeOffset CreatedAt { get; set; }
/// <summary>
/// The SID/UID of the <see cref="User"/> on Windows/POSIX respectively
/// </summary>
[Permissions(DenyWrite = true)]
public string SystemIdentifier { get; set; }
/// <summary>
/// The name of the <see cref="User"/>
/// </summary>
[Permissions(WriteRight = AdministrationRights.EditUsers)]
[Required]
public string Name { get; set; }
/// <summary>
/// The <see cref="Rights.AdministrationRights"/> for the <see cref="User"/>
/// </summary>
[Permissions(WriteRight = AdministrationRights.EditUsers)]
public AdministrationRights AdministrationRights { get; set; }
/// <summary>
/// The <see cref="Rights.InstanceManagerRights"/> for the <see cref="User"/>
/// </summary>
[Permissions(WriteRight = AdministrationRights.EditUsers)]
public InstanceManagerRights InstanceManagerRights { get; set; }
}
}
+12 -32
View File
@@ -1,46 +1,26 @@
using System;
namespace Tgstation.Server.Api.Models
namespace Tgstation.Server.Api.Models
{
/// <summary>
/// Represents a long running job on the server. Model is read-only, updates attempt to cancel the job
/// </summary>
[Model]
public sealed class Job
public sealed class Job : Internal.Job
{
/// <summary>
/// The <see cref="Job"/> ID
/// </summary>
public long Id { get; set; }
/// <summary>
/// Human readable description of the <see cref="Job"/>
/// </summary>
public string Description { get; set; }
/// <summary>
/// When the <see cref="Job"/> was started
/// </summary>
public DateTimeOffset StartedAt { get; set; }
/// <summary>
/// When the <see cref="Job"/> stopped
/// </summary>
public DateTimeOffset StoppedAt { get; set; }
/// <summary>
/// If the <see cref="Job"/> was cancelled
/// </summary>
public bool Cancelled { get; set; }
/// <summary>
/// If the <see cref="Job"/> has incremental progress, this will range from 1 - 100. 0 otherwise
/// </summary>
[Permissions(DenyWrite = true)]
public int Progress { get; set; }
/// <summary>
/// If the current user has permission to cancel the job. Will be <see langword="null"/> if <see cref="StoppedAt"/> isn't
/// If the current user has permission to cancel the job
/// </summary>
public bool? UserCanCancel { get; set; }
[Permissions(DenyWrite = true)]
public bool UserCanCancel { get; set; }
/// <summary>
/// The <see cref="User"/> that started the job
/// </summary>
[Permissions(DenyWrite = true)]
public User StartedBy { get; set; }
}
}
+10 -47
View File
@@ -6,8 +6,7 @@ namespace Tgstation.Server.Api.Models
/// <summary>
/// Represents a git repository
/// </summary>
[Model(RightsType.Repository, ReadRight = RepositoryRights.Read, RequiresInstance = true)]
public sealed class Repository
public sealed class Repository : Internal.RepositorySettings
{
/// <summary>
/// The origin URL. If <see langword="null"/>, the <see cref="Repository"/> does not exist
@@ -19,7 +18,13 @@ namespace Tgstation.Server.Api.Models
/// The commit HEAD points to
/// </summary>
[Permissions(WriteRight = RepositoryRights.SetSha)]
public string Sha { get; set; }
public string NewRevision { get; set; }
/// <summary>
/// The current <see cref="Models.RevisionInformation"/> for the <see cref="Repository"/>
/// </summary>
[Permissions(DenyWrite = true)]
public RevisionInformation RevisionInformation { get; set; }
/// <summary>
/// The branch or tag HEAD points to
@@ -28,51 +33,9 @@ namespace Tgstation.Server.Api.Models
public string Reference { get; set; }
/// <summary>
/// The name of the committer
/// </summary>
[Permissions(WriteRight = RepositoryRights.ChangeCommitter)]
public string CommitterName { get; set; }
/// <summary>
/// The e-mail of the committer
/// </summary>
[Permissions(WriteRight = RepositoryRights.ChangeCommitter)]
public string CommitterEmail { get; set; }
/// <summary>
/// Associated list of tag name -> sha for repository compiles. Not modifiable
/// </summary>
[Permissions(DenyWrite = true)]
public IReadOnlyDictionary<string, string> Backups { get; set; }
/// <summary>
/// Associated list of GitHub pull request number -> sha for merged pull requests. Adding a <see langword="null"/> value to this list will merge the latest commit of the pull request numbered by the key
/// <see cref="TestMergeParameters"/> for new <see cref="TestMerge"/>s
/// </summary>
[Permissions(WriteRight = RepositoryRights.MergePullRequest)]
public Dictionary<int, string> PullRequests { get; set; }
/// <summary>
/// The username to access the git repository with
/// </summary>
[Permissions(ReadRight = RepositoryRights.ChangeCredentials, WriteRight = RepositoryRights.ChangeCredentials)]
public string AccessUser { get; set; }
/// <summary>
/// The token/password to access the git repository with
/// </summary>
[Permissions(ReadRight = RepositoryRights.ChangeCredentials, WriteRight = RepositoryRights.ChangeCredentials)]
public string AccessToken { get; set; }
/// <summary>
/// If commits created from testmerges are pushed to the remote
/// </summary>
[Permissions(WriteRight = RepositoryRights.ChangeTestMergeCommits)]
public bool PushTestMergeCommits { get; set; }
/// <summary>
/// How often the <see cref="Repository"/> automatically updates in minutes
/// </summary>
[Permissions(WriteRight = RepositoryRights.ChangeAutoUpdate)]
public int? AutoUpdateInterval { get; set; }
public List<TestMergeParameters> NewTestMerges { get; set; }
}
}
@@ -0,0 +1,13 @@
using System.Collections.Generic;
namespace Tgstation.Server.Api.Models
{
/// <inheritdoc />
public sealed class RevisionInformation : Internal.RevisionInformation
{
/// <summary>
/// The <see cref="TestMerge"/>s active in the <see cref="RevisionInformation"/>
/// </summary>
public List<TestMerge> TestMerges { get; set; }
}
}
@@ -0,0 +1,11 @@
namespace Tgstation.Server.Api.Models
{
/// <inheritdoc />
public sealed class TestMerge : Internal.TestMerge
{
/// <summary>
/// The <see cref="User"/> who created the <see cref="TestMerge"/>
/// </summary>
public User MergedBy { get; set; }
}
}
@@ -0,0 +1,21 @@
using System.ComponentModel.DataAnnotations;
namespace Tgstation.Server.Api.Models
{
/// <summary>
/// Parameters for creating a <see cref="TestMerge"/>
/// </summary>
public class TestMergeParameters
{
/// <summary>
/// The number of the pull request
/// </summary>
public int Number { get; set; }
/// <summary>
/// The sha of the pull request revision to merge
/// </summary>
[Required]
public string PullRequestRevision { get; set; }
}
}
+5 -30
View File
@@ -1,38 +1,13 @@
using System;
using System.Net;
using Tgstation.Server.Api.Rights;
namespace Tgstation.Server.Api.Models
namespace Tgstation.Server.Api.Models
{
/// <summary>
/// Represents an access token for the server. Read action generates a new one. Update action expires all for user
/// Represents a JWT returned by the API
/// </summary>
[Model(RightsType.Token, CanList = true)]
public sealed class Token
{
/// <summary>
/// The id of the <see cref="Token"/>. Not modifiable
/// The value of the JWT
/// </summary>
public long Id { get; }
/// <summary>
/// The user agent that created the <see cref="Token"/>
/// </summary>
public string ClientUserAgent { get; set; }
/// <summary>
/// The <see cref="IPAddress"/> the <see cref="Token"/> was originally issued to
/// </summary>
public IPAddress IssuedTo { get; set; }
/// <summary>
/// When the <see cref="Token"/> was originally issued
/// </summary>
public DateTimeOffset IssuedAt { get; set; }
/// <summary>
/// The token <see cref="string"/>. Not modifiable, only appears once
/// </summary>
public string Value { get; set; }
public string Bearer { get; set; }
}
}
}
+5 -32
View File
@@ -1,39 +1,12 @@
using Tgstation.Server.Api.Rights;
namespace Tgstation.Server.Api.Models
namespace Tgstation.Server.Api.Models
{
/// <summary>
/// Represents a server <see cref="User"/>
/// </summary>
[Model(RightsType.Administration, WriteRight = AdministrationRights.EditUsers)]
public sealed class User
/// <inheritdoc />
public sealed class User : Internal.User
{
/// <summary>
/// The ID of the <see cref="User"/>
/// The <see cref="User"/> who created this <see cref="User"/>
/// </summary>
[Permissions(DenyWrite = true)]
public long Id { get; set; }
/// <summary>
/// The SID/UID of the <see cref="User"/> on Windows/POSIX respectively
/// </summary>
[Permissions(DenyWrite = true)]
public string SystemId { get; set; }
/// <summary>
/// The name of the <see cref="User"/>
/// </summary>
[Permissions(DenyWrite = true)]
public string Name { get; set; }
/// <summary>
/// The <see cref="Rights.AdministrationRights"/> for the <see cref="User"/>
/// </summary>
public AdministrationRights AdministrationRights { get; set; }
/// <summary>
/// The <see cref="Rights.InstanceManagerRights"/> for the <see cref="User"/>
/// </summary>
public InstanceManagerRights InstanceManagerRights { get; set; }
public User CreatedBy { get; set; }
}
}
@@ -13,11 +13,11 @@ namespace Tgstation.Server.Api.Rights
/// </summary>
None = 0,
/// <summary>
/// User can change <see cref="Models.Administration.SystemAuthenticationGroup"/>
/// User can change <see cref="Models.Internal.ServerSettings.SystemAuthenticationGroup"/>
/// </summary>
ChangeAuthenticationGroup = 1,
/// <summary>
/// User can change <see cref="Models.Administration.EnableTelemetry"/>
/// User can change <see cref="Models.Internal.ServerSettings.EnableTelemetry"/>
/// </summary>
ChangeTelemetry = 2,
/// <summary>
@@ -33,9 +33,8 @@ namespace Tgstation.Server.Api.Rights
/// </summary>
ChangeVersion = 16,
/// <summary>
/// User can change <see cref="Models.Administration.UpstreamRepository"/>
/// User can change <see cref="Models.Internal.ServerSettings.UpstreamRepository"/>
/// </summary>
SetUpstreamRepository
SetUpstreamRepository = 32
}
}
@@ -3,10 +3,10 @@
namespace Tgstation.Server.Api.Rights
{
/// <summary>
/// Rights for <see cref="Models.Chat"/>
/// Rights for <see cref="Models.ChatSettings"/>
/// </summary>
[Flags]
public enum ChatRights
public enum ChatSettingsRights
{
/// <summary>
/// User has no rights
@@ -21,9 +21,9 @@ namespace Tgstation.Server.Api.Rights
/// </summary>
SetIrcSettings = 2,
/// <summary>
/// User can change the IRC channels
/// User can change the chat channels
/// </summary>
SetIrcChannels = 4,
SetChannels = 4,
/// <summary>
/// User can enable/disable the Discord bot
/// </summary>
@@ -32,9 +32,5 @@ namespace Tgstation.Server.Api.Rights
/// User can change the Discord settings
/// </summary>
SetDiscordSettings = 16,
/// <summary>
/// User can change the Discord channels
/// </summary>
SetDiscordChannels = 32,
}
}
@@ -13,7 +13,7 @@ namespace Tgstation.Server.Api.Rights
/// </summary>
None = 0,
/// <summary>
/// User can read <see cref="Models.DreamDaemon.PullRequests"/>, <see cref="Models.DreamDaemon.Sha"/>, and <see cref="Models.DreamDaemon.OriginSha"/>
/// User can read <see cref="Models.DreamDaemon.CompileJob"/>
/// </summary>
ReadRevision = 1,
/// <summary>
@@ -21,27 +21,27 @@ namespace Tgstation.Server.Api.Rights
/// </summary>
SetPorts = 2,
/// <summary>
/// User can change <see cref="Models.DreamDaemon.AutoStart"/>
/// User can change <see cref="Models.Internal.DreamDaemonSettings.AutoStart"/>
/// </summary>
SetAutoStart = 4,
/// <summary>
/// User set <see cref="Models.DreamDaemon.SecurirtyLevel"/>
/// User set <see cref="Models.Internal.DreamDaemonSettings.SecurityLevel"/>
/// </summary>
SetSecurity = 8,
/// <summary>
/// User can read all ports, <see cref="Models.DreamDaemon.SoftRestart"/>, <see cref="Models.DreamDaemon.SoftShutdown"/>, <see cref="Models.DreamDaemon.Status"/>, <see cref="Models.DreamDaemon.AllowWebClient"/>, and <see cref="Models.DreamDaemon.AutoStart"/>
/// User can read all ports, <see cref="Models.Internal.DreamDaemonSettings.SoftRestart"/>, <see cref="Models.Internal.DreamDaemonSettings.SoftShutdown"/>, <see cref="Models.DreamDaemon.Status"/>, <see cref="Models.Internal.DreamDaemonSettings.AllowWebClient"/>, and <see cref="Models.Internal.DreamDaemonSettings.AutoStart"/>
/// </summary>
ReadMetadata = 16,
/// <summary>
/// User can change <see cref="Models.DreamDaemon.AllowWebClient"/>
/// User can change <see cref="Models.Internal.DreamDaemonSettings.AllowWebClient"/>
/// </summary>
SetWebClient = 32,
/// <summary>
/// User can enable <see cref="Models.DreamDaemon.SoftRestart"/>
/// User can enable <see cref="Models.Internal.DreamDaemonSettings.SoftRestart"/>
/// </summary>
SoftRestart = 64,
/// <summary>
/// User can enable <see cref="Models.DreamDaemon.SoftShutdown"/>
/// User can enable <see cref="Models.Internal.DreamDaemonSettings.SoftShutdown"/>
/// </summary>
SoftShutdown = 128,
/// <summary>
@@ -53,7 +53,7 @@ namespace Tgstation.Server.Api.Rights
/// </summary>
Shutdown = 512,
/// <summary>
/// User can start <see cref="Models.DreamDaemon"/> and disable <see cref="Models.DreamDaemon.SoftRestart"/> and <see cref="Models.DreamDaemon.SoftShutdown"/>
/// User can start <see cref="Models.DreamDaemon"/> and disable <see cref="Models.Internal.DreamDaemonSettings.SoftRestart"/> and <see cref="Models.Internal.DreamDaemonSettings.SoftShutdown"/>
/// </summary>
Start = 1024
}
@@ -25,11 +25,11 @@ namespace Tgstation.Server.Api.Rights
/// </summary>
CancelCompile = 4,
/// <summary>
/// User may modify <see cref="Models.DreamMaker.AutoCompileInterval"/>
/// User may modify <see cref="Models.Internal.DreamMakerSettings.AutoCompileInterval"/>
/// </summary>
SetAutoCompile = 8,
/// <summary>
/// User may modify <see cref="Models.DreamMaker.TargetDme"/>
/// User may modify <see cref="Models.Internal.DreamMakerSettings.TargetDme"/>
/// </summary>
SetDme = 16
}
@@ -39,6 +39,10 @@ namespace Tgstation.Server.Api.Rights
/// <summary>
/// User can view all <see cref="Models.Instance"/>s
/// </summary>
List = 64
List = 64,
/// <summary>
/// User can change <see cref="Models.Instance.ConfigurationAllowed"/>
/// </summary>
SetConfiguration = 128
}
}
@@ -29,19 +29,19 @@ namespace Tgstation.Server.Api.Rights
/// </summary>
UpdateBranch = 16,
/// <summary>
/// User may change <see cref="Models.Repository.CommitterName"/> and <see cref="Models.Repository.CommitterEmail"/>
/// User may change <see cref="Models.Internal.RepositorySettings.CommitterName"/> and <see cref="Models.Internal.RepositorySettings.CommitterEmail"/>
/// </summary>
ChangeCommitter = 32,
/// <summary>
/// User may change <see cref="Models.Repository.PushTestMergeCommits"/>
/// User may change <see cref="Models.Internal.RepositorySettings.PushTestMergeCommits"/>
/// </summary>
ChangeTestMergeCommits = 64,
/// <summary>
/// User may change <see cref="Models.Repository.AutoUpdateInterval"/>
/// User may change <see cref="Models.Internal.RepositorySettings.AutoUpdateInterval"/>
/// </summary>
ChangeAutoUpdate = 128,
/// <summary>
/// User may read and change <see cref="Models.Repository.AccessUser"/> and <see cref="Models.Repository.AccessToken"/>
/// User may read and change <see cref="Models.Internal.RepositorySettings.AccessUser"/> and <see cref="Models.Internal.RepositorySettings.AccessToken"/>
/// </summary>
ChangeCredentials = 256,
/// <summary>
@@ -49,7 +49,7 @@ namespace Tgstation.Server.Api.Rights
/// </summary>
SetReference = 512,
/// <summary>
/// User may read all fields in the <see cref="Models.Repository"/> with the exception of <see cref="Models.Repository.AccessToken"/>
/// User may read all fields in the <see cref="Models.Repository"/> with the exception of <see cref="Models.Internal.RepositorySettings.AccessToken"/>
/// </summary>
Read = 1024,
}
@@ -15,13 +15,12 @@ namespace Tgstation.Server.Api.Rights
{
{ RightsType.Administration, typeof(AdministrationRights) },
{ RightsType.InstanceManager, typeof(InstanceManagerRights) },
{ RightsType.Token, typeof(TokenRights) },
{ RightsType.Repository, typeof(RepositoryRights) },
{ RightsType.Byond, typeof(ByondRights) },
{ RightsType.DreamMaker, typeof(DreamMakerRights) },
{ RightsType.DreamDaemon, typeof(DreamDaemonRights) },
{ RightsType.Chat, typeof(ChatRights) },
{ RightsType.ChatSettings, typeof(ChatSettingsRights) },
{ RightsType.Configuration, typeof(ConfigurationRights) },
{ RightsType.InstanceUser, typeof(InstanceUserRights) }
};
@@ -31,6 +30,33 @@ namespace Tgstation.Server.Api.Rights
/// </summary>
/// <param name="rightsType">The <see cref="RightsType"/> to lookup</param>
/// <returns>The <see cref="Enum"/> <see cref="Type"/> of the given <paramref name="rightsType"/></returns>
public static Type TypeToRight(RightsType rightsType) => typeMap[rightsType];
public static Type RightToType(RightsType rightsType) => typeMap[rightsType];
/// <summary>
/// Gets the role claim name used for a given <paramref name="right"/>
/// </summary>
/// <typeparam name="TRight">The <see cref="RightsType"/></typeparam>
/// <param name="right">The <typeparamref name="TRight"/></param>
/// <returns>A <see cref="string"/> representing the claim role name</returns>
public static string RoleName<TRight>(TRight right) => String.Concat(typeof(TRight).Name, '.', right.ToString());
/// <summary>
/// Gets the role claim name used for a given <paramref name="rightsType"/> and <paramref name="right"/>
/// </summary>
/// <param name="rightsType">The <see cref="RightsType"/></param>
/// <param name="right">The right value</param>
/// <returns>A <see cref="string"/> representing the claim role name</returns>
public static string RoleName(RightsType rightsType, Enum right)
{
var enumType = typeMap[rightsType];
return String.Concat(enumType.Name, '.', Enum.GetName(enumType, right));
}
/// <summary>
/// Check if a given <paramref name="rightsType"/> is meant for an <see cref="Models.Instance"/>
/// </summary>
/// <param name="rightsType">The <see cref="RightsType"/> to check</param>
/// <returns><see langword="true"/> if <paramref name="rightsType"/> is an instance right, <see langword="false"/> otherwise</returns>
public static bool IsInstanceRight(RightsType rightsType) => !(rightsType == RightsType.Administration || rightsType == RightsType.InstanceManager);
}
}
@@ -13,11 +13,6 @@
/// <see cref="InstanceManagerRights"/>
/// </summary>
InstanceManager,
/// <summary>
/// <see cref="TokenRights"/>
/// </summary>
Token,
/// <summary>
/// <see cref="RepositoryRights"/>
/// </summary>
@@ -35,9 +30,9 @@
/// </summary>
DreamDaemon,
/// <summary>
/// <see cref="ChatRights"/>
/// <see cref="ChatSettingsRights"/>
/// </summary>
Chat,
ChatSettings,
/// <summary>
/// <see cref="ConfigurationRights"/>
/// </summary>
@@ -1,32 +0,0 @@
using System;
namespace Tgstation.Server.Api.Rights
{
/// <summary>
/// Rights for <see cref="Models.Token"/>s
/// </summary>
[Flags]
public enum TokenRights
{
/// <summary>
/// User has no rights
/// </summary>
None = 0,
/// <summary>
/// User can create <see cref="Models.Token"/>s
/// </summary>
Create = 1,
/// <summary>
/// User can delete <see cref="Models.Token"/>s
/// </summary>
Delete = 2,
/// <summary>
/// User can list all their <see cref="Models.Token"/>s
/// </summary>
List = 4,
/// <summary>
/// User can perform all the actions they can for themselves on behalf of other users except <see cref="Create"/>
/// </summary>
Admin = 8,
}
}
@@ -10,16 +10,18 @@
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<WarningsAsErrors />
<DocumentationFile>bin\Release\netstandard2.0\Tgstation.Server.Api.xml</DocumentationFile>
<NoWarn>1701;1702;1705;CA2227</NoWarn>
<NoWarn>1701;1702;1705;CA2227;CA1819</NoWarn>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<LangVersion>latest</LangVersion>
<NoWarn>1701;1702;1705;CA2227</NoWarn>
<NoWarn>1701;1702;1705;CA2227;CA1819</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Http.Extensions" Version="2.1.0-preview2-final" />
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.6.0" />
<PackageReference Include="System.ComponentModel.Annotations" Version="4.4.1" />
</ItemGroup>
</Project>
@@ -8,21 +8,21 @@ namespace Tgstation.Server.Client.Components
/// <summary>
/// For managing the chat bots
/// </summary>
public interface IChatClient : IRightsClient<ChatRights>
public interface IChatSettingsClient : IRightsClient<ChatSettingsRights>
{
/// <summary>
/// Get the <see cref="Chat"/> represented by the <see cref="IChatClient"/>
/// Get the <see cref="ChatSettings"/> represented by the <see cref="IChatSettingsClient"/>
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="Chat"/> represented by the <see cref="IChatClient"/></returns>
Task<Chat> Read(CancellationToken cancellationToken);
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="ChatSettings"/> represented by the <see cref="IChatSettingsClient"/></returns>
Task<ChatSettings> Read(CancellationToken cancellationToken);
/// <summary>
/// Updates the <see cref="Chat"/> setttings
/// Updates the <see cref="ChatSettings"/> setttings
/// </summary>
/// <param name="chat">The <see cref="Chat"/> to update</param>
/// <param name="chat">The <see cref="ChatSettings"/> to update</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task Update(Chat chat, CancellationToken cancellationToken);
Task Update(ChatSettings chat, CancellationToken cancellationToken);
}
}
@@ -32,7 +32,7 @@ namespace Tgstation.Server.Client.Components
Task Shutdown(CancellationToken cancellationToken);
/// <summary>
/// Update <see cref="DreamDaemon"/>. This may trigger <see cref="DreamDaemon.SoftRestart"/>
/// Update <see cref="DreamDaemon"/>. This may trigger <see cref="Api.Models.Internal.DreamDaemonSettings.SoftRestart"/>
/// </summary>
/// <param name="dreamDaemon">The <see cref="DreamDaemon"/> to update</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
@@ -15,7 +15,7 @@ namespace Tgstation.Server.Client.Components
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="DreamMaker"/> represented by the <see cref="IDreamMakerClient"/></returns>
Task<Chat> Read(CancellationToken cancellationToken);
Task<ChatSettings> Read(CancellationToken cancellationToken);
/// <summary>
/// Updates the <see cref="DreamMaker"/> setttings
@@ -35,9 +35,9 @@ namespace Tgstation.Server.Client.Components
IInstanceUserClient Users { get; }
/// <summary>
/// Access the <see cref="IChatClient"/>
/// Access the <see cref="IChatSettingsClient"/>
/// </summary>
IChatClient Chat { get; }
IChatSettingsClient Chat { get; }
/// <summary>
/// Access the <see cref="IDreamMakerClient"/>
@@ -1,57 +0,0 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Rights;
namespace Tgstation.Server.Client.Components
{
/// <summary>
/// <see cref="IServerClient"/> for managing <see cref="Token"/>s
/// </summary>
public interface ITokenClient: IRightsClient<TokenRights>
{
/// <summary>
/// Generate a new <see cref="Token"/> for the connected user/ipaddress, deleting the current one if applicable
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the new <see cref="Token"/></returns>
Task<Token> Read(CancellationToken cancellationToken);
/// <summary>
/// Gets all active <see cref="Token"/>s for the <see cref="IServerClient"/>
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting a <see cref="IReadOnlyList{T}"/> of active <see cref="Token"/>s for the <see cref="IServerClient"/></returns>
Task<IReadOnlyList<Token>> GetClientInfos(CancellationToken cancellationToken);
/// <summary>
/// Gets all active <see cref="Token"/>s for all users
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting a <see cref="IReadOnlyDictionary{TKey, TValue}"/> of active <see cref="Token"/>s for the <see cref="IServerClient"/> keyed by username</returns>
Task<IReadOnlyDictionary<string, Token>> GetAllInfos(CancellationToken cancellationToken);
/// <summary>
/// Invalidate the specified active <paramref name="token"/>
/// </summary>
/// <param name="token">The <see cref="Token"/> to invalidate</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task Invalidate(Token token, CancellationToken cancellationToken);
/// <summary>
/// Invalidate all active <see cref="Token"/>s for a <see cref="IServerClient"/>
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task InvalidateAllClient(CancellationToken cancellationToken);
/// <summary>
/// Invalidate all active <see cref="Token"/>s
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task InvalidateAll(CancellationToken cancellationToken);
}
}
@@ -30,11 +30,6 @@ namespace Tgstation.Server.Client
/// </summary>
int RequeryRate { get; set; }
/// <summary>
/// Access the <see cref="ITokenClient"/>
/// </summary>
ITokenClient Tokens { get; }
/// <summary>
/// Access the <see cref="IInstanceManagerClient"/>
/// </summary>
@@ -24,7 +24,7 @@ namespace Tgstation.Server.Client
/// Create a <see cref="IServerClient"/>
/// </summary>
/// <param name="hostname">The URL to access tgstation-server at</param>
/// <param name="token">The <see cref="Api.Models.Token.Value"/> to access the API with</param>
/// <param name="token">The <see cref="Api.Models.Token.Bearer"/> to access the API with</param>
/// <param name="timeout">The initial timeout for the connection</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in a new <see cref="IServerClient"/></returns>
@@ -0,0 +1,21 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:51882/",
"sslPort": 0
}
},
"profiles": {
"Tgstation.Server.Host.Console": {
"commandName": "Project",
"workingDirectory": "bin\\Debug\\netcoreapp2.0",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:51885/"
}
}
}
+3 -4
View File
@@ -7,19 +7,18 @@ namespace Tgstation.Server.Host.Startup
/// <summary>
/// Represents the host
/// </summary>
public interface IServer : IDisposable
public interface IServer
{
/// <summary>
/// The path to the updated assembly to run if any. Populated once <see cref="RunAsync(string[], CancellationToken)"/> returns
/// The path to the updated assembly to run if any. Populated once <see cref="RunAsync(CancellationToken)"/> returns
/// </summary>
string UpdatePath { get; }
/// <summary>
/// Runs the <see cref="IServer"/>
/// </summary>
/// <param name="args">The arguments for the <see cref="IServer"/></param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task RunAsync(string[] args, CancellationToken cancellationToken);
Task RunAsync(CancellationToken cancellationToken);
}
}
@@ -5,10 +5,11 @@
/// </summary>
public interface IServerFactory
{
/// <summary>
/// Create a <see cref="IServer"/>
/// </summary>
/// <returns>A new <see cref="IServer"/></returns>
IServer CreateServer();
/// <summary>
/// Create a <see cref="IServer"/>
/// </summary>
/// <param name="args">The arguments for the <see cref="IServer"/></param>
/// <returns>A new <see cref="IServer"/></returns>
IServer CreateServer(string[] args);
}
}
@@ -25,8 +25,9 @@ namespace Tgstation.Server.Host.Watchdog
/// <summary>
/// Loads the <see cref="Assembly"/> at <see cref="assemblyPath"/> and creates an <see cref="IServer"/> from it
/// </summary>
/// <param name="args">The arguments for the <see cref="IServer"/></param>
/// <returns>A new <see cref="IServer"/></returns>
public IServer CreateServer()
public IServer CreateServer(string[] args)
{
var assembly = LoadFromAssemblyPath(assemblyPath);
//find the IServerFactory implementation
@@ -35,7 +36,7 @@ namespace Tgstation.Server.Host.Watchdog
var serverFactoryImplementationType = assembly.GetTypes().Where(x => serverFactoryInterfaceType.IsAssignableFrom(x)).First();
var serverFactory = (IServerFactory)Activator.CreateInstance(serverFactoryImplementationType);
return serverFactory.CreateServer();
return serverFactory.CreateServer(args);
}
//honestly have no idea what this is for, but the examples i see just return null and it seems to work just fine
+12 -17
View File
@@ -43,24 +43,19 @@ namespace Tgstation.Server.Host.Watchdog
//first run the host we started with
var serverFactory = initialServerFactory;
var assemblyPath = serverFactory.GetType().Assembly.Location;
do
{
string updatePath;
using (var server = serverFactory.CreateServer())
{
serverFactory = null;
await server.RunAsync(args, cancellationToken).ConfigureAwait(false);
updatePath = server.UpdatePath;
}
do
{
var server = serverFactory.CreateServer(args);
await server.RunAsync(cancellationToken).ConfigureAwait(false);
if (updatePath == null)
break;
activeAssemblyDeleter.DeleteActiveAssembly(assemblyPath);
File.Move(updatePath, assemblyPath);
serverFactory = isolatedAssemblyLoader.CreateIsolatedServerFactory(assemblyPath);
}
while (!cancellationToken.IsCancellationRequested);
if (server.UpdatePath == null)
break;
activeAssemblyDeleter.DeleteActiveAssembly(assemblyPath);
File.Move(server.UpdatePath, assemblyPath);
serverFactory = isolatedAssemblyLoader.CreateIsolatedServerFactory(assemblyPath);
}
while (!cancellationToken.IsCancellationRequested);
}
}
}
@@ -0,0 +1,23 @@
namespace Tgstation.Server.Host.Configuration
{
/// <summary>
/// Configuration options for the <see cref="Models.DatabaseContext{TParentContext}"/>
/// </summary>
sealed class DatabaseConfiguration
{
/// <summary>
/// The key for the <see cref="Microsoft.Extensions.Configuration.IConfigurationSection"/> the <see cref="DatabaseConfiguration"/> resides in
/// </summary>
public const string Section = "Database";
/// <summary>
/// The <see cref="Configuration.DatabaseType"/> to create
/// </summary>
public DatabaseType DatabaseType { get; set; }
/// <summary>
/// The connection string for the database
/// </summary>
public string ConnectionString { get; set; }
}
}
@@ -0,0 +1,21 @@
namespace Tgstation.Server.Host.Configuration
{
/// <summary>
/// Type of database to user
/// </summary>
enum DatabaseType
{
/// <summary>
/// Use Microsoft SQL Server
/// </summary>
SqlServer,
/// <summary>
/// Use MySQL/MariaDB
/// </summary>
MySql,
/// <summary>
/// Use SQLite 3
/// </summary>
Sqlite
}
}
@@ -0,0 +1,132 @@
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Threading.Tasks;
using Tgstation.Server.Api;
using Tgstation.Server.Api.Rights;
using Tgstation.Server.Host.Models;
using Tgstation.Server.Host.Security;
namespace Tgstation.Server.Host.Controllers
{
/// <summary>
/// A <see cref="Controller"/> for API functions
/// </summary>
[Produces(ApiHeaders.ApplicationJson)]
[Consumes(ApiHeaders.ApplicationJson)]
public abstract class ApiController : Controller
{
/// <summary>
/// The <see cref="ApiHeaders"/> for the operation
/// </summary>
protected ApiHeaders ApiHeaders { get; private set; }
/// <summary>
/// The <see cref="IDatabaseContext"/> for the operation
/// </summary>
protected IDatabaseContext DatabaseContext { get; }
/// <summary>
/// The <see cref="IAuthenticationContext"/> for the operation
/// </summary>
protected IAuthenticationContext AuthenticationContext { get; }
/// <summary>
/// The <see cref="Instance"/> for the operation
/// </summary>
protected Instance Instance { get; }
/// <summary>
/// Runs after a <see cref="Api.Models.Token"/> has been validated. Creates the <see cref="IAuthenticationContext"/> for the <see cref="ControllerBase.Request"/>
/// </summary>
/// <param name="context">The <see cref="TokenValidatedContext"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
public static async Task OnTokenValidated(TokenValidatedContext context)
{
var databaseContext = context.HttpContext.RequestServices.GetRequiredService<IDatabaseContext>();
var authenticationContextFactory = context.HttpContext.RequestServices.GetRequiredService<IAuthenticationContextFactory>();
var userIdClaim = context.Principal.FindFirst(JwtRegisteredClaimNames.Sub);
if (userIdClaim == default(Claim))
throw new InvalidOperationException("Missing required claim!");
long userId;
try
{
userId = Int64.Parse(userIdClaim.Value, CultureInfo.InvariantCulture);
}
catch (Exception e)
{
throw new InvalidOperationException("Failed to parse user ID!", e);
}
ApiHeaders apiHeaders;
try
{
apiHeaders = new ApiHeaders(context.HttpContext.Request.GetTypedHeaders());
}
catch
{
//let OnActionExecutionAsync handle the reponse
return;
}
await authenticationContextFactory.CreateAuthenticationContext(userId, apiHeaders.InstanceId, context.HttpContext.RequestAborted).ConfigureAwait(false);
var authenticationContext = authenticationContextFactory.CurrentAuthenticationContext;
var enumerator = Enum.GetValues(typeof(RightsType));
var claims = new List<Claim>();
foreach (RightsType I in enumerator)
{
var rightInt = authenticationContext.GetRight(I);
var rightEnum = RightsHelper.RightToType(I);
var right = (Enum)Enum.ToObject(rightEnum, authenticationContext.GetRight(I));
foreach(Enum J in Enum.GetValues(rightEnum))
if(right.HasFlag(J))
claims.Add(new Claim(ClaimTypes.Role, RightsHelper.RoleName(I, J)));
}
context.Principal.AddIdentity(new ClaimsIdentity(claims));
}
/// <summary>
/// Construct an <see cref="ApiController"/>
/// </summary>
/// <param name="databaseContext">The value of <see cref="DatabaseContext"/></param>
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="ApiController"/></param>
public ApiController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory)
{
DatabaseContext = databaseContext ?? throw new ArgumentNullException(nameof(databaseContext));
if (authenticationContextFactory == null)
throw new ArgumentNullException(nameof(authenticationContextFactory));
AuthenticationContext = authenticationContextFactory.CurrentAuthenticationContext;
Instance = AuthenticationContext?.InstanceUser?.Instance;
}
/// <inheritdoc />
public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
//validate the headers
try
{
ApiHeaders = new ApiHeaders(Request.GetTypedHeaders());
}
catch (InvalidOperationException e)
{
await BadRequest(new { message = e.Message }).ExecuteResultAsync(context).ConfigureAwait(false);
return;
}
await base.OnActionExecutionAsync(context, next).ConfigureAwait(false);
}
}
}
@@ -0,0 +1,110 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api;
using Tgstation.Server.Host.Core;
using Tgstation.Server.Host.Models;
using Tgstation.Server.Host.Security;
namespace Tgstation.Server.Host.Controllers
{
/// <summary>
/// Main <see cref="ApiController"/> for the <see cref="Application"/>
/// </summary>
[Route("/")]
public sealed class HomeController : ApiController
{
/// <summary>
/// The <see cref="ITokenFactory"/> for the <see cref="HomeController"/>
/// </summary>
readonly ITokenFactory tokenFactory;
/// <summary>
/// The <see cref="ISystemIdentityFactory"/> for the <see cref="HomeController"/>
/// </summary>
readonly ISystemIdentityFactory systemIdentityFactory;
/// <summary>
/// The <see cref="ICryptographySuite"/> for the <see cref="HomeController"/>
/// </summary>
readonly ICryptographySuite cryptographySuite;
/// <summary>
/// Construct a <see cref="HomeController"/>
/// </summary>
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> for the <see cref="ApiController"/></param>
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="ApiController"/></param>
/// <param name="tokenFactory">The value of <see cref="tokenFactory"/></param>
/// <param name="systemIdentityFactory">The value of <see cref="systemIdentityFactory"/></param>
/// <param name="cryptographySuite">The value of <see cref="cryptographySuite"/></param>
public HomeController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ITokenFactory tokenFactory, ISystemIdentityFactory systemIdentityFactory, ICryptographySuite cryptographySuite) : base(databaseContext, authenticationContextFactory)
{
this.tokenFactory = tokenFactory ?? throw new ArgumentNullException(nameof(tokenFactory));
this.systemIdentityFactory = systemIdentityFactory ?? throw new ArgumentNullException(nameof(systemIdentityFactory));
this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite));
}
/// <summary>
/// Returns the version of the <see cref="Application"/>
/// </summary>
/// <returns><see cref="Application.Version"/></returns>
[TgsAuthorize]
[HttpGet]
public JsonResult Home() => Json(Application.Version);
/// <summary>
/// Attempt to authenticate a <see cref="User"/> using <see cref="ApiController.ApiHeaders"/>
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IActionResult"/> of the operation</returns>
[HttpPost]
public async Task<IActionResult> CreateToken(CancellationToken cancellationToken)
{
if (ApiHeaders.IsTokenAuthentication)
return BadRequest(new { message = "Cannot create a token using another token!" });
var user = await DatabaseContext.Users.Where(x => x.Name == ApiHeaders.Username).Select(x => new User{
Id = x.Id,
PasswordHash = x.PasswordHash,
SystemIdentifier = x.SystemIdentifier,
Enabled = x.Enabled
}).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
if (user == null)
return Unauthorized();
if (user.PasswordHash != null)
{
var originalHash = user.PasswordHash;
if (!cryptographySuite.CheckUserPassword(user, ApiHeaders.Password))
return Unauthorized();
if (user.PasswordHash != originalHash)
{
var updatedUser = new User
{
Id = user.Id,
PasswordHash = originalHash
};
DatabaseContext.Users.Attach(updatedUser);
updatedUser.PasswordHash = user.PasswordHash;
await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
}
}
else
try
{
var systemIdentity = systemIdentityFactory.CreateSystemIdentity(ApiHeaders.Username, ApiHeaders.Password);
}
catch
{
return Unauthorized();
}
if (!user.Enabled)
return Forbid();
return Json(tokenFactory.CreateToken(user));
}
}
}
@@ -0,0 +1,72 @@
using Microsoft.AspNetCore.Mvc;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api;
using Tgstation.Server.Host.Models;
using Tgstation.Server.Host.Security;
namespace Tgstation.Server.Host.Controllers
{
/// <summary>
/// An <see cref="ApiController"/> representing a <typeparamref name="TModel"/>
/// </summary>
/// <typeparam name="TModel">The model being represented</typeparam>
public abstract class ModelController<TModel> : ApiController where TModel : class
{
/// <summary>
/// The <see cref="ModelAttribute"/> of the <typeparamref name="TModel"/>
/// </summary>
protected static readonly ModelAttribute ModelAttribute = (ModelAttribute)typeof(TModel).GetCustomAttributes(typeof(ModelAttribute), true).First();
/// <summary>
/// Construct a <see cref="ModelController{TModel}"/>
/// </summary>
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> for the <see cref="ApiController"/></param>
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="ApiController"/></param>
public ModelController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory) : base(databaseContext, authenticationContextFactory) { }
/// <summary>
/// Attempt to create a <paramref name="model"/>
/// </summary>
/// <param name="model">The <typeparamref name="TModel"/> being created</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IActionResult"/> of the operation</returns>
[HttpPut]
public virtual Task<IActionResult> Create([FromBody]TModel model, CancellationToken cancellationToken) => Task.FromResult((IActionResult)NotFound());
/// <summary>
/// Attempt to read a <typeparamref name="TModel"/>
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IActionResult"/> of the operation</returns>
[HttpGet]
public virtual Task<IActionResult> Read(CancellationToken cancellationToken) => Task.FromResult((IActionResult)NotFound());
/// <summary>
/// Attempt to update a <paramref name="model"/>
/// </summary>
/// <param name="model">The <typeparamref name="TModel"/> being updated</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IActionResult"/> of the operation</returns>
[HttpPost]
public virtual Task<IActionResult> Update([FromBody]TModel model, CancellationToken cancellationToken) => Task.FromResult((IActionResult)NotFound());
/// <summary>
/// Attempt to delete a <paramref name="model"/>
/// </summary>
/// <param name="model">The <typeparamref name="TModel"/> being deleted</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IActionResult"/> of the operation</returns>
[HttpDelete]
public virtual Task<IActionResult> Delete([FromBody]TModel model, CancellationToken cancellationToken) => Task.FromResult((IActionResult)NotFound());
/// <summary>
/// Attempt to list entries of the <typeparamref name="TModel"/>
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IActionResult"/> of the operation</returns>
[HttpGet("/List")]
public virtual Task<IActionResult> List(CancellationToken cancellationToken) => Task.FromResult((IActionResult)NotFound());
}
}
@@ -0,0 +1,158 @@
using Cyberboss.AspNetCore.AsyncInitializer;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
using System;
using System.Globalization;
using System.IdentityModel.Tokens.Jwt;
using System.Reflection;
using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.Controllers;
using Tgstation.Server.Host.Models;
using Tgstation.Server.Host.Security;
namespace Tgstation.Server.Host.Core
{
/// <summary>
/// Configures the ASP.NET Core web application
/// </summary>
sealed class Application
{
/// <summary>
/// The version of the <see cref="Application"/>
/// </summary>
public static readonly Version Version = Assembly.GetExecutingAssembly().GetName().Version;
/// <summary>
/// The <see cref="IConfiguration"/> for the <see cref="Application"/>
/// </summary>
readonly IConfiguration configuration;
/// <summary>
/// The <see cref="IHostingEnvironment"/> for the <see cref="Application"/>
/// </summary>
readonly IHostingEnvironment hostingEnvironment;
/// <summary>
/// Construct an <see cref="Application"/>
/// </summary>
/// <param name="configuration">The value of <see cref="configuration"/></param>
/// <param name="hostingEnvironment">The value of <see cref="hostingEnvironment"/></param>
public Application(IConfiguration configuration, IHostingEnvironment hostingEnvironment)
{
this.configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
this.hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment));
}
/// <summary>
/// Configure dependency injected services
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> to configure</param>
#pragma warning disable CA1822 // Mark members as static
public void ConfigureServices(IServiceCollection services)
#pragma warning restore CA1822 // Mark members as static
{
if (services == null)
throw new ArgumentNullException(nameof(services));
var workingDir = Environment.CurrentDirectory;
var databaseConfigurationSection = configuration.GetSection(DatabaseConfiguration.Section);
services.Configure<DatabaseConfiguration>(databaseConfigurationSection);
services.AddOptions();
const string scheme = "JwtBearer";
services.AddAuthentication((options) =>
{
options.DefaultAuthenticateScheme = scheme;
options.DefaultChallengeScheme = scheme;
}).AddJwtBearer(scheme, jwtBearerOptions =>
{
jwtBearerOptions.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(TokenFactory.TokenSigningKey),
ValidateIssuer = true,
ValidIssuer = TokenFactory.TokenIssuer,
ValidateLifetime = true,
ValidateAudience = true,
ValidAudience = TokenFactory.TokenAudience,
ClockSkew = TimeSpan.FromMinutes(5),
RequireSignedTokens = true,
RequireExpirationTime = true
};
jwtBearerOptions.Events = new JwtBearerEvents
{
OnTokenValidated = ApiController.OnTokenValidated
};
});
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); //fucking converts 'sub' to M$ bs
services.AddMvc();
var databaseConfiguration = databaseConfigurationSection.Get<DatabaseConfiguration>();
void ConfigureDatabase(DbContextOptionsBuilder builder)
{
if (hostingEnvironment.IsDevelopment())
builder.EnableSensitiveDataLogging();
};
switch (databaseConfiguration.DatabaseType)
{
case DatabaseType.MySql:
services.AddDbContext<MySqlDatabaseContext>(ConfigureDatabase);
services.AddScoped<IDatabaseContext>(x => x.GetRequiredService<MySqlDatabaseContext>());
break;
case DatabaseType.Sqlite:
services.AddDbContext<SqliteDatabaseContext>(ConfigureDatabase);
services.AddScoped<IDatabaseContext>(x => x.GetRequiredService<SqliteDatabaseContext>());
break;
case DatabaseType.SqlServer:
services.AddDbContext<SqlServerDatabaseContext>(ConfigureDatabase);
services.AddScoped<IDatabaseContext>(x => x.GetRequiredService<SqlServerDatabaseContext>());
break;
default:
throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid {0}!", nameof(DatabaseType)));
}
services.AddScoped<IAuthenticationContextFactory, AuthenticationContextFactory>();
services.AddSingleton<ICryptographySuite, CryptographySuite>();
services.AddSingleton<IDatabaseSeeder, DatabaseSeeder>();
services.AddSingleton<IPasswordHasher<User>, PasswordHasher<User>>();
services.AddSingleton<ITokenFactory, TokenFactory>();
services.AddSingleton<ISystemIdentityFactory, SystemIdentityFactory>();
}
/// <summary>
/// Configure the <see cref="Application"/>
/// </summary>
/// <param name="applicationBuilder">The <see cref="IApplicationBuilder"/> to configure</param>
public void Configure(IApplicationBuilder applicationBuilder)
{
if (applicationBuilder == null)
throw new ArgumentNullException(nameof(applicationBuilder));
if (hostingEnvironment.IsDevelopment())
applicationBuilder.UseDeveloperExceptionPage();
applicationBuilder.UseAsyncInitialization(async (cancellationToken) =>
{
using (var scope = applicationBuilder.ApplicationServices.CreateScope())
await scope.ServiceProvider.GetRequiredService<IDatabaseContext>().Initialize(cancellationToken).ConfigureAwait(false);
});
applicationBuilder.UseAuthentication();
applicationBuilder.UseMvc();
}
}
}
@@ -0,0 +1,14 @@
namespace Tgstation.Server.Host.Core
{
/// <summary>
/// Represents a service that may take an updated <see cref="Host"/> assembly and run it, stopping the current assembly in the process
/// </summary>
interface IServerUpdateConsumer
{
/// <summary>
/// Run a new <see cref="Host"/> assembly and stop the current one. This will likely trigger all active <see cref="System.Threading.CancellationToken"/>s
/// </summary>
/// <param name="updatePath">The path to the new <see cref="Host"/> assembly</param>
void ApplyUpdate(string updatePath);
}
}
@@ -0,0 +1,21 @@
namespace Tgstation.Server.Host.Models
{
/// <inheritdoc />
public sealed class ChatChannel : Api.Models.ChatChannel
{
/// <summary>
/// The row Id
/// </summary>
public long Id { get; set; }
/// <summary>
/// The <see cref="ChatSettings.Id"/>
/// </summary>
public long ChatSettingsId { get; set; }
/// <summary>
/// The <see cref="Models.ChatSettings"/>
/// </summary>
public ChatSettings ChatSettings { get; set; }
}
}
@@ -0,0 +1,30 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace Tgstation.Server.Host.Models
{
/// <inheritdoc />
public sealed class ChatSettings : Api.Models.Internal.ChatSettings
{
/// <summary>
/// The row Id
/// </summary>
public long Id { get; set; }
/// <summary>
/// The <see cref="Api.Models.Instance.Id"/>
/// </summary>
public long InstanceId { get; set; }
/// <summary>
/// The parent <see cref="Models.Instance"/>
/// </summary>
[Required]
public Instance Instance { get; set; }
/// <summary>
/// See <see cref="Api.Models.ChatSettings.Channels"/>
/// </summary>
public List<ChatChannel> Channels { get; set; }
}
}
@@ -0,0 +1,20 @@
using System.ComponentModel.DataAnnotations;
namespace Tgstation.Server.Host.Models
{
/// <inheritdoc />
public sealed class CompileJob : Api.Models.Internal.CompileJob
{
/// <summary>
/// See <see cref="Api.Models.CompileJob.TriggeredBy"/>
/// </summary>
[Required]
public User TriggeredBy { get; set; }
/// <summary>
/// See <see cref="Api.Models.CompileJob.RevisionInformation"/>
/// </summary>
[Required]
public RevisionInformation RevisionInformation { get; set; }
}
}
@@ -0,0 +1,159 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Host.Configuration;
using ZNetCS.AspNetCore.Logging.EntityFrameworkCore;
namespace Tgstation.Server.Host.Models
{
/// <inheritdoc />
abstract class DatabaseContext<TParentContext> : DbContext, IDatabaseContext where TParentContext : DbContext
{
/// <inheritdoc />
public DbSet<ServerSettings> ServerSettings { get; set; }
/// <inheritdoc />
public DbSet<User> Users { get; set; }
/// <inheritdoc />
public DbSet<Instance> Instances { get; set; }
/// <summary>
/// The <see cref="DbSet{TEntity}"/> for <see cref="Log"/>s
/// </summary>
public DbSet<Log> Logs { get; set; }
/// <summary>
/// The <see cref="InstanceUser"/>s in the <see cref="DatabaseContext{TParentContext}"/>
/// </summary>
public DbSet<InstanceUser> InstanceUsers { get; set; }
/// <summary>
/// The <see cref="ChatChannel"/>s in the <see cref="DatabaseContext{TParentContext}"/>
/// </summary>
public DbSet<ChatChannel> ChatChannels { get; set; }
/// <summary>
/// The <see cref="ChatSettings"/>s in the <see cref="DatabaseContext{TParentContext}"/>
/// </summary>
public DbSet<ChatSettings> ChatSettings { get; set; }
/// <summary>
/// The <see cref="Models.DreamDaemonSettings"/> in the <see cref="DatabaseContext{TParentContext}"/>
/// </summary>
public DbSet<DreamDaemonSettings> DreamDaemonSettings { get; set; }
/// <summary>
/// The <see cref="Models.DreamMakerSettings"/> in the <see cref="DatabaseContext{TParentContext}"/>
/// </summary>
public DbSet<DreamMakerSettings> DreamMakerSettings { get; set; }
/// <summary>
/// The <see cref="CompileJob"/>s in the <see cref="DatabaseContext{TParentContext}"/>
/// </summary>
public DbSet<CompileJob> CompileJobs { get; set; }
/// <summary>
/// The <see cref="Job"/>s in the <see cref="DatabaseContext{TParentContext}"/>
/// </summary>
public DbSet<Job> Jobs { get; set; }
/// <summary>
/// The <see cref="TestMerge"/>s in the <see cref="DatabaseContext{TParentContext}"/>
/// </summary>
public DbSet<TestMerge> TestMerges { get; set; }
/// <summary>
/// The <see cref="RevisionInformation"/>s in the <see cref="DatabaseContext{TParentContext}"/>
/// </summary>
public DbSet<RevisionInformation> RevisionInformations { get; set; }
/// <summary>
/// The <see cref="Models.RepositorySettings"/> in the <see cref="DatabaseContext{TParentContext}"/>
/// </summary>
public DbSet<RepositorySettings> RepositorySettings { get; set; }
/// <summary>
/// The connection string for the <see cref="DatabaseContext{TParentContext}"/>
/// </summary>
protected string ConnectionString => databaseConfiguration.ConnectionString;
/// <summary>
/// The <see cref="DatabaseConfiguration"/> for the <see cref="DatabaseContext{TParentContext}"/>
/// </summary>
readonly DatabaseConfiguration databaseConfiguration;
/// <summary>
/// The <see cref="ILoggerFactory"/> for the <see cref="DatabaseContext{TParentContext}"/>
/// </summary>
readonly ILoggerFactory loggerFactory;
/// <summary>
/// The <see cref="IDatabaseSeeder"/> for the <see cref="DatabaseContext{TParentContext}"/>
/// </summary>
readonly IDatabaseSeeder databaseSeeder;
/// <summary>
/// Construct a <see cref="DatabaseContext{TParentContext}"/>
/// </summary>
/// <param name="dbContextOptions">The <see cref="DbContextOptions{TParentContext}"/> for the <see cref="DatabaseContext{TParentContext}"/></param>
/// <param name="databaseConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="databaseConfiguration"/></param>
/// <param name="loggerFactory">The value of <see cref="loggerFactory"/></param>
/// <param name="databaseSeeder">The value of <see cref="databaseSeeder"/></param>
public DatabaseContext(DbContextOptions<TParentContext> dbContextOptions, IOptions<DatabaseConfiguration> databaseConfigurationOptions, ILoggerFactory loggerFactory, IDatabaseSeeder databaseSeeder) : base(dbContextOptions)
{
databaseConfiguration = databaseConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(databaseConfigurationOptions));
this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
this.databaseSeeder = databaseSeeder ?? throw new ArgumentNullException(nameof(databaseSeeder));
}
/// <inheritdoc />
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// build default model.
LogModelBuilderHelper.Build(modelBuilder.Entity<Log>());
modelBuilder.Entity<Log>().ToTable(nameof(Logs));
modelBuilder.Entity<RevisionInformation>().HasIndex(x => x.Revision).IsUnique();
var user = modelBuilder.Entity<User>();
user.HasIndex(x => new { x.Name, x.SystemIdentifier }).IsUnique();
user.HasOne(x => x.CreatedBy).WithMany(x => x.CreatedUsers).OnDelete(DeleteBehavior.Restrict);
var chatChannel = modelBuilder.Entity<ChatChannel>();
chatChannel.HasIndex(x => new { x.ChatSettingsId, x.IrcChannel }).IsUnique();
chatChannel.HasIndex(x => new { x.ChatSettingsId, x.DiscordChannelId }).IsUnique();
}
/// <inheritdoc />
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
optionsBuilder.UseLoggerFactory(loggerFactory);
}
/// <inheritdoc />
public async Task<ServerSettings> GetServerSettings(CancellationToken cancellationToken)
{
var settings = await ServerSettings.FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
if (settings == default(ServerSettings))
{
settings = new ServerSettings();
ServerSettings.Add(settings);
}
return settings;
}
/// <inheritdoc />
public async Task Initialize(CancellationToken cancellationToken)
{
#if DEBUG
await Database.EnsureCreatedAsync().ConfigureAwait(false);
var wasEmpty = (await Users.CountAsync().ConfigureAwait(false)) == 0;
#else
var migrations = await Database.GetAppliedMigrationsAsync().ConfigureAwait(false);
var wasEmpty = !migrations.Any();
await Database.MigrateAsync(cancellationToken).ConfigureAwait(false);
#endif
if (wasEmpty)
await databaseSeeder.SeedDatabase(this, cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc />
public Task Save(CancellationToken cancellationToken) => SaveChangesAsync(cancellationToken);
}
}
@@ -0,0 +1,55 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api.Rights;
using Tgstation.Server.Host.Security;
namespace Tgstation.Server.Host.Models
{
/// <inheritdoc />
sealed class DatabaseSeeder : IDatabaseSeeder
{
/// <summary>
/// The default password mode admin password
/// </summary>
const string DefaultAdminPassword = "ISolemlySwearToDeleteTheDataDirectory";
/// <summary>
/// The default git repository to pull server updates from
/// </summary>
const string DefaultUpstreamRepository = "https://github.com/tgstation/tgstation-server";
/// <summary>
/// The <see cref="ICryptographySuite"/> for the <see cref="DatabaseContext{TParentContext}"/>
/// </summary>
readonly ICryptographySuite cryptographySuite;
/// <summary>
/// Construct a <see cref="DatabaseSeeder"/>
/// </summary>
/// <param name="cryptographySuite">The value of <see cref="cryptographySuite"/></param>
public DatabaseSeeder(ICryptographySuite cryptographySuite) => this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite));
/// <inheritdoc />
public async Task SeedDatabase(IDatabaseContext databaseContext, CancellationToken cancellationToken)
{
var admin = new User
{
AdministrationRights = (AdministrationRights)~0,
CreatedAt = DateTimeOffset.Now,
InstanceManagerRights = (InstanceManagerRights)~0,
Name = "Admin",
Enabled = true,
};
cryptographySuite.SetUserPassword(admin, DefaultAdminPassword);
databaseContext.Users.Add(admin);
var serverSettings = await databaseContext.GetServerSettings(cancellationToken).ConfigureAwait(false);
serverSettings.EnableTelemetry = true;
serverSettings.UpstreamRepository = DefaultUpstreamRepository;
await databaseContext.Save(cancellationToken).ConfigureAwait(false);
}
}
}
@@ -0,0 +1,39 @@
using System.ComponentModel.DataAnnotations;
namespace Tgstation.Server.Host.Models
{
/// <inheritdoc />
public sealed class DreamDaemonSettings : Api.Models.Internal.DreamDaemonSettings
{
/// <summary>
/// The row Id
/// </summary>
public long Id { get; set; }
/// <summary>
/// The PID of a currently running DD instance
/// </summary>
public int? ProcessId { get; set; }
/// <summary>
/// The access token used for communication with DD
/// </summary>
public string AccessToken { get; set; }
/// <summary>
/// The <see cref="Api.Models.Instance.Id"/>
/// </summary>
public long InstanceId { get; set; }
/// <summary>
/// The parent <see cref="Models.Instance"/>
/// </summary>
[Required]
public Instance Instance { get; set; }
/// <summary>
/// See <see cref="Api.Models.DreamDaemon.CompileJob"/>
/// </summary>
public CompileJob CompileJob { get; set; }
}
}
@@ -0,0 +1,29 @@
using System.ComponentModel.DataAnnotations;
namespace Tgstation.Server.Host.Models
{
/// <inheritdoc />
public sealed class DreamMakerSettings : Api.Models.Internal.DreamMakerSettings
{
/// <summary>
/// The row Id
/// </summary>
public long Id { get; set; }
/// <summary>
/// The <see cref="Api.Models.Instance.Id"/>
/// </summary>
public long InstanceId { get; set; }
/// <summary>
/// The parent <see cref="Models.Instance"/>
/// </summary>
[Required]
public Instance Instance { get; set; }
/// <summary>
/// <see cref="Api.Models.DreamMaker.LastJob"/>
/// </summary>
public CompileJob LastJob { get; set; }
}
}
@@ -0,0 +1,43 @@
using Microsoft.EntityFrameworkCore;
using System.Threading;
using System.Threading.Tasks;
namespace Tgstation.Server.Host.Models
{
/// <summary>
/// Represents the database
/// </summary>
public interface IDatabaseContext
{
/// <summary>
/// The <see cref="User"/>s in the <see cref="IDatabaseContext"/>
/// </summary>
DbSet<User> Users { get; }
/// <summary>
/// The <see cref="Instances"/>s in the <see cref="IDatabaseContext"/>
/// </summary>
DbSet<Instance> Instances { get; }
/// <summary>
/// Get the <see cref="ServerSettings"/> in the <see cref="IDatabaseContext"/>
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="ServerSettings"/> in the <see cref="IDatabaseContext"/></returns>
Task<ServerSettings> GetServerSettings(CancellationToken cancellationToken);
/// <summary>
/// Saves changes made to the <see cref="IDatabaseContext"/>
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task Save(CancellationToken cancellationToken);
/// <summary>
/// Creates and migrates the <see cref="IDatabaseContext"/>
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task Initialize(CancellationToken cancellationToken);
}
}
@@ -0,0 +1,19 @@
using System.Threading;
using System.Threading.Tasks;
namespace Tgstation.Server.Host.Models
{
/// <summary>
/// For initially seeding a database
/// </summary>
interface IDatabaseSeeder
{
/// <summary>
/// Initially seed a given <paramref name="databaseContext"/>
/// </summary>
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> to seed</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task SeedDatabase(IDatabaseContext databaseContext, CancellationToken cancellationToken);
}
}
@@ -0,0 +1,50 @@
using System.Collections.Generic;
namespace Tgstation.Server.Host.Models
{
/// <summary>
/// Represents an <see cref="Api.Models.Instance"/> in the database
/// </summary>
public sealed class Instance : Api.Models.Instance
{
/// <summary>
/// The <see cref="Models.ChatSettings"/> for the <see cref="Instance"/>
/// </summary>
public ChatSettings ChatSettings { get; set; }
/// <summary>
/// The <see cref="Models.DreamMakerSettings"/> for the <see cref="Instance"/>
/// </summary>
public DreamMakerSettings DreamMakerSettings { get; set; }
/// <summary>
/// The <see cref="Models.DreamDaemonSettings"/> for the <see cref="Instance"/>
/// </summary>
public DreamDaemonSettings DreamDaemonSettings { get; set; }
/// <summary>
/// The <see cref="Models.RepositorySettings"/> for the <see cref="Instance"/>
/// </summary>
public RepositorySettings RepositorySettings { get; set; }
/// <summary>
/// The <see cref="InstanceUser"/>s in the <see cref="Instance"/>
/// </summary>
public List<InstanceUser> InstanceUsers { get; set; }
/// <summary>
/// The <see cref="TestMerge"/>s in the <see cref="Instance"/>
/// </summary>
public List<TestMerge> TestMerges { get; set; }
/// <summary>
/// The <see cref="CompileJob"/>s in the <see cref="Instance"/>
/// </summary>
public List<CompileJob> CompileJobs { get; set; }
/// <summary>
/// The <see cref="Jobs"/> in the <see cref="Instance"/>
/// </summary>
public List<Job> Jobs { get; set; }
}
}
@@ -0,0 +1,19 @@
using System.ComponentModel.DataAnnotations;
namespace Tgstation.Server.Host.Models
{
/// <inheritdoc />
public sealed class InstanceUser : Api.Models.InstanceUser
{
/// <summary>
/// The row Id
/// </summary>
public long Id { get; set; }
/// <summary>
/// The <see cref="Models.Instance"/> the <see cref="InstanceUser"/> belongs to
/// </summary>
[Required]
public Instance Instance { get; set; }
}
}
+14
View File
@@ -0,0 +1,14 @@
using System.ComponentModel.DataAnnotations;
namespace Tgstation.Server.Host.Models
{
/// <inheritdoc />
public sealed class Job : Api.Models.Internal.Job
{
/// <summary>
/// See <see cref="Api.Models.Job.StartedBy"/>
/// </summary>
[Required]
public User StartedBy { get; set; }
}
}
@@ -0,0 +1,29 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
using System.IO;
using System.Reflection;
using Tgstation.Server.Host.Configuration;
namespace Tgstation.Server.Host.Models.Migrations
{
/// <summary>
/// Contains helpers for creating design time <see cref="Models.DatabaseContext{TParentContext}"/>s
/// </summary>
static class DesignTimeDbContextFactoryHelpers
{
/// <summary>
/// Path to the json file to use for migrations configuration
/// </summary>
const string MigrationsJson = "appsettings.Development.json";
/// <inheritdoc />
public static IOptions<DatabaseConfiguration> GetDbContextOptions()
{
var builder = new ConfigurationBuilder();
builder.SetBasePath(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location));
builder.AddJsonFile(MigrationsJson);
var configuration = builder.Build();
return Options.Create(configuration.GetSection(DatabaseConfiguration.Section).Get<DatabaseConfiguration>());
}
}
}
@@ -0,0 +1,15 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.Extensions.Logging;
using Tgstation.Server.Host.Security;
namespace Tgstation.Server.Host.Models.Migrations
{
/// <inheritdoc />
sealed class MySqlDesignTimeDbContextFactory : IDesignTimeDbContextFactory<MySqlDatabaseContext>
{
/// <inheritdoc />
public MySqlDatabaseContext CreateDbContext(string[] args) => new MySqlDatabaseContext(new DbContextOptions<MySqlDatabaseContext>(), DesignTimeDbContextFactoryHelpers.GetDbContextOptions(), new LoggerFactory(), new DatabaseSeeder(new CryptographySuite(new PasswordHasher<User>())));
}
}
@@ -0,0 +1,15 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.Extensions.Logging;
using Tgstation.Server.Host.Security;
namespace Tgstation.Server.Host.Models.Migrations
{
/// <inheritdoc />
sealed class SqlServerDesignTimeDbContextFactory : IDesignTimeDbContextFactory<SqlServerDatabaseContext>
{
/// <inheritdoc />
public SqlServerDatabaseContext CreateDbContext(string[] args) => new SqlServerDatabaseContext(new DbContextOptions<SqlServerDatabaseContext>(), DesignTimeDbContextFactoryHelpers.GetDbContextOptions(), new LoggerFactory(), new DatabaseSeeder(new CryptographySuite(new PasswordHasher<User>())));
}
}
@@ -0,0 +1,15 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.Extensions.Logging;
using Tgstation.Server.Host.Security;
namespace Tgstation.Server.Host.Models.Migrations
{
/// <inheritdoc />
sealed class SqliteDesignTimeDbContextFactory : IDesignTimeDbContextFactory<SqliteDatabaseContext>
{
/// <inheritdoc />
public SqliteDatabaseContext CreateDbContext(string[] args) => new SqliteDatabaseContext(new DbContextOptions<SqliteDatabaseContext>(), DesignTimeDbContextFactoryHelpers.GetDbContextOptions(), new LoggerFactory(), new DatabaseSeeder(new CryptographySuite(new PasswordHasher<User>())));
}
}
@@ -0,0 +1,30 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Tgstation.Server.Host.Configuration;
namespace Tgstation.Server.Host.Models
{
/// <summary>
/// <see cref="DatabaseContext{TParentContext}"/> for MySQL
/// </summary>
sealed class MySqlDatabaseContext : DatabaseContext<MySqlDatabaseContext>
{
/// <summary>
/// Construct a <see cref="MySqlDatabaseContext"/>
/// </summary>
/// <param name="dbContextOptions">The <see cref="DbContextOptions{TContext}"/> for the <see cref="DatabaseContext{TParentContext}"/></param>
/// <param name="databaseConfiguration">The <see cref="IOptions{TOptions}"/> of <see cref="DatabaseConfiguration"/> for the <see cref="DatabaseContext{TParentContext}"/></param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/> for the <see cref="DatabaseContext{TParentContext}"/></param>
/// <param name="databaseSeeder">The <see cref="IDatabaseSeeder"/> for the <see cref="DatabaseContext{TParentContext}"/></param>
public MySqlDatabaseContext(DbContextOptions<MySqlDatabaseContext> dbContextOptions, IOptions<DatabaseConfiguration> databaseConfiguration, ILoggerFactory loggerFactory, IDatabaseSeeder databaseSeeder) : base(dbContextOptions, databaseConfiguration, loggerFactory, databaseSeeder)
{ }
/// <inheritdoc />
protected override void OnConfiguring(DbContextOptionsBuilder options)
{
base.OnConfiguring(options);
options.UseMySQL(ConnectionString);
}
}
}
@@ -0,0 +1,29 @@
using System.ComponentModel.DataAnnotations;
namespace Tgstation.Server.Host.Models
{
/// <inheritdoc />
public sealed class RepositorySettings : Api.Models.Internal.RepositorySettings
{
/// <summary>
/// The row Id
/// </summary>
public long Id { get; set; }
/// <summary>
/// The <see cref="Api.Models.Instance.Id"/>
/// </summary>
public long InstanceId { get; set; }
/// <summary>
/// The parent <see cref="Models.Instance"/>
/// </summary>
[Required]
public Instance Instance { get; set; }
/// <summary>
/// See <see cref="Api.Models.Repository.RevisionInformation"/>
/// </summary>
public RevisionInformation RevisionInformation { get; set; }
}
}
@@ -0,0 +1,18 @@
using System.Collections.Generic;
namespace Tgstation.Server.Host.Models
{
/// <inheritdoc />
public sealed class RevisionInformation : Api.Models.Internal.RevisionInformation
{
/// <summary>
/// The row Id
/// </summary>
public long Id { get; set; }
/// <summary>
/// See <see cref="Api.Models.RevisionInformation.TestMerges"/>
/// </summary>
public List<TestMerge> TestMerges { get; set; }
}
}
@@ -0,0 +1,11 @@
namespace Tgstation.Server.Host.Models
{
/// <inheritdoc />
public sealed class ServerSettings : Api.Models.Internal.ServerSettings
{
/// <summary>
/// The row Id
/// </summary>
public long Id { get; set; }
}
}
@@ -0,0 +1,30 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Tgstation.Server.Host.Configuration;
namespace Tgstation.Server.Host.Models
{
/// <summary>
/// <see cref="DatabaseContext{TParentContext}"/> for Sqlserver
/// </summary>
sealed class SqlServerDatabaseContext : DatabaseContext<SqlServerDatabaseContext>
{
/// <summary>
/// Construct a <see cref="SqlServerDatabaseContext"/>
/// </summary>
/// <param name="dbContextOptions">The <see cref="DbContextOptions{TContext}"/> for the <see cref="DatabaseContext{TParentContext}"/></param>
/// <param name="databaseConfiguration">The <see cref="IOptions{TOptions}"/> of <see cref="DatabaseConfiguration"/> for the <see cref="DatabaseContext{TParentContext}"/></param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/> for the <see cref="DatabaseContext{TParentContext}"/></param>
/// <param name="databaseSeeder">The <see cref="IDatabaseSeeder"/> for the <see cref="DatabaseContext{TParentContext}"/></param>
public SqlServerDatabaseContext(DbContextOptions<SqlServerDatabaseContext> dbContextOptions, IOptions<DatabaseConfiguration> databaseConfiguration, ILoggerFactory loggerFactory, IDatabaseSeeder databaseSeeder) : base(dbContextOptions, databaseConfiguration, loggerFactory, databaseSeeder)
{ }
/// <inheritdoc />
protected override void OnConfiguring(DbContextOptionsBuilder options)
{
base.OnConfiguring(options);
options.UseSqlServer(ConnectionString);
}
}
}
@@ -0,0 +1,30 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Tgstation.Server.Host.Configuration;
namespace Tgstation.Server.Host.Models
{
/// <summary>
/// <see cref="DatabaseContext{TParentContext}"/> for Sqlite
/// </summary>
sealed class SqliteDatabaseContext : DatabaseContext<SqliteDatabaseContext>
{
/// <summary>
/// Construct a <see cref="SqliteDatabaseContext"/>
/// </summary>
/// <param name="dbContextOptions">The <see cref="DbContextOptions{TContext}"/> for the <see cref="DatabaseContext{TParentContext}"/></param>
/// <param name="databaseConfiguration">The <see cref="IOptions{TOptions}"/> of <see cref="DatabaseConfiguration"/> for the <see cref="DatabaseContext{TParentContext}"/></param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/> for the <see cref="DatabaseContext{TParentContext}"/></param>
/// <param name="databaseSeeder">The <see cref="IDatabaseSeeder"/> for the <see cref="DatabaseContext{TParentContext}"/></param>
public SqliteDatabaseContext(DbContextOptions<SqliteDatabaseContext> dbContextOptions, IOptions<DatabaseConfiguration> databaseConfiguration, ILoggerFactory loggerFactory, IDatabaseSeeder databaseSeeder) : base(dbContextOptions, databaseConfiguration, loggerFactory, databaseSeeder)
{ }
/// <inheritdoc />
protected override void OnConfiguring(DbContextOptionsBuilder options)
{
base.OnConfiguring(options);
options.UseSqlite(ConnectionString);
}
}
}
@@ -0,0 +1,14 @@
using System.ComponentModel.DataAnnotations;
namespace Tgstation.Server.Host.Models
{
/// <inheritdoc />
public sealed class TestMerge : Api.Models.Internal.TestMerge
{
/// <summary>
/// See <see cref="Api.Models.TestMerge.MergedBy"/>
/// </summary>
[Required]
public User MergedBy { get; set; }
}
}
+28
View File
@@ -0,0 +1,28 @@
using System.Collections.Generic;
namespace Tgstation.Server.Host.Models
{
/// <inheritdoc />
public sealed class User : Api.Models.Internal.User
{
/// <summary>
/// The hash of the user's password
/// </summary>
public string PasswordHash { get; set; }
/// <summary>
/// See <see cref="Api.Models.User"/>
/// </summary>
public User CreatedBy { get; set; }
/// <summary>
/// <see cref="User"/>s created by this <see cref="User"/>
/// </summary>
public List<User> CreatedUsers { get; set; }
/// <summary>
/// The <see cref="InstanceUser"/>s for the <see cref="User"/>
/// </summary>
public List<InstanceUser> InstanceUsers { get; set; }
}
}
@@ -0,0 +1,61 @@
using System;
using System.Linq;
using Tgstation.Server.Api.Rights;
using Tgstation.Server.Host.Models;
namespace Tgstation.Server.Host.Security
{
/// <summary>
/// Manages <see cref="Api.Models.User"/>s for a scope
/// </summary>
sealed class AuthenticationContext : IAuthenticationContext
{
/// <inheritdoc />
public User User { get; }
/// <inheritdoc />
public InstanceUser InstanceUser { get; }
/// <inheritdoc />
public ISystemIdentity SystemIdentity { get; }
/// <summary>
/// Construct a <see cref="IAuthenticationContext"/>
/// </summary>
/// <param name="systemIdentity">The value of <see cref="SystemIdentity"/></param>
/// <param name="user">The value of <see cref="User"/></param>
/// <param name="instanceUser">The value of <see cref="InstanceUser"/></param>
public AuthenticationContext(ISystemIdentity systemIdentity, User user, InstanceUser instanceUser)
{
User = user ?? throw new ArgumentNullException(nameof(user));
if (systemIdentity == null && User.SystemIdentifier != null)
throw new ArgumentNullException(nameof(systemIdentity));
InstanceUser = instanceUser;
SystemIdentity = systemIdentity;
}
/// <inheritdoc />
public void Dispose() => SystemIdentity.Dispose();
/// <inheritdoc />
public IAuthenticationContext Clone() => new AuthenticationContext(SystemIdentity.Clone(), User, InstanceUser);
/// <inheritdoc />
public int GetRight(RightsType rightsType)
{
var isInstance = RightsHelper.IsInstanceRight(rightsType);
if (isInstance && InstanceUser == null)
return 0;
var rightsEnum = RightsHelper.RightToType(rightsType);
// use the api versions because they're the ones that contain the actual properties
var typeToCheck = isInstance ? typeof(InstanceUser) : typeof(User);
var prop = typeToCheck.GetProperties().Where(x => x.PropertyType == rightsEnum).First();
var right = prop.GetMethod.Invoke(isInstance ? (object)InstanceUser : User, Array.Empty<object>());
return (int)right;
}
}
}
@@ -0,0 +1,57 @@
using Microsoft.EntityFrameworkCore;
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Host.Models;
namespace Tgstation.Server.Host.Security
{
/// <inheritdoc />
sealed class AuthenticationContextFactory : IAuthenticationContextFactory
{
/// <inheritdoc />
public IAuthenticationContext CurrentAuthenticationContext { get; private set; }
/// <summary>
/// The <see cref="ISystemIdentityFactory"/> for the <see cref="AuthenticationContextFactory"/>
/// </summary>
readonly ISystemIdentityFactory systemIdentityFactory;
/// <summary>
/// The <see cref="IDatabaseContext"/> for the <see cref="AuthenticationContextFactory"/>
/// </summary>
readonly IDatabaseContext databaseContext;
/// <summary>
/// Construct an <see cref="AuthenticationContextFactory"/>
/// </summary>
/// <param name="systemIdentityFactory">The value of <see cref="systemIdentityFactory"/></param>
/// <param name="databaseContext">The value of <see cref="databaseContext"/></param>
public AuthenticationContextFactory(ISystemIdentityFactory systemIdentityFactory, IDatabaseContext databaseContext)
{
this.systemIdentityFactory = systemIdentityFactory ?? throw new ArgumentNullException(nameof(systemIdentityFactory));
this.databaseContext = databaseContext ?? throw new ArgumentNullException(nameof(databaseContext));
}
/// <inheritdoc />
public async Task CreateAuthenticationContext(long userId, long? instanceId, CancellationToken cancellationToken)
{
if (CurrentAuthenticationContext != null)
throw new InvalidOperationException("Authentication context has already been loaded");
var userQuery = databaseContext.Users.Where(x => x.Id == userId);
if (instanceId.HasValue)
userQuery = userQuery.Include(x => x.InstanceUsers.Where(y => y.Id == instanceId));
var user = await userQuery.FirstAsync(cancellationToken).ConfigureAwait(false);
InstanceUser instanceUser = null;
if (instanceId.HasValue)
instanceUser = user.InstanceUsers.First();
CurrentAuthenticationContext = new AuthenticationContext(user.SystemIdentifier != null ? systemIdentityFactory.CreateSystemIdentity(user) : null, user, instanceUser);
}
}
}
@@ -0,0 +1,60 @@
using Microsoft.AspNetCore.Identity;
using System;
using System.Security.Cryptography;
using Tgstation.Server.Host.Models;
namespace Tgstation.Server.Host.Security
{
/// <inheritdoc />
sealed class CryptographySuite : ICryptographySuite
{
/// <summary>
/// Generates a secure set of <see cref="byte"/>s
/// </summary>
/// <returns>A secure set of <see cref="byte"/>s</returns>
public static byte[] GetSecureBytes(int amount)
{
using (var rng = new RNGCryptoServiceProvider())
{
var byt = new byte[amount];
rng.GetBytes(byt);
return byt;
}
}
/// <summary>
/// The <see cref="IPasswordHasher{TUser}"/> for the <see cref="CryptographySuite"/>
/// </summary>
readonly IPasswordHasher<User> passwordHasher;
/// <summary>
/// Construct a <see cref="CryptographySuite"/>
/// </summary>
/// <param name="passwordHasher">The value of <see cref="passwordHasher"/></param>
public CryptographySuite(IPasswordHasher<User> passwordHasher) => this.passwordHasher = passwordHasher ?? throw new ArgumentNullException(nameof(passwordHasher));
/// <inheritdoc />
public void SetUserPassword(User user, string newPassword)
{
if (user == null)
throw new ArgumentNullException(nameof(user));
if (String.IsNullOrEmpty(newPassword))
throw new ArgumentNullException(nameof(newPassword));
user.PasswordHash = passwordHasher.HashPassword(user, newPassword);
}
/// <inheritdoc />
public bool CheckUserPassword(User user, string password)
{
switch(passwordHasher.VerifyHashedPassword(user, user.PasswordHash, password))
{
case PasswordVerificationResult.Failed:
return false;
case PasswordVerificationResult.SuccessRehashNeeded:
user.PasswordHash = passwordHasher.HashPassword(user, password);
break;
}
return true;
}
}
}
@@ -0,0 +1,40 @@
using System;
using Tgstation.Server.Api.Rights;
using Tgstation.Server.Host.Models;
namespace Tgstation.Server.Host.Security
{
/// <summary>
/// Represents the currently authenticated <see cref="Api.Models.User"/>
/// </summary>
public interface IAuthenticationContext : IDisposable
{
/// <summary>
/// The authenticated user
/// </summary>
User User { get; }
/// <summary>
/// The <see cref="InstanceUser"/> of <see cref="User"/> if applicable
/// </summary>
InstanceUser InstanceUser { get; }
/// <summary>
/// Get the value of a given <paramref name="rightsType"/>
/// </summary>
/// <param name="rightsType">The <see cref="RightsType"/> of the right to get</param>
/// <returns>The value of <paramref name="rightsType"/>. Note that if <see cref="InstanceUser"/> is <see langword="null"/> all <see cref="Instance"/> based rights will return 0</returns>
int GetRight(RightsType rightsType);
/// <summary>
/// The <see cref="ISystemIdentity"/> of <see cref="User"/> if applicable
/// </summary>
ISystemIdentity SystemIdentity { get; }
/// <summary>
/// Creates a copy of the <see cref="IAuthenticationContext"/>
/// </summary>
/// <returns>A new <see cref="IAuthenticationContext"/></returns>
IAuthenticationContext Clone();
}
}
@@ -0,0 +1,25 @@
using System.Threading;
using System.Threading.Tasks;
namespace Tgstation.Server.Host.Security
{
/// <summary>
/// For creating and accessing authentication contexts
/// </summary>
public interface IAuthenticationContextFactory
{
/// <summary>
/// The <see cref="IAuthenticationContext"/> the <see cref="IAuthenticationContextFactory"/> created
/// </summary>
IAuthenticationContext CurrentAuthenticationContext { get; }
/// <summary>
/// Create an <see cref="IAuthenticationContext"/> to populate <see cref="CurrentAuthenticationContext"/>
/// </summary>
/// <param name="userId">The <see cref="Api.Models.Internal.User.Id"/> of the <see cref="IAuthenticationContext.User"/></param>
/// <param name="instanceId">The <see cref="Api.Models.Instance.Id"/> of the operation</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task CreateAuthenticationContext(long userId, long? instanceId, CancellationToken cancellationToken);
}
}
@@ -0,0 +1,25 @@
using Tgstation.Server.Host.Models;
namespace Tgstation.Server.Host.Security
{
/// <summary>
/// Contains various cryptographic functions
/// </summary>
public interface ICryptographySuite
{
/// <summary>
/// Sets a <see cref="User.PasswordHash"/> for a given <paramref name="user"/>
/// </summary>
/// <param name="user">The <see cref="User"/> whos <see cref="User.PasswordHash"/> is to be set</param>
/// <param name="newPassword">The new password for the <see cref="User"/></param>
void SetUserPassword(User user, string newPassword);
/// <summary>
/// Checks a given <paramref name="password"/> matches a given <paramref name="user"/>'s <see cref="User.PasswordHash"/>. This may result in <see cref="User.PasswordHash"/> being modified and this should be persisted
/// </summary>
/// <param name="user">The <see cref="User"/> to check</param>
/// <param name="password">The password to check</param>
/// <returns><see langword="true"/> if <paramref name="password"/> matches the hash, <see langword="false"/> otherwise</returns>
bool CheckUserPassword(User user, string password);
}
}
@@ -0,0 +1,42 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Tgstation.Server.Host.Security
{
/// <summary>
/// Represents a user on the current <see cref="System.Runtime.InteropServices.OSPlatform"/>
/// </summary>
public interface ISystemIdentity : IDisposable
{
/// <summary>
/// A unique identifier for the user
/// </summary>
string Uid { get; }
/// <summary>
/// The user's name
/// </summary>
string Username { get; }
/// <summary>
/// Groups the user is in
/// </summary>
IEnumerable<string> Groups { get; }
/// <summary>
/// Clone the <see cref="ISystemIdentity"/> creating another copy that must have <see cref="IDisposable.Dispose"/> called on it
/// </summary>
/// <returns>A new <see cref="ISystemIdentity"/> mirroring the current one</returns>
ISystemIdentity Clone();
/// <summary>
/// Runs a given <paramref name="action"/> in the context of the <see cref="ISystemIdentity"/>
/// </summary>
/// <param name="action">The <see cref="Action"/> to perform, should be simple and not use any <see cref="Task"/>s or threading</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task RunImpersonated(Action action, CancellationToken cancellationToken);
}
}
@@ -0,0 +1,25 @@
using Tgstation.Server.Host.Models;
namespace Tgstation.Server.Host.Security
{
/// <summary>
/// Factory for <see cref="ISystemIdentity"/>s
/// </summary>
public interface ISystemIdentityFactory
{
/// <summary>
/// Create a <see cref="ISystemIdentity"/> for a given <paramref name="user"/>
/// </summary>
/// <param name="user">The user to create a <see cref="ISystemIdentity"/> for</param>
/// <returns>A new <see cref="ISystemIdentity"/> or <see langword="null"/> if the <paramref name="user"/> has no <see cref="ISystemIdentity"/></returns>
ISystemIdentity CreateSystemIdentity(User user);
/// <summary>
/// Create a <see cref="ISystemIdentity"/> for a given username and password
/// </summary>
/// <param name="username">The username of the user</param>
/// <param name="password">The password of the user</param>
/// <returns>A new <see cref="ISystemIdentity"/></returns>
ISystemIdentity CreateSystemIdentity(string username, string password);
}
}
@@ -0,0 +1,19 @@
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api.Models;
namespace Tgstation.Server.Host.Security
{
/// <summary>
/// For creating <see cref="Token"/>s
/// </summary>
public interface ITokenFactory
{
/// <summary>
/// Create a <see cref="Token"/> for a given <paramref name="user"/>
/// </summary>
/// <param name="user">The <see cref="Models.User"/> to create the token for. Must have the <see cref="Api.Models.Internal.User.Id"/> field available</param>
/// <returns>A new <see cref="Token"/></returns>
Token CreateToken(Models.User user);
}
}
@@ -0,0 +1,21 @@
using System;
using Tgstation.Server.Host.Models;
namespace Tgstation.Server.Host.Security
{
/// <inheritdoc />
sealed class SystemIdentityFactory : ISystemIdentityFactory
{
/// <inheritdoc />
public ISystemIdentity CreateSystemIdentity(User user)
{
throw new NotImplementedException();
}
/// <inheritdoc />
public ISystemIdentity CreateSystemIdentity(string username, string password)
{
throw new NotImplementedException();
}
}
}
@@ -0,0 +1,40 @@
using Microsoft.IdentityModel.Tokens;
using System;
using System.Globalization;
using System.IdentityModel.Tokens.Jwt;
using System.Reflection;
using System.Security.Claims;
using System.Text;
using Tgstation.Server.Api.Models;
namespace Tgstation.Server.Host.Security
{
/// <inheritdoc />
sealed class TokenFactory : ITokenFactory
{
public static readonly string TokenAudience = typeof(Token).Assembly.GetName().Name;
public static readonly string TokenIssuer = Assembly.GetExecutingAssembly().GetName().Name;
public static readonly byte[] TokenSigningKey = CryptographySuite.GetSecureBytes(256);
/// <inheritdoc />
public Token CreateToken(Models.User user)
{
if (user == null)
throw new ArgumentNullException(nameof(user));
var claims = new Claim[]
{
new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString(CultureInfo.InvariantCulture)),
new Claim(JwtRegisteredClaimNames.Exp, $"{DateTimeOffset.Now.AddHours(1).ToUnixTimeSeconds()}"),
new Claim(JwtRegisteredClaimNames.Nbf, $"{DateTimeOffset.Now.ToUnixTimeSeconds()}"),
new Claim(JwtRegisteredClaimNames.Iss, TokenIssuer),
new Claim(JwtRegisteredClaimNames.Aud, TokenAudience)
};
var key = new SymmetricSecurityKey(TokenSigningKey);
var token = new JwtSecurityToken(new JwtHeader(new SigningCredentials(key, SecurityAlgorithms.HmacSha256)), new JwtPayload(claims));
return new Token { Bearer = new JwtSecurityTokenHandler().WriteToken(token) };
}
}
}
+46 -5
View File
@@ -1,19 +1,60 @@
using System.Threading;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Host.Core;
using Tgstation.Server.Host.Startup;
namespace Tgstation.Server.Host
{
/// <inheritdoc />
sealed class Server : IServer
sealed class Server : IServer, IServerUpdateConsumer
{
/// <inheritdoc />
public string UpdatePath => null;
public string UpdatePath { get; private set; }
/// <summary>
/// The <see cref="IWebHostBuilder"/> for the <see cref="Server"/>
/// </summary>
readonly IWebHostBuilder webHostBuilder;
/// <summary>
/// The <see cref="cancellationTokenSource"/> for the <see cref="Server"/>
/// </summary>
CancellationTokenSource cancellationTokenSource;
/// <summary>
/// Construct a <see cref="Server"/>
/// </summary>
/// <param name="webHostBuilder">The value of <see cref="webHostBuilder"/></param>
public Server(IWebHostBuilder webHostBuilder) => this.webHostBuilder = webHostBuilder ?? throw new ArgumentNullException(nameof(webHostBuilder));
/// <inheritdoc />
public void Dispose() { }
[ExcludeFromCodeCoverage]
public async Task RunAsync(CancellationToken cancellationToken)
{
cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
using (cancellationTokenSource)
using (var webHost = webHostBuilder
.UseStartup<Application>()
.ConfigureServices((serviceCollection) => serviceCollection.AddSingleton<IServerUpdateConsumer>(this))
.Build()
)
await webHost.RunAsync(cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc />
public Task RunAsync(string[] args, CancellationToken cancellationToken) => Task.CompletedTask;
public void ApplyUpdate(string updatePath)
{
lock (this)
{
if (updatePath != null)
throw new InvalidOperationException("ApplyUpdate has already been called!");
UpdatePath = updatePath;
}
cancellationTokenSource.Cancel();
}
}
}
+3 -2
View File
@@ -1,4 +1,5 @@
using Tgstation.Server.Host.Startup;
using Microsoft.AspNetCore;
using Tgstation.Server.Host.Startup;
namespace Tgstation.Server.Host
{
@@ -6,6 +7,6 @@ namespace Tgstation.Server.Host
public sealed class ServerFactory : IServerFactory
{
/// <inheritdoc />
public IServer CreateServer() => new Server();
public IServer CreateServer(string[] args) => new Server(WebHost.CreateDefaultBuilder(args));
}
}
@@ -0,0 +1,72 @@
using System;
using Microsoft.AspNetCore.Authorization;
using Tgstation.Server.Api.Rights;
namespace Tgstation.Server.Host
{
/// <summary>
/// Helper for using the <see cref="AuthorizeAttribute"/> with the <see cref="Api.Rights"/> system
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
sealed class TgsAuthorizeAttribute : AuthorizeAttribute
{
/// <summary>
/// Construct a <see cref="TgsAuthorizeAttribute"/>
/// </summary>
public TgsAuthorizeAttribute() { }
/// <summary>
/// Construct a <see cref="TgsAuthorizeAttribute"/> for <see cref="AdministrationRights"/>
/// </summary>
/// <param name="requiredRights">The rights required</param>
public TgsAuthorizeAttribute(AdministrationRights requiredRights) => Roles = RightsHelper.RoleName(requiredRights);
/// <summary>
/// Construct a <see cref="TgsAuthorizeAttribute"/> for <see cref="InstanceManagerRights"/>
/// </summary>
/// <param name="requiredRights">The rights required</param>
public TgsAuthorizeAttribute(InstanceManagerRights requiredRights) => Roles = RightsHelper.RoleName(requiredRights);
/// <summary>
/// Construct a <see cref="TgsAuthorizeAttribute"/> for <see cref="RepositoryRights"/>
/// </summary>
/// <param name="requiredRights">The rights required</param>
public TgsAuthorizeAttribute(RepositoryRights requiredRights) => Roles = RightsHelper.RoleName(requiredRights);
/// <summary>
/// Construct a <see cref="TgsAuthorizeAttribute"/> for <see cref="ByondRights"/>
/// </summary>
/// <param name="requiredRights">The rights required</param>
public TgsAuthorizeAttribute(ByondRights requiredRights) => Roles = RightsHelper.RoleName(requiredRights);
/// <summary>
/// Construct a <see cref="TgsAuthorizeAttribute"/> for <see cref="DreamMakerRights"/>
/// </summary>
/// <param name="requiredRights">The rights required</param>
public TgsAuthorizeAttribute(DreamMakerRights requiredRights) => Roles = RightsHelper.RoleName(requiredRights);
/// <summary>
/// Construct a <see cref="TgsAuthorizeAttribute"/> for <see cref="DreamDaemonRights"/>
/// </summary>
/// <param name="requiredRights">The rights required</param>
public TgsAuthorizeAttribute(DreamDaemonRights requiredRights) => Roles = RightsHelper.RoleName(requiredRights);
/// <summary>
/// Construct a <see cref="TgsAuthorizeAttribute"/> for <see cref="ChatSettingsRights"/>
/// </summary>
/// <param name="requiredRights">The rights required</param>
public TgsAuthorizeAttribute(ChatSettingsRights requiredRights) => Roles = RightsHelper.RoleName(requiredRights);
/// <summary>
/// Construct a <see cref="TgsAuthorizeAttribute"/> for <see cref="ConfigurationRights"/>
/// </summary>
/// <param name="requiredRights">The rights required</param>
public TgsAuthorizeAttribute(ConfigurationRights requiredRights) => Roles = RightsHelper.RoleName(requiredRights);
/// <summary>
/// Construct a <see cref="TgsAuthorizeAttribute"/> for <see cref="InstanceUserRights"/>
/// </summary>
/// <param name="requiredRights">The rights required</param>
public TgsAuthorizeAttribute(InstanceUserRights requiredRights) => Roles = RightsHelper.RoleName(requiredRights);
}
}

Some files were not shown because too many files have changed in this diff Show More