mirror of
https://github.com/tgstation/tgstation-server.git
synced 2026-08-27 07:04:57 +01:00
ConfigurationController
This commit is contained in:
@@ -1,16 +1,40 @@
|
||||
using Tgstation.Server.Api.Models.Internal;
|
||||
using Tgstation.Server.Api.Rights;
|
||||
|
||||
namespace Tgstation.Server.Api.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a static game file. Create and delete actions uncerimonuously overwrite/delete files
|
||||
/// Represents a game configuration file. Create and delete actions uncerimonuously overwrite/delete files
|
||||
/// </summary>
|
||||
#pragma warning disable CA1724 // System.Configuration name conflict
|
||||
public sealed class ConfigurationFile : ConfigurationFileMetadata
|
||||
#pragma warning restore CA1724 // System.Configuration name conflict
|
||||
[Model(RightsType.Configuration, CanCrud = true, CanList = true, RequiresInstance = true, ReadRight = ConfigurationRights.Read, WriteRight = ConfigurationRights.Write)]
|
||||
public sealed class ConfigurationFile
|
||||
{
|
||||
/// <summary>
|
||||
/// The content of the <see cref="ConfigurationFile"/>. Will be <see langword="null"/> if <see cref="ConfigurationFileMetadata.ReadDenied"/> is <see langword="true"/> or during listing operations
|
||||
/// The path to the <see cref="ConfigurationFile"/> file
|
||||
/// </summary>
|
||||
[Permissions(DenyWrite = true)]
|
||||
public string Path { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If read access to the <see cref="ConfigurationFile"/> file was denied
|
||||
/// </summary>
|
||||
[Permissions(DenyWrite = true)]
|
||||
public bool? ReadDenied { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If <see cref="Path"/> represents a directory. Will only be <see langword="true"/> if <see cref="ReadDenied"/> is <see langword="true"/>
|
||||
/// </summary>
|
||||
[Permissions(DenyWrite = true)]
|
||||
public bool? IsDirectory { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The MD5 hash of the file when last read by the user. Will be <see langword="null"/> if <see cref="ReadDenied"/> is <see langword="true"/>. If this doesn't match during update actions, the write will be denied with error code 409
|
||||
/// </summary>
|
||||
[Permissions(DenyWrite = true)]
|
||||
public string LastReadHash { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The content of the <see cref="ConfigurationFile"/>. Will be <see langword="null"/> if <see cref="ReadDenied"/> is <see langword="true"/> or during listing operations
|
||||
/// </summary>
|
||||
public byte[] Content { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
using Tgstation.Server.Api.Rights;
|
||||
|
||||
namespace Tgstation.Server.Api.Models.Internal
|
||||
{
|
||||
/// <summary>
|
||||
/// Metadata about a <see cref="ConfigurationFile"/> file
|
||||
/// </summary>
|
||||
[Model(RightsType.Configuration, CanCrud = true, CanList = true, RequiresInstance = true, ReadRight = ConfigurationRights.Read, WriteRight = ConfigurationRights.Write)]
|
||||
public class ConfigurationFileMetadata
|
||||
{
|
||||
/// <summary>
|
||||
/// The path to the <see cref="ConfigurationFile"/> file
|
||||
/// </summary>
|
||||
public string Path { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If read access to the <see cref="ConfigurationFile"/> file was denied
|
||||
/// </summary>
|
||||
[Permissions(DenyWrite = true)]
|
||||
public bool ReadDenied { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If <see cref="Path"/> represents a directory. Will only be <see langword="true"/> if <see cref="ReadDenied"/> is <see langword="true"/>
|
||||
/// </summary>
|
||||
[Permissions(DenyWrite = true)]
|
||||
public bool IsDirectory { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The MD5 hash of the file when last read by the user. Will be <see langword="null"/> if <see cref="ReadDenied"/> is <see langword="true"/>. If this doesn't match during update actions, the write will be denied with error code 409
|
||||
/// </summary>
|
||||
public string LastReadHash { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,7 @@ namespace Tgstation.Server.Api.Rights
|
||||
/// </summary>
|
||||
List = 64,
|
||||
/// <summary>
|
||||
/// User can change <see cref="Models.Instance.ConfigurationAllowed"/>
|
||||
/// User can change <see cref="Models.Instance.ConfigurationType"/>
|
||||
/// </summary>
|
||||
SetConfiguration = 128,
|
||||
/// <summary>
|
||||
|
||||
@@ -19,14 +19,6 @@ namespace Tgstation.Server.Api.Rights
|
||||
/// <summary>
|
||||
/// Allow write access to <see cref="Models.InstanceUser"/> for the <see cref="Models.Instance"/>
|
||||
/// </summary>
|
||||
WriteUsers = 2,
|
||||
/// <summary>
|
||||
/// User can read configuration files if the instance allows it
|
||||
/// </summary>
|
||||
ReadConfiguration = 4,
|
||||
/// <summary>
|
||||
/// Users can write configuration files if the instance allows it
|
||||
/// </summary>
|
||||
EditConfiguration = 8,
|
||||
WriteUsers = 2
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Tgstation.Server.Api.Models.Internal;
|
||||
using Tgstation.Server.Api.Models;
|
||||
using Tgstation.Server.Host.Security;
|
||||
|
||||
namespace Tgstation.Server.Host.Components
|
||||
@@ -28,13 +28,13 @@ namespace Tgstation.Server.Host.Components
|
||||
Task SymlinkStaticFilesTo(string destination, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Get <see cref="ConfigurationFileMetadata"/> for all items in a given <paramref name="configurationRelativePath"/>
|
||||
/// Get <see cref="ConfigurationFile"/> for all items in a given <paramref name="configurationRelativePath"/>
|
||||
/// </summary>
|
||||
/// <param name="configurationRelativePath">The relative path in the Configuration directory</param>
|
||||
/// <param name="systemIdentity">The <see cref="ISystemIdentity"/> for the operation. If <see langword="null"/>, the operation will be performed as the user of the <see cref="Core.Application"/></param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="ConfigurationFileMetadata"/> for the items in the directory</returns>
|
||||
Task<IReadOnlyList<ConfigurationFileMetadata>> ListDirectory(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken);
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="ConfigurationFile"/>s for the items in the directory. <see cref="ConfigurationFile.Content"/> and <see cref="ConfigurationFile.LastReadHash"/> will both be <see langword="null"/></returns>
|
||||
Task<IReadOnlyList<ConfigurationFile>> ListDirectory(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Reads a given <paramref name="configurationRelativePath"/>
|
||||
@@ -43,7 +43,7 @@ namespace Tgstation.Server.Host.Components
|
||||
/// <param name="systemIdentity">The <see cref="ISystemIdentity"/> for the operation. If <see langword="null"/>, the operation will be performed as the user of the <see cref="Core.Application"/></param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="Api.Models.ConfigurationFile"/> of the file</returns>
|
||||
Task<Api.Models.ConfigurationFile> Read(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken);
|
||||
Task<ConfigurationFile> Read(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Writes to a given <paramref name="configurationRelativePath"/>
|
||||
@@ -52,7 +52,7 @@ namespace Tgstation.Server.Host.Components
|
||||
/// <param name="systemIdentity">The <see cref="ISystemIdentity"/> for the operation. If <see langword="null"/>, the operation will be performed as the user of the <see cref="Core.Application"/></param>
|
||||
/// <param name="data">The data to write. If <see langword="null"/>, the file is deleted</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation. Usage may result in partial writes</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in <see langword="true"/> if the operation succeeded, <see langword="false"/> if it failed due to permission errors</returns>
|
||||
Task<bool> Write(string configurationRelativePath, ISystemIdentity systemIdentity, byte[] data, CancellationToken cancellationToken);
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="ConfigurationFile.LastReadHash"/> value for <paramref name="data"/></returns>
|
||||
Task<string> Write(string configurationRelativePath, ISystemIdentity systemIdentity, byte[] data, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Tgstation.Server.Api.Models;
|
||||
using Tgstation.Server.Api.Rights;
|
||||
using Tgstation.Server.Host.Components;
|
||||
using Tgstation.Server.Host.Models;
|
||||
using Tgstation.Server.Host.Security;
|
||||
|
||||
namespace Tgstation.Server.Host.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="ModelController{TModel}"/> for <see cref="ConfigurationFile"/>s
|
||||
/// </summary>
|
||||
[Route("/Configuration")]
|
||||
public sealed class ConfigurationController : ModelController<ConfigurationFile>
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="IInstanceManager"/> for the <see cref="ConfigurationController"/>
|
||||
/// </summary>
|
||||
readonly IInstanceManager instanceManager;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ILogger"/> for the <see cref="ConfigurationController"/>
|
||||
/// </summary>
|
||||
readonly ILogger<ConfigurationController> logger;
|
||||
|
||||
/// <summary>
|
||||
/// Construct a <see cref="UserController"/>
|
||||
/// </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="instanceManager">The value of <see cref="instanceManager"/></param>
|
||||
/// <param name="logger">The value of <see cref="logger"/></param>
|
||||
public ConfigurationController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, ILogger<ConfigurationController> logger) : base(databaseContext, authenticationContextFactory)
|
||||
{
|
||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
[TgsAuthorize(ConfigurationRights.Write)]
|
||||
public override async Task<IActionResult> Update([FromBody] ConfigurationFile model, CancellationToken cancellationToken)
|
||||
{
|
||||
if (Instance.ConfigurationType == ConfigurationType.Disallowed)
|
||||
return Forbid();
|
||||
|
||||
var config = instanceManager.GetInstance(Instance).Configuration;
|
||||
try
|
||||
{
|
||||
var originalFile = await config.Read(model.Path, AuthenticationContext.SystemIdentity, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (model.LastReadHash != originalFile.LastReadHash)
|
||||
return Conflict();
|
||||
|
||||
originalFile.LastReadHash = await config.Write(model.Path, AuthenticationContext.SystemIdentity, model.Content, cancellationToken).ConfigureAwait(false);
|
||||
originalFile.Content = null;
|
||||
|
||||
return Json(originalFile);
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
return BadRequest(new { message = "Attempted to delete required folder!" });
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
return Forbid();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the contents of a file at a <paramref name="path"/>
|
||||
/// </summary>
|
||||
/// <param name="path">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>
|
||||
[HttpGet("/File/{path}")]
|
||||
[TgsAuthorize(ConfigurationRights.Read)]
|
||||
public async Task<IActionResult> File(string path, CancellationToken cancellationToken)
|
||||
{
|
||||
if (Instance.ConfigurationType == ConfigurationType.Disallowed)
|
||||
return Forbid();
|
||||
|
||||
var result = await instanceManager.GetInstance(Instance).Configuration.Read(path, AuthenticationContext.SystemIdentity, cancellationToken).ConfigureAwait(false);
|
||||
if (result == null || result.IsDirectory.Value)
|
||||
return NotFound();
|
||||
|
||||
return Json(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the contents of a directory at a <paramref name="path"/>
|
||||
/// </summary>
|
||||
/// <param name="path">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("/Directory/{path}")]
|
||||
[TgsAuthorize(ConfigurationRights.List)]
|
||||
public async Task<IActionResult> Directory(string path, CancellationToken cancellationToken)
|
||||
{
|
||||
if (Instance.ConfigurationType == ConfigurationType.Disallowed)
|
||||
return Forbid();
|
||||
|
||||
var result = await instanceManager.GetInstance(Instance).Configuration.ListDirectory(path, AuthenticationContext.SystemIdentity, cancellationToken).ConfigureAwait(false);
|
||||
if (result == null)
|
||||
return NotFound();
|
||||
|
||||
return Json(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user