diff --git a/src/Tgstation.Server.Api/Models/Administration.cs b/src/Tgstation.Server.Api/Models/Administration.cs index 1aed2dad16..374d82a178 100644 --- a/src/Tgstation.Server.Api/Models/Administration.cs +++ b/src/Tgstation.Server.Api/Models/Administration.cs @@ -7,7 +7,6 @@ namespace Tgstation.Server.Api.Models /// /// Metadata about an installation /// - [Model(RightsType.Administration)] public sealed class Administration : Internal.ServerSettings { /// diff --git a/src/Tgstation.Server.Api/Models/ChatChannel.cs b/src/Tgstation.Server.Api/Models/ChatChannel.cs index 737b41203d..1ad3c74629 100644 --- a/src/Tgstation.Server.Api/Models/ChatChannel.cs +++ b/src/Tgstation.Server.Api/Models/ChatChannel.cs @@ -3,13 +3,8 @@ /// /// Indicates a chat channel /// - public sealed class ChatChannel + public class ChatChannel { - /// - /// The column ID - /// - public long Id { get; set; } - /// /// The IRC channel name /// diff --git a/src/Tgstation.Server.Api/Models/ChatSettings.cs b/src/Tgstation.Server.Api/Models/ChatSettings.cs index 593e6c4a8c..a6350f0aa3 100644 --- a/src/Tgstation.Server.Api/Models/ChatSettings.cs +++ b/src/Tgstation.Server.Api/Models/ChatSettings.cs @@ -1,63 +1,21 @@ using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; using Tgstation.Server.Api.Rights; namespace Tgstation.Server.Api.Models { - /// - /// Manage the server chat bots - /// - [Model(RightsType.Chat, RequiresInstance = true)] - public class ChatSettings + /// + public sealed class ChatSettings : Internal.ChatSettings { - /// - /// If the IRC client is enabled - /// - [Permissions(WriteRight = ChatSettingsRights.SetIrcEnabled)] - public bool IrcEnabled { get; set; } - - /// - /// The IRC server name - /// - [Permissions(ReadRight = ChatSettingsRights.SetIrcSettings, WriteRight = ChatSettingsRights.SetIrcSettings)] - [Required] - public string IrcHost { get; set; } - - /// - /// The IRC server port - /// - [Permissions(ReadRight = ChatSettingsRights.SetIrcSettings, WriteRight = ChatSettingsRights.SetIrcSettings)] - public ushort IrcPort { get; set; } - - /// - /// The IRC server NickServ password - /// - [Permissions(ReadRight = ChatSettingsRights.SetIrcSettings, WriteRight = ChatSettingsRights.SetIrcSettings)] - public string IrcNickServPassword { get; set; } - - /// - /// If the Discord bot is enabled - /// - [Permissions(WriteRight = ChatSettingsRights.SetDiscordEnabled)] - public bool DiscordEnabled { get; set; } - - /// - /// The Discord bot token - /// - [Permissions(ReadRight = ChatSettingsRights.SetDiscordSettings, WriteRight = ChatSettingsRights.SetDiscordSettings)] - public string DiscordBotToken { get; set; } - /// /// Channels the bot should listen/announce in and allow admin commands /// [Permissions(WriteRight = ChatSettingsRights.SetDiscordChannels)] - public virtual List AdminChannels { get; set; } + public List AdminChannels { get; set; } /// /// Channels the Discord bot should listen/announce in /// [Permissions(WriteRight = ChatSettingsRights.SetDiscordChannels)] - public virtual List GeneralChannels { get; set; } + public List GeneralChannels { get; set; } } } diff --git a/src/Tgstation.Server.Api/Models/Internal/ChatSettings.cs b/src/Tgstation.Server.Api/Models/Internal/ChatSettings.cs new file mode 100644 index 0000000000..b98d62b906 --- /dev/null +++ b/src/Tgstation.Server.Api/Models/Internal/ChatSettings.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Tgstation.Server.Api.Rights; + +namespace Tgstation.Server.Api.Models.Internal +{ + /// + /// Manage the server chat bots + /// + [Model(RightsType.Chat, RequiresInstance = true)] + public class ChatSettings + { + /// + /// If the IRC client is enabled + /// + [Permissions(WriteRight = ChatSettingsRights.SetIrcEnabled)] + public bool IrcEnabled { get; set; } + + /// + /// The IRC server name + /// + [Permissions(ReadRight = ChatSettingsRights.SetIrcSettings, WriteRight = ChatSettingsRights.SetIrcSettings)] + [Required] + public string IrcHost { get; set; } + + /// + /// The IRC server port + /// + [Permissions(ReadRight = ChatSettingsRights.SetIrcSettings, WriteRight = ChatSettingsRights.SetIrcSettings)] + public ushort IrcPort { get; set; } + + /// + /// The IRC server NickServ password + /// + [Permissions(ReadRight = ChatSettingsRights.SetIrcSettings, WriteRight = ChatSettingsRights.SetIrcSettings)] + public string IrcNickServPassword { get; set; } + + /// + /// If the Discord bot is enabled + /// + [Permissions(WriteRight = ChatSettingsRights.SetDiscordEnabled)] + public bool DiscordEnabled { get; set; } + + /// + /// The Discord bot token + /// + [Permissions(ReadRight = ChatSettingsRights.SetDiscordSettings, WriteRight = ChatSettingsRights.SetDiscordSettings)] + public string DiscordBotToken { get; set; } + } +} diff --git a/src/Tgstation.Server.Api/Models/Internal/ServerSettings.cs b/src/Tgstation.Server.Api/Models/Internal/ServerSettings.cs index 7aa96da12b..9a46e42237 100644 --- a/src/Tgstation.Server.Api/Models/Internal/ServerSettings.cs +++ b/src/Tgstation.Server.Api/Models/Internal/ServerSettings.cs @@ -2,6 +2,7 @@ namespace Tgstation.Server.Api.Models.Internal { + [Model(RightsType.Administration)] public class ServerSettings { /// diff --git a/src/Tgstation.Server.Api/Models/Internal/User.cs b/src/Tgstation.Server.Api/Models/Internal/User.cs new file mode 100644 index 0000000000..0368a0d1b9 --- /dev/null +++ b/src/Tgstation.Server.Api/Models/Internal/User.cs @@ -0,0 +1,48 @@ +using System; +using System.ComponentModel.DataAnnotations; +using Tgstation.Server.Api.Rights; + +namespace Tgstation.Server.Api.Models.Internal +{ + /// + /// Represents a server + /// + [Model(RightsType.Administration, WriteRight = AdministrationRights.EditUsers)] + public class User + { + /// + /// The ID of the + /// + [Permissions(DenyWrite = true)] + public long Id { get; set; } + + [Permissions(DenyWrite = true)] + [Required] + public DateTimeOffset CreatedAt { get; set; } + + /// + /// The SID/UID of the on Windows/POSIX respectively + /// + [Permissions(DenyWrite = true)] + public string SystemIdentifier { get; set; } + + /// + /// The name of the + /// + [Permissions(WriteRight = AdministrationRights.EditUsers)] + [Required] + public string Name { get; set; } + + /// + /// The for the + /// + [Permissions(WriteRight = AdministrationRights.EditUsers)] + public AdministrationRights AdministrationRights { get; set; } + + /// + /// The for the + /// + [Permissions(WriteRight = AdministrationRights.EditUsers)] + public InstanceManagerRights InstanceManagerRights { get; set; } + } +} \ No newline at end of file diff --git a/src/Tgstation.Server.Api/Models/User.cs b/src/Tgstation.Server.Api/Models/User.cs index 70bd28bed1..bb8c604138 100644 --- a/src/Tgstation.Server.Api/Models/User.cs +++ b/src/Tgstation.Server.Api/Models/User.cs @@ -1,51 +1,11 @@ -using System; -using System.ComponentModel.DataAnnotations; -using Tgstation.Server.Api.Rights; - -namespace Tgstation.Server.Api.Models +namespace Tgstation.Server.Api.Models { /// /// Represents a server /// - [Model(RightsType.Administration, WriteRight = AdministrationRights.EditUsers)] - public class User + public sealed class User : Internal.User { - /// - /// The ID of the - /// - [Permissions(DenyWrite = true)] - public long Id { get; set; } - - [Permissions(DenyWrite = true)] - [Required] - public DateTimeOffset CreatedAt { get; set; } - - /// - /// The SID/UID of the on Windows/POSIX respectively - /// - [Permissions(DenyWrite = true)] - public string SystemIdentifier { get; set; } - [Permissions(DenyWrite = true)] public bool Enabled { get; set; } - - /// - /// The name of the - /// - [Permissions(WriteRight = AdministrationRights.EditUsers)] - [Required] - public string Name { get; set; } - - /// - /// The for the - /// - [Permissions(WriteRight = AdministrationRights.EditUsers)] - public AdministrationRights AdministrationRights { get; set; } - - /// - /// The for the - /// - [Permissions(WriteRight = AdministrationRights.EditUsers)] - public InstanceManagerRights InstanceManagerRights { get; set; } } } \ No newline at end of file diff --git a/src/Tgstation.Server.Api/Rights/RightsHelper.cs b/src/Tgstation.Server.Api/Rights/RightsHelper.cs index 0fc6e5efcc..0f754c4d13 100644 --- a/src/Tgstation.Server.Api/Rights/RightsHelper.cs +++ b/src/Tgstation.Server.Api/Rights/RightsHelper.cs @@ -15,7 +15,6 @@ namespace Tgstation.Server.Api.Rights { { RightsType.Administration, typeof(AdministrationRights) }, { RightsType.InstanceManager, typeof(InstanceManagerRights) }, - { RightsType.Token, typeof(TokenRights) }, { RightsType.Repository, typeof(RepositoryRights) }, { RightsType.Byond, typeof(ByondRights) }, diff --git a/src/Tgstation.Server.Api/Rights/RightsType.cs b/src/Tgstation.Server.Api/Rights/RightsType.cs index a15d9d2d03..5b4ff0722c 100644 --- a/src/Tgstation.Server.Api/Rights/RightsType.cs +++ b/src/Tgstation.Server.Api/Rights/RightsType.cs @@ -13,11 +13,6 @@ /// /// InstanceManager, - /// - /// - /// - Token, - /// /// /// diff --git a/src/Tgstation.Server.Api/Rights/TokenRights.cs b/src/Tgstation.Server.Api/Rights/TokenRights.cs deleted file mode 100644 index 98f5bf3f03..0000000000 --- a/src/Tgstation.Server.Api/Rights/TokenRights.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; - -namespace Tgstation.Server.Api.Rights -{ - /// - /// Rights for s - /// - [Flags] - public enum TokenRights - { - /// - /// User has no rights - /// - None = 0, - /// - /// User can create s - /// - Create = 1, - /// - /// User can delete s - /// - Delete = 2, - /// - /// User can list all their s - /// - List = 4, - /// - /// User can perform all the actions they can for themselves on behalf of other users except - /// - Admin = 8, - } -} diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index 2a8d4f6f78..3e392b41d2 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -1,6 +1,7 @@ using Cyberboss.AspNetCore.AsyncInitializer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using System; @@ -46,6 +47,10 @@ namespace Tgstation.Server.Host.Core services.AddDbContext(); services.AddScoped(x => x.GetRequiredService()); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton, PasswordHasher>(); } /// diff --git a/src/Tgstation.Server.Host/Core/CryptographySuite.cs b/src/Tgstation.Server.Host/Core/CryptographySuite.cs new file mode 100644 index 0000000000..9cd7294780 --- /dev/null +++ b/src/Tgstation.Server.Host/Core/CryptographySuite.cs @@ -0,0 +1,70 @@ +using Microsoft.AspNetCore.Identity; +using System; +using System.Security.Cryptography; +using System.Text; +using Tgstation.Server.Host.Models; + +namespace Tgstation.Server.Host.Core +{ + /// + sealed class CryptographySuite : ICryptographySuite + { + /// + /// The length of secure strings used in the application + /// + public const int SecureStringLength = 40; + + /// + /// Generates a secure ascii of length + /// + /// A secure ascii of length + static string GenerateSecureString() + { + using (var rng = new RNGCryptoServiceProvider()) + { + var byt = new byte[1]; + var result = new StringBuilder + { + Capacity = SecureStringLength + }; + while (result.Length < SecureStringLength) + { + rng.GetBytes(byt); + var chr = (char)byt[0]; + if (Char.IsLetterOrDigit(chr)) + result.Append(chr); + } + return result.ToString(); + } + } + + /// + /// The for the + /// + readonly IPasswordHasher passwordHasher; + + /// + /// Construct a + /// + /// The value of + public CryptographySuite(IPasswordHasher passwordHasher) => this.passwordHasher = passwordHasher ?? throw new ArgumentNullException(nameof(passwordHasher)); + + /// + public void RegenerateUserToken(User user) + { + if (user == null) + throw new ArgumentNullException(nameof(user)); + user.TokenSecret = GenerateSecureString(); + } + + /// + public void SetUserPassword(User user, string newPassword) + { + if (user == null) + throw new ArgumentNullException(nameof(user)); + if (String.IsNullOrEmpty(newPassword)) + throw new ArgumentNullException(nameof(newPassword)); + user.PasswordHash = passwordHasher.HashPassword(user, newPassword); + } + } +} diff --git a/src/Tgstation.Server.Host/Core/ICryptographySuite.cs b/src/Tgstation.Server.Host/Core/ICryptographySuite.cs new file mode 100644 index 0000000000..f773c17301 --- /dev/null +++ b/src/Tgstation.Server.Host/Core/ICryptographySuite.cs @@ -0,0 +1,23 @@ +using Tgstation.Server.Host.Models; + +namespace Tgstation.Server.Host.Core +{ + /// + /// Contains various cryptographic functions + /// + interface ICryptographySuite + { + /// + /// Sets a for a given + /// + /// The whos is to be set + /// The new password for the + void SetUserPassword(User user, string newPassword); + + /// + /// Sets a new for a givent + /// + /// The whos is to be set + void RegenerateUserToken(User user); + } +} diff --git a/src/Tgstation.Server.Host/Models/ChatChannel.cs b/src/Tgstation.Server.Host/Models/ChatChannel.cs new file mode 100644 index 0000000000..8429585cdf --- /dev/null +++ b/src/Tgstation.Server.Host/Models/ChatChannel.cs @@ -0,0 +1,10 @@ +namespace Tgstation.Server.Host.Models +{ + sealed class ChatChannel : Api.Models.ChatChannel + { + /// + /// The column ID + /// + public long Id { get; set; } + } +} diff --git a/src/Tgstation.Server.Host/Models/ChatSettings.cs b/src/Tgstation.Server.Host/Models/ChatSettings.cs index 0b798988c0..c62d8e23dd 100644 --- a/src/Tgstation.Server.Host/Models/ChatSettings.cs +++ b/src/Tgstation.Server.Host/Models/ChatSettings.cs @@ -1,10 +1,9 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using Tgstation.Server.Api.Models; namespace Tgstation.Server.Host.Models { - sealed class ChatSettings : Api.Models.ChatSettings + sealed class ChatSettings : Api.Models.Internal.ChatSettings { public long Id { get; set; } @@ -12,5 +11,9 @@ namespace Tgstation.Server.Host.Models [Required] public Instance Instance { get; set; } + + public List AdminChannels { get; set; } + + public List GeneralChannels { get; set; } } } diff --git a/src/Tgstation.Server.Host/Models/DatabaseContext.cs b/src/Tgstation.Server.Host/Models/DatabaseContext.cs index 5880046f97..1a6cf81e00 100644 --- a/src/Tgstation.Server.Host/Models/DatabaseContext.cs +++ b/src/Tgstation.Server.Host/Models/DatabaseContext.cs @@ -3,10 +3,14 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using System; +using System.Linq; +using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api.Models; +using Tgstation.Server.Api.Rights; using Tgstation.Server.Host.Configuration; +using Tgstation.Server.Host.Core; using ZNetCS.AspNetCore.Logging.EntityFrameworkCore; namespace Tgstation.Server.Host.Models @@ -49,6 +53,10 @@ namespace Tgstation.Server.Host.Models /// The for the /// readonly IHostingEnvironment hostingEnvironment; + /// + /// The for the + /// + readonly IDatabaseSeeder databaseSeeder; /// /// Construct a @@ -57,11 +65,13 @@ namespace Tgstation.Server.Host.Models /// The containing the value of /// The value of /// The value of - public DatabaseContext(DbContextOptions dbContextOptions, IOptions databaseConfigurationOptions, ILoggerFactory loggerFactory, IHostingEnvironment hostingEnvironment) : base(dbContextOptions) + /// The value of + public DatabaseContext(DbContextOptions dbContextOptions, IOptions databaseConfigurationOptions, ILoggerFactory loggerFactory, IHostingEnvironment hostingEnvironment, IDatabaseSeeder databaseSeeder) : base(dbContextOptions) { databaseConfiguration = databaseConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(databaseConfigurationOptions)); this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); this.hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment)); + this.databaseSeeder = databaseSeeder ?? throw new ArgumentNullException(nameof(databaseSeeder)); } /// @@ -98,13 +108,25 @@ namespace Tgstation.Server.Host.Models } /// - public Task GetServerSettings(CancellationToken cancellationToken) => ServerSettings.FirstOrDefaultAsync(cancellationToken); + public async Task GetServerSettings(CancellationToken cancellationToken) + { + var settings = await ServerSettings.FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); + if (settings == default(ServerSettings)) + { + settings = new ServerSettings(); + ServerSettings.Add(settings); + } + return settings; + } /// public async Task Initialize(CancellationToken cancellationToken) { - await Database.EnsureCreatedAsync(cancellationToken).ConfigureAwait(false); + var migrations = await Database.GetAppliedMigrationsAsync().ConfigureAwait(false); + var wasEmpty = !migrations.Any(); await Database.MigrateAsync(cancellationToken).ConfigureAwait(false); + if (wasEmpty) + await databaseSeeder.SeedDatabase(this, cancellationToken).ConfigureAwait(false); } /// diff --git a/src/Tgstation.Server.Host/Models/DatabaseSeeder.cs b/src/Tgstation.Server.Host/Models/DatabaseSeeder.cs new file mode 100644 index 0000000000..278692554e --- /dev/null +++ b/src/Tgstation.Server.Host/Models/DatabaseSeeder.cs @@ -0,0 +1,55 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Tgstation.Server.Api.Rights; +using Tgstation.Server.Host.Core; + +namespace Tgstation.Server.Host.Models +{ + /// + sealed class DatabaseSeeder : IDatabaseSeeder + { + /// + /// The default password mode admin password + /// + const string DefaultAdminPassword = "ISolemlySwearToDeleteTheDataDirectory"; + + /// + /// The default git repository to pull server updates from + /// + const string DefaultUpstreamRepository = "https://github.com/tgstation/tgstation-server"; + + /// + /// The for the + /// + readonly ICryptographySuite cryptographySuite; + + /// + /// Construct a + /// + /// The value of + public DatabaseSeeder(ICryptographySuite cryptographySuite) => this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite)); + + /// + public async Task SeedDatabase(IDatabaseContext databaseContext, CancellationToken cancellationToken) + { + var admin = new User + { + AdministrationRights = (AdministrationRights)~0, + CreatedAt = DateTimeOffset.Now, + InstanceManagerRights = (InstanceManagerRights)~0, + Name = "Admin" + }; + cryptographySuite.RegenerateUserToken(admin); + cryptographySuite.SetUserPassword(admin, DefaultAdminPassword); + databaseContext.Users.Add(admin); + + var serverSettings = await databaseContext.GetServerSettings(cancellationToken).ConfigureAwait(false); + + serverSettings.EnableTelemetry = true; + serverSettings.UpstreamRepository = DefaultUpstreamRepository; + + await databaseContext.Save(cancellationToken).ConfigureAwait(false); + } + } +} diff --git a/src/Tgstation.Server.Host/Models/IDatabaseSeeder.cs b/src/Tgstation.Server.Host/Models/IDatabaseSeeder.cs new file mode 100644 index 0000000000..f57620fd0a --- /dev/null +++ b/src/Tgstation.Server.Host/Models/IDatabaseSeeder.cs @@ -0,0 +1,19 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace Tgstation.Server.Host.Models +{ + /// + /// For initially seeding a database + /// + interface IDatabaseSeeder + { + /// + /// Initially seed a given + /// + /// The to seed + /// The for the operation + /// A representing the running operation + Task SeedDatabase(IDatabaseContext databaseContext, CancellationToken cancellationToken); + } +} diff --git a/src/Tgstation.Server.Host/Models/ServerSettings.cs b/src/Tgstation.Server.Host/Models/ServerSettings.cs index 365d9b3aa2..8a84475dad 100644 --- a/src/Tgstation.Server.Host/Models/ServerSettings.cs +++ b/src/Tgstation.Server.Host/Models/ServerSettings.cs @@ -5,11 +5,5 @@ namespace Tgstation.Server.Host.Models sealed class ServerSettings : Api.Models.Internal.ServerSettings { public long Id { get; set; } - - /// - /// The value used for the to encrypt JWTs - /// - [Required] - public byte[] TokenSecret { get; set; } } } diff --git a/src/Tgstation.Server.Host/Models/User.cs b/src/Tgstation.Server.Host/Models/User.cs index 149768c00d..be2d1ae09d 100644 --- a/src/Tgstation.Server.Host/Models/User.cs +++ b/src/Tgstation.Server.Host/Models/User.cs @@ -1,15 +1,21 @@ using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using Tgstation.Server.Host.Core; namespace Tgstation.Server.Host.Models { /// /// Represents a in the database /// - sealed class User : Api.Models.User + sealed class User : Api.Models.Internal.User { public string PasswordHash { get; set; } - public string PasswordSalt { get; set; } + /// + /// The value used for the to encrypt JWTs + /// + [StringLength(CryptographySuite.SecureStringLength)] + public string TokenSecret { get; set; } /// /// The s for the diff --git a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj index 3b031e0289..f97f3f42ea 100644 --- a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj +++ b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj @@ -38,6 +38,7 @@ +