diff --git a/src/Tgstation.Server.Api/Models/InstanceUser.cs b/src/Tgstation.Server.Api/Models/InstanceUser.cs index 4e25521934..58ed24033c 100644 --- a/src/Tgstation.Server.Api/Models/InstanceUser.cs +++ b/src/Tgstation.Server.Api/Models/InstanceUser.cs @@ -13,13 +13,15 @@ namespace Tgstation.Server.Api.Models /// The of the the belongs to /// [Permissions(DenyWrite = true)] - public long UserId { get; set; } + [Required] + public long? UserId { get; set; } /// /// The of the the belongs to /// [Permissions(DenyWrite = true)] - public long InstanceId { get; set; } + [Required] + public long? InstanceId { get; set; } /// /// The of the diff --git a/src/Tgstation.Server.Host/Controllers/InstanceController.cs b/src/Tgstation.Server.Host/Controllers/InstanceController.cs index d5665f6a4b..fb18f46897 100644 --- a/src/Tgstation.Server.Host/Controllers/InstanceController.cs +++ b/src/Tgstation.Server.Host/Controllers/InstanceController.cs @@ -3,6 +3,7 @@ using Microsoft.EntityFrameworkCore; using System; using System.Linq; using System.Net; +using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api.Rights; @@ -63,7 +64,7 @@ namespace Tgstation.Server.Host.Controllers DreamMakerSettings = new DreamMakerSettings(), Name = model.Name, Online = false, - Path = model.Path, + Path = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? model.Path.ToUpperInvariant() : model.Path, RepositorySettings = new RepositorySettings() }; @@ -91,15 +92,12 @@ namespace Tgstation.Server.Host.Controllers throw; } } - catch (Exception e) + catch (DbUpdateConcurrencyException e) { return Conflict(new { message = e.Message }); } - - model.Online = newInstance.Online; - model.Id = newInstance.Id; - - return Json(model); + + return Json(newInstance.ToApi()); } /// diff --git a/src/Tgstation.Server.Host/Controllers/InstanceUserController.cs b/src/Tgstation.Server.Host/Controllers/InstanceUserController.cs new file mode 100644 index 0000000000..abe94e092c --- /dev/null +++ b/src/Tgstation.Server.Host/Controllers/InstanceUserController.cs @@ -0,0 +1,161 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using System; +using System.Linq; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using Tgstation.Server.Api.Models; +using Tgstation.Server.Api.Rights; +using Tgstation.Server.Host.Models; +using Tgstation.Server.Host.Security; +using Z.EntityFramework.Plus; + +namespace Tgstation.Server.Host.Controllers +{ + /// + /// For managing s + /// + [Route("/" + nameof(Models.InstanceUser))] + public sealed class InstanceUserController : ModelController + { + /// + /// The for the + /// + readonly ILogger logger; + + /// + /// Construct a + /// + /// The for the + /// The for the + /// The value of + public InstanceUserController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger logger) : base(databaseContext, authenticationContextFactory) + { + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + /// + /// Checks a for errors + /// + /// The to check + /// A explaining any errors, if none + BadRequestObjectResult StandardModelChecks(Api.Models.InstanceUser model) + { + if (model == null) + throw new ArgumentNullException(nameof(model)); + + if (model.InstanceId.HasValue && model.InstanceId != Instance.Id) + return BadRequest(new { message = "InstanceId does not match headers!" }); + + if (!model.InstanceId.HasValue) + return BadRequest(new { message = "Missing UserId!" }); + + return null; + } + + /// + [TgsAuthorize(AdministrationRights.EditUsers)] + public override async Task Create([FromBody] Api.Models.InstanceUser model, CancellationToken cancellationToken) + { + var test = StandardModelChecks(model); + if (test != null) + return test; + + var dbUser = new Models.InstanceUser + { + ByondRights = model.ByondRights ?? ByondRights.None, + ChatSettingsRights = model.ChatSettingsRights ?? ChatSettingsRights.None, + ConfigurationRights = model.ConfigurationRights ?? ConfigurationRights.None, + DreamDaemonRights = model.DreamDaemonRights ?? DreamDaemonRights.None, + DreamMakerRights = model.DreamMakerRights ?? DreamMakerRights.None, + RepositoryRights = model.RepositoryRights ?? RepositoryRights.None, + UserId = model.UserId + }; + + var ourInstance = new Models.Instance + { + Id = Instance.Id + }; + + DatabaseContext.Instances.Attach(ourInstance); + + ourInstance.InstanceUsers.Add(dbUser); + + try + { + await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); + } + catch (DbUpdateConcurrencyException e) + { + return Conflict(new { message = e.Message }); + } + return Json(dbUser.ToApi()); + } + + /// + [TgsAuthorize(AdministrationRights.EditUsers)] + public override async Task Update([FromBody] Api.Models.InstanceUser model, CancellationToken cancellationToken) + { + var test = StandardModelChecks(model); + if (test != null) + return test; + + var originalUser = await DatabaseContext.Instances.Where(x => x.Id == Instance.Id).SelectMany(x => x.InstanceUsers).Where(x => x.UserId == model.UserId).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); + if (originalUser == null) + return StatusCode((int)HttpStatusCode.Gone); + + originalUser.ByondRights = model.ByondRights ?? originalUser.ByondRights; + originalUser.ChatSettingsRights = model.ChatSettingsRights ?? originalUser.ChatSettingsRights; + originalUser.ConfigurationRights = model.ConfigurationRights ?? originalUser.ConfigurationRights; + originalUser.DreamDaemonRights = model.DreamDaemonRights ?? originalUser.DreamDaemonRights; + originalUser.DreamMakerRights = model.DreamMakerRights ?? originalUser.DreamMakerRights; + + try + { + await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); + } + catch (DbUpdateConcurrencyException e) + { + return Conflict(new { message = e.Message }); + } + return Json(originalUser.ToApi()); + } + + /// + [TgsAuthorize] + public override Task Read(CancellationToken cancellationToken) => Task.FromResult(AuthenticationContext.InstanceUser != null ? (IActionResult)Json(AuthenticationContext.InstanceUser.ToApi()) : NotFound()); + + /// + [TgsAuthorize(InstanceUserRights.ReadUsers)] + public override async Task 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())); + } + + /// + [TgsAuthorize(InstanceUserRights.ReadUsers)] + public override async Task GetId(long id, CancellationToken cancellationToken) + { + //this functions as userId + var user = await DatabaseContext.Instances.Where(x => x.Id == id).SelectMany(x => x.InstanceUsers).Where(x => x.UserId == id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); + if (user == default) + return NotFound(); + return Json(user.ToApi()); + } + + /// + [TgsAuthorize(AdministrationRights.EditUsers)] + public override async Task Delete([FromBody] Api.Models.InstanceUser model, CancellationToken cancellationToken) + { + var test = StandardModelChecks(model); + if (test != null) + return test; + + await DatabaseContext.Instances.Where(x => x.Id == Instance.Id).SelectMany(x => x.InstanceUsers).Where(x => x.UserId == model.UserId).DeleteAsync(cancellationToken).ConfigureAwait(false); + return Ok(); + } + } +} diff --git a/src/Tgstation.Server.Host/Controllers/JobController.cs b/src/Tgstation.Server.Host/Controllers/JobController.cs index a84d50c8f5..cefec12234 100644 --- a/src/Tgstation.Server.Host/Controllers/JobController.cs +++ b/src/Tgstation.Server.Host/Controllers/JobController.cs @@ -40,7 +40,7 @@ namespace Tgstation.Server.Host.Controllers IQueryable query = DatabaseContext.Jobs; if (Instance != null) { - if (!AuthenticationContext.InstanceUser.AnyRights) + if (AuthenticationContext.InstanceUser?.AnyRights != true) return Forbid(); query = query.Where(x => x.Instance.Id == Instance.Id); } diff --git a/src/Tgstation.Server.Host/Controllers/UsersController.cs b/src/Tgstation.Server.Host/Controllers/UserController.cs similarity index 89% rename from src/Tgstation.Server.Host/Controllers/UsersController.cs rename to src/Tgstation.Server.Host/Controllers/UserController.cs index c7ccafa5d7..50be28d1dd 100644 --- a/src/Tgstation.Server.Host/Controllers/UsersController.cs +++ b/src/Tgstation.Server.Host/Controllers/UserController.cs @@ -18,32 +18,32 @@ namespace Tgstation.Server.Host.Controllers /// For managing s /// [Route("/" + nameof(Models.User))] - public sealed class UsersController : ModelController + public sealed class UserController : ModelController { /// - /// The for the + /// The for the /// readonly ISystemIdentityFactory systemIdentityFactory; /// - /// The for the + /// The for the /// readonly ICryptographySuite cryptographySuite; /// - /// The for the + /// The for the /// - readonly ILogger logger; + readonly ILogger logger; /// - /// Construct a + /// Construct a /// /// The for the /// The for the /// The value of /// The value of /// The value of - public UsersController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ISystemIdentityFactory systemIdentityFactory, ICryptographySuite cryptographySuite, ILogger logger) : base(databaseContext, authenticationContextFactory) + public UserController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ISystemIdentityFactory systemIdentityFactory, ICryptographySuite cryptographySuite, ILogger logger) : base(databaseContext, authenticationContextFactory) { this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); this.systemIdentityFactory = systemIdentityFactory ?? throw new ArgumentNullException(nameof(systemIdentityFactory)); @@ -110,9 +110,9 @@ namespace Tgstation.Server.Host.Controllers throw; } } - catch (DbUpdateConcurrencyException) + catch (DbUpdateConcurrencyException e) { - return Conflict(); + return Conflict(new { message = e.Message }); } return Json(dbUser.ToApi()); @@ -160,7 +160,7 @@ namespace Tgstation.Server.Host.Controllers public override async Task List(CancellationToken cancellationToken) { var users = await DatabaseContext.Users.ToListAsync(cancellationToken).ConfigureAwait(false); - return Json(users); + return Json(users.Select(x => x.ToApi())); } /// @@ -170,7 +170,7 @@ namespace Tgstation.Server.Host.Controllers var user = await DatabaseContext.Users.Where(x => x.Id == id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); if (user == default) return NotFound(); - return Json(user); + return Json(user.ToApi()); } } } diff --git a/src/Tgstation.Server.Host/Models/DatabaseContext.cs b/src/Tgstation.Server.Host/Models/DatabaseContext.cs index 5ca0997c1e..44099444d8 100644 --- a/src/Tgstation.Server.Host/Models/DatabaseContext.cs +++ b/src/Tgstation.Server.Host/Models/DatabaseContext.cs @@ -106,13 +106,13 @@ namespace Tgstation.Server.Host.Models // build default model. LogModelBuilderHelper.Build(modelBuilder.Entity()); modelBuilder.Entity().ToTable(nameof(Logs)); - + modelBuilder.Entity().HasIndex(x => x.Path).IsUnique(); modelBuilder.Entity().HasIndex(x => x.Commit).IsUnique(); var user = modelBuilder.Entity(); user.HasIndex(x => x.CanonicalName).IsUnique(); user.HasOne(x => x.CreatedBy).WithMany(x => x.CreatedUsers).OnDelete(DeleteBehavior.Restrict); - modelBuilder.Entity().HasIndex(x => new { x.UserId, x.Instance }).IsUnique(); + modelBuilder.Entity().HasIndex(x => new { x.UserId, x.InstanceId }).IsUnique(); var chatChannel = modelBuilder.Entity(); chatChannel.HasIndex(x => new { x.ChatSettingsId, x.IrcChannel }).IsUnique(); diff --git a/src/Tgstation.Server.Host/Models/Instance.cs b/src/Tgstation.Server.Host/Models/Instance.cs index a9d82058d2..290417b1fe 100644 --- a/src/Tgstation.Server.Host/Models/Instance.cs +++ b/src/Tgstation.Server.Host/Models/Instance.cs @@ -1,11 +1,12 @@ using System.Collections.Generic; +using Tgstation.Server.Api.Models; namespace Tgstation.Server.Host.Models { /// /// Represents an in the database /// - public sealed class Instance : Api.Models.Instance + public sealed class Instance : Api.Models.Instance, IApiConvertable { /// /// The for the @@ -51,5 +52,16 @@ namespace Tgstation.Server.Host.Models /// The in the /// public List Jobs { get; set; } + + /// + public Api.Models.Instance ToApi() => new Api.Models.Instance + { + AutoUpdateInterval = AutoUpdateInterval, + ConfigurationAllowed = ConfigurationAllowed, + Id = Id, + Name = Name, + Path = Path, + Online = Online + }; } } diff --git a/src/Tgstation.Server.Host/Models/InstanceUser.cs b/src/Tgstation.Server.Host/Models/InstanceUser.cs index 37fc313e04..43d9fae655 100644 --- a/src/Tgstation.Server.Host/Models/InstanceUser.cs +++ b/src/Tgstation.Server.Host/Models/InstanceUser.cs @@ -1,9 +1,10 @@ using System.ComponentModel.DataAnnotations; +using Tgstation.Server.Api.Models; namespace Tgstation.Server.Host.Models { /// - public sealed class InstanceUser : Api.Models.InstanceUser + public sealed class InstanceUser : Api.Models.InstanceUser, IApiConvertable { /// /// The row Id @@ -24,5 +25,18 @@ namespace Tgstation.Server.Host.Models ConfigurationRights != Api.Rights.ConfigurationRights.None || DreamDaemonRights != Api.Rights.DreamDaemonRights.None || DreamMakerRights != Api.Rights.DreamMakerRights.None; + + /// + public Api.Models.InstanceUser ToApi() => new Api.Models.InstanceUser + { + ByondRights = ByondRights, + ChatSettingsRights = ChatSettingsRights, + ConfigurationRights = ConfigurationRights, + DreamDaemonRights = DreamDaemonRights, + DreamMakerRights = DreamMakerRights, + InstanceId = InstanceId, + RepositoryRights = RepositoryRights, + UserId = UserId + }; } }