ConfigurationController

This commit is contained in:
Jordan Brown
2018-07-22 18:02:41 -04:00
parent 7cf4e870b2
commit cee2e76bdf
6 changed files with 151 additions and 55 deletions
@@ -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);
}
}
}