mirror of
https://github.com/tgstation/tgstation-server.git
synced 2026-08-25 14:06:41 +01:00
Merge branch 'master' into 886-YouMustTest
This commit is contained in:
@@ -25,6 +25,9 @@ if($publish_dox){
|
||||
git config user.email "tgstation-server@tgstation13.org"
|
||||
echo '# THIS BRANCH IS AUTO GENERATED BY APPVEYOR CI' > README.md
|
||||
|
||||
# Add in the swagger specification
|
||||
mv C:/swagger.json "$doxdir/swagger.json"
|
||||
|
||||
# Need to create a .nojekyll file to allow filenames starting with an underscore
|
||||
# to be seen on the gh-pages site. Therefore creating an empty .nojekyll file.
|
||||
echo "" > .nojekyll
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
|
||||
@tableofcontents
|
||||
|
||||
@section api_swag OpenAPI Spec
|
||||
|
||||
TGS4 has a, from code, generated OpenAPI 3.0 specification. It is much more authorative than these documents.
|
||||
|
||||
The most up to date version should be found here: https://raw.githubusercontent.com/tgstation/tgstation-server/gh-pages/swagger.json
|
||||
|
||||
You can use the API explorer SwaggerUI to interact with it: https://petstore.swagger.io
|
||||
|
||||
@section api_intro Introduction
|
||||
|
||||
The TGS4 API is designed to be a fully realized RESTful service. Once hosted, follow the specified protocol for developing new clients or one off requests that provide full control over the server
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
|
||||
namespace Tgstation.Server.Api
|
||||
{
|
||||
@@ -23,27 +24,32 @@ namespace Tgstation.Server.Api
|
||||
/// <summary>
|
||||
/// The <see cref="ApiVersion"/> header key
|
||||
/// </summary>
|
||||
const string ApiVersionHeader = "Api";
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Username"/> header key
|
||||
/// </summary>
|
||||
const string UsernameHeader = "Username";
|
||||
public const string ApiVersionHeader = "api";
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="InstanceId"/> header key
|
||||
/// </summary>
|
||||
const string InstanceIdHeader = "Instance";
|
||||
public const string InstanceIdHeader = "instance";
|
||||
|
||||
/// <summary>
|
||||
/// The JWT authentication header scheme
|
||||
/// </summary>
|
||||
const string JwtAuthenticationScheme = "Bearer";
|
||||
public const string JwtAuthenticationScheme = "bearer";
|
||||
|
||||
/// <summary>
|
||||
/// The password authentication header scheme
|
||||
/// The JWT authentication header scheme
|
||||
/// </summary>
|
||||
const string PasswordAuthenticationScheme = "Password";
|
||||
public const string BasicAuthenticationScheme = "basic";
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Username"/> header key
|
||||
/// </summary>
|
||||
const string UsernameHeader = "username";
|
||||
|
||||
/// <summary>
|
||||
/// The basic authentication header scheme
|
||||
/// </summary>
|
||||
const string PasswordAuthenticationScheme = "password";
|
||||
|
||||
/// <summary>
|
||||
/// The current <see cref="System.Reflection.AssemblyName"/>
|
||||
@@ -180,7 +186,9 @@ namespace Tgstation.Server.Api
|
||||
InstanceId = instanceId;
|
||||
}
|
||||
|
||||
switch (scheme)
|
||||
#pragma warning disable CA1308 // Normalize strings to uppercase
|
||||
switch (scheme.ToLowerInvariant())
|
||||
#pragma warning restore CA1308 // Normalize strings to uppercase
|
||||
{
|
||||
case JwtAuthenticationScheme:
|
||||
Token = parameter;
|
||||
@@ -197,6 +205,25 @@ namespace Tgstation.Server.Api
|
||||
if (fail)
|
||||
throw new InvalidOperationException("Missing Username header!");
|
||||
break;
|
||||
case BasicAuthenticationScheme:
|
||||
string joinedString;
|
||||
try
|
||||
{
|
||||
var base64Bytes = Convert.FromBase64String(parameter);
|
||||
joinedString = Encoding.UTF8.GetString(base64Bytes);
|
||||
}
|
||||
catch
|
||||
{
|
||||
throw new InvalidOperationException("Invalid basic Authorization header!");
|
||||
}
|
||||
|
||||
var basicAuthSplits = joinedString.Split(new char[] { ':' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (basicAuthSplits.Length < 2)
|
||||
throw new InvalidOperationException("Invalid basic Authorization header!");
|
||||
|
||||
Username = basicAuthSplits.First();
|
||||
Password = String.Concat(basicAuthSplits.Skip(1));
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException("Invalid authentication scheme!");
|
||||
}
|
||||
@@ -241,10 +268,9 @@ namespace Tgstation.Server.Api
|
||||
if (IsTokenAuthentication)
|
||||
headers.Authorization = new AuthenticationHeaderValue(JwtAuthenticationScheme, Token);
|
||||
else
|
||||
{
|
||||
headers.Authorization = new AuthenticationHeaderValue(PasswordAuthenticationScheme, Password);
|
||||
headers.Add(UsernameHeader, Username);
|
||||
}
|
||||
headers.Authorization = new AuthenticationHeaderValue(
|
||||
BasicAuthenticationScheme,
|
||||
Convert.ToBase64String(Encoding.UTF8.GetBytes($"{Username}:{Password}")));
|
||||
|
||||
headers.UserAgent.Add(new ProductInfoHeaderValue(UserAgent));
|
||||
headers.Add(ApiVersionHeader, new ProductHeaderValue(AssemblyName.Name, ApiVersion.ToString()).ToString());
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace Tgstation.Server.Api.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Base of <see cref="CompileJob"/>s.
|
||||
/// </summary>
|
||||
public class EntityId
|
||||
{
|
||||
/// <summary>
|
||||
/// The ID of the entity.
|
||||
/// </summary>
|
||||
public long Id { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -6,13 +6,8 @@ namespace Tgstation.Server.Api.Models.Internal
|
||||
/// <summary>
|
||||
/// Represents a run of <see cref="DreamMaker"/>
|
||||
/// </summary>
|
||||
public class CompileJob
|
||||
public class CompileJob : EntityId
|
||||
{
|
||||
/// <summary>
|
||||
/// The ID of the job
|
||||
/// </summary>
|
||||
public long Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The .dme file used for compilation
|
||||
/// </summary>
|
||||
|
||||
@@ -7,13 +7,8 @@ namespace Tgstation.Server.Api.Models.Internal
|
||||
/// <summary>
|
||||
/// Represents a long running job
|
||||
/// </summary>
|
||||
public class Job
|
||||
public class Job : EntityId
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="Job"/> ID
|
||||
/// </summary>
|
||||
public long Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// English description of the <see cref="Job"/>
|
||||
/// </summary>
|
||||
|
||||
@@ -60,7 +60,7 @@ namespace Tgstation.Server.Api.Rights
|
||||
/// <returns>A <see cref="string"/> representing the claim role name</returns>
|
||||
public static string RoleName(RightsType rightsType, Enum right)
|
||||
{
|
||||
var enumType = TypeMap[rightsType];
|
||||
var enumType = RightToType(rightsType);
|
||||
return String.Concat(enumType.Name, '.', Enum.GetName(enumType, right));
|
||||
}
|
||||
|
||||
|
||||
@@ -78,6 +78,11 @@ namespace Tgstation.Server.Api
|
||||
/// </summary>
|
||||
public const string Jobs = Root + nameof(Models.Job);
|
||||
|
||||
/// <summary>
|
||||
/// The postfix for list operations
|
||||
/// </summary>
|
||||
public const string List = "List";
|
||||
|
||||
/// <summary>
|
||||
/// Apply an <paramref name="id"/> postfix to a <paramref name="route"/>
|
||||
/// </summary>
|
||||
@@ -91,6 +96,6 @@ namespace Tgstation.Server.Api
|
||||
/// </summary>
|
||||
/// <param name="route">The route</param>
|
||||
/// <returns>The <paramref name="route"/> with /List appended</returns>
|
||||
public static string List(string route) => String.Format(CultureInfo.InvariantCulture, "{0}/List", route);
|
||||
public static string ListRoute(string route) => String.Format(CultureInfo.InvariantCulture, "{0}/{1}", route, List);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,7 +112,11 @@ namespace Tgstation.Server.Client
|
||||
throw new MethodNotSupportedException();
|
||||
case HttpStatusCode.InternalServerError:
|
||||
// response json is html
|
||||
throw new ServerErrorException(json);
|
||||
throw new ServerErrorException(errorMessage ?? new ErrorMessage
|
||||
{
|
||||
Message = "An internal server error occurred!",
|
||||
SeverApiVersion = null
|
||||
}, response.StatusCode);
|
||||
case (HttpStatusCode)429:
|
||||
// rate limited
|
||||
response.Headers.TryGetValues("Retry-After", out var values);
|
||||
|
||||
@@ -35,7 +35,7 @@ namespace Tgstation.Server.Client.Components
|
||||
public Task<Byond> ActiveVersion(CancellationToken cancellationToken) => apiClient.Read<Byond>(Routes.Byond, instance.Id, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<Byond>> InstalledVersions(CancellationToken cancellationToken) => apiClient.Read<IReadOnlyList<Byond>>(Routes.List(Routes.Byond), instance.Id, cancellationToken);
|
||||
public Task<IReadOnlyList<Byond>> InstalledVersions(CancellationToken cancellationToken) => apiClient.Read<IReadOnlyList<Byond>>(Routes.ListRoute(Routes.Byond), instance.Id, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<Byond> SetActiveVersion(Byond byond, CancellationToken cancellationToken) => apiClient.Update<Byond, Byond>(Routes.Byond, byond ?? throw new ArgumentNullException(nameof(byond)), instance.Id, cancellationToken);
|
||||
|
||||
@@ -38,7 +38,7 @@ namespace Tgstation.Server.Client.Components
|
||||
public Task Delete(ChatBot settings, CancellationToken cancellationToken) => apiClient.Delete(Routes.SetID(Routes.Chat, settings?.Id ?? throw new ArgumentNullException(nameof(settings))), instance.Id, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<ChatBot>> List(CancellationToken cancellationToken) => apiClient.Read<IReadOnlyList<ChatBot>>(Routes.List(Routes.Chat), instance.Id, cancellationToken);
|
||||
public Task<IReadOnlyList<ChatBot>> List(CancellationToken cancellationToken) => apiClient.Read<IReadOnlyList<ChatBot>>(Routes.ListRoute(Routes.Chat), instance.Id, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<ChatBot> Update(ChatBot settings, CancellationToken cancellationToken) => apiClient.Update<ChatBot, ChatBot>(Routes.Chat, settings ?? throw new ArgumentNullException(nameof(settings)), instance.Id, cancellationToken);
|
||||
|
||||
@@ -52,7 +52,7 @@ namespace Tgstation.Server.Client.Components
|
||||
public Task<ConfigurationFile> CreateDirectory(ConfigurationFile directory, CancellationToken cancellationToken) => apiClient.Create<ConfigurationFile, ConfigurationFile>(Routes.Configuration, directory, instance.Id, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<ConfigurationFile>> List(string directory, CancellationToken cancellationToken) => apiClient.Read<IReadOnlyList<ConfigurationFile>>(Routes.List(Routes.Configuration) + SanitizeGetPath(directory), instance.Id, cancellationToken);
|
||||
public Task<IReadOnlyList<ConfigurationFile>> List(string directory, CancellationToken cancellationToken) => apiClient.Read<IReadOnlyList<ConfigurationFile>>(Routes.ListRoute(Routes.Configuration) + SanitizeGetPath(directory), instance.Id, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<ConfigurationFile> Read(ConfigurationFile file, CancellationToken cancellationToken)
|
||||
|
||||
@@ -35,10 +35,10 @@ namespace Tgstation.Server.Client.Components
|
||||
public Task<Job> Compile(CancellationToken cancellationToken) => apiClient.Create<Job>(Routes.DreamMaker, instance.Id, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<CompileJob> GetCompileJob(CompileJob compileJob, CancellationToken cancellationToken) => apiClient.Read<CompileJob>(Routes.SetID(Routes.DreamMaker, compileJob?.Id ?? throw new ArgumentNullException(nameof(compileJob))), instance.Id, cancellationToken);
|
||||
public Task<CompileJob> GetCompileJob(EntityId compileJob, CancellationToken cancellationToken) => apiClient.Read<CompileJob>(Routes.SetID(Routes.DreamMaker, compileJob?.Id ?? throw new ArgumentNullException(nameof(compileJob))), instance.Id, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<CompileJob>> GetJobIds(CancellationToken cancellationToken) => apiClient.Read<IReadOnlyList<CompileJob>>(Routes.List(Routes.DreamMaker), instance.Id, cancellationToken);
|
||||
public Task<IReadOnlyList<EntityId>> GetJobIds(CancellationToken cancellationToken) => apiClient.Read<IReadOnlyList<EntityId>>(Routes.ListRoute(Routes.DreamMaker), instance.Id, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<DreamMaker> Read(CancellationToken cancellationToken) => apiClient.Read<DreamMaker>(Routes.DreamMaker, instance.Id, cancellationToken);
|
||||
|
||||
@@ -33,18 +33,18 @@ namespace Tgstation.Server.Client.Components
|
||||
Task<Job> Compile(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="Api.Models.Internal.CompileJob.Id"/>s of all <see cref="CompileJob"/>s for the instance
|
||||
/// Gets the <see cref="EntityId"/>s of all <see cref="CompileJob"/>s for the instance
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in a <see cref="IReadOnlyList{T}"/> of <see cref="CompileJob"/>s with only the <see cref="Api.Models.Internal.CompileJob.Id"/> field populated</returns>
|
||||
Task<IReadOnlyList<CompileJob>> GetJobIds(CancellationToken cancellationToken);
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in a <see cref="IReadOnlyList{T}"/> of <see cref="CompileJob"/> <see cref="EntityId"/>s.</returns>
|
||||
Task<IReadOnlyList<EntityId>> GetJobIds(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Get a <paramref name="compileJob"/>
|
||||
/// </summary>
|
||||
/// <param name="compileJob">The <see cref="CompileJob"/> to get</param>
|
||||
/// <param name="compileJob">The <see cref="CompileJob"/> <see cref="EntityId"/> to get</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="CompileJob"/></returns>
|
||||
Task<CompileJob> GetCompileJob(CompileJob compileJob, CancellationToken cancellationToken);
|
||||
Task<CompileJob> GetCompileJob(EntityId compileJob, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Tgstation.Server.Api.Models;
|
||||
@@ -12,11 +11,11 @@ namespace Tgstation.Server.Client.Components
|
||||
public interface IJobsClient
|
||||
{
|
||||
/// <summary>
|
||||
/// List the <see cref="Api.Models.Internal.Job.Id"/>s in the <see cref="Instance"/>
|
||||
/// List the <see cref="Job"/> <see cref="EntityId"/>s in the <see cref="Instance"/>
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in a <see cref="IReadOnlyList{T}"/> of the <see cref="Api.Models.Internal.Job.Id"/>s in the <see cref="Instance"/></returns>
|
||||
Task<IReadOnlyList<Job>> List(CancellationToken cancellationToken);
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in a <see cref="IReadOnlyList{T}"/> of the <see cref="Job"/> <see cref="EntityId"/>s in the <see cref="Instance"/></returns>
|
||||
Task<IReadOnlyList<EntityId>> List(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// List the active <see cref="Job"/>s in the <see cref="Instance"/>
|
||||
@@ -28,10 +27,10 @@ namespace Tgstation.Server.Client.Components
|
||||
/// <summary>
|
||||
/// Get a <paramref name="job"/>
|
||||
/// </summary>
|
||||
/// <param name="job">The <see cref="Job"/> to get</param>
|
||||
/// <param name="job">The <see cref="Job"/>'s <see cref="EntityId"/> to get</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="Job"/></returns>
|
||||
Task<Job> GetId(Job job, CancellationToken cancellationToken);
|
||||
Task<Job> GetId(EntityId job, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Cancels a <paramref name="job"/>
|
||||
|
||||
@@ -44,7 +44,7 @@ namespace Tgstation.Server.Client.Components
|
||||
public Task<InstanceUser> Update(InstanceUser instanceUser, CancellationToken cancellationToken) => apiClient.Update<InstanceUser, InstanceUser>(Routes.InstanceUser, instanceUser ?? throw new ArgumentNullException(nameof(instanceUser)), instance.Id, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<InstanceUser>> List(CancellationToken cancellationToken) => apiClient.Read<IReadOnlyList<InstanceUser>>(Routes.List(Routes.InstanceUser), instance.Id, cancellationToken);
|
||||
public Task<IReadOnlyList<InstanceUser>> List(CancellationToken cancellationToken) => apiClient.Read<IReadOnlyList<InstanceUser>>(Routes.ListRoute(Routes.InstanceUser), instance.Id, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<InstanceUser> GetId(InstanceUser instanceUser, CancellationToken cancellationToken) => apiClient.Read<InstanceUser>(Routes.SetID(Routes.InstanceUser, instanceUser?.UserId ?? throw new ArgumentNullException(nameof(instanceUser))), instance.Id, cancellationToken);
|
||||
|
||||
@@ -35,12 +35,12 @@ namespace Tgstation.Server.Client.Components
|
||||
public Task Cancel(Job job, CancellationToken cancellationToken) => apiClient.Delete(Routes.SetID(Routes.Jobs, job?.Id ?? throw new ArgumentNullException(nameof(job))), instance.Id, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<Job>> List(CancellationToken cancellationToken) => apiClient.Read<IReadOnlyList<Job>>(Routes.List(Routes.Jobs), instance.Id, cancellationToken);
|
||||
public Task<IReadOnlyList<EntityId>> List(CancellationToken cancellationToken) => apiClient.Read<IReadOnlyList<EntityId>>(Routes.ListRoute(Routes.Jobs), instance.Id, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<Job>> ListActive(CancellationToken cancellationToken) => apiClient.Read<IReadOnlyList<Job>>(Routes.Jobs, instance.Id, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<Job> GetId(Job job, CancellationToken cancellationToken) => apiClient.Read<Job>(Routes.SetID(Routes.Jobs, job?.Id ?? throw new ArgumentNullException(nameof(job))), instance.Id, cancellationToken);
|
||||
public Task<Job> GetId(EntityId job, CancellationToken cancellationToken) => apiClient.Read<Job>(Routes.SetID(Routes.Jobs, job?.Id ?? throw new ArgumentNullException(nameof(job))), instance.Id, cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -39,7 +39,7 @@ namespace Tgstation.Server.Client
|
||||
public Task Detach(Instance instance, CancellationToken cancellationToken) => apiClient.Delete(Routes.SetID(Routes.InstanceManager, instance?.Id ?? throw new ArgumentNullException(nameof(instance))), cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<Instance>> List(CancellationToken cancellationToken) => apiClient.Read<IReadOnlyList<Instance>>(Routes.List(Routes.InstanceManager), cancellationToken);
|
||||
public Task<IReadOnlyList<Instance>> List(CancellationToken cancellationToken) => apiClient.Read<IReadOnlyList<Instance>>(Routes.ListRoute(Routes.InstanceManager), cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<Instance> Update(Instance instance, CancellationToken cancellationToken) => apiClient.Update<Instance, Instance>(Routes.InstanceManager, instance ?? throw new ArgumentNullException(nameof(instance)), cancellationToken);
|
||||
|
||||
@@ -9,27 +9,18 @@ namespace Tgstation.Server.Client
|
||||
/// </summary>
|
||||
public sealed class ServerErrorException : ClientException
|
||||
{
|
||||
/// <summary>
|
||||
/// The raw HTML of the error
|
||||
/// </summary>
|
||||
public string Html { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Construct an <see cref="ServerErrorException"/>
|
||||
/// </summary>
|
||||
public ServerErrorException() { }
|
||||
|
||||
/// <summary>
|
||||
/// Construct an <see cref="ServerErrorException"/> with <paramref name="html"/>
|
||||
/// Construct an <see cref="ServerErrorException"/> with a given <paramref name="errorMessage"/>
|
||||
/// </summary>
|
||||
/// <param name="html">The raw HTML response of the <see cref="ServerErrorException"/></param>
|
||||
public ServerErrorException(string html) : base(new ErrorMessage
|
||||
/// <param name="errorMessage">The <see cref="ErrorMessage"/> for the <see cref="ClientException"/></param>
|
||||
/// <param name="statusCode">The <see cref="HttpStatusCode"/> for the <see cref="ClientException"/></param>
|
||||
public ServerErrorException(ErrorMessage errorMessage, HttpStatusCode statusCode) : base(errorMessage, statusCode)
|
||||
{
|
||||
Message = "An internal server error occurred!",
|
||||
SeverApiVersion = null
|
||||
}, HttpStatusCode.InternalServerError)
|
||||
{
|
||||
Html = html;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -31,7 +31,7 @@ namespace Tgstation.Server.Client
|
||||
public Task<User> GetId(User user, CancellationToken cancellationToken) => apiClient.Read<User>(Routes.SetID(Routes.User, user?.Id ?? throw new ArgumentNullException(nameof(user))), cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<User>> List(CancellationToken cancellationToken) => apiClient.Read<IReadOnlyList<User>>(Routes.List(Routes.User), cancellationToken);
|
||||
public Task<IReadOnlyList<User>> List(CancellationToken cancellationToken) => apiClient.Read<IReadOnlyList<User>>(Routes.ListRoute(Routes.User), cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<User> Read(CancellationToken cancellationToken) => apiClient.Read<User>(Routes.User, cancellationToken);
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Tgstation.Server.Host.IO;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Byond
|
||||
{
|
||||
/// <inheritdoc />
|
||||
abstract class ByondInstallerBase : IByondInstaller
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public abstract string DreamDaemonName { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract string DreamMakerName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the URL formatter string for downloading a byond version of {0:Major} {1:Minor}.
|
||||
/// </summary>
|
||||
protected abstract string ByondRevisionsURLTemplate { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="IIOManager"/> for the <see cref="ByondInstallerBase"/>.
|
||||
/// </summary>
|
||||
protected IIOManager IOManager { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="ILogger"/> for the <see cref="ByondInstallerBase"/>.
|
||||
/// </summary>
|
||||
protected ILogger<ByondInstallerBase> Logger { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ByondInstallerBase"/> <see langword="class"/>.
|
||||
/// </summary>
|
||||
/// <param name="ioManager">The value of <see cref="IOManager"/>.</param>
|
||||
/// <param name="logger">The value of <see cref="Logger"/>.</param>
|
||||
protected ByondInstallerBase(IIOManager ioManager, ILogger<ByondInstallerBase> logger)
|
||||
{
|
||||
IOManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
|
||||
Logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract Task CleanCache(CancellationToken cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract Task InstallByond(string path, Version version, CancellationToken cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<byte[]> DownloadVersion(Version version, CancellationToken cancellationToken)
|
||||
{
|
||||
if (version == null)
|
||||
throw new ArgumentNullException(nameof(version));
|
||||
|
||||
var url = String.Format(CultureInfo.InvariantCulture, ByondRevisionsURLTemplate, version.Major, version.Minor);
|
||||
|
||||
Logger.LogTrace("Downloading from: {0}", url);
|
||||
|
||||
return IOManager.DownloadFile(new Uri(url), cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,13 +11,8 @@ namespace Tgstation.Server.Host.Components.Byond
|
||||
/// <summary>
|
||||
/// <see cref="IByondInstaller"/> for Posix systems
|
||||
/// </summary>
|
||||
sealed class PosixByondInstaller : IByondInstaller
|
||||
sealed class PosixByondInstaller : ByondInstallerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// The URL format string for getting BYOND linux version {0}.{1} zipfile
|
||||
/// </summary>
|
||||
const string ByondRevisionsURLTemplate = "https://secure.byond.com/download/build/{0}/{0}.{1}_byond_linux.zip";
|
||||
|
||||
/// <summary>
|
||||
/// Path to the BYOND cache
|
||||
/// </summary>
|
||||
@@ -28,45 +23,37 @@ namespace Tgstation.Server.Host.Components.Byond
|
||||
const string ShellScriptExtension = ".sh";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string DreamDaemonName => DreamDaemonExecutableName + ShellScriptExtension;
|
||||
public override string DreamDaemonName => DreamDaemonExecutableName + ShellScriptExtension;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string DreamMakerName => DreamMakerExecutableName + ShellScriptExtension;
|
||||
public override string DreamMakerName => DreamMakerExecutableName + ShellScriptExtension;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IIOManager"/> for the <see cref="PosixByondInstaller"/>
|
||||
/// </summary>
|
||||
readonly IIOManager ioManager;
|
||||
/// <inheritdoc />
|
||||
protected override string ByondRevisionsURLTemplate => "https://secure.byond.com/download/build/{0}/{0}.{1}_byond_linux.zip";
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IPostWriteHandler"/> for the <see cref="PosixByondInstaller"/>
|
||||
/// </summary>
|
||||
readonly IPostWriteHandler postWriteHandler;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ILogger"/> for the <see cref="PosixByondInstaller"/>
|
||||
/// </summary>
|
||||
readonly ILogger<PosixByondInstaller> logger;
|
||||
|
||||
/// <summary>
|
||||
/// Construct a <see cref="WindowsByondInstaller"/>
|
||||
/// </summary>
|
||||
/// <param name="ioManager">The value of <see cref="ioManager"/></param>
|
||||
/// <param name="postWriteHandler">The value of <see cref="postWriteHandler"/></param>
|
||||
/// <param name="logger">The value of <see cref="logger"/></param>
|
||||
public PosixByondInstaller(IIOManager ioManager, IPostWriteHandler postWriteHandler, ILogger<PosixByondInstaller> logger)
|
||||
/// <param name="ioManager">The <see cref="IIOManager"/> for the <see cref="ByondInstallerBase"/>.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="ByondInstallerBase"/>.</param>
|
||||
public PosixByondInstaller(IPostWriteHandler postWriteHandler, IIOManager ioManager, ILogger<PosixByondInstaller> logger)
|
||||
: base(ioManager, logger)
|
||||
{
|
||||
this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
|
||||
this.postWriteHandler = postWriteHandler ?? throw new ArgumentNullException(nameof(postWriteHandler));
|
||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task CleanCache(CancellationToken cancellationToken)
|
||||
public override async Task CleanCache(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await ioManager.DeleteDirectory(ByondCachePath, cancellationToken).ConfigureAwait(false);
|
||||
await IOManager.DeleteDirectory(ByondCachePath, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
@@ -74,23 +61,12 @@ namespace Tgstation.Server.Host.Components.Byond
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogWarning("Error deleting BYOND cache! Exception: {0}", e);
|
||||
Logger.LogWarning("Error deleting BYOND cache! Exception: {0}", e);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<byte[]> DownloadVersion(Version version, CancellationToken cancellationToken)
|
||||
{
|
||||
if (version == null)
|
||||
throw new ArgumentNullException(nameof(version));
|
||||
|
||||
var url = String.Format(CultureInfo.InvariantCulture, ByondRevisionsURLTemplate, version.Major, version.Minor);
|
||||
|
||||
return await ioManager.DownloadFile(new Uri(url), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task InstallByond(string path, Version version, CancellationToken cancellationToken)
|
||||
public override Task InstallByond(string path, Version version, CancellationToken cancellationToken)
|
||||
{
|
||||
if (path == null)
|
||||
throw new ArgumentNullException(nameof(path));
|
||||
@@ -106,16 +82,16 @@ namespace Tgstation.Server.Host.Components.Byond
|
||||
|
||||
async Task WriteAndMakeExecutable(string fullPath, string script)
|
||||
{
|
||||
await ioManager.WriteAllBytes(fullPath, Encoding.ASCII.GetBytes(script), cancellationToken).ConfigureAwait(false);
|
||||
await IOManager.WriteAllBytes(fullPath, Encoding.ASCII.GetBytes(script), cancellationToken).ConfigureAwait(false);
|
||||
postWriteHandler.HandleWrite(fullPath);
|
||||
}
|
||||
|
||||
var basePath = ioManager.ConcatPath(path, ByondManager.BinPath);
|
||||
var basePath = IOManager.ConcatPath(path, ByondManager.BinPath);
|
||||
|
||||
var task = Task.WhenAll(WriteAndMakeExecutable(ioManager.ConcatPath(basePath, DreamDaemonName), dreamDaemonScript), WriteAndMakeExecutable(ioManager.ConcatPath(basePath, DreamMakerName), dreamMakerScript));
|
||||
var task = Task.WhenAll(WriteAndMakeExecutable(IOManager.ConcatPath(basePath, DreamDaemonName), dreamDaemonScript), WriteAndMakeExecutable(IOManager.ConcatPath(basePath, DreamMakerName), dreamMakerScript));
|
||||
|
||||
postWriteHandler.HandleWrite(ioManager.ConcatPath(basePath, DreamDaemonExecutableName));
|
||||
postWriteHandler.HandleWrite(ioManager.ConcatPath(basePath, DreamMakerExecutableName));
|
||||
postWriteHandler.HandleWrite(IOManager.ConcatPath(basePath, DreamDaemonExecutableName));
|
||||
postWriteHandler.HandleWrite(IOManager.ConcatPath(basePath, DreamMakerExecutableName));
|
||||
|
||||
return task;
|
||||
}
|
||||
|
||||
@@ -12,13 +12,8 @@ namespace Tgstation.Server.Host.Components.Byond
|
||||
/// <summary>
|
||||
/// <see cref="IByondInstaller"/> for windows systems
|
||||
/// </summary>
|
||||
sealed class WindowsByondInstaller : IByondInstaller, IDisposable
|
||||
sealed class WindowsByondInstaller : ByondInstallerBase, IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The URL format string for getting BYOND windows version {0}.{1} zipfile
|
||||
/// </summary>
|
||||
const string ByondRevisionsURLTemplate = "https://secure.byond.com/download/build/{0}/{0}.{1}_byond.zip";
|
||||
|
||||
/// <summary>
|
||||
/// Directory to byond installation configuration
|
||||
/// </summary>
|
||||
@@ -40,26 +35,19 @@ namespace Tgstation.Server.Host.Components.Byond
|
||||
const string ByondDXDir = "byond/directx";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string DreamDaemonName => "dreamdaemon.exe";
|
||||
public override string DreamDaemonName => "dreamdaemon.exe";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string DreamMakerName => "dm.exe";
|
||||
public override string DreamMakerName => "dm.exe";
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IIOManager"/> for the <see cref="WindowsByondInstaller"/>
|
||||
/// </summary>
|
||||
readonly IIOManager ioManager;
|
||||
/// <inheritdoc />
|
||||
protected override string ByondRevisionsURLTemplate => "https://secure.byond.com/download/build/{0}/{0}.{1}_byond.zip";
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IProcessExecutor"/> for the <see cref="WindowsByondInstaller"/>
|
||||
/// </summary>
|
||||
readonly IProcessExecutor processExecutor;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ILogger"/> for the <see cref="WindowsByondInstaller"/>
|
||||
/// </summary>
|
||||
readonly ILogger<WindowsByondInstaller> logger;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="SemaphoreSlim"/> for the <see cref="WindowsByondInstaller"/>
|
||||
/// </summary>
|
||||
@@ -73,14 +61,13 @@ namespace Tgstation.Server.Host.Components.Byond
|
||||
/// <summary>
|
||||
/// Construct a <see cref="WindowsByondInstaller"/>
|
||||
/// </summary>
|
||||
/// <param name="ioManager">The value of <see cref="ioManager"/></param>
|
||||
/// <param name="processExecutor">The value of <see cref="processExecutor"/></param>
|
||||
/// <param name="logger">The value of <see cref="logger"/></param>
|
||||
public WindowsByondInstaller(IIOManager ioManager, IProcessExecutor processExecutor, ILogger<WindowsByondInstaller> logger)
|
||||
/// <param name="ioManager">The <see cref="IIOManager"/> for the <see cref="ByondInstallerBase"/>.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="ByondInstallerBase"/>.</param>
|
||||
public WindowsByondInstaller(IProcessExecutor processExecutor, IIOManager ioManager, ILogger<WindowsByondInstaller> logger)
|
||||
: base(ioManager, logger)
|
||||
{
|
||||
this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
|
||||
this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor));
|
||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
|
||||
semaphore = new SemaphoreSlim(1);
|
||||
installedDirectX = false;
|
||||
@@ -90,11 +77,11 @@ namespace Tgstation.Server.Host.Components.Byond
|
||||
public void Dispose() => semaphore.Dispose();
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task CleanCache(CancellationToken cancellationToken)
|
||||
public override async Task CleanCache(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await ioManager.DeleteDirectory(ioManager.ConcatPath(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "byond/cache"), cancellationToken).ConfigureAwait(false);
|
||||
await IOManager.DeleteDirectory(IOManager.ConcatPath(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "byond/cache"), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch(OperationCanceledException)
|
||||
{
|
||||
@@ -102,26 +89,18 @@ namespace Tgstation.Server.Host.Components.Byond
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogWarning("Error deleting BYOND cache! Exception: {0}", e);
|
||||
Logger.LogWarning("Error deleting BYOND cache! Exception: {0}", e);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<byte[]> DownloadVersion(Version version, CancellationToken cancellationToken)
|
||||
{
|
||||
var url = String.Format(CultureInfo.InvariantCulture, ByondRevisionsURLTemplate, version.Major, version.Minor);
|
||||
|
||||
return ioManager.DownloadFile(new Uri(url), cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task InstallByond(string path, Version version, CancellationToken cancellationToken)
|
||||
public override async Task InstallByond(string path, Version version, CancellationToken cancellationToken)
|
||||
{
|
||||
async Task SetNoPromptTrusted()
|
||||
{
|
||||
var configPath = ioManager.ConcatPath(path, ByondConfigDir);
|
||||
await ioManager.CreateDirectory(configPath, cancellationToken).ConfigureAwait(false);
|
||||
await ioManager.WriteAllBytes(ioManager.ConcatPath(configPath, ByondDDConfig), Encoding.UTF8.GetBytes(ByondNoPromptTrustedMode), cancellationToken).ConfigureAwait(false);
|
||||
var configPath = IOManager.ConcatPath(path, ByondConfigDir);
|
||||
await IOManager.CreateDirectory(configPath, cancellationToken).ConfigureAwait(false);
|
||||
await IOManager.WriteAllBytes(IOManager.ConcatPath(configPath, ByondDDConfig), Encoding.UTF8.GetBytes(ByondNoPromptTrustedMode), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var setNoPromptTrustedModeTask = SetNoPromptTrusted();
|
||||
@@ -135,13 +114,13 @@ namespace Tgstation.Server.Host.Components.Byond
|
||||
{
|
||||
// ^check again because race conditions
|
||||
// always install it, it's pretty fast and will do better redundancy checking than us
|
||||
var rbdx = ioManager.ConcatPath(path, ByondDXDir);
|
||||
var rbdx = IOManager.ConcatPath(path, ByondDXDir);
|
||||
|
||||
// noShellExecute because we aren't doing runas shennanigans
|
||||
IProcess directXInstaller;
|
||||
try
|
||||
{
|
||||
directXInstaller = processExecutor.LaunchProcess(ioManager.ConcatPath(rbdx, "DXSETUP.exe"), rbdx, "/silent", noShellExecute: true);
|
||||
directXInstaller = processExecutor.LaunchProcess(IOManager.ConcatPath(rbdx, "DXSETUP.exe"), rbdx, "/silent", noShellExecute: true);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
||||
@@ -22,10 +22,10 @@ using Tgstation.Server.Host.Security;
|
||||
namespace Tgstation.Server.Host.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="ModelController{TModel}"/> for <see cref="Administration"/>
|
||||
/// <see cref="ApiController"/> for <see cref="Administration"/> purposes
|
||||
/// </summary>
|
||||
[Route(Routes.Administration)]
|
||||
public sealed class AdministrationController : ModelController<Administration>
|
||||
public sealed class AdministrationController : ApiController
|
||||
{
|
||||
const string RestartNotSupportedException = "This deployment of tgstation-server is lacking the Tgstation.Server.Host.Watchdog component. Restarts and version changes cannot be completed!";
|
||||
|
||||
@@ -79,7 +79,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="ApiController"/></param>
|
||||
/// <param name="updatesConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing value of <see cref="updatesConfiguration"/></param>
|
||||
/// <param name="generalConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing value of <see cref="generalConfiguration"/></param>
|
||||
public AdministrationController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IGitHubClientFactory gitHubClientFactory, IServerControl serverUpdater, IApplication application, IIOManager ioManager, IPlatformIdentifier platformIdentifier, ILogger<AdministrationController> logger, IOptions<UpdatesConfiguration> updatesConfigurationOptions, IOptions<GeneralConfiguration> generalConfigurationOptions) : base(databaseContext, authenticationContextFactory, logger, false)
|
||||
public AdministrationController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IGitHubClientFactory gitHubClientFactory, IServerControl serverUpdater, IApplication application, IIOManager ioManager, IPlatformIdentifier platformIdentifier, ILogger<AdministrationController> logger, IOptions<UpdatesConfiguration> updatesConfigurationOptions, IOptions<GeneralConfiguration> generalConfigurationOptions) : base(databaseContext, authenticationContextFactory, logger, false, true)
|
||||
{
|
||||
this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory));
|
||||
this.serverUpdater = serverUpdater ?? throw new ArgumentNullException(nameof(serverUpdater));
|
||||
@@ -152,9 +152,19 @@ namespace Tgstation.Server.Host.Controllers
|
||||
|
||||
IGitHubClient GetGitHubClient() => String.IsNullOrEmpty(generalConfiguration.GitHubAccessToken) ? gitHubClientFactory.CreateClient() : gitHubClientFactory.CreateClient(generalConfiguration.GitHubAccessToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>
|
||||
/// Get <see cref="Administration"/> server information.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IActionResult"/> for the operation.</returns>
|
||||
/// <response code="200">Retrieved <see cref="Administration"/> data successfully.</response>
|
||||
/// <response code="424">The GitHub API rate limit was hit. See response header Retry-After.</response>
|
||||
/// <response code="429">A GitHub API error occurred. See error message for details.</response>
|
||||
[HttpGet]
|
||||
[TgsAuthorize]
|
||||
public override async Task<IActionResult> Read(CancellationToken cancellationToken)
|
||||
[ProducesResponseType(typeof(Administration), 200)]
|
||||
[ProducesResponseType(424)]
|
||||
[ProducesResponseType(typeof(ErrorMessage), 429)]
|
||||
public async Task<IActionResult> Read()
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -192,13 +202,24 @@ namespace Tgstation.Server.Host.Controllers
|
||||
catch (ApiException e)
|
||||
{
|
||||
Logger.LogWarning(OctokitException, e);
|
||||
return StatusCode((int)HttpStatusCode.FailedDependency);
|
||||
return StatusCode((int)HttpStatusCode.FailedDependency, new ErrorMessage
|
||||
{
|
||||
Message = e.Message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>
|
||||
/// Attempt to perform a server upgrade.
|
||||
/// </summary>
|
||||
/// <param name="model">The model containing the <see cref="Administration.NewVersion"/> to update to.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IActionResult"/> for the operation.</returns>
|
||||
/// <response code="422">Upgrade operations are unavailable due to the launch configuration of TGS.</response>
|
||||
[HttpPost]
|
||||
[TgsAuthorize(AdministrationRights.ChangeVersion)]
|
||||
public override async Task<IActionResult> Update([FromBody] Administration model, CancellationToken cancellationToken)
|
||||
[ProducesResponseType(typeof(ErrorMessage), 422)]
|
||||
public async Task<IActionResult> Update([FromBody] Administration model, CancellationToken cancellationToken)
|
||||
{
|
||||
if (model == null)
|
||||
throw new ArgumentNullException(nameof(model));
|
||||
@@ -222,8 +243,12 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// Attempts to restart the server
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IActionResult"/> of the request</returns>
|
||||
[HttpDelete]
|
||||
/// <response code="200">Restart begun successfully.</response>
|
||||
/// <response code="422">Restart operations are unavailable due to the launch configuration of TGS.</response>
|
||||
[HttpDelete("{id}")]
|
||||
[TgsAuthorize(AdministrationRights.RestartHost)]
|
||||
[ProducesResponseType(200)]
|
||||
[ProducesResponseType(typeof(ErrorMessage), 422)]
|
||||
public async Task<IActionResult> Delete()
|
||||
{
|
||||
try
|
||||
|
||||
@@ -18,6 +18,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// A <see cref="Controller"/> for API functions
|
||||
/// </summary>
|
||||
[Produces(ApiHeaders.ApplicationJson)]
|
||||
[ApiController]
|
||||
public abstract class ApiController : Controller
|
||||
{
|
||||
/// <summary>
|
||||
@@ -129,25 +130,9 @@ namespace Tgstation.Server.Host.Controllers
|
||||
|
||||
if (ModelState?.IsValid == false)
|
||||
{
|
||||
var errorMessages = ModelState.SelectMany(x => x.Value.Errors).Select(x => x.ErrorMessage).ToList();
|
||||
|
||||
// HACK
|
||||
// do some fuckery to remove RequiredAttribute errors
|
||||
for (var I = 0; I < errorMessages.Count; ++I)
|
||||
{
|
||||
var message = errorMessages[I];
|
||||
if (message.StartsWith("The ", StringComparison.Ordinal) && message.EndsWith(" field is required.", StringComparison.Ordinal))
|
||||
{
|
||||
errorMessages.RemoveAt(I);
|
||||
--I;
|
||||
}
|
||||
}
|
||||
|
||||
if (errorMessages.Count > 0)
|
||||
{
|
||||
await BadRequest(new ErrorMessage { Message = String.Join(Environment.NewLine, errorMessages) }).ExecuteResultAsync(context).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
var errorMessages = ModelState.SelectMany(x => x.Value.Errors).Select(x => x.ErrorMessage);
|
||||
await BadRequest(new ErrorMessage { Message = String.Join(Environment.NewLine, errorMessages) }).ExecuteResultAsync(context).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (ApiHeaders != null)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -19,7 +19,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// Controller for managing <see cref="Api.Models.Byond.Version"/>s
|
||||
/// </summary>
|
||||
[Route(Routes.Byond)]
|
||||
public sealed class ByondController : ModelController<Api.Models.Byond>
|
||||
public sealed class ByondController : ApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="IInstanceManager"/> for the <see cref="ByondController"/>
|
||||
@@ -39,31 +39,53 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// <param name="instanceManager">The value of <see cref="instanceManager"/></param>
|
||||
/// <param name="jobManager">The value of <see cref="jobManager"/></param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="ApiController"/></param>
|
||||
public ByondController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, IJobManager jobManager, ILogger<ByondController> logger) : base(databaseContext, authenticationContextFactory, logger, true)
|
||||
public ByondController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, IJobManager jobManager, ILogger<ByondController> logger) : base(databaseContext, authenticationContextFactory, logger, true, true)
|
||||
{
|
||||
this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
|
||||
this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>
|
||||
/// Gets the active <see cref="Api.Models.Byond"/> version.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IActionResult"/> for the operation.</returns>
|
||||
/// <response code="200">Retrieved version information successfully.</response>
|
||||
[HttpGet]
|
||||
[TgsAuthorize(ByondRights.ReadActive)]
|
||||
public override Task<IActionResult> Read(CancellationToken cancellationToken) => Task.FromResult<IActionResult>(
|
||||
[ProducesResponseType(typeof(Api.Models.Byond), 200)]
|
||||
public Task<IActionResult> Read() => Task.FromResult<IActionResult>(
|
||||
Json(new Api.Models.Byond
|
||||
{
|
||||
Version = instanceManager.GetInstance(Instance).ByondManager.ActiveVersion
|
||||
}));
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>
|
||||
/// Lists installed <see cref="Api.Models.Byond"/> versions.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IActionResult"/> for the operation.</returns>
|
||||
/// <response code="200">Retrieved version information successfully.</response>
|
||||
[HttpGet(Routes.List)]
|
||||
[TgsAuthorize(ByondRights.ListInstalled)]
|
||||
public override Task<IActionResult> List(CancellationToken cancellationToken) => Task.FromResult<IActionResult>(
|
||||
[ProducesResponseType(typeof(IEnumerable<Api.Models.Byond>), 200)]
|
||||
public Task<IActionResult> List() => Task.FromResult<IActionResult>(
|
||||
Json(instanceManager.GetInstance(Instance).ByondManager.InstalledVersions.Select(x => new Api.Models.Byond
|
||||
{
|
||||
Version = x
|
||||
})));
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>
|
||||
/// Changes the active BYOND version to the one specified in a given <paramref name="model"/>.
|
||||
/// </summary>
|
||||
/// <param name="model">The <see cref="Api.Models.Byond.Version"/> to switch to.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IActionResult"/> for the operation.</returns>
|
||||
/// <response code="200">Switched active version successfully.</response>
|
||||
/// <response code="202">Created <see cref="Api.Models.Job"/> to install and switch active version successfully.</response>
|
||||
[HttpPost]
|
||||
[TgsAuthorize(ByondRights.ChangeVersion)]
|
||||
public override async Task<IActionResult> Update([FromBody] Api.Models.Byond model, CancellationToken cancellationToken)
|
||||
[ProducesResponseType(typeof(Api.Models.Byond), 200)]
|
||||
[ProducesResponseType(typeof(Api.Models.Byond), 202)]
|
||||
public async Task<IActionResult> Update([FromBody] Api.Models.Byond model, CancellationToken cancellationToken)
|
||||
{
|
||||
if (model == null)
|
||||
throw new ArgumentNullException(nameof(model));
|
||||
@@ -90,7 +112,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
// run the install through the job manager
|
||||
var job = new Models.Job
|
||||
{
|
||||
Description = String.Format(CultureInfo.InvariantCulture, "Install BYOND version {0}", installingVersion),
|
||||
Description = $"Install BYOND version {installingVersion}",
|
||||
StartedBy = AuthenticationContext.User,
|
||||
CancelRightsType = RightsType.Byond,
|
||||
CancelRight = (ulong)ByondRights.CancelInstall,
|
||||
|
||||
@@ -20,10 +20,10 @@ using Z.EntityFramework.Plus;
|
||||
namespace Tgstation.Server.Host.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="ModelController{TModel}"/> for managing <see cref="Api.Models.ChatBot"/>s
|
||||
/// <see cref="ApiController"/> for managing <see cref="Api.Models.ChatBot"/>s
|
||||
/// </summary>
|
||||
[Route(Routes.Chat)]
|
||||
public sealed class ChatController : ModelController<Api.Models.ChatBot>
|
||||
public sealed class ChatController : ApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="IInstanceManager"/> for the <see cref="ChatController"/>
|
||||
@@ -37,7 +37,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="ApiController"/></param>
|
||||
/// <param name="instanceManager">The value of <see cref="instanceManager"/></param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="ApiController"/></param>
|
||||
public ChatController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, ILogger<ChatController> logger) : base(databaseContext, authenticationContextFactory, logger, true)
|
||||
public ChatController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, ILogger<ChatController> logger) : base(databaseContext, authenticationContextFactory, logger, true, true)
|
||||
{
|
||||
this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
|
||||
}
|
||||
@@ -57,9 +57,17 @@ namespace Tgstation.Server.Host.Controllers
|
||||
Tag = api.Tag
|
||||
};
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>
|
||||
/// Create a new chat bot <paramref name="model"/>.
|
||||
/// </summary>
|
||||
/// <param name="model">The <see cref="Api.Models.ChatBot"/> to create.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IActionResult"/> for the operation.</returns>
|
||||
/// <response code="201">Created chat bot successfully.</response>
|
||||
[HttpPut]
|
||||
[TgsAuthorize(ChatBotRights.Create)]
|
||||
public override async Task<IActionResult> Create([FromBody] Api.Models.ChatBot model, CancellationToken cancellationToken)
|
||||
[ProducesResponseType(typeof(Api.Models.ChatBot), 201)]
|
||||
public async Task<IActionResult> Create([FromBody] Api.Models.ChatBot model, CancellationToken cancellationToken)
|
||||
{
|
||||
if (model == null)
|
||||
throw new ArgumentNullException(nameof(model));
|
||||
@@ -129,9 +137,17 @@ namespace Tgstation.Server.Host.Controllers
|
||||
return StatusCode((int)HttpStatusCode.Created, dbModel.ToApi());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>
|
||||
/// Delete a <see cref="Api.Models.ChatBot"/>.
|
||||
/// </summary>
|
||||
/// <param name="id">The <see cref="Api.Models.Internal.ChatBot.Id"/> to delete.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IActionResult"/> for the operation.</returns>
|
||||
/// <response code="200">Chat bot deleted or does not exist.</response>
|
||||
[HttpDelete("{id}")]
|
||||
[TgsAuthorize(ChatBotRights.Delete)]
|
||||
public override async Task<IActionResult> Delete(long id, CancellationToken cancellationToken)
|
||||
[ProducesResponseType(200)]
|
||||
public async Task<IActionResult> Delete(long id, CancellationToken cancellationToken)
|
||||
{
|
||||
var instance = instanceManager.GetInstance(Instance);
|
||||
await Task.WhenAll(instance.Chat.DeleteConnection(id, cancellationToken), DatabaseContext.ChatBots.Where(x => x.Id == id).DeleteAsync(cancellationToken)).ConfigureAwait(false);
|
||||
@@ -139,9 +155,16 @@ namespace Tgstation.Server.Host.Controllers
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>
|
||||
/// List <see cref="Api.Models.ChatBot"/>s.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IActionResult"/> for the operation.</returns>
|
||||
/// <response code="200">Listed chat bots successfully.</response>
|
||||
[HttpGet(Routes.List)]
|
||||
[TgsAuthorize(ChatBotRights.Read)]
|
||||
public override async Task<IActionResult> List(CancellationToken cancellationToken)
|
||||
[ProducesResponseType(typeof(IEnumerable<Api.Models.ChatBot>), 200)]
|
||||
public async Task<IActionResult> List(CancellationToken cancellationToken)
|
||||
{
|
||||
var query = DatabaseContext.ChatBots.Where(x => x.InstanceId == Instance.Id).Include(x => x.Channels);
|
||||
|
||||
@@ -156,9 +179,19 @@ namespace Tgstation.Server.Host.Controllers
|
||||
return Json(results.Select(x => x.ToApi()));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>
|
||||
/// Get a specific <see cref="Api.Models.ChatBot"/>.
|
||||
/// </summary>
|
||||
/// <param name="id">The <see cref="Api.Models.Internal.ChatBot.Id"/> to retrieve.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IActionResult"/> for the operation.</returns>
|
||||
/// <response code="200">Retrieved <see cref="Api.Models.ChatBot"/> successfully.</response>
|
||||
/// <response code="410">Chat bot does not exist.</response>
|
||||
[HttpGet("{id}")]
|
||||
[TgsAuthorize(ChatBotRights.Read)]
|
||||
public override async Task<IActionResult> GetId(long id, CancellationToken cancellationToken)
|
||||
[ProducesResponseType(typeof(Api.Models.ChatBot), 200)]
|
||||
[ProducesResponseType(410)]
|
||||
public async Task<IActionResult> GetId(long id, CancellationToken cancellationToken)
|
||||
{
|
||||
var query = DatabaseContext.ChatBots.Where(x => x.Id == id).Include(x => x.Channels);
|
||||
|
||||
@@ -174,10 +207,19 @@ namespace Tgstation.Server.Host.Controllers
|
||||
return Json(results.ToApi());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
#pragma warning disable CA1506 // TODO: Decomplexify
|
||||
/// <summary>
|
||||
/// Updates a chat bot <paramref name="model"/>.
|
||||
/// </summary>
|
||||
/// <param name="model">The <see cref="Api.Models.ChatBot"/> update to apply.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IActionResult"/> for the operation.</returns>
|
||||
/// <response code="200">Update applied successfully. <see cref="Api.Models.ChatBot"/> may or may not be returned based on user permissions.</response>
|
||||
[HttpPost]
|
||||
[TgsAuthorize(ChatBotRights.WriteChannels | ChatBotRights.WriteConnectionString | ChatBotRights.WriteEnabled | ChatBotRights.WriteName | ChatBotRights.WriteProvider)]
|
||||
public override async Task<IActionResult> Update([FromBody] Api.Models.ChatBot model, CancellationToken cancellationToken)
|
||||
[ProducesResponseType(200)]
|
||||
[ProducesResponseType(typeof(Api.Models.ChatBot), 200)]
|
||||
#pragma warning disable CA1506 // TODO: Decomplexify
|
||||
public async Task<IActionResult> Update([FromBody] Api.Models.ChatBot model, CancellationToken cancellationToken)
|
||||
{
|
||||
if (model == null)
|
||||
throw new ArgumentNullException(nameof(model));
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
@@ -16,10 +17,10 @@ using Tgstation.Server.Host.Security;
|
||||
namespace Tgstation.Server.Host.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="ModelController{TModel}"/> for <see cref="ConfigurationFile"/>s
|
||||
/// The <see cref="ApiController"/> for <see cref="ConfigurationFile"/>s
|
||||
/// </summary>
|
||||
[Route(Routes.Configuration)]
|
||||
public sealed class ConfigurationController : ModelController<ConfigurationFile>
|
||||
public sealed class ConfigurationController : ApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="IInstanceManager"/> for the <see cref="ConfigurationController"/>
|
||||
@@ -39,7 +40,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// <param name="instanceManager">The value of <see cref="instanceManager"/></param>
|
||||
/// <param name="ioManager">The value of <see cref="ioManager"/></param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="ApiController"/></param>
|
||||
public ConfigurationController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, IIOManager ioManager, ILogger<ConfigurationController> logger) : base(databaseContext, authenticationContextFactory, logger, true)
|
||||
public ConfigurationController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, IIOManager ioManager, ILogger<ConfigurationController> logger) : base(databaseContext, authenticationContextFactory, logger, true, true)
|
||||
{
|
||||
this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
|
||||
this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
|
||||
@@ -63,9 +64,21 @@ namespace Tgstation.Server.Host.Controllers
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>
|
||||
/// Write to a configuration file.
|
||||
/// </summary>
|
||||
/// <param name="model">The <see cref="ConfigurationFile"/> representing the file.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IActionResult"/> for the operation.</returns>
|
||||
/// <response code="200">File updated successfully.</response>
|
||||
/// <response code="200">File created successfully.</response>
|
||||
/// <response code="501">POSIX system impersonation requested but not implemented.</response>
|
||||
[HttpPost]
|
||||
[TgsAuthorize(ConfigurationRights.Write)]
|
||||
public override async Task<IActionResult> Update([FromBody] ConfigurationFile model, CancellationToken cancellationToken)
|
||||
[ProducesResponseType(typeof(ConfigurationFile), 200)]
|
||||
[ProducesResponseType(typeof(ConfigurationFile), 201)]
|
||||
[ProducesResponseType(501)]
|
||||
public async Task<IActionResult> Update([FromBody] ConfigurationFile model, CancellationToken cancellationToken)
|
||||
{
|
||||
if (model == null)
|
||||
throw new ArgumentNullException(nameof(model));
|
||||
@@ -106,8 +119,13 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// <param name="filePath">The path of the file to get</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IActionResult"/> for the operation</returns>
|
||||
/// <response code="410">File not found on disk.</response>
|
||||
/// <response code="501">POSIX system impersonation requested but not implemented.</response>
|
||||
[HttpGet(Routes.File + "/{*filePath}")]
|
||||
[TgsAuthorize(ConfigurationRights.Read)]
|
||||
[ProducesResponseType(typeof(ConfigurationFile), 200)]
|
||||
[ProducesResponseType(410)]
|
||||
[ProducesResponseType(501)]
|
||||
public async Task<IActionResult> File(string filePath, CancellationToken cancellationToken)
|
||||
{
|
||||
if (ForbidDueToModeConflicts(filePath, out var systemIdentity))
|
||||
@@ -141,8 +159,13 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// <param name="directoryPath">The path of the directory to get</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IActionResult"/> for the operation</returns>
|
||||
[HttpGet("List/{*directoryPath}")]
|
||||
/// <response code="410">Directory not found on disk.</response>
|
||||
/// <response code="501">POSIX system impersonation requested but not implemented.</response>
|
||||
[HttpGet(Routes.List + "/{*directoryPath}")]
|
||||
[TgsAuthorize(ConfigurationRights.List)]
|
||||
[ProducesResponseType(typeof(IReadOnlyList<ConfigurationFile>), 200)]
|
||||
[ProducesResponseType(410)]
|
||||
[ProducesResponseType(501)]
|
||||
public async Task<IActionResult> Directory(string directoryPath, CancellationToken cancellationToken)
|
||||
{
|
||||
if (ForbidDueToModeConflicts(directoryPath, out var systemIdentity))
|
||||
@@ -166,13 +189,35 @@ namespace Tgstation.Server.Host.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>
|
||||
/// Get the contents of the root configuration directory.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IActionResult"/> for the operation.</returns>
|
||||
/// <response code="410">Directory not found on disk.</response>
|
||||
/// <response code="501">POSIX system impersonation requested but not implemented.</response>
|
||||
[HttpGet(Routes.List)]
|
||||
[TgsAuthorize(ConfigurationRights.List)]
|
||||
public override Task<IActionResult> List(CancellationToken cancellationToken) => Directory(null, cancellationToken);
|
||||
[ProducesResponseType(typeof(IReadOnlyList<ConfigurationFile>), 200)]
|
||||
[ProducesResponseType(410)]
|
||||
[ProducesResponseType(501)]
|
||||
public Task<IActionResult> List(CancellationToken cancellationToken) => Directory(null, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>
|
||||
/// Create a configuration directory.
|
||||
/// </summary>
|
||||
/// <param name="model">The <see cref="ConfigurationFile"/> representing the directory.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IActionResult"/> for the operation.</returns>
|
||||
/// <response code="200">Directory already exists.</response>
|
||||
/// <response code="201">Directory created successfully.</response>
|
||||
/// <response code="501">POSIX system impersonation requested but not implemented.</response>
|
||||
[HttpPut]
|
||||
[TgsAuthorize(ConfigurationRights.Write)]
|
||||
public override async Task<IActionResult> Create([FromBody] ConfigurationFile model, CancellationToken cancellationToken)
|
||||
[ProducesResponseType(typeof(ConfigurationFile), 200)]
|
||||
[ProducesResponseType(typeof(ConfigurationFile), 201)]
|
||||
[ProducesResponseType(501)]
|
||||
public async Task<IActionResult> Create([FromBody] ConfigurationFile model, CancellationToken cancellationToken)
|
||||
{
|
||||
if (model == null)
|
||||
throw new ArgumentNullException(nameof(model));
|
||||
@@ -185,6 +230,14 @@ namespace Tgstation.Server.Host.Controllers
|
||||
model.IsDirectory = true;
|
||||
return await instanceManager.GetInstance(Instance).Configuration.CreateDirectory(model.Path, systemIdentity, cancellationToken).ConfigureAwait(false) ? (IActionResult)Json(model) : StatusCode((int)HttpStatusCode.Created, model);
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
Logger.LogInformation("IOException while creating directory {0}: {1}", model.Path, e);
|
||||
return Conflict(new ErrorMessage
|
||||
{
|
||||
Message = e.Message
|
||||
});
|
||||
}
|
||||
catch (NotImplementedException)
|
||||
{
|
||||
return StatusCode((int)HttpStatusCode.NotImplemented);
|
||||
@@ -201,8 +254,12 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// <param name="directory">A <see cref="ConfigurationFile"/> representing the path to the directory to delete</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>
|
||||
/// <response code="200">Empty directory deleted successfully.</response>
|
||||
/// <response code="501">POSIX system impersonation requested but not implemented.</response>
|
||||
[HttpDelete]
|
||||
[TgsAuthorize(ConfigurationRights.Delete)]
|
||||
[ProducesResponseType(200)]
|
||||
[ProducesResponseType(501)]
|
||||
public async Task<IActionResult> Delete([FromBody] ConfigurationFile directory, CancellationToken cancellationToken)
|
||||
{
|
||||
if (directory == null)
|
||||
|
||||
@@ -20,10 +20,10 @@ using Tgstation.Server.Host.Security;
|
||||
namespace Tgstation.Server.Host.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="ModelController{TModel}"/> for managing the <see cref="DreamDaemon"/>
|
||||
/// <see cref="ApiController"/> for managing the <see cref="DreamDaemon"/>
|
||||
/// </summary>
|
||||
[Route(Routes.DreamDaemon)]
|
||||
public sealed class DreamDaemonController : ModelController<DreamDaemon>
|
||||
public sealed class DreamDaemonController : ApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="IJobManager"/> for the <see cref="DreamMakerController"/>
|
||||
@@ -43,15 +43,24 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// <param name="jobManager">The value of <see cref="jobManager"/></param>
|
||||
/// <param name="instanceManager">The value of <see cref="instanceManager"/></param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="ApiController"/></param>
|
||||
public DreamDaemonController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, IInstanceManager instanceManager, ILogger<DreamDaemonController> logger) : base(databaseContext, authenticationContextFactory, logger, true)
|
||||
public DreamDaemonController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, IInstanceManager instanceManager, ILogger<DreamDaemonController> logger) : base(databaseContext, authenticationContextFactory, logger, true, true)
|
||||
{
|
||||
this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
|
||||
this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>
|
||||
/// Launches the watchdog.
|
||||
/// </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>
|
||||
/// <response code="202"><see cref="Api.Models.Job"/> to launch the watchdog started successfully.</response>
|
||||
/// <response code="410">Watchdog already running.</response>
|
||||
[HttpPut]
|
||||
[TgsAuthorize(DreamDaemonRights.Start)]
|
||||
public override async Task<IActionResult> Create([FromBody] DreamDaemon model, CancellationToken cancellationToken)
|
||||
[ProducesResponseType(typeof(Api.Models.Job), 202)]
|
||||
[ProducesResponseType(410)]
|
||||
public async Task<IActionResult> Create(CancellationToken cancellationToken)
|
||||
{
|
||||
// alias for launching DD
|
||||
var instance = instanceManager.GetInstance(Instance);
|
||||
@@ -71,9 +80,17 @@ namespace Tgstation.Server.Host.Controllers
|
||||
return Accepted(job.ToApi());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>
|
||||
/// Get the watchdog status.
|
||||
/// </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>
|
||||
/// <response code="202">Read <see cref="DreamDaemon"/> information successfully.</response>
|
||||
[HttpGet]
|
||||
[TgsAuthorize(DreamDaemonRights.ReadMetadata | DreamDaemonRights.ReadRevision)]
|
||||
public override Task<IActionResult> Read(CancellationToken cancellationToken) => ReadImpl(null, cancellationToken);
|
||||
[ProducesResponseType(typeof(DreamDaemon), 200)]
|
||||
[ProducesResponseType(410)]
|
||||
public Task<IActionResult> Read(CancellationToken cancellationToken) => ReadImpl(null, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of <see cref="Read(CancellationToken)"/>
|
||||
@@ -128,12 +145,14 @@ namespace Tgstation.Server.Host.Controllers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops DreamDaemon if it's running
|
||||
/// Stops the Watchdog if it's running.
|
||||
/// </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>
|
||||
[HttpDelete]
|
||||
/// <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>
|
||||
/// <response code="200">Watchdog terminated.</response>
|
||||
[HttpDelete("{id}")]
|
||||
[TgsAuthorize(DreamDaemonRights.Shutdown)]
|
||||
[ProducesResponseType(200)]
|
||||
public async Task<IActionResult> Delete(CancellationToken cancellationToken)
|
||||
{
|
||||
var instance = instanceManager.GetInstance(Instance);
|
||||
@@ -141,10 +160,20 @@ namespace Tgstation.Server.Host.Controllers
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
#pragma warning disable CA1506 // TODO: Decomplexify
|
||||
/// <summary>
|
||||
/// Update watchdog settings to be applied at next server reboot.
|
||||
/// </summary>
|
||||
/// <param name="model">The updated <see cref="DreamDaemon"/> settings.</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>
|
||||
/// <response code="200">Settings applied successfully.</response>
|
||||
/// <response code="410">Instance no longer available.</response>
|
||||
[HttpPost]
|
||||
[TgsAuthorize(DreamDaemonRights.SetAutoStart | DreamDaemonRights.SetPorts | DreamDaemonRights.SetSecurity | DreamDaemonRights.SetWebClient | DreamDaemonRights.SoftRestart | DreamDaemonRights.SoftShutdown | DreamDaemonRights.Start | DreamDaemonRights.SetStartupTimeout)]
|
||||
public override async Task<IActionResult> Update([FromBody] DreamDaemon model, CancellationToken cancellationToken)
|
||||
[ProducesResponseType(typeof(DreamDaemon), 200)]
|
||||
[ProducesResponseType(410)]
|
||||
#pragma warning disable CA1506 // TODO: Decomplexify
|
||||
public async Task<IActionResult> Update([FromBody] DreamDaemon model, CancellationToken cancellationToken)
|
||||
{
|
||||
if (model == null)
|
||||
throw new ArgumentNullException(nameof(model));
|
||||
@@ -213,12 +242,14 @@ namespace Tgstation.Server.Host.Controllers
|
||||
#pragma warning restore CA1506
|
||||
|
||||
/// <summary>
|
||||
/// Handle a HTTP PATCH to the <see cref="DreamDaemonController"/>
|
||||
/// Creates a <see cref="Api.Models.Job"/> to restart the Watchdog. It will start if it wasn't already running.
|
||||
/// </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 request</returns>
|
||||
/// <response code="202">Restart <see cref="Api.Models.Job"/> started successfully.</response>
|
||||
[HttpPatch]
|
||||
[TgsAuthorize(DreamDaemonRights.Restart)]
|
||||
[ProducesResponseType(typeof(Api.Models.Job), 202)]
|
||||
public async Task<IActionResult> Restart(CancellationToken cancellationToken)
|
||||
{
|
||||
var job = new Models.Job
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
@@ -17,11 +18,11 @@ using Tgstation.Server.Host.Security;
|
||||
namespace Tgstation.Server.Host.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// Controller for managing the compiler
|
||||
/// <see cref="ApiController"/> for managing the deployment system.
|
||||
/// </summary>
|
||||
[Route(Routes.DreamMaker)]
|
||||
#pragma warning disable CA1506 // TODO: Decomplexify
|
||||
public sealed class DreamMakerController : ModelController<DreamMaker>
|
||||
public sealed class DreamMakerController : ApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="IJobManager"/> for the <see cref="DreamMakerController"/>
|
||||
@@ -41,24 +42,41 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// <param name="jobManager">The value of <see cref="jobManager"/></param>
|
||||
/// <param name="instanceManager">The value of <see cref="instanceManager"/></param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="ApiController"/></param>
|
||||
public DreamMakerController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, IInstanceManager instanceManager, ILogger<DreamMakerController> logger) : base(databaseContext, authenticationContextFactory, logger, true)
|
||||
public DreamMakerController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, IInstanceManager instanceManager, ILogger<DreamMakerController> logger) : base(databaseContext, authenticationContextFactory, logger, true, true)
|
||||
{
|
||||
this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
|
||||
this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>
|
||||
/// Read current <see cref="DreamMaker"/> status.
|
||||
/// </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 request.</returns>
|
||||
/// <response code="200">Read <see cref="DreamMaker"/> status successfully.</response>
|
||||
[HttpGet]
|
||||
[TgsAuthorize(DreamMakerRights.Read)]
|
||||
public override async Task<IActionResult> Read(CancellationToken cancellationToken)
|
||||
[ProducesResponseType(typeof(DreamMaker), 200)]
|
||||
public async Task<IActionResult> Read(CancellationToken cancellationToken)
|
||||
{
|
||||
var instance = instanceManager.GetInstance(Instance);
|
||||
var dreamMakerSettings = await DatabaseContext.DreamMakerSettings.Where(x => x.InstanceId == Instance.Id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
|
||||
return Json(dreamMakerSettings.ToApi());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>
|
||||
/// Get a <see cref="Api.Models.CompileJob"/> specified by a given <paramref name="id"/>.
|
||||
/// </summary>
|
||||
/// <param name="id">The <see cref="EntityId.Id"/>.</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 request.</returns>
|
||||
/// <response code="200"><see cref="Api.Models.CompileJob"/> retrieved successfully.</response>
|
||||
/// <response code="404">Specified compile job does not exist in this instance.</response>
|
||||
[HttpGet("{id}")]
|
||||
[TgsAuthorize(DreamMakerRights.CompileJobs)]
|
||||
public override async Task<IActionResult> GetId(long id, CancellationToken cancellationToken)
|
||||
[ProducesResponseType(typeof(Api.Models.CompileJob), 200)]
|
||||
[ProducesResponseType(404)]
|
||||
public async Task<IActionResult> GetId(long id, CancellationToken cancellationToken)
|
||||
{
|
||||
var compileJob = await DatabaseContext.CompileJobs
|
||||
.Where(x => x.Id == id && x.Job.Instance.Id == Instance.Id)
|
||||
@@ -71,20 +89,34 @@ namespace Tgstation.Server.Host.Controllers
|
||||
return Json(compileJob.ToApi());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>
|
||||
/// List all <see cref="Api.Models.CompileJob"/> <see cref="EntityId"/>s for the instance.
|
||||
/// </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 request.</returns>
|
||||
/// <response code="200">Retrieved <see cref="EntityId"/>s successfully.</response>
|
||||
[HttpGet(Routes.List)]
|
||||
[TgsAuthorize(DreamMakerRights.CompileJobs)]
|
||||
public override async Task<IActionResult> List(CancellationToken cancellationToken)
|
||||
[ProducesResponseType(typeof(List<EntityId>), 200)]
|
||||
public async Task<IActionResult> List(CancellationToken cancellationToken)
|
||||
{
|
||||
var compileJobs = await DatabaseContext.CompileJobs.Where(x => x.Job.Instance.Id == Instance.Id).OrderByDescending(x => x.Job.StoppedAt).Select(x => new Api.Models.CompileJob
|
||||
var compileJobs = await DatabaseContext.CompileJobs.Where(x => x.Job.Instance.Id == Instance.Id).OrderByDescending(x => x.Job.StoppedAt).Select(x => new EntityId
|
||||
{
|
||||
Id = x.Id
|
||||
}).ToListAsync(cancellationToken).ConfigureAwait(false);
|
||||
return Json(compileJobs);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>
|
||||
/// Begin deploying repository code.
|
||||
/// </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 request.</returns>
|
||||
/// <response code="202">Created deployment <see cref="Api.Models.Job"/> successfully.</response>
|
||||
[HttpPut]
|
||||
[TgsAuthorize(DreamMakerRights.Compile)]
|
||||
public override async Task<IActionResult> Create([FromBody] DreamMaker model, CancellationToken cancellationToken)
|
||||
[ProducesResponseType(typeof(Api.Models.Job), 202)]
|
||||
public async Task<IActionResult> Create(CancellationToken cancellationToken)
|
||||
{
|
||||
var job = new Models.Job
|
||||
{
|
||||
@@ -98,10 +130,24 @@ namespace Tgstation.Server.Host.Controllers
|
||||
return Accepted(job.ToApi());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>
|
||||
/// Update deployment settings.
|
||||
/// </summary>
|
||||
/// <param name="model">The updated <see cref="DreamMaker"/> settings.</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 request.</returns>
|
||||
/// <response code="200">Changes applied successfully. The updated <see cref="DreamMaker"/> settings will be returned based on user permissions.</response>
|
||||
/// <response code="410">Instance no longer available.</response>
|
||||
[HttpPost]
|
||||
[TgsAuthorize(DreamMakerRights.SetDme | DreamMakerRights.SetApiValidationPort | DreamMakerRights.SetApiValidationPort)]
|
||||
public override async Task<IActionResult> Update([FromBody] DreamMaker model, CancellationToken cancellationToken)
|
||||
[ProducesResponseType(typeof(DreamMaker), 200)]
|
||||
[ProducesResponseType(200)]
|
||||
[ProducesResponseType(410)]
|
||||
public async Task<IActionResult> Update([FromBody] DreamMaker model, CancellationToken cancellationToken)
|
||||
{
|
||||
if (model == null)
|
||||
throw new ArgumentNullException(nameof(model));
|
||||
|
||||
if (model.ApiValidationPort == 0)
|
||||
return BadRequest(new ErrorMessage { Message = "API Validation port cannot be 0!" });
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net.Mime;
|
||||
@@ -85,10 +88,14 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// <summary>
|
||||
/// Main page of the <see cref="Application"/>
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="Api.Models.ServerInformation"/> of the <see cref="Application"/> if a properly authenticated API request, the web control panel if on a browser and enabled, <see cref="UnauthorizedResult"/> otherwise</returns>
|
||||
/// <returns>
|
||||
/// The <see cref="Api.Models.ServerInformation"/> of the <see cref="Application"/> if a properly authenticated API request, the web control panel if on a browser and enabled, <see cref="UnauthorizedResult"/> otherwise.
|
||||
/// </returns>
|
||||
/// <response code="200"><see cref="Api.Models.ServerInformation"/> retrieved successfully.</response>
|
||||
[HttpGet]
|
||||
[TgsAuthorize]
|
||||
[AllowAnonymous]
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(Api.Models.ServerInformation), 200)]
|
||||
public IActionResult Home()
|
||||
{
|
||||
if (AuthenticationContext != null)
|
||||
@@ -113,12 +120,33 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// </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>
|
||||
/// <response code="200">User logged in and <see cref="Api.Models.Token"/> generated successfully.</response>
|
||||
/// <response code="401">User authentication failed.</response>
|
||||
/// <response code="403">User authenticated but is disabled by an administrator.</response>
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(Api.Models.Token), 200)]
|
||||
[ProducesResponseType(401)]
|
||||
[ProducesResponseType(403)]
|
||||
#pragma warning disable CA1506 // TODO: Decomplexify
|
||||
public async Task<IActionResult> CreateToken(CancellationToken cancellationToken)
|
||||
{
|
||||
if (ApiHeaders == null)
|
||||
return BadRequest(new Api.Models.ErrorMessage { Message = "Missing API headers!" });
|
||||
{
|
||||
// Get the exact error
|
||||
var errorMessage = "Missing API headers!";
|
||||
try
|
||||
{
|
||||
var _ = new ApiHeaders(Request.GetTypedHeaders());
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
errorMessage = ex.Message;
|
||||
}
|
||||
|
||||
Response.Headers.Add(HeaderNames.WWWAuthenticate, new StringValues("basic realm=\"Create TGS4 bearer token\""));
|
||||
|
||||
return BadRequest(new Api.Models.ErrorMessage { Message = errorMessage });
|
||||
}
|
||||
|
||||
if (ApiHeaders.IsTokenAuthentication)
|
||||
return BadRequest(new Api.Models.ErrorMessage { Message = "Cannot create a token using another token!" });
|
||||
@@ -197,7 +225,10 @@ namespace Tgstation.Server.Host.Controllers
|
||||
|
||||
// Now that the bookeeping is done, tell them to fuck off if necessary
|
||||
if (!user.Enabled.Value)
|
||||
{
|
||||
Logger.LogTrace("Not logging in disabled user {0}.", user.Id);
|
||||
return Forbid();
|
||||
}
|
||||
|
||||
var token = await tokenFactory.CreateToken(user, cancellationToken).ConfigureAwait(false);
|
||||
if (usingSystemIdentity)
|
||||
|
||||
@@ -23,11 +23,11 @@ using Tgstation.Server.Host.Security;
|
||||
namespace Tgstation.Server.Host.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// Controller for managing <see cref="Components.Instance"/>s
|
||||
/// <see cref="ApiController"/> for managing <see cref="Components.Instance"/>s
|
||||
/// </summary>
|
||||
[Route(Routes.InstanceManager)]
|
||||
#pragma warning disable CA1506 // TODO: Decomplexify
|
||||
public sealed class InstanceController : ModelController<Api.Models.Instance>
|
||||
public sealed class InstanceController : ApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// File name to allow attaching instances
|
||||
@@ -72,7 +72,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// <param name="application">The value of <see cref="application"/></param>
|
||||
/// <param name="platformIdentifier">The value of <see cref="platformIdentifier"/></param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="ApiController"/></param>
|
||||
public InstanceController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, IInstanceManager instanceManager, IIOManager ioManager, IApplication application, IPlatformIdentifier platformIdentifier, ILogger<InstanceController> logger) : base(databaseContext, authenticationContextFactory, logger, false)
|
||||
public InstanceController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, IInstanceManager instanceManager, IIOManager ioManager, IApplication application, IPlatformIdentifier platformIdentifier, ILogger<InstanceController> logger) : base(databaseContext, authenticationContextFactory, logger, false, true)
|
||||
{
|
||||
this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
|
||||
this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
|
||||
@@ -108,9 +108,19 @@ namespace Tgstation.Server.Host.Controllers
|
||||
UserId = AuthenticationContext.User.Id
|
||||
};
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>
|
||||
/// Create or attach an <see cref="Api.Models.Instance"/>.
|
||||
/// </summary>
|
||||
/// <param name="model">The <see cref="Api.Models.Instance"/> settings.</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 request.</returns>
|
||||
/// <response code="200">Instance attached successfully.</response>
|
||||
/// <response code="201">Instance created successfully.</response>
|
||||
[HttpPut]
|
||||
[TgsAuthorize(InstanceManagerRights.Create)]
|
||||
public override async Task<IActionResult> Create([FromBody] Api.Models.Instance model, CancellationToken cancellationToken)
|
||||
[ProducesResponseType(typeof(Api.Models.Instance), 200)]
|
||||
[ProducesResponseType(typeof(Api.Models.Instance), 201)]
|
||||
public async Task<IActionResult> Create([FromBody] Api.Models.Instance model, CancellationToken cancellationToken)
|
||||
{
|
||||
if (model == null)
|
||||
throw new ArgumentNullException(nameof(model));
|
||||
@@ -130,7 +140,14 @@ namespace Tgstation.Server.Host.Controllers
|
||||
}, out var normalizedLocalPath);
|
||||
|
||||
if (rawPath.StartsWith(normalizedLocalPath, StringComparison.Ordinal))
|
||||
return Conflict("Instances cannot be created in the installation directory!");
|
||||
{
|
||||
bool sameLength = rawPath.Length == normalizedLocalPath.Length;
|
||||
char dirSeparatorChar = rawPath.ToCharArray()[normalizedLocalPath.Length];
|
||||
if(sameLength
|
||||
|| dirSeparatorChar == Path.DirectorySeparatorChar
|
||||
|| dirSeparatorChar == Path.AltDirectorySeparatorChar)
|
||||
return Conflict(new ErrorMessage { Message = "Instances cannot be created in the installation directory!" });
|
||||
}
|
||||
|
||||
var dirExistsTask = ioManager.DirectoryExists(model.Path, cancellationToken);
|
||||
bool attached = false;
|
||||
@@ -214,9 +231,19 @@ namespace Tgstation.Server.Host.Controllers
|
||||
return attached ? (IActionResult)Json(api) : StatusCode((int)HttpStatusCode.Created, api);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>
|
||||
/// Detach an <see cref="Api.Models.Instance"/> with the given <paramref name="id"/>.
|
||||
/// </summary>
|
||||
/// <param name="id">The <see cref="Api.Models.Instance.Id"/> to detach.</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 request.</returns>
|
||||
/// <response code="200">Instance detatched successfully.</response>
|
||||
/// <response code="410">Instance not available.</response>
|
||||
[HttpDelete("{id}")]
|
||||
[TgsAuthorize(InstanceManagerRights.Delete)]
|
||||
public override async Task<IActionResult> Delete(long id, CancellationToken cancellationToken)
|
||||
[ProducesResponseType(200)]
|
||||
[ProducesResponseType(410)]
|
||||
public async Task<IActionResult> Delete(long id, CancellationToken cancellationToken)
|
||||
{
|
||||
var originalModel = await DatabaseContext.Instances.Where(x => x.Id == id)
|
||||
.Include(x => x.WatchdogReattachInformation)
|
||||
@@ -248,11 +275,24 @@ namespace Tgstation.Server.Host.Controllers
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>
|
||||
/// Modify an <see cref="Api.Models.Instance"/>'s settings.
|
||||
/// </summary>
|
||||
/// <param name="model">The updated <see cref="Api.Models.Instance"/> settings.</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 request.</returns>
|
||||
/// <response code="200">Instance updated successfully.</response>
|
||||
/// <response code="202">Instance updated successfully and relocation job created.</response>
|
||||
[HttpPost]
|
||||
[TgsAuthorize(InstanceManagerRights.Relocate | InstanceManagerRights.Rename | InstanceManagerRights.SetAutoUpdate | InstanceManagerRights.SetConfiguration | InstanceManagerRights.SetOnline)]
|
||||
#pragma warning disable CA1502 // TODO: Decomplexify
|
||||
public override async Task<IActionResult> Update([FromBody] Api.Models.Instance model, CancellationToken cancellationToken)
|
||||
[ProducesResponseType(typeof(Api.Models.Instance), 200)]
|
||||
[ProducesResponseType(410)]
|
||||
#pragma warning disable CA1502 // TODO: Decomplexify
|
||||
public async Task<IActionResult> Update([FromBody] Api.Models.Instance model, CancellationToken cancellationToken)
|
||||
{
|
||||
if (model == null)
|
||||
throw new ArgumentNullException(nameof(model));
|
||||
|
||||
var instanceQuery = DatabaseContext.Instances.Where(x => x.Id == model.Id);
|
||||
|
||||
var moveJob = await instanceQuery
|
||||
@@ -372,7 +412,9 @@ namespace Tgstation.Server.Host.Controllers
|
||||
{
|
||||
Id = originalModel.Id
|
||||
};
|
||||
if (originalModelPath != null)
|
||||
|
||||
var moving = originalModelPath != null;
|
||||
if (moving)
|
||||
{
|
||||
var job = new Models.Job
|
||||
{
|
||||
@@ -390,13 +432,20 @@ namespace Tgstation.Server.Host.Controllers
|
||||
if (originalModel.Online.Value && model.AutoUpdateInterval.HasValue && oldAutoUpdateInterval != model.AutoUpdateInterval)
|
||||
await instanceManager.GetInstance(originalModel).SetAutoUpdateInterval(model.AutoUpdateInterval.Value).ConfigureAwait(false);
|
||||
|
||||
return Json(api);
|
||||
return moving ? (IActionResult)Accepted(api) : Json(api);
|
||||
}
|
||||
#pragma warning restore CA1502
|
||||
#pragma warning restore CA1502
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>
|
||||
/// List <see cref="Api.Models.Instance"/>s.
|
||||
/// </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 request.</returns>
|
||||
/// <response code="200">Retrieved <see cref="Api.Models.Instance"/>s successfully.</response>
|
||||
[HttpGet(Routes.List)]
|
||||
[TgsAuthorize(InstanceManagerRights.List | InstanceManagerRights.Read)]
|
||||
public override async Task<IActionResult> List(CancellationToken cancellationToken)
|
||||
[ProducesResponseType(typeof(IEnumerable<Api.Models.Instance>), 200)]
|
||||
public async Task<IActionResult> List(CancellationToken cancellationToken)
|
||||
{
|
||||
IQueryable<Models.Instance> query = DatabaseContext.Instances;
|
||||
if (!AuthenticationContext.User.InstanceManagerRights.Value.HasFlag(InstanceManagerRights.List))
|
||||
@@ -418,9 +467,19 @@ namespace Tgstation.Server.Host.Controllers
|
||||
return Json(apis);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>
|
||||
/// Get a specific <see cref="Api.Models.Instance"/>.
|
||||
/// </summary>
|
||||
/// <param name="id">The <see cref="Api.Models.Instance.Id"/> to retrieve.</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 request.</returns>
|
||||
/// <response code="200">Retrieved <see cref="Api.Models.Instance"/> successfully.</response>
|
||||
/// <response code="410">Instance not available.</response>
|
||||
[HttpGet("{id}")]
|
||||
[TgsAuthorize(InstanceManagerRights.List | InstanceManagerRights.Read)]
|
||||
public override async Task<IActionResult> GetId(long id, CancellationToken cancellationToken)
|
||||
[ProducesResponseType(typeof(Api.Models.Instance), 200)]
|
||||
[ProducesResponseType(410)]
|
||||
public async Task<IActionResult> GetId(long id, CancellationToken cancellationToken)
|
||||
{
|
||||
var query = DatabaseContext.Instances.Where(x => x.Id == id);
|
||||
var cantList = !AuthenticationContext.User.InstanceManagerRights.Value.HasFlag(InstanceManagerRights.List);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
@@ -16,10 +17,10 @@ using Z.EntityFramework.Plus;
|
||||
namespace Tgstation.Server.Host.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// For managing <see cref="User"/>s
|
||||
/// <see cref="ApiController"/> for managing <see cref="InstanceUser"/>s.
|
||||
/// </summary>
|
||||
[Route(Routes.InstanceUser)]
|
||||
public sealed class InstanceUserController : ModelController<Api.Models.InstanceUser>
|
||||
public sealed class InstanceUserController : ApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// Construct a <see cref="UserController"/>
|
||||
@@ -27,7 +28,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// <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="logger">The <see cref="ILogger"/> for the <see cref="ApiController"/></param>
|
||||
public InstanceUserController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger<InstanceUserController> logger) : base(databaseContext, authenticationContextFactory, logger, true) // false instance requirement, we handle this ourself
|
||||
public InstanceUserController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger<InstanceUserController> logger) : base(databaseContext, authenticationContextFactory, logger, true, true)
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
@@ -46,9 +47,17 @@ namespace Tgstation.Server.Host.Controllers
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>
|
||||
/// Create am <see cref="Api.Models.InstanceUser"/>.
|
||||
/// </summary>
|
||||
/// <param name="model">The <see cref="Api.Models.InstanceUser"/> to create.</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 request.</returns>
|
||||
/// <response code="201"><see cref="Api.Models.InstanceUser"/> created successfully.</response>
|
||||
[HttpPut]
|
||||
[TgsAuthorize(InstanceUserRights.CreateUsers)]
|
||||
public override async Task<IActionResult> Create([FromBody] Api.Models.InstanceUser model, CancellationToken cancellationToken)
|
||||
[ProducesResponseType(typeof(Api.Models.InstanceUser), 201)]
|
||||
public async Task<IActionResult> Create([FromBody] Api.Models.InstanceUser model, CancellationToken cancellationToken)
|
||||
{
|
||||
var test = StandardModelChecks(model);
|
||||
if (test != null)
|
||||
@@ -73,10 +82,20 @@ namespace Tgstation.Server.Host.Controllers
|
||||
return StatusCode((int)HttpStatusCode.Created, dbUser.ToApi());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>
|
||||
/// Update the permissions for an <see cref="Api.Models.InstanceUser"/>.
|
||||
/// </summary>
|
||||
/// <param name="model">The updated <see cref="Api.Models.InstanceUser"/>.</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 request.</returns>
|
||||
/// <response code="200"><see cref="Api.Models.InstanceUser"/> updated successfully.</response>
|
||||
/// <response code="410">Instance user unavailable.</response>
|
||||
[HttpPost]
|
||||
[TgsAuthorize(InstanceUserRights.WriteUsers)]
|
||||
[ProducesResponseType(typeof(Api.Models.InstanceUser), 200)]
|
||||
[ProducesResponseType(410)]
|
||||
#pragma warning disable CA1506 // TODO: Decomplexify
|
||||
public override async Task<IActionResult> Update([FromBody] Api.Models.InstanceUser model, CancellationToken cancellationToken)
|
||||
public async Task<IActionResult> Update([FromBody] Api.Models.InstanceUser model, CancellationToken cancellationToken)
|
||||
{
|
||||
var test = StandardModelChecks(model);
|
||||
if (test != null)
|
||||
@@ -100,23 +119,45 @@ namespace Tgstation.Server.Host.Controllers
|
||||
UserId = originalUser.UserId
|
||||
});
|
||||
}
|
||||
#pragma warning restore CA1506
|
||||
|
||||
/// <inheritdoc />
|
||||
#pragma warning restore CA1506
|
||||
/// <summary>
|
||||
/// Read the active <see cref="Api.Models.InstanceUser"/>.
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="IActionResult"/> of the request.</returns>
|
||||
/// <response code="200"><see cref="Api.Models.InstanceUser"/> retrieved successfully.</response>
|
||||
[HttpGet]
|
||||
[TgsAuthorize]
|
||||
public override Task<IActionResult> Read(CancellationToken cancellationToken) => Task.FromResult(AuthenticationContext.InstanceUser != null ? (IActionResult)Json(AuthenticationContext.InstanceUser.ToApi()) : NotFound());
|
||||
[ProducesResponseType(typeof(Api.Models.InstanceUser), 200)]
|
||||
public IActionResult Read() => Json(AuthenticationContext.InstanceUser.ToApi());
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>
|
||||
/// Lists <see cref="Api.Models.InstanceUser"/>s for the instance.
|
||||
/// </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 request.</returns>
|
||||
/// <response code="200">Retrieved <see cref="Api.Models.InstanceUser"/>s successfully.</response>
|
||||
[HttpGet(Routes.List)]
|
||||
[TgsAuthorize(InstanceUserRights.ReadUsers)]
|
||||
public override async Task<IActionResult> List(CancellationToken cancellationToken)
|
||||
[ProducesResponseType(typeof(IEnumerable<Api.Models.InstanceUser>), 200)]
|
||||
public async Task<IActionResult> List(CancellationToken cancellationToken)
|
||||
{
|
||||
var users = await DatabaseContext.Instances.Where(x => x.Id == Instance.Id).SelectMany(x => x.InstanceUsers).ToListAsync(cancellationToken).ConfigureAwait(false);
|
||||
return Json(users.Select(x => x.ToApi()));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>
|
||||
/// Gets a specific <see cref="Api.Models.InstanceUser"/>.
|
||||
/// </summary>
|
||||
/// <param name="id">The <see cref="Api.Models.InstanceUser.UserId"/>.</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 request.</returns>
|
||||
/// <response code="200">Retrieve <see cref="Api.Models.InstanceUser"/> successfully.</response>
|
||||
/// <response code="410">Instance user unavailable.</response>
|
||||
[HttpGet("{id}")]
|
||||
[TgsAuthorize(InstanceUserRights.ReadUsers)]
|
||||
public override async Task<IActionResult> GetId(long id, CancellationToken cancellationToken)
|
||||
[ProducesResponseType(typeof(Api.Models.InstanceUser), 200)]
|
||||
[ProducesResponseType(410)]
|
||||
public async Task<IActionResult> GetId(long id, CancellationToken cancellationToken)
|
||||
{
|
||||
// this functions as userId
|
||||
var user = await DatabaseContext.Instances.Where(x => x.Id == Instance.Id).SelectMany(x => x.InstanceUsers).Where(x => x.UserId == id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
|
||||
@@ -125,9 +166,17 @@ namespace Tgstation.Server.Host.Controllers
|
||||
return Json(user.ToApi());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>
|
||||
/// Delete an <see cref="Api.Models.InstanceUser"/>.
|
||||
/// </summary>
|
||||
/// <param name="id">The <see cref="Api.Models.InstanceUser.UserId"/> to delete.</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 request.</returns>
|
||||
/// <response code="200"><see cref="Api.Models.InstanceUser"/> deleted or no longer exists.</response>
|
||||
[HttpDelete("{id}")]
|
||||
[TgsAuthorize(InstanceUserRights.WriteUsers)]
|
||||
public override async Task<IActionResult> Delete(long id, CancellationToken cancellationToken)
|
||||
[ProducesResponseType(200)]
|
||||
public async Task<IActionResult> Delete(long id, CancellationToken cancellationToken)
|
||||
{
|
||||
await DatabaseContext.Instances.Where(x => x.Id == Instance.Id).SelectMany(x => x.InstanceUsers).Where(x => x.UserId == id).DeleteAsync(cancellationToken).ConfigureAwait(false);
|
||||
return Ok();
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
@@ -14,10 +15,10 @@ using Tgstation.Server.Host.Security;
|
||||
namespace Tgstation.Server.Host.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="ModelController{TModel}"/> for <see cref="Api.Models.Job"/>s
|
||||
/// <see cref="ApiController"/> for <see cref="Api.Models.Job"/>s
|
||||
/// </summary>
|
||||
[Route(Routes.Jobs)]
|
||||
public sealed class JobController : ModelController<Api.Models.Job>
|
||||
public sealed class JobController : ApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="IJobManager"/> for the <see cref="JobController"/>
|
||||
@@ -31,37 +32,63 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="ApiController"/></param>
|
||||
/// <param name="jobManager">The value of <see cref="jobManager"/></param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="ApiController"/></param>
|
||||
public JobController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, ILogger<JobController> logger) : base(databaseContext, authenticationContextFactory, logger, true)
|
||||
public JobController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, ILogger<JobController> logger) : base(databaseContext, authenticationContextFactory, logger, true, true)
|
||||
{
|
||||
this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>
|
||||
/// Get active <see cref="Api.Models.Job"/>s for the instance.
|
||||
/// </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 request.</returns>
|
||||
/// <response code="200">Retrieved active <see cref="Api.Models.Job"/>s successfully.</response>
|
||||
[HttpGet]
|
||||
[TgsAuthorize]
|
||||
public override async Task<IActionResult> Read(CancellationToken cancellationToken)
|
||||
[ProducesResponseType(typeof(IEnumerable<Api.Models.Job>), 200)]
|
||||
public async Task<IActionResult> Read(CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await DatabaseContext.Jobs.Where(x => x.Instance.Id == Instance.Id && !x.StoppedAt.HasValue).OrderByDescending(x => x.StartedAt).ToListAsync(cancellationToken).ConfigureAwait(false);
|
||||
return Json(result.Select(x => x.ToApi()));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>
|
||||
/// List all <see cref="Api.Models.Job"/> <see cref="Api.Models.EntityId"/>s for the instance in reverse creation order.
|
||||
/// </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 request.</returns>
|
||||
/// <response code="200">Retrieved <see cref="Api.Models.Job"/> <see cref="Api.Models.EntityId"/>s successfully.</response>
|
||||
[HttpGet(Routes.List)]
|
||||
[TgsAuthorize]
|
||||
public override async Task<IActionResult> List(CancellationToken cancellationToken)
|
||||
[ProducesResponseType(typeof(List<Api.Models.EntityId>), 200)]
|
||||
public async Task<IActionResult> List(CancellationToken cancellationToken)
|
||||
{
|
||||
// you KNOW this will need pagination eventually right?
|
||||
var jobs = await DatabaseContext.Jobs.Where(x => x.Instance.Id == Instance.Id).OrderByDescending(x => x.StartedAt).Select(x => new Api.Models.Job
|
||||
var jobs = await DatabaseContext.Jobs.Where(x => x.Instance.Id == Instance.Id).OrderByDescending(x => x.StartedAt).Select(x => new Api.Models.EntityId
|
||||
{
|
||||
Id = x.Id
|
||||
}).ToListAsync(cancellationToken).ConfigureAwait(false);
|
||||
return Json(jobs);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>
|
||||
/// Cancel a running <see cref="Api.Models.Job"/>.
|
||||
/// </summary>
|
||||
/// <param name="id">The <see cref="Api.Models.EntityId.Id"/> of the <see cref="Api.Models.Job"/> to cancel.</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 request.</returns>
|
||||
/// <response code="202"><see cref="Api.Models.Job"/> cancellation requested successfully.</response>
|
||||
/// <response code="404"><see cref="Api.Models.Job"/> does not exist in this instance.</response>
|
||||
/// <response code="410"><see cref="Api.Models.Job"/> already cancelled or completed.</response>
|
||||
[HttpDelete("{id}")]
|
||||
[TgsAuthorize]
|
||||
public override async Task<IActionResult> Delete(long id, CancellationToken cancellationToken)
|
||||
[ProducesResponseType(202)]
|
||||
[ProducesResponseType(404)]
|
||||
[ProducesResponseType(410)]
|
||||
public async Task<IActionResult> Delete(long id, CancellationToken cancellationToken)
|
||||
{
|
||||
// don't care if an instance post or not at this point
|
||||
var job = await DatabaseContext.Jobs.Where(x => x.Id == id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
|
||||
var job = await DatabaseContext.Jobs.Where(x => x.Id == id && x.Instance.Id == Instance.Id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (job == default(Job))
|
||||
return NotFound();
|
||||
|
||||
@@ -75,11 +102,21 @@ namespace Tgstation.Server.Host.Controllers
|
||||
return cancelled ? (IActionResult)Accepted() : StatusCode((int)HttpStatusCode.Gone);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>
|
||||
/// Get a specific <see cref="Api.Models.Job"/>.
|
||||
/// </summary>
|
||||
/// <param name="id">The <see cref="Api.Models.EntityId.Id"/> of the <see cref="Api.Models.Job"/> to retrieve.</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 request.</returns>
|
||||
/// <response code="200">Retrieved <see cref="Api.Models.Job"/> successfully.</response>
|
||||
/// <response code="404"><see cref="Api.Models.Job"/> does not exist in this instance.</response>
|
||||
[HttpGet("{id}")]
|
||||
[TgsAuthorize]
|
||||
public override async Task<IActionResult> GetId(long id, CancellationToken cancellationToken)
|
||||
[ProducesResponseType(typeof(Api.Models.Job), 200)]
|
||||
[ProducesResponseType(404)]
|
||||
public async Task<IActionResult> GetId(long id, CancellationToken cancellationToken)
|
||||
{
|
||||
var job = await DatabaseContext.Jobs.Where(x => x.Id == id).Include(x => x.StartedBy).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
|
||||
var job = await DatabaseContext.Jobs.Where(x => x.Id == id && x.Instance.Id == Instance.Id).Include(x => x.StartedBy).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (job == default(Job))
|
||||
return NotFound();
|
||||
var api = job.ToApi();
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
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>
|
||||
/// 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>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="ApiController"/></param>
|
||||
/// <param name="requireInstance">If the <see cref="ModelController{TModel}"/> requires an <see cref="IAuthenticationContext.InstanceUser"/></param>
|
||||
public ModelController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger logger, bool requireInstance) : base(databaseContext, authenticationContextFactory, logger, requireInstance, true) { }
|
||||
|
||||
/// <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 get a specific a <typeparamref name="TModel"/>
|
||||
/// </summary>
|
||||
/// <param name="id">The ID of the model to get</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>
|
||||
[HttpGet("{id}")]
|
||||
public virtual Task<IActionResult> GetId(long id, 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 model with a particular <paramref name="id"/>
|
||||
/// </summary>
|
||||
/// <param name="id">The ID of the model to delete</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("{id}")]
|
||||
public virtual Task<IActionResult> Delete(long id, 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());
|
||||
}
|
||||
}
|
||||
@@ -23,11 +23,11 @@ using Tgstation.Server.Host.Security;
|
||||
namespace Tgstation.Server.Host.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// Controller for managing the <see cref="Repository"/>s
|
||||
/// <see cref="ApiController"/> for managing the <see cref="Repository"/>s
|
||||
/// </summary>
|
||||
[Route(Routes.Repository)]
|
||||
#pragma warning disable CA1506 // TODO: Decomplexify
|
||||
public sealed class RepositoryController : ModelController<Repository>
|
||||
public sealed class RepositoryController : ApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="IInstanceManager"/> for the <see cref="RepositoryController"/>
|
||||
@@ -59,7 +59,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// <param name="jobManager">The value of <see cref="jobManager"/></param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="ApiController"/></param>
|
||||
/// <param name="generalConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing value of <see cref="generalConfiguration"/></param>
|
||||
public RepositoryController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, IGitHubClientFactory gitHubClientFactory, IJobManager jobManager, ILogger<RepositoryController> logger, IOptions<GeneralConfiguration> generalConfigurationOptions) : base(databaseContext, authenticationContextFactory, logger, true)
|
||||
public RepositoryController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, IGitHubClientFactory gitHubClientFactory, IJobManager jobManager, ILogger<RepositoryController> logger, IOptions<GeneralConfiguration> generalConfigurationOptions) : base(databaseContext, authenticationContextFactory, logger, true, true)
|
||||
{
|
||||
this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
|
||||
this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory));
|
||||
@@ -126,9 +126,19 @@ namespace Tgstation.Server.Host.Controllers
|
||||
return needsDbUpdate;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>
|
||||
/// Begin cloning the repository if it doesn't exist.
|
||||
/// </summary>
|
||||
/// <param name="model">Initial <see cref="Repository"/> settings.</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 request.</returns>
|
||||
/// <response code="201">The <see cref="Repository"/> was created successfully and the <see cref="Api.Models.Job"/> to clone it has begun.</response>
|
||||
/// <response code="410">Instance no longer available.</response>
|
||||
[HttpPut]
|
||||
[TgsAuthorize(RepositoryRights.SetOrigin)]
|
||||
public override async Task<IActionResult> Create([FromBody] Repository model, CancellationToken cancellationToken)
|
||||
[ProducesResponseType(typeof(Repository), 201)]
|
||||
[ProducesResponseType(410)]
|
||||
public async Task<IActionResult> Create([FromBody] Repository model, CancellationToken cancellationToken)
|
||||
{
|
||||
if (model == null)
|
||||
throw new ArgumentNullException(nameof(model));
|
||||
@@ -214,11 +224,16 @@ namespace Tgstation.Server.Host.Controllers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete the <see cref="Repository"/>
|
||||
/// Delete the <see cref="Repository"/>.
|
||||
/// </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>
|
||||
/// <response code="202">Job to delete the repository created successfully.</response>
|
||||
/// <response code="410">Instance no longer available.</response>
|
||||
[HttpDelete("{id}")]
|
||||
[TgsAuthorize(RepositoryRights.Delete)]
|
||||
[ProducesResponseType(typeof(Repository), 202)]
|
||||
[ProducesResponseType(410)]
|
||||
public async Task<IActionResult> Delete(CancellationToken cancellationToken)
|
||||
{
|
||||
var currentModel = await DatabaseContext.RepositorySettings.Where(x => x.InstanceId == Instance.Id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
|
||||
@@ -245,9 +260,20 @@ namespace Tgstation.Server.Host.Controllers
|
||||
return Accepted(api);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>
|
||||
/// Get <see cref="Repository"/> status.
|
||||
/// </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>
|
||||
/// <response code="200">Retrieved the <see cref="Repository"/> settings successfully.</response>
|
||||
/// <response code="201">Retrieved the <see cref="Repository"/> settings successfully, though they did not previously exist.</response>
|
||||
/// <response code="410">Instance no longer available.</response>
|
||||
[HttpGet]
|
||||
[TgsAuthorize(RepositoryRights.Read)]
|
||||
public override async Task<IActionResult> Read(CancellationToken cancellationToken)
|
||||
[ProducesResponseType(typeof(Repository), 200)]
|
||||
[ProducesResponseType(typeof(Repository), 201)]
|
||||
[ProducesResponseType(410)]
|
||||
public async Task<IActionResult> Read(CancellationToken cancellationToken)
|
||||
{
|
||||
var currentModel = await DatabaseContext.RepositorySettings.Where(x => x.InstanceId == Instance.Id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
@@ -273,7 +299,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
{
|
||||
if (repo != null && await PopulateApi(api, repo, DatabaseContext, Instance, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
// user may have fucked with the repo without telling us, do what we can
|
||||
// user may have fucked with the repo manually, do what we can
|
||||
await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
|
||||
return StatusCode((int)HttpStatusCode.Created, api);
|
||||
}
|
||||
@@ -282,11 +308,23 @@ namespace Tgstation.Server.Host.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>
|
||||
/// Perform updats to the <see cref="Repository"/>.
|
||||
/// </summary>
|
||||
/// <param name="model">The updated <see cref="Repository"/>.</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>
|
||||
/// <response code="200">Updated the <see cref="Repository"/> settings successfully.</response>
|
||||
/// <response code="201">Updated the <see cref="Repository"/> settings successfully and a <see cref="Api.Models.Job"/> was created to make the requested git changes.</response>
|
||||
/// <response code="410">Instance no longer available.</response>
|
||||
[HttpPost]
|
||||
[TgsAuthorize(RepositoryRights.ChangeAutoUpdateSettings | RepositoryRights.ChangeCommitter | RepositoryRights.ChangeCredentials | RepositoryRights.ChangeTestMergeCommits | RepositoryRights.MergePullRequest | RepositoryRights.SetReference | RepositoryRights.SetSha | RepositoryRights.UpdateBranch)]
|
||||
[ProducesResponseType(typeof(Repository), 200)]
|
||||
[ProducesResponseType(typeof(Repository), 202)]
|
||||
[ProducesResponseType(410)]
|
||||
#pragma warning disable CA1502 // TODO: Decomplexify
|
||||
#pragma warning disable CA1505
|
||||
public override async Task<IActionResult> Update([FromBody]Repository model, CancellationToken cancellationToken)
|
||||
public async Task<IActionResult> Update([FromBody]Repository model, CancellationToken cancellationToken)
|
||||
{
|
||||
if (model == null)
|
||||
throw new ArgumentNullException(nameof(model));
|
||||
|
||||
@@ -11,6 +11,11 @@ namespace Tgstation.Server.Host.Controllers
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
|
||||
sealed class TgsAuthorizeAttribute : AuthorizeAttribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the <see cref="Api.Rights.RightsType"/> associated with the <see cref="TgsAuthorizeAttribute"/> if any.
|
||||
/// </summary>
|
||||
public RightsType? RightsType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Construct a <see cref="TgsAuthorizeAttribute"/>
|
||||
/// </summary>
|
||||
@@ -23,6 +28,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
public TgsAuthorizeAttribute(AdministrationRights requiredRights)
|
||||
{
|
||||
Roles = RightsHelper.RoleNames(requiredRights);
|
||||
RightsType = Api.Rights.RightsType.Administration;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -32,6 +38,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
public TgsAuthorizeAttribute(InstanceManagerRights requiredRights)
|
||||
{
|
||||
Roles = RightsHelper.RoleNames(requiredRights);
|
||||
RightsType = Api.Rights.RightsType.InstanceManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -41,6 +48,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
public TgsAuthorizeAttribute(RepositoryRights requiredRights)
|
||||
{
|
||||
Roles = RightsHelper.RoleNames(requiredRights);
|
||||
RightsType = Api.Rights.RightsType.Repository;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -50,6 +58,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
public TgsAuthorizeAttribute(ByondRights requiredRights)
|
||||
{
|
||||
Roles = RightsHelper.RoleNames(requiredRights);
|
||||
RightsType = Api.Rights.RightsType.Byond;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -59,6 +68,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
public TgsAuthorizeAttribute(DreamMakerRights requiredRights)
|
||||
{
|
||||
Roles = RightsHelper.RoleNames(requiredRights);
|
||||
RightsType = Api.Rights.RightsType.DreamMaker;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -68,6 +78,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
public TgsAuthorizeAttribute(DreamDaemonRights requiredRights)
|
||||
{
|
||||
Roles = RightsHelper.RoleNames(requiredRights);
|
||||
RightsType = Api.Rights.RightsType.DreamDaemon;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -77,6 +88,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
public TgsAuthorizeAttribute(ChatBotRights requiredRights)
|
||||
{
|
||||
Roles = RightsHelper.RoleNames(requiredRights);
|
||||
RightsType = Api.Rights.RightsType.ChatBots;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -86,6 +98,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
public TgsAuthorizeAttribute(ConfigurationRights requiredRights)
|
||||
{
|
||||
Roles = RightsHelper.RoleNames(requiredRights);
|
||||
RightsType = Api.Rights.RightsType.Configuration;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -95,6 +108,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
public TgsAuthorizeAttribute(InstanceUserRights requiredRights)
|
||||
{
|
||||
Roles = RightsHelper.RoleNames(requiredRights);
|
||||
RightsType = Api.Rights.RightsType.InstanceUser;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using Microsoft.OpenApi.Any;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using Tgstation.Server.Api;
|
||||
using Tgstation.Server.Api.Models;
|
||||
using Tgstation.Server.Api.Rights;
|
||||
|
||||
namespace Tgstation.Server.Host.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="IOperationFilter"/> and <see cref="IDocumentFilter"/> for the server.
|
||||
/// </summary>
|
||||
sealed class TgsOpenApiFilters : IOperationFilter, IDocumentFilter
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="OpenApiSecurityScheme"/> name for password authentication.
|
||||
/// </summary>
|
||||
public const string PasswordSecuritySchemeId = "Password_Login_Scheme";
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="OpenApiSecurityScheme"/> name for token authentication.
|
||||
/// </summary>
|
||||
public const string TokenSecuritySchemeId = "Token_Authorization_Scheme";
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Apply(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
if (operation == null)
|
||||
throw new ArgumentNullException(nameof(operation));
|
||||
if (context == null)
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
|
||||
var authAttributes = context
|
||||
.MethodInfo
|
||||
.DeclaringType
|
||||
.GetCustomAttributes(true)
|
||||
.Union(
|
||||
context
|
||||
.MethodInfo
|
||||
.GetCustomAttributes(true))
|
||||
.OfType<TgsAuthorizeAttribute>();
|
||||
|
||||
if (authAttributes.Any())
|
||||
{
|
||||
var tokenScheme = new OpenApiSecurityScheme
|
||||
{
|
||||
Reference = new OpenApiReference
|
||||
{
|
||||
Type = ReferenceType.SecurityScheme,
|
||||
Id = TokenSecuritySchemeId
|
||||
}
|
||||
};
|
||||
|
||||
operation.Security = new List<OpenApiSecurityRequirement>
|
||||
{
|
||||
new OpenApiSecurityRequirement
|
||||
{
|
||||
{
|
||||
tokenScheme,
|
||||
new List<string>()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (authAttributes.Any(attr => attr.RightsType.HasValue && RightsHelper.IsInstanceRight(attr.RightsType.Value)))
|
||||
operation.Parameters.Add(new OpenApiParameter
|
||||
{
|
||||
Reference = new OpenApiReference
|
||||
{
|
||||
Type = ReferenceType.Header,
|
||||
Id = ApiHeaders.InstanceIdHeader
|
||||
}
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
// HomeController.CreateToken
|
||||
var passwordScheme = new OpenApiSecurityScheme
|
||||
{
|
||||
Reference = new OpenApiReference
|
||||
{
|
||||
Type = ReferenceType.SecurityScheme,
|
||||
Id = PasswordSecuritySchemeId
|
||||
}
|
||||
};
|
||||
|
||||
operation.Security = new List<OpenApiSecurityRequirement>
|
||||
{
|
||||
new OpenApiSecurityRequirement
|
||||
{
|
||||
{
|
||||
passwordScheme,
|
||||
new List<string>()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
|
||||
{
|
||||
swaggerDoc.Components.Headers.Add(ApiHeaders.InstanceIdHeader, new OpenApiHeader
|
||||
{
|
||||
Description = "The instance ID being accessed",
|
||||
Required = true,
|
||||
Style = ParameterStyle.Simple
|
||||
});
|
||||
|
||||
swaggerDoc.Components.Headers.Add(ApiHeaders.ApiVersionHeader, new OpenApiHeader
|
||||
{
|
||||
Description = "The API version being used in the form \"Tgstation.Server.Api/[API version]\"",
|
||||
Required = true,
|
||||
Style = ParameterStyle.Simple,
|
||||
Example = new OpenApiString($"Tgstation.Server.Api/{ApiHeaders.Version}")
|
||||
});
|
||||
|
||||
swaggerDoc.Components.Headers.Add(HeaderNames.UserAgent, new OpenApiHeader
|
||||
{
|
||||
Description = "The user agent of the calling client.",
|
||||
Required = true,
|
||||
Style = ParameterStyle.Simple,
|
||||
Example = new OpenApiString("Your-user-agent/1.0.0.0")
|
||||
});
|
||||
|
||||
foreach (var operation in swaggerDoc
|
||||
.Paths
|
||||
.SelectMany(path => path.Value.Operations)
|
||||
.Select(kvp => kvp.Value))
|
||||
{
|
||||
operation.Parameters.Add(new OpenApiParameter
|
||||
{
|
||||
Reference = new OpenApiReference
|
||||
{
|
||||
Type = ReferenceType.Header,
|
||||
Id = ApiHeaders.ApiVersionHeader
|
||||
}
|
||||
});
|
||||
|
||||
operation.Parameters.Add(new OpenApiParameter
|
||||
{
|
||||
Reference = new OpenApiReference
|
||||
{
|
||||
Type = ReferenceType.Header,
|
||||
Id = HeaderNames.UserAgent
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var errorMessageContent = new Dictionary<string, OpenApiMediaType>
|
||||
{
|
||||
{
|
||||
ApiHeaders.ApplicationJson,
|
||||
new OpenApiMediaType
|
||||
{
|
||||
Schema = new OpenApiSchema
|
||||
{
|
||||
Reference = new OpenApiReference
|
||||
{
|
||||
Id = nameof(ErrorMessage),
|
||||
Type = ReferenceType.Schema
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void AddDefaultResponse(HttpStatusCode code, OpenApiResponse concrete)
|
||||
{
|
||||
string responseKey = $"{(int)code}";
|
||||
|
||||
swaggerDoc.Components.Responses.Add(responseKey, concrete);
|
||||
|
||||
var referenceResponse = new OpenApiResponse
|
||||
{
|
||||
Reference = new OpenApiReference
|
||||
{
|
||||
Type = ReferenceType.Response,
|
||||
Id = responseKey
|
||||
}
|
||||
};
|
||||
|
||||
foreach (var path in swaggerDoc.Paths)
|
||||
foreach (var operation in path.Value.Operations)
|
||||
operation.Value.Responses.TryAdd(responseKey, referenceResponse);
|
||||
}
|
||||
|
||||
AddDefaultResponse(HttpStatusCode.BadRequest, new OpenApiResponse
|
||||
{
|
||||
Description = "A badly formatted request was made. See error message for details.",
|
||||
Content = errorMessageContent,
|
||||
});
|
||||
|
||||
AddDefaultResponse(HttpStatusCode.Unauthorized, new OpenApiResponse
|
||||
{
|
||||
Description = "No/invalid token provided."
|
||||
});
|
||||
|
||||
AddDefaultResponse(HttpStatusCode.Forbidden, new OpenApiResponse
|
||||
{
|
||||
Description = "User lacks sufficient permissions for the operation."
|
||||
});
|
||||
|
||||
AddDefaultResponse(HttpStatusCode.Conflict, new OpenApiResponse
|
||||
{
|
||||
Description = "A data integrity check failed while performing the operation. See error message for details.",
|
||||
Content = errorMessageContent
|
||||
});
|
||||
|
||||
AddDefaultResponse(HttpStatusCode.InternalServerError, new OpenApiResponse
|
||||
{
|
||||
Description = "The server encountered an unhandled error. See error message for details.",
|
||||
Content = errorMessageContent
|
||||
});
|
||||
|
||||
AddDefaultResponse(HttpStatusCode.ServiceUnavailable, new OpenApiResponse
|
||||
{
|
||||
Description = "The server may be starting up or shutting down."
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
@@ -19,10 +18,10 @@ using Tgstation.Server.Host.Security;
|
||||
namespace Tgstation.Server.Host.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// For managing <see cref="User"/>s
|
||||
/// <see cref="ApiController"/> for managing <see cref="User"/>s.
|
||||
/// </summary>
|
||||
[Route(Routes.User)]
|
||||
public sealed class UserController : ModelController<UserUpdate>
|
||||
public sealed class UserController : ApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="ISystemIdentityFactory"/> for the <see cref="UserController"/>
|
||||
@@ -53,7 +52,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// <param name="cryptographySuite">The value of <see cref="cryptographySuite"/></param>
|
||||
/// <param name="logger">The value of <see cref="logger"/></param>
|
||||
/// <param name="generalConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="generalConfiguration"/></param>
|
||||
public UserController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ISystemIdentityFactory systemIdentityFactory, ICryptographySuite cryptographySuite, ILogger<UserController> logger, IOptions<GeneralConfiguration> generalConfigurationOptions) : base(databaseContext, authenticationContextFactory, logger, false)
|
||||
public UserController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ISystemIdentityFactory systemIdentityFactory, ICryptographySuite cryptographySuite, ILogger<UserController> logger, IOptions<GeneralConfiguration> generalConfigurationOptions) : base(databaseContext, authenticationContextFactory, logger, false, true)
|
||||
{
|
||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
this.systemIdentityFactory = systemIdentityFactory ?? throw new ArgumentNullException(nameof(systemIdentityFactory));
|
||||
@@ -61,9 +60,47 @@ namespace Tgstation.Server.Host.Controllers
|
||||
generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>
|
||||
/// Check if a given <paramref name="model"/> has a valid <see cref="Api.Models.Internal.User.Name"/> specified.
|
||||
/// </summary>
|
||||
/// <param name="model">The <see cref="UserUpdate"/> to check.</param>
|
||||
/// <returns><see langword="null"/> if <paramref name="model"/> is valid, a <see cref="BadRequestObjectResult"/> otherwise.</returns>
|
||||
BadRequestObjectResult CheckValidName(UserUpdate model)
|
||||
{
|
||||
if (model.Name != null && model.Name.Contains(':', StringComparison.InvariantCulture))
|
||||
return BadRequest(new ErrorMessage { Message = "Username must not contain colons!" });
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempt to change the password of a given <paramref name="dbUser"/>.
|
||||
/// </summary>
|
||||
/// <param name="dbUser">The <see cref="Models.User"/> to update.</param>
|
||||
/// <param name="newPassword">The new password.</param>
|
||||
/// <returns><see langword="null"/> on success, <see cref="BadRequestObjectResult"/> if <paramref name="newPassword"/> is too short.</returns>
|
||||
BadRequestObjectResult TrySetPassword(Models.User dbUser, string newPassword)
|
||||
{
|
||||
if (newPassword.Length < generalConfiguration.MinimumPasswordLength)
|
||||
return BadRequest(new ErrorMessage { Message = $"Password must be at least {generalConfiguration.MinimumPasswordLength} characters long!" });
|
||||
cryptographySuite.SetUserPassword(dbUser, newPassword, true);
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a <see cref="Api.Models.User"/>.
|
||||
/// </summary>
|
||||
/// <param name="model">The <see cref="Api.Models.User"/> to create.</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>
|
||||
/// <response code="201"><see cref="Api.Models.User"/> created successfully.</response>
|
||||
/// <response code="410">The <see cref="Api.Models.Internal.User.SystemIdentifier"/> requested could not be loaded.</response>
|
||||
/// <response code="501">A system user was requested but this is not implemented on POSIX.</response>
|
||||
[HttpPut]
|
||||
[TgsAuthorize(AdministrationRights.WriteUsers)]
|
||||
public override async Task<IActionResult> Create([FromBody] UserUpdate model, CancellationToken cancellationToken)
|
||||
[ProducesResponseType(typeof(Api.Models.User), 201)]
|
||||
[ProducesResponseType(410)]
|
||||
[ProducesResponseType(501)]
|
||||
public async Task<IActionResult> Create([FromBody] UserUpdate model, CancellationToken cancellationToken)
|
||||
{
|
||||
if (model == null)
|
||||
throw new ArgumentNullException(nameof(model));
|
||||
@@ -78,6 +115,10 @@ namespace Tgstation.Server.Host.Controllers
|
||||
if (!(model.Name == null ^ model.SystemIdentifier == null))
|
||||
return BadRequest(new ErrorMessage { Message = "User must have a name if and only if user has no system identifier!" });
|
||||
|
||||
var fail = CheckValidName(model);
|
||||
if (fail != null)
|
||||
return fail;
|
||||
|
||||
var dbUser = new Models.User
|
||||
{
|
||||
AdministrationRights = model.AdministrationRights ?? AdministrationRights.None,
|
||||
@@ -107,9 +148,9 @@ namespace Tgstation.Server.Host.Controllers
|
||||
}
|
||||
else
|
||||
{
|
||||
if (model.Password.Length < generalConfiguration.MinimumPasswordLength)
|
||||
return BadRequest(new ErrorMessage { Message = String.Format(CultureInfo.InvariantCulture, "Password must be at least {0} characters long!", generalConfiguration.MinimumPasswordLength) });
|
||||
cryptographySuite.SetUserPassword(dbUser, model.Password, true);
|
||||
var result = TrySetPassword(dbUser, model.Password);
|
||||
if (result != null)
|
||||
return result;
|
||||
}
|
||||
|
||||
dbUser.CanonicalName = dbUser.Name.ToUpperInvariant();
|
||||
@@ -121,32 +162,48 @@ namespace Tgstation.Server.Host.Controllers
|
||||
return StatusCode((int)HttpStatusCode.Created, dbUser.ToApi(true));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>
|
||||
/// Update a <see cref="Api.Models.User"/>.
|
||||
/// </summary>
|
||||
/// <param name="model">The <see cref="Api.Models.User"/> to update.</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>
|
||||
/// <response code="200"><see cref="Api.Models.User"/> updated successfully.</response>
|
||||
/// <response code="404">Requested <see cref="Api.Models.Internal.User.Id"/> does not exist.</response>
|
||||
[HttpPost]
|
||||
[TgsAuthorize(AdministrationRights.WriteUsers | AdministrationRights.EditOwnPassword)]
|
||||
public override async Task<IActionResult> Update([FromBody] UserUpdate model, CancellationToken cancellationToken)
|
||||
[ProducesResponseType(typeof(Api.Models.User), 200)]
|
||||
[ProducesResponseType(404)]
|
||||
public async Task<IActionResult> Update([FromBody] UserUpdate model, CancellationToken cancellationToken)
|
||||
{
|
||||
if (model == null)
|
||||
throw new ArgumentNullException(nameof(model));
|
||||
|
||||
var passwordEditOnly = !AuthenticationContext.User.AdministrationRights.Value.HasFlag(AdministrationRights.WriteUsers);
|
||||
var callerAdministrationRights = (AdministrationRights)AuthenticationContext.GetRight(RightsType.Administration);
|
||||
var passwordEditOnly = !callerAdministrationRights.HasFlag(AdministrationRights.WriteUsers);
|
||||
|
||||
var originalUser = passwordEditOnly ? AuthenticationContext.User : await DatabaseContext.Users.Where(x => x.Id == model.Id)
|
||||
.Include(x => x.CreatedBy)
|
||||
.FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
|
||||
var originalUser = passwordEditOnly
|
||||
? AuthenticationContext.User
|
||||
: await DatabaseContext.Users.Where(x => x.Id == model.Id)
|
||||
.Include(x => x.CreatedBy)
|
||||
.FirstOrDefaultAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
if (originalUser == default)
|
||||
return NotFound();
|
||||
|
||||
if (passwordEditOnly && (model.Id != originalUser.Id || model.InstanceManagerRights.HasValue || model.AdministrationRights.HasValue || model.Enabled.HasValue || model.SystemIdentifier != null || model.Name != null))
|
||||
// Ensure they are only trying to edit password (system identity change will trigger a bad request)
|
||||
if (passwordEditOnly && (model.Id != originalUser.Id || model.InstanceManagerRights.HasValue || model.AdministrationRights.HasValue || model.Enabled.HasValue || model.Name != null))
|
||||
return Forbid();
|
||||
|
||||
if (model.SystemIdentifier != null && model.SystemIdentifier != originalUser.SystemIdentifier)
|
||||
return BadRequest(new ErrorMessage { Message = "Cannot change a user's system identifier!" });
|
||||
|
||||
if (model.Password != null)
|
||||
{
|
||||
if (originalUser.PasswordHash == null)
|
||||
return BadRequest(new ErrorMessage { Message = "Cannot convert a system user to a password user!" });
|
||||
cryptographySuite.SetUserPassword(originalUser, model.Password, false);
|
||||
var result = TrySetPassword(originalUser, model.Password);
|
||||
if (result != null)
|
||||
return result;
|
||||
}
|
||||
else if (model.SystemIdentifier != null && model.SystemIdentifier != originalUser.SystemIdentifier)
|
||||
return BadRequest(new ErrorMessage { Message = "Cannot change a user's system identifier!" });
|
||||
|
||||
if (model.Name != null && model.Name.ToUpperInvariant() != originalUser.CanonicalName)
|
||||
return BadRequest(new ErrorMessage { Message = "Can only change capitalization of a user's name!" });
|
||||
@@ -154,23 +211,46 @@ namespace Tgstation.Server.Host.Controllers
|
||||
originalUser.InstanceManagerRights = model.InstanceManagerRights ?? originalUser.InstanceManagerRights;
|
||||
originalUser.AdministrationRights = model.AdministrationRights ?? originalUser.AdministrationRights;
|
||||
originalUser.Enabled = model.Enabled ?? originalUser.Enabled;
|
||||
|
||||
var fail = CheckValidName(model);
|
||||
if (fail != null)
|
||||
return fail;
|
||||
|
||||
originalUser.Name = model.Name ?? originalUser.Name;
|
||||
|
||||
await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return Json(model.Id == originalUser.Id || (AuthenticationContext.GetRight(RightsType.Administration) & (ulong)AdministrationRights.ReadUsers) != 0 ? originalUser.ToApi(true) : new Api.Models.User
|
||||
{
|
||||
Id = originalUser.Id
|
||||
});
|
||||
// return id only if not a self update or and cannot read users
|
||||
return Json(
|
||||
model.Id == originalUser.Id
|
||||
|| callerAdministrationRights.HasFlag(AdministrationRights.ReadUsers)
|
||||
? originalUser.ToApi(true)
|
||||
: new Api.Models.User
|
||||
{
|
||||
Id = originalUser.Id
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>
|
||||
/// Get information about the current <see cref="Api.Models.User"/>.
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="IActionResult"/> of the operation.</returns>
|
||||
/// <response code="200">The <see cref="Api.Models.User"/> was retrieved successfully.</response>
|
||||
[HttpGet]
|
||||
[TgsAuthorize]
|
||||
public override Task<IActionResult> Read(CancellationToken cancellationToken) => Task.FromResult<IActionResult>(Json(AuthenticationContext.User.ToApi(true)));
|
||||
[ProducesResponseType(typeof(Api.Models.User), 200)]
|
||||
public IActionResult Read() => Json(AuthenticationContext.User.ToApi(true));
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>
|
||||
/// List all <see cref="Api.Models.User"/>s in the server.
|
||||
/// </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>
|
||||
/// <response code="200">Retrieved <see cref="Api.Models.User"/>s successfully.</response>
|
||||
[HttpGet(Routes.List)]
|
||||
[TgsAuthorize(AdministrationRights.ReadUsers)]
|
||||
public override async Task<IActionResult> List(CancellationToken cancellationToken)
|
||||
[ProducesResponseType(typeof(IEnumerable<Api.Models.User>), 200)]
|
||||
public async Task<IActionResult> List(CancellationToken cancellationToken)
|
||||
{
|
||||
var users = await DatabaseContext.Users
|
||||
.Include(x => x.CreatedBy)
|
||||
@@ -178,12 +258,22 @@ namespace Tgstation.Server.Host.Controllers
|
||||
return Json(users.Select(x => x.ToApi(true)));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>
|
||||
/// Get a specific <see cref="Api.Models.User"/>.
|
||||
/// </summary>
|
||||
/// <param name="id">The <see cref="Api.Models.Internal.User.Id"/> to retrieve.</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>
|
||||
/// <response code="200">The <see cref="Api.Models.User"/> was retrieved successfully.</response>
|
||||
/// <response code="404">The <see cref="Api.Models.User"/> does not exist.</response>
|
||||
[HttpGet("{id}")]
|
||||
[TgsAuthorize]
|
||||
public override async Task<IActionResult> GetId(long id, CancellationToken cancellationToken)
|
||||
[ProducesResponseType(typeof(Api.Models.User), 200)]
|
||||
[ProducesResponseType(404)]
|
||||
public async Task<IActionResult> GetId(long id, CancellationToken cancellationToken)
|
||||
{
|
||||
if (id == AuthenticationContext.User.Id)
|
||||
return await Read(cancellationToken).ConfigureAwait(false);
|
||||
return Read();
|
||||
|
||||
if (!((AdministrationRights)AuthenticationContext.GetRight(RightsType.Administration)).HasFlag(AdministrationRights.ReadUsers))
|
||||
return Forbid();
|
||||
|
||||
@@ -5,12 +5,15 @@ using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Cors.Infrastructure;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using Serilog;
|
||||
@@ -19,14 +22,16 @@ using Serilog.Formatting.Display;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Reflection;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Tgstation.Server.Api;
|
||||
using Tgstation.Server.Host.Components;
|
||||
using Tgstation.Server.Host.Components.Byond;
|
||||
using Tgstation.Server.Host.Components.Chat;
|
||||
using Tgstation.Server.Host.Components.Repository;
|
||||
using Tgstation.Server.Host.Components.Watchdog;
|
||||
using Tgstation.Server.Host.Configuration;
|
||||
using Tgstation.Server.Host.Controllers;
|
||||
using Tgstation.Server.Host.IO;
|
||||
using Tgstation.Server.Host.Models;
|
||||
using Tgstation.Server.Host.Security;
|
||||
@@ -34,7 +39,7 @@ using Tgstation.Server.Host.Security;
|
||||
namespace Tgstation.Server.Host.Core
|
||||
{
|
||||
/// <inheritdoc />
|
||||
#pragma warning disable CA1506
|
||||
#pragma warning disable CA1506
|
||||
sealed class Application : IApplication
|
||||
{
|
||||
/// <inheritdoc />
|
||||
@@ -51,6 +56,11 @@ namespace Tgstation.Server.Host.Core
|
||||
/// </summary>
|
||||
readonly IConfiguration configuration;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IAssemblyInformationProvider"/> for the <see cref="Application"/>.
|
||||
/// </summary>
|
||||
readonly IAssemblyInformationProvider assemblyInformationProvider;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Microsoft.AspNetCore.Hosting.IHostingEnvironment"/> for the <see cref="Application"/>
|
||||
/// </summary>
|
||||
@@ -70,15 +80,20 @@ namespace Tgstation.Server.Host.Core
|
||||
/// Construct an <see cref="Application"/>
|
||||
/// </summary>
|
||||
/// <param name="configuration">The value of <see cref="configuration"/></param>
|
||||
/// <param name="assemblyInformationProvider">The <see cref="IAssemblyInformationProvider"/> for the <see cref="Application"/>.</param>
|
||||
/// <param name="hostingEnvironment">The value of <see cref="hostingEnvironment"/></param>
|
||||
public Application(IConfiguration configuration, Microsoft.AspNetCore.Hosting.IHostingEnvironment hostingEnvironment)
|
||||
public Application(
|
||||
IConfiguration configuration,
|
||||
IAssemblyInformationProvider assemblyInformationProvider,
|
||||
Microsoft.AspNetCore.Hosting.IHostingEnvironment hostingEnvironment)
|
||||
{
|
||||
this.configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
|
||||
this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider));
|
||||
this.hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment));
|
||||
|
||||
startupTcs = new TaskCompletionSource<object>();
|
||||
|
||||
Version = Assembly.GetExecutingAssembly().GetName().Version;
|
||||
Version = assemblyInformationProvider.Name.Version;
|
||||
VersionString = String.Format(CultureInfo.InvariantCulture, "{0} v{1}", VersionPrefix, Version);
|
||||
}
|
||||
|
||||
@@ -221,15 +236,61 @@ namespace Tgstation.Server.Host.Core
|
||||
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
|
||||
|
||||
// add mvc, configure the json serializer settings
|
||||
services.AddMvc().AddJsonOptions(options =>
|
||||
{
|
||||
options.AllowInputFormatterExceptionMessages = true;
|
||||
options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
|
||||
options.SerializerSettings.CheckAdditionalContent = true;
|
||||
options.SerializerSettings.MissingMemberHandling = MissingMemberHandling.Error;
|
||||
options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
|
||||
options.SerializerSettings.Converters = new[] { new VersionConverter() };
|
||||
});
|
||||
services
|
||||
.AddMvc(options =>
|
||||
{
|
||||
var dataAnnotationValidator = options.ModelValidatorProviders.Single(validator => validator.GetType().Name == "DataAnnotationsModelValidatorProvider");
|
||||
options.ModelValidatorProviders.Remove(dataAnnotationValidator);
|
||||
})
|
||||
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1)
|
||||
.AddJsonOptions(options =>
|
||||
{
|
||||
options.AllowInputFormatterExceptionMessages = true;
|
||||
options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
|
||||
options.SerializerSettings.CheckAdditionalContent = true;
|
||||
options.SerializerSettings.MissingMemberHandling = MissingMemberHandling.Error;
|
||||
options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
|
||||
options.SerializerSettings.Converters = new[] { new VersionConverter() };
|
||||
});
|
||||
|
||||
if (hostingEnvironment.IsDevelopment())
|
||||
services.AddSwaggerGen(
|
||||
c =>
|
||||
{
|
||||
c.SwaggerDoc(
|
||||
"v1",
|
||||
new OpenApiInfo
|
||||
{
|
||||
Title = "TGS API",
|
||||
Version = "v4"
|
||||
});
|
||||
|
||||
// Important to do this before applying our own filters
|
||||
// Otherwise we'll get NullReferenceExceptions on parameters to be setup in our document filter
|
||||
var assemblyLocation = assemblyInformationProvider.Path;
|
||||
var filePath = ioManager.ConcatPath(ioManager.GetDirectoryName(assemblyLocation), String.Concat(ioManager.GetFileNameWithoutExtension(assemblyLocation), ".xml"));
|
||||
c.IncludeXmlComments(filePath);
|
||||
|
||||
c.OperationFilter<TgsOpenApiFilters>();
|
||||
c.DocumentFilter<TgsOpenApiFilters>();
|
||||
|
||||
c.AddSecurityDefinition(TgsOpenApiFilters.PasswordSecuritySchemeId, new OpenApiSecurityScheme
|
||||
{
|
||||
In = ParameterLocation.Header,
|
||||
Type = SecuritySchemeType.Http,
|
||||
Name = HeaderNames.Authorization,
|
||||
Scheme = ApiHeaders.BasicAuthenticationScheme
|
||||
});
|
||||
|
||||
c.AddSecurityDefinition(TgsOpenApiFilters.TokenSecuritySchemeId, new OpenApiSecurityScheme
|
||||
{
|
||||
BearerFormat = "JWT",
|
||||
In = ParameterLocation.Header,
|
||||
Type = SecuritySchemeType.Http,
|
||||
Name = HeaderNames.Authorization,
|
||||
Scheme = ApiHeaders.JwtAuthenticationScheme
|
||||
});
|
||||
});
|
||||
|
||||
// enable browser detection
|
||||
services.AddDetectionCore().AddBrowser();
|
||||
@@ -352,17 +413,26 @@ namespace Tgstation.Server.Host.Core
|
||||
logger.LogTrace("Web Root: {0}", hostingEnvironment.WebRootPath);
|
||||
|
||||
// attempt to restart the server if the configuration changes
|
||||
if(serverControl.WatchdogPresent)
|
||||
if (serverControl.WatchdogPresent)
|
||||
ChangeToken.OnChange(configuration.GetReloadToken, () => serverControl.Restart());
|
||||
|
||||
// setup the HTTP request pipeline
|
||||
// Final point where we wrap exceptions in a 500 (ErrorMessage) response
|
||||
applicationBuilder.UseServerErrorHandling();
|
||||
|
||||
// should anything after this throw an exception, catch it and display a detailed html page
|
||||
applicationBuilder.UseDeveloperExceptionPage(); // it is not worth it to limit this, you should only ever get it if you're an authorized user
|
||||
if (hostingEnvironment.IsDevelopment())
|
||||
applicationBuilder.UseDeveloperExceptionPage(); // it is not worth it to limit this, you should only ever get it if you're an authorized user
|
||||
|
||||
// suppress OperationCancelledExceptions, they are just aborted HTTP requests
|
||||
applicationBuilder.UseCancelledRequestSuppression();
|
||||
|
||||
if (hostingEnvironment.IsDevelopment())
|
||||
{
|
||||
applicationBuilder.UseSwagger();
|
||||
applicationBuilder.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "TGS API V4"));
|
||||
}
|
||||
|
||||
// Set up CORS based on configuration if necessary
|
||||
Action<CorsPolicyBuilder> corsBuilder = null;
|
||||
if (controlPanelConfiguration.AllowAnyOrigin)
|
||||
|
||||
@@ -6,6 +6,7 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Net;
|
||||
using Tgstation.Server.Api.Models;
|
||||
|
||||
namespace Tgstation.Server.Host.Core
|
||||
@@ -69,5 +70,39 @@ namespace Tgstation.Server.Host.Core
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Suppress all in flight exceptions with error 500.
|
||||
/// </summary>
|
||||
/// <param name="applicationBuilder">The <see cref="IApplicationBuilder"/> to configure</param>
|
||||
public static void UseServerErrorHandling(this IApplicationBuilder applicationBuilder)
|
||||
{
|
||||
if (applicationBuilder == null)
|
||||
throw new ArgumentNullException(nameof(applicationBuilder));
|
||||
applicationBuilder.Use(async (context, next) =>
|
||||
{
|
||||
var logger = GetLogger(context);
|
||||
try
|
||||
{
|
||||
await next().ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogError("Failed request: {0}", e);
|
||||
await new ObjectResult(
|
||||
new ErrorMessage
|
||||
{
|
||||
Message = $"A unhandled exception has occurred: {e}"
|
||||
})
|
||||
{
|
||||
StatusCode = (int)HttpStatusCode.InternalServerError
|
||||
}
|
||||
.ExecuteResultAsync(new ActionContext
|
||||
{
|
||||
HttpContext = context
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
using System.Reflection;
|
||||
|
||||
namespace Tgstation.Server.Host.Core
|
||||
{
|
||||
/// <inheritdoc />
|
||||
sealed class AssemblyInformationProvider : IAssemblyInformationProvider
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string Path { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public AssemblyName Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AssemblyInformationProvider"/> <see langword="class"/>.
|
||||
/// </summary>
|
||||
public AssemblyInformationProvider()
|
||||
{
|
||||
Assembly assembly = Assembly.GetExecutingAssembly();
|
||||
Path = assembly.Location;
|
||||
Name = assembly.GetName();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System.Reflection;
|
||||
|
||||
namespace Tgstation.Server.Host.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// For retrieving the <see cref="Assembly"/>'s location.
|
||||
/// </summary>
|
||||
interface IAssemblyInformationProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the path to the executing assembly.
|
||||
/// </summary>
|
||||
string Path { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="AssemblyName"/>.
|
||||
/// </summary>
|
||||
AssemblyName Name { get; }
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ namespace Tgstation.Server.Host.Core
|
||||
readonly ILogger<JobManager> logger;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="Dictionary{TKey, TValue}"/> of <see cref="Api.Models.Internal.Job.Id"/> to running <see cref="JobHandler"/>s
|
||||
/// <see cref="Dictionary{TKey, TValue}"/> of <see cref="Job"/> <see cref="Api.Models.EntityId.Id"/>s to running <see cref="JobHandler"/>s
|
||||
/// </summary>
|
||||
readonly Dictionary<long, JobHandler> jobs;
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ using System.Data.SqlClient;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Tgstation.Server.Host.Configuration;
|
||||
@@ -163,11 +164,39 @@ namespace Tgstation.Server.Host.Core
|
||||
}
|
||||
while (true);
|
||||
|
||||
await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
|
||||
await console.WriteAsync("Enter the server's address and port (blank for local): ", false, cancellationToken).ConfigureAwait(false);
|
||||
var serverAddress = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
|
||||
if (String.IsNullOrWhiteSpace(serverAddress))
|
||||
serverAddress = null;
|
||||
string serverAddress;
|
||||
uint? mySQLServerPort = null;
|
||||
do
|
||||
{
|
||||
await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
|
||||
await console.WriteAsync("Enter the server's address and port [<server>:<port> or <server>] (blank for local): ", false, cancellationToken).ConfigureAwait(false);
|
||||
serverAddress = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
|
||||
if (String.IsNullOrWhiteSpace(serverAddress))
|
||||
{
|
||||
serverAddress = null;
|
||||
break;
|
||||
}
|
||||
else if (databaseConfiguration.DatabaseType != DatabaseType.SqlServer)
|
||||
{
|
||||
var m = Regex.Match(serverAddress, @"^(?<server>.+):(?<port>.+)$");
|
||||
if (m.Success)
|
||||
{
|
||||
serverAddress = m.Groups["server"].Value;
|
||||
if (uint.TryParse(m.Groups["port"].Value, out uint port))
|
||||
{
|
||||
mySQLServerPort = port;
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
await console.WriteAsync($@"Failed to parse port ""{m.Groups["port"].Value}"", please try again.", true, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
else break;
|
||||
}
|
||||
else break;
|
||||
}
|
||||
while (true);
|
||||
|
||||
await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
|
||||
await console.WriteAsync("Enter the database name (Can be from previous installation. Otherwise, should not exist): ", false, cancellationToken).ConfigureAwait(false);
|
||||
@@ -243,6 +272,9 @@ namespace Tgstation.Server.Host.Core
|
||||
Password = password
|
||||
};
|
||||
|
||||
if (mySQLServerPort.HasValue)
|
||||
csb.Port = mySQLServerPort.Value;
|
||||
|
||||
CreateTestConnection(csb.ConnectionString);
|
||||
csb.Database = databaseName;
|
||||
databaseConfiguration.ConnectionString = csb.ConnectionString;
|
||||
@@ -611,41 +643,60 @@ namespace Tgstation.Server.Host.Core
|
||||
}
|
||||
|
||||
var userConfigFileName = String.Format(CultureInfo.InvariantCulture, "appsettings.{0}.json", hostingEnvironment.EnvironmentName);
|
||||
var exists = await ioManager.FileExists(userConfigFileName, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
bool shouldRunBasedOnAutodetect;
|
||||
if (exists)
|
||||
async Task HandleSetupCancel()
|
||||
{
|
||||
var bytes = await ioManager.ReadAllBytes(userConfigFileName, cancellationToken).ConfigureAwait(false);
|
||||
var contents = Encoding.UTF8.GetString(bytes);
|
||||
var existingConfigIsEmpty = String.IsNullOrWhiteSpace(contents) || contents.Trim() == "{}";
|
||||
logger.LogTrace("Configuration json detected. Empty: {0}", existingConfigIsEmpty);
|
||||
shouldRunBasedOnAutodetect = existingConfigIsEmpty;
|
||||
}
|
||||
else
|
||||
{
|
||||
shouldRunBasedOnAutodetect = true;
|
||||
logger.LogTrace("No configuration json detected");
|
||||
await console.WriteAsync(String.Empty, true, default).ConfigureAwait(false);
|
||||
await console.WriteAsync("Aborting setup!", true, default).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (!shouldRunBasedOnAutodetect)
|
||||
{
|
||||
if (forceRun)
|
||||
// Link passed cancellationToken with cancel key press
|
||||
Task finalTask = Task.CompletedTask;
|
||||
using (var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, console.CancelKeyPress))
|
||||
using ((cancellationToken = cts.Token).Register(() => finalTask = HandleSetupCancel()))
|
||||
try
|
||||
{
|
||||
logger.LogTrace("Asking user to bypass due to force run request...");
|
||||
await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "The configuration settings are requesting the setup wizard be run, but you already appear to have a configuration file ({0})!", userConfigFileName), true, cancellationToken).ConfigureAwait(false);
|
||||
var exists = await ioManager.FileExists(userConfigFileName, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
forceRun = await PromptYesNo("Continue running setup wizard? (y/n): ", cancellationToken).ConfigureAwait(false);
|
||||
bool shouldRunBasedOnAutodetect;
|
||||
if (exists)
|
||||
{
|
||||
var bytes = await ioManager.ReadAllBytes(userConfigFileName, cancellationToken).ConfigureAwait(false);
|
||||
var contents = Encoding.UTF8.GetString(bytes);
|
||||
var existingConfigIsEmpty = String.IsNullOrWhiteSpace(contents) || contents.Trim() == "{}";
|
||||
logger.LogTrace("Configuration json detected. Empty: {0}", existingConfigIsEmpty);
|
||||
shouldRunBasedOnAutodetect = existingConfigIsEmpty;
|
||||
}
|
||||
else
|
||||
{
|
||||
shouldRunBasedOnAutodetect = true;
|
||||
logger.LogTrace("No configuration json detected");
|
||||
}
|
||||
|
||||
if (!shouldRunBasedOnAutodetect)
|
||||
{
|
||||
if (forceRun)
|
||||
{
|
||||
logger.LogTrace("Asking user to bypass due to force run request...");
|
||||
await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "The configuration settings are requesting the setup wizard be run, but you already appear to have a configuration file ({0})!", userConfigFileName), true, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
forceRun = await PromptYesNo("Continue running setup wizard? (y/n): ", cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (!forceRun)
|
||||
return false;
|
||||
}
|
||||
|
||||
// flush the logs to prevent console conflicts
|
||||
await asyncDelayer.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false);
|
||||
|
||||
await RunWizard(userConfigFileName, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await finalTask.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (!forceRun)
|
||||
return false;
|
||||
}
|
||||
|
||||
// flush the logs to prevent console conflicts
|
||||
await asyncDelayer.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false);
|
||||
|
||||
await RunWizard(userConfigFileName, cancellationToken).ConfigureAwait(false);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,50 @@ using System.Threading.Tasks;
|
||||
namespace Tgstation.Server.Host.IO
|
||||
{
|
||||
/// <inheritdoc />
|
||||
sealed class Console : IConsole
|
||||
sealed class Console : IConsole, IDisposable
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public bool Available => Environment.UserInteractive;
|
||||
|
||||
/// <inheritdoc />
|
||||
public CancellationToken CancelKeyPress => cancelKeyCts.Token;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="CancellationTokenSource"/> for <see cref="CancelKeyPress"/>.
|
||||
/// </summary>
|
||||
readonly CancellationTokenSource cancelKeyCts;
|
||||
|
||||
/// <summary>
|
||||
/// If the <see cref="Console"/> was disposed;
|
||||
/// </summary>
|
||||
bool disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Console"/> <see langword="class"/>.
|
||||
/// </summary>
|
||||
public Console()
|
||||
{
|
||||
cancelKeyCts = new CancellationTokenSource();
|
||||
System.Console.CancelKeyPress += (sender, e) =>
|
||||
{
|
||||
lock (cancelKeyCts)
|
||||
{
|
||||
if (!disposed)
|
||||
cancelKeyCts.Cancel();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
lock (cancelKeyCts)
|
||||
{
|
||||
cancelKeyCts.Dispose();
|
||||
disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
void CheckAvailable()
|
||||
{
|
||||
if (!Available)
|
||||
|
||||
@@ -13,6 +13,11 @@ namespace Tgstation.Server.Host.IO
|
||||
/// </summary>
|
||||
bool Available { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="CancellationToken"/> that triggers if Crtl+C or an equivalent is pressed.
|
||||
/// </summary>
|
||||
CancellationToken CancelKeyPress { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Write some <paramref name="text"/> to the <see cref="IConsole"/>
|
||||
/// </summary>
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace Tgstation.Server.Host.Models
|
||||
public Job Job { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Api.Models.Internal.Job.Id"/> of <see cref="Job"/>
|
||||
/// The <see cref="Api.Models.EntityId.Id"/> of <see cref="Job"/>
|
||||
/// </summary>
|
||||
public long JobId { get; set; }
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using Tgstation.Server.Host.Configuration;
|
||||
using Tgstation.Server.Host.Core;
|
||||
using Tgstation.Server.Host.IO;
|
||||
|
||||
namespace Tgstation.Server.Host.Models.Migrations
|
||||
{
|
||||
@@ -28,7 +28,9 @@ namespace Tgstation.Server.Host.Models.Migrations
|
||||
public static IOptions<DatabaseConfiguration> GetDbContextOptions()
|
||||
{
|
||||
var builder = new ConfigurationBuilder();
|
||||
builder.SetBasePath(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location));
|
||||
var assemblyInfoProvider = new AssemblyInformationProvider();
|
||||
var ioManager = new DefaultIOManager();
|
||||
builder.SetBasePath(ioManager.GetDirectoryName(assemblyInfoProvider.Path));
|
||||
builder.AddJsonFile(RootJson);
|
||||
builder.AddJsonFile(DevJson);
|
||||
var configuration = builder.Build();
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace Tgstation.Server.Host
|
||||
/// The <see cref="IServerFactory"/> to use
|
||||
/// </summary>
|
||||
#pragma warning disable SA1401 // Fields must be private
|
||||
internal static IServerFactory ServerFactory = new ServerFactory();
|
||||
internal static IServerFactory ServerFactory = Host.ServerFactory.CreateDefault();
|
||||
#pragma warning restore SA1401 // Fields must be private
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Reflection;
|
||||
using System.Security.Claims;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -42,7 +41,11 @@ namespace Tgstation.Server.Host.Security
|
||||
/// </summary>
|
||||
/// <param name="asyncDelayer">The value of <see cref="asyncDelayer"/></param>
|
||||
/// <param name="cryptographySuite">The <see cref="ICryptographySuite"/> used for generating the <see cref="ValidationParameters"/></param>
|
||||
public TokenFactory(IAsyncDelayer asyncDelayer, ICryptographySuite cryptographySuite)
|
||||
/// <param name="assemblyInformationProvider">The <see cref="IAssemblyInformationProvider"/> used to generate the issuer name.</param>
|
||||
public TokenFactory(
|
||||
IAsyncDelayer asyncDelayer,
|
||||
ICryptographySuite cryptographySuite,
|
||||
IAssemblyInformationProvider assemblyInformationProvider)
|
||||
{
|
||||
ValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
@@ -50,7 +53,7 @@ namespace Tgstation.Server.Host.Security
|
||||
IssuerSigningKey = new SymmetricSecurityKey(cryptographySuite.GetSecureBytes(TokenSigningKeyByteAmount)),
|
||||
|
||||
ValidateIssuer = true,
|
||||
ValidIssuer = Assembly.GetExecutingAssembly().GetName().Name,
|
||||
ValidIssuer = assemblyInformationProvider.Name.Name,
|
||||
|
||||
ValidateLifetime = true,
|
||||
ValidateAudience = true,
|
||||
|
||||
@@ -30,6 +30,11 @@ namespace Tgstation.Server.Host
|
||||
/// </summary>
|
||||
readonly IWebHostBuilder webHostBuilder;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IIOManager"/> for the <see cref="Server"/>.
|
||||
/// </summary>
|
||||
readonly IIOManager ioManager;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IRestartHandler"/>s to run when the <see cref="Server"/> restarts
|
||||
/// </summary>
|
||||
@@ -69,10 +74,12 @@ namespace Tgstation.Server.Host
|
||||
/// Construct a <see cref="Server"/>
|
||||
/// </summary>
|
||||
/// <param name="webHostBuilder">The value of <see cref="webHostBuilder"/></param>
|
||||
/// <param name="ioManager">The value of <see cref="ioManager"/>.</param>
|
||||
/// <param name="updatePath">The value of <see cref="updatePath"/></param>
|
||||
public Server(IWebHostBuilder webHostBuilder, string updatePath)
|
||||
public Server(IWebHostBuilder webHostBuilder, IIOManager ioManager, string updatePath)
|
||||
{
|
||||
this.webHostBuilder = webHostBuilder ?? throw new ArgumentNullException(nameof(webHostBuilder));
|
||||
this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
|
||||
this.updatePath = updatePath;
|
||||
|
||||
webHostBuilder.ConfigureServices(serviceCollection => serviceCollection.AddSingleton<IServerControl>(this));
|
||||
|
||||
@@ -1,29 +1,65 @@
|
||||
using Microsoft.AspNetCore;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using Tgstation.Server.Host.Core;
|
||||
using Tgstation.Server.Host.IO;
|
||||
|
||||
namespace Tgstation.Server.Host
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public sealed class ServerFactory : IServerFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="IAssemblyInformationProvider"/> for the <see cref="ServerFactory"/>.
|
||||
/// </summary>
|
||||
readonly IAssemblyInformationProvider assemblyInformationProvider;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IIOManager"/> for the <see cref="ServerFactory"/>.
|
||||
/// </summary>
|
||||
readonly IIOManager ioManager;
|
||||
|
||||
/// <summary>
|
||||
/// Create the default <see cref="IServerFactory"/>.
|
||||
/// </summary>
|
||||
/// <returns>A new <see cref="IServerFactory"/> with the default settings.</returns>
|
||||
public static IServerFactory CreateDefault()
|
||||
=> new ServerFactory(
|
||||
new AssemblyInformationProvider(),
|
||||
new DefaultIOManager());
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ServerFactory"/>.
|
||||
/// </summary>
|
||||
/// <param name="assemblyInformationProvider">The value of <see cref="assemblyInformationProvider"/>.</param>
|
||||
/// <param name="ioManager">The value of <see cref="ioManager"/>.</param>
|
||||
internal ServerFactory(IAssemblyInformationProvider assemblyInformationProvider, IIOManager ioManager)
|
||||
{
|
||||
this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider));
|
||||
this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IServer CreateServer(string[] args, string updatePath)
|
||||
{
|
||||
var webHost = WebHost.CreateDefaultBuilder(args ?? throw new ArgumentNullException(nameof(args)))
|
||||
.ConfigureAppConfiguration((context, configurationBuilder) => configurationBuilder.SetBasePath(Directory.GetCurrentDirectory()))
|
||||
.ConfigureServices(serviceCollection =>
|
||||
{
|
||||
serviceCollection.AddSingleton(ioManager);
|
||||
serviceCollection.AddSingleton(assemblyInformationProvider);
|
||||
})
|
||||
.UseStartup<Application>()
|
||||
.SuppressStatusMessages(true)
|
||||
.UseShutdownTimeout(TimeSpan.FromMinutes(1));
|
||||
|
||||
if(updatePath != null)
|
||||
webHost.UseContentRoot(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location));
|
||||
webHost.UseContentRoot(Path.GetDirectoryName(assemblyInformationProvider.Path));
|
||||
|
||||
return new Server(webHost, updatePath);
|
||||
return new Server(webHost, ioManager, updatePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +83,7 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.0.0-rc5" />
|
||||
<PackageReference Include="System.Diagnostics.PerformanceCounter" Version="4.7.0" />
|
||||
<PackageReference Include="System.DirectoryServices.AccountManagement" Version="4.7.0" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="5.6.0" />
|
||||
|
||||
@@ -14,22 +14,22 @@ namespace Tgstation.Server.Host.Components.Byond.Tests
|
||||
public void TestConstruction()
|
||||
{
|
||||
Assert.ThrowsException<ArgumentNullException>(() => new PosixByondInstaller(null, null, null));
|
||||
var mockIOManager = new Mock<IIOManager>();
|
||||
Assert.ThrowsException<ArgumentNullException>(() => new PosixByondInstaller(mockIOManager.Object, null, null));
|
||||
var mockPostWriteHandler = new Mock<IPostWriteHandler>();
|
||||
Assert.ThrowsException<ArgumentNullException>(() => new PosixByondInstaller(mockIOManager.Object, mockPostWriteHandler.Object, null));
|
||||
Assert.ThrowsException<ArgumentNullException>(() => new PosixByondInstaller(mockPostWriteHandler.Object, null, null));
|
||||
var mockIOManager = new Mock<IIOManager>();
|
||||
Assert.ThrowsException<ArgumentNullException>(() => new PosixByondInstaller(mockPostWriteHandler.Object, mockIOManager.Object, null));
|
||||
|
||||
var mockLogger = new Mock<ILogger<PosixByondInstaller>>();
|
||||
new PosixByondInstaller(mockIOManager.Object, mockPostWriteHandler.Object, mockLogger.Object);
|
||||
new PosixByondInstaller(mockPostWriteHandler.Object, mockIOManager.Object, mockLogger.Object);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task TestCacheClean()
|
||||
{
|
||||
var mockIOManager = new Mock<IIOManager>();
|
||||
var mockPostWriteHandler = new Mock<IPostWriteHandler>();
|
||||
var mockIOManager = new Mock<IIOManager>();
|
||||
var mockLogger = new Mock<ILogger<PosixByondInstaller>>();
|
||||
var installer = new PosixByondInstaller(mockIOManager.Object, mockPostWriteHandler.Object, mockLogger.Object);
|
||||
var installer = new PosixByondInstaller(mockPostWriteHandler.Object, mockIOManager.Object, mockLogger.Object);
|
||||
|
||||
const string ByondCachePath = "~/.byond/cache";
|
||||
|
||||
@@ -37,7 +37,7 @@ namespace Tgstation.Server.Host.Components.Byond.Tests
|
||||
|
||||
await installer.CleanCache(default);
|
||||
|
||||
mockIOManager.Verify();
|
||||
mockPostWriteHandler.Verify();
|
||||
|
||||
|
||||
mockIOManager.Setup(x => x.DeleteDirectory(ByondCachePath, default)).Throws(new OperationCanceledException()).Verifiable();
|
||||
@@ -60,7 +60,7 @@ namespace Tgstation.Server.Host.Components.Byond.Tests
|
||||
var mockIOManager = new Mock<IIOManager>();
|
||||
var mockPostWriteHandler = new Mock<IPostWriteHandler>();
|
||||
var mockLogger = new Mock<ILogger<PosixByondInstaller>>();
|
||||
var installer = new PosixByondInstaller(mockIOManager.Object, mockPostWriteHandler.Object, mockLogger.Object);
|
||||
var installer = new PosixByondInstaller(mockPostWriteHandler.Object, mockIOManager.Object, mockLogger.Object);
|
||||
|
||||
await Assert.ThrowsExceptionAsync<ArgumentNullException>(() => installer.DownloadVersion(null, default)).ConfigureAwait(false);
|
||||
|
||||
@@ -79,7 +79,7 @@ namespace Tgstation.Server.Host.Components.Byond.Tests
|
||||
var mockIOManager = new Mock<IIOManager>();
|
||||
var mockPostWriteHandler = new Mock<IPostWriteHandler>();
|
||||
var mockLogger = new Mock<ILogger<PosixByondInstaller>>();
|
||||
var installer = new PosixByondInstaller(mockIOManager.Object, mockPostWriteHandler.Object, mockLogger.Object);
|
||||
var installer = new PosixByondInstaller(mockPostWriteHandler.Object, mockIOManager.Object, mockLogger.Object);
|
||||
|
||||
const string FakePath = "fake";
|
||||
await Assert.ThrowsExceptionAsync<ArgumentNullException>(() => installer.InstallByond(null, null, default)).ConfigureAwait(false);
|
||||
|
||||
@@ -23,13 +23,18 @@ namespace Tgstation.Server.Host.Core.Tests
|
||||
[TestMethod]
|
||||
public void TestMethodThrows()
|
||||
{
|
||||
Assert.ThrowsException<ArgumentNullException>(() => new Application(null, null));
|
||||
Assert.ThrowsException<ArgumentNullException>(() => new Application(null, null, null));
|
||||
var mockConfiguration = new Mock<IConfiguration>();
|
||||
Assert.ThrowsException<ArgumentNullException>(() => new Application(mockConfiguration.Object, null));
|
||||
Assert.ThrowsException<ArgumentNullException>(() => new Application(mockConfiguration.Object, null, null));
|
||||
|
||||
var mockAssemblyInfo = new Mock<IAssemblyInformationProvider>();
|
||||
mockAssemblyInfo.SetupGet(x => x.Name).Returns(typeof(Application).Assembly.GetName());
|
||||
|
||||
Assert.ThrowsException<ArgumentNullException>(() => new Application(mockConfiguration.Object, mockAssemblyInfo.Object, null));
|
||||
|
||||
var mockHostingEnvironment = new Mock<IHostingEnvironment>();
|
||||
|
||||
var app = new Application(mockConfiguration.Object, mockHostingEnvironment.Object);
|
||||
var app = new Application(mockConfiguration.Object, mockAssemblyInfo.Object, mockHostingEnvironment.Object);
|
||||
|
||||
Assert.ThrowsException<ArgumentNullException>(() => app.ConfigureServices(null));
|
||||
Assert.ThrowsException<ArgumentNullException>(() => app.Configure(null, null, null, null, null));
|
||||
@@ -69,11 +74,11 @@ namespace Tgstation.Server.Host.Core.Tests
|
||||
public void TestConfigureServicesThrowsWhenSetupWizardConfigurationDemands()
|
||||
{
|
||||
var mockConfiguration = new Mock<IConfiguration>();
|
||||
Assert.ThrowsException<ArgumentNullException>(() => new Application(mockConfiguration.Object, null));
|
||||
|
||||
var mockAssemblyInfo = new Mock<IAssemblyInformationProvider>();
|
||||
mockAssemblyInfo.SetupGet(x => x.Name).Returns(typeof(Application).Assembly.GetName());
|
||||
var mockHostingEnvironment = new Mock<IHostingEnvironment>();
|
||||
|
||||
var app = new Application(mockConfiguration.Object, mockHostingEnvironment.Object);
|
||||
var app = new Application(mockConfiguration.Object, mockAssemblyInfo.Object, mockHostingEnvironment.Object);
|
||||
|
||||
var mockOptions = new Mock<IOptions<GeneralConfiguration>>();
|
||||
mockOptions.SetupGet(x => x.Value).Returns(new GeneralConfiguration
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Moq;
|
||||
using System;
|
||||
using Tgstation.Server.Host.Core;
|
||||
using Tgstation.Server.Host.IO;
|
||||
|
||||
namespace Tgstation.Server.Host.Tests
|
||||
{
|
||||
@@ -9,10 +12,20 @@ namespace Tgstation.Server.Host.Tests
|
||||
[TestClass]
|
||||
public sealed class TestServerFactory
|
||||
{
|
||||
[TestMethod]
|
||||
public void TestContructor()
|
||||
{
|
||||
Assert.ThrowsException<ArgumentNullException>(() => new ServerFactory(null, null));
|
||||
IAssemblyInformationProvider assemblyInformationProvider = Mock.Of<IAssemblyInformationProvider>();
|
||||
Assert.ThrowsException<ArgumentNullException>(() => new ServerFactory(assemblyInformationProvider, null));
|
||||
IIOManager ioManager = Mock.Of<IIOManager>();
|
||||
new ServerFactory(assemblyInformationProvider, ioManager);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestWorksWithoutUpdatePath()
|
||||
{
|
||||
var factory = new ServerFactory();
|
||||
var factory = ServerFactory.CreateDefault();
|
||||
|
||||
Assert.ThrowsException<ArgumentNullException>(() => factory.CreateServer(null, null));
|
||||
factory.CreateServer(Array.Empty<string>(), null);
|
||||
@@ -21,7 +34,7 @@ namespace Tgstation.Server.Host.Tests
|
||||
[TestMethod]
|
||||
public void TestWorksWithUpdatePath()
|
||||
{
|
||||
var factory = new ServerFactory();
|
||||
var factory = ServerFactory.CreateDefault();
|
||||
const string Path = "/test";
|
||||
|
||||
Assert.ThrowsException<ArgumentNullException>(() => factory.CreateServer(null, null));
|
||||
|
||||
@@ -29,7 +29,7 @@ namespace Tgstation.Server.Tests
|
||||
try
|
||||
{
|
||||
var updatePath = Path.Combine(updatePathRoot, Guid.NewGuid().ToString());
|
||||
var server = new TestingServer(updatePath);
|
||||
var server = new TestingServer(clientFactory, updatePath);
|
||||
using (var serverCts = new CancellationTokenSource())
|
||||
{
|
||||
var cancellationToken = serverCts.Token;
|
||||
@@ -98,7 +98,7 @@ namespace Tgstation.Server.Tests
|
||||
[TestCategory("SkipWhenLiveUnitTesting")]
|
||||
public async Task TestStandardOperation()
|
||||
{
|
||||
var server = new TestingServer(null);
|
||||
var server = new TestingServer(clientFactory, null);
|
||||
using (var serverCts = new CancellationTokenSource())
|
||||
{
|
||||
var cancellationToken = serverCts.Token;
|
||||
|
||||
@@ -3,8 +3,11 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Tgstation.Server.Api.Models;
|
||||
using Tgstation.Server.Client;
|
||||
using Tgstation.Server.Host;
|
||||
using Tgstation.Server.Host.Configuration;
|
||||
|
||||
@@ -19,8 +22,14 @@ namespace Tgstation.Server.Tests
|
||||
|
||||
readonly IServer realServer;
|
||||
|
||||
public TestingServer(string updatePath)
|
||||
readonly IServerClientFactory serverClientFactory;
|
||||
|
||||
readonly bool dumpOpenAPISpecpath;
|
||||
|
||||
public TestingServer(IServerClientFactory serverClientFactory, string updatePath)
|
||||
{
|
||||
this.serverClientFactory = serverClientFactory;
|
||||
|
||||
Directory = Path.GetTempFileName();
|
||||
File.Delete(Directory);
|
||||
System.IO.Directory.CreateDirectory(Directory);
|
||||
@@ -31,6 +40,7 @@ namespace Tgstation.Server.Tests
|
||||
var databaseType = Environment.GetEnvironmentVariable("TGS4_TEST_DATABASE_TYPE");
|
||||
var connectionString = Environment.GetEnvironmentVariable("TGS4_TEST_CONNECTION_STRING");
|
||||
var gitHubAccessToken = Environment.GetEnvironmentVariable("TGS4_TEST_GITHUB_TOKEN");
|
||||
var dumpOpenAPISpecPathEnvVar = Environment.GetEnvironmentVariable("TGS4_TEST_DUMP_API_SPEC");
|
||||
|
||||
if (String.IsNullOrEmpty(databaseType))
|
||||
Assert.Inconclusive("No database type configured in env var TGS4_TEST_DATABASE_TYPE!");
|
||||
@@ -40,6 +50,8 @@ namespace Tgstation.Server.Tests
|
||||
|
||||
if (String.IsNullOrEmpty(gitHubAccessToken))
|
||||
Console.WriteLine("WARNING: No GitHub access token configured, test may fail due to rate limits!");
|
||||
|
||||
dumpOpenAPISpecpath = !String.IsNullOrEmpty(dumpOpenAPISpecPathEnvVar);
|
||||
|
||||
var args = new List<string>()
|
||||
{
|
||||
@@ -53,7 +65,10 @@ namespace Tgstation.Server.Tests
|
||||
if (!String.IsNullOrEmpty(gitHubAccessToken))
|
||||
args.Add(String.Format(CultureInfo.InvariantCulture, "General:GitHubAccessToken={0}", gitHubAccessToken));
|
||||
|
||||
realServer = new ServerFactory().CreateServer(args.ToArray(), updatePath);
|
||||
if (dumpOpenAPISpecpath)
|
||||
Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Development");
|
||||
|
||||
realServer = ServerFactory.CreateDefault().CreateServer(args.ToArray(), updatePath);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
@@ -61,6 +76,41 @@ namespace Tgstation.Server.Tests
|
||||
System.IO.Directory.Delete(Directory, true);
|
||||
}
|
||||
|
||||
public Task RunAsync(CancellationToken cancellationToken) => realServer.RunAsync(cancellationToken);
|
||||
public async Task RunAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
Task runTask = realServer.RunAsync(cancellationToken);
|
||||
|
||||
if (dumpOpenAPISpecpath)
|
||||
{
|
||||
var giveUpAt = DateTimeOffset.Now.AddSeconds(60);
|
||||
do
|
||||
{
|
||||
try
|
||||
{
|
||||
var client = await serverClientFactory.CreateServerClient(Url, User.AdminName, User.DefaultAdminPassword).ConfigureAwait(false);
|
||||
break;
|
||||
}
|
||||
catch (ServiceUnavailableException)
|
||||
{
|
||||
//migrating, to be expected
|
||||
if (DateTimeOffset.Now > giveUpAt)
|
||||
throw;
|
||||
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
|
||||
}
|
||||
} while (true);
|
||||
|
||||
// Dump swagger to disk
|
||||
// This is purely for CI
|
||||
var webRequest = WebRequest.Create(Url.ToString() + "swagger/v1/swagger.json");
|
||||
using (var response = webRequest.GetResponse())
|
||||
using (var content = response.GetResponseStream())
|
||||
using (var output = new FileStream(@"C:\swagger.json", FileMode.Create))
|
||||
{
|
||||
await content.CopyToAsync(output);
|
||||
}
|
||||
}
|
||||
|
||||
await runTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user