Slowly coming together

This commit is contained in:
Cyberboss
2018-04-10 13:22:58 -04:00
parent f9799ca51b
commit 1d329c1605
21 changed files with 328 additions and 146 deletions
@@ -7,7 +7,6 @@ namespace Tgstation.Server.Api.Models
/// <summary>
/// Metadata about an installation
/// </summary>
[Model(RightsType.Administration)]
public sealed class Administration : Internal.ServerSettings
{
/// <summary>
@@ -3,13 +3,8 @@
/// <summary>
/// Indicates a chat channel
/// </summary>
public sealed class ChatChannel
public class ChatChannel
{
/// <summary>
/// The column ID
/// </summary>
public long Id { get; set; }
/// <summary>
/// The IRC channel name
/// </summary>
@@ -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
{
/// <summary>
/// Manage the server chat bots
/// </summary>
[Model(RightsType.Chat, RequiresInstance = true)]
public class ChatSettings
/// <inheritdoc />
public sealed class ChatSettings : Internal.ChatSettings
{
/// <summary>
/// If the IRC client is enabled
/// </summary>
[Permissions(WriteRight = ChatSettingsRights.SetIrcEnabled)]
public bool IrcEnabled { get; set; }
/// <summary>
/// The IRC server name
/// </summary>
[Permissions(ReadRight = ChatSettingsRights.SetIrcSettings, WriteRight = ChatSettingsRights.SetIrcSettings)]
[Required]
public string IrcHost { get; set; }
/// <summary>
/// The IRC server port
/// </summary>
[Permissions(ReadRight = ChatSettingsRights.SetIrcSettings, WriteRight = ChatSettingsRights.SetIrcSettings)]
public ushort IrcPort { get; set; }
/// <summary>
/// The IRC server NickServ password
/// </summary>
[Permissions(ReadRight = ChatSettingsRights.SetIrcSettings, WriteRight = ChatSettingsRights.SetIrcSettings)]
public string IrcNickServPassword { get; set; }
/// <summary>
/// If the Discord bot is enabled
/// </summary>
[Permissions(WriteRight = ChatSettingsRights.SetDiscordEnabled)]
public bool DiscordEnabled { get; set; }
/// <summary>
/// The Discord bot token
/// </summary>
[Permissions(ReadRight = ChatSettingsRights.SetDiscordSettings, WriteRight = ChatSettingsRights.SetDiscordSettings)]
public string DiscordBotToken { get; set; }
/// <summary>
/// Channels the bot should listen/announce in and allow admin commands
/// </summary>
[Permissions(WriteRight = ChatSettingsRights.SetDiscordChannels)]
public virtual List<ChatChannel> AdminChannels { get; set; }
public List<ChatChannel> AdminChannels { get; set; }
/// <summary>
/// Channels the Discord bot should listen/announce in
/// </summary>
[Permissions(WriteRight = ChatSettingsRights.SetDiscordChannels)]
public virtual List<ChatChannel> GeneralChannels { get; set; }
public List<ChatChannel> GeneralChannels { get; set; }
}
}
@@ -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
{
/// <summary>
/// Manage the server chat bots
/// </summary>
[Model(RightsType.Chat, RequiresInstance = true)]
public class ChatSettings
{
/// <summary>
/// If the IRC client is enabled
/// </summary>
[Permissions(WriteRight = ChatSettingsRights.SetIrcEnabled)]
public bool IrcEnabled { get; set; }
/// <summary>
/// The IRC server name
/// </summary>
[Permissions(ReadRight = ChatSettingsRights.SetIrcSettings, WriteRight = ChatSettingsRights.SetIrcSettings)]
[Required]
public string IrcHost { get; set; }
/// <summary>
/// The IRC server port
/// </summary>
[Permissions(ReadRight = ChatSettingsRights.SetIrcSettings, WriteRight = ChatSettingsRights.SetIrcSettings)]
public ushort IrcPort { get; set; }
/// <summary>
/// The IRC server NickServ password
/// </summary>
[Permissions(ReadRight = ChatSettingsRights.SetIrcSettings, WriteRight = ChatSettingsRights.SetIrcSettings)]
public string IrcNickServPassword { get; set; }
/// <summary>
/// If the Discord bot is enabled
/// </summary>
[Permissions(WriteRight = ChatSettingsRights.SetDiscordEnabled)]
public bool DiscordEnabled { get; set; }
/// <summary>
/// The Discord bot token
/// </summary>
[Permissions(ReadRight = ChatSettingsRights.SetDiscordSettings, WriteRight = ChatSettingsRights.SetDiscordSettings)]
public string DiscordBotToken { get; set; }
}
}
@@ -2,6 +2,7 @@
namespace Tgstation.Server.Api.Models.Internal
{
[Model(RightsType.Administration)]
public class ServerSettings
{
/// <summary>
@@ -0,0 +1,48 @@
using System;
using System.ComponentModel.DataAnnotations;
using Tgstation.Server.Api.Rights;
namespace Tgstation.Server.Api.Models.Internal
{
/// <summary>
/// Represents a server <see cref="User"/>
/// </summary>
[Model(RightsType.Administration, WriteRight = AdministrationRights.EditUsers)]
public class User
{
/// <summary>
/// The ID of the <see cref="User"/>
/// </summary>
[Permissions(DenyWrite = true)]
public long Id { get; set; }
[Permissions(DenyWrite = true)]
[Required]
public DateTimeOffset CreatedAt { get; set; }
/// <summary>
/// The SID/UID of the <see cref="User"/> on Windows/POSIX respectively
/// </summary>
[Permissions(DenyWrite = true)]
public string SystemIdentifier { get; set; }
/// <summary>
/// The name of the <see cref="User"/>
/// </summary>
[Permissions(WriteRight = AdministrationRights.EditUsers)]
[Required]
public string Name { get; set; }
/// <summary>
/// The <see cref="Rights.AdministrationRights"/> for the <see cref="User"/>
/// </summary>
[Permissions(WriteRight = AdministrationRights.EditUsers)]
public AdministrationRights AdministrationRights { get; set; }
/// <summary>
/// The <see cref="Rights.InstanceManagerRights"/> for the <see cref="User"/>
/// </summary>
[Permissions(WriteRight = AdministrationRights.EditUsers)]
public InstanceManagerRights InstanceManagerRights { get; set; }
}
}
+2 -42
View File
@@ -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
{
/// <summary>
/// Represents a server <see cref="User"/>
/// </summary>
[Model(RightsType.Administration, WriteRight = AdministrationRights.EditUsers)]
public class User
public sealed class User : Internal.User
{
/// <summary>
/// The ID of the <see cref="User"/>
/// </summary>
[Permissions(DenyWrite = true)]
public long Id { get; set; }
[Permissions(DenyWrite = true)]
[Required]
public DateTimeOffset CreatedAt { get; set; }
/// <summary>
/// The SID/UID of the <see cref="User"/> on Windows/POSIX respectively
/// </summary>
[Permissions(DenyWrite = true)]
public string SystemIdentifier { get; set; }
[Permissions(DenyWrite = true)]
public bool Enabled { get; set; }
/// <summary>
/// The name of the <see cref="User"/>
/// </summary>
[Permissions(WriteRight = AdministrationRights.EditUsers)]
[Required]
public string Name { get; set; }
/// <summary>
/// The <see cref="Rights.AdministrationRights"/> for the <see cref="User"/>
/// </summary>
[Permissions(WriteRight = AdministrationRights.EditUsers)]
public AdministrationRights AdministrationRights { get; set; }
/// <summary>
/// The <see cref="Rights.InstanceManagerRights"/> for the <see cref="User"/>
/// </summary>
[Permissions(WriteRight = AdministrationRights.EditUsers)]
public InstanceManagerRights InstanceManagerRights { get; set; }
}
}
@@ -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) },
@@ -13,11 +13,6 @@
/// <see cref="InstanceManagerRights"/>
/// </summary>
InstanceManager,
/// <summary>
/// <see cref="TokenRights"/>
/// </summary>
Token,
/// <summary>
/// <see cref="RepositoryRights"/>
/// </summary>
@@ -1,32 +0,0 @@
using System;
namespace Tgstation.Server.Api.Rights
{
/// <summary>
/// Rights for <see cref="Models.Token"/>s
/// </summary>
[Flags]
public enum TokenRights
{
/// <summary>
/// User has no rights
/// </summary>
None = 0,
/// <summary>
/// User can create <see cref="Models.Token"/>s
/// </summary>
Create = 1,
/// <summary>
/// User can delete <see cref="Models.Token"/>s
/// </summary>
Delete = 2,
/// <summary>
/// User can list all their <see cref="Models.Token"/>s
/// </summary>
List = 4,
/// <summary>
/// User can perform all the actions they can for themselves on behalf of other users except <see cref="Create"/>
/// </summary>
Admin = 8,
}
}
@@ -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<DatabaseContext>();
services.AddScoped<IDatabaseContext>(x => x.GetRequiredService<DatabaseContext>());
services.AddSingleton<ICryptographySuite, CryptographySuite>();
services.AddSingleton<IDatabaseSeeder, DatabaseSeeder>();
services.AddSingleton<IPasswordHasher<User>, PasswordHasher<User>>();
}
/// <summary>
@@ -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
{
/// <inheritdoc />
sealed class CryptographySuite : ICryptographySuite
{
/// <summary>
/// The length of secure strings used in the application
/// </summary>
public const int SecureStringLength = 40;
/// <summary>
/// Generates a secure ascii <see cref="string"/> of length <see cref="SecureStringLength"/>
/// </summary>
/// <returns>A secure ascii <see cref="string"/> of length <see cref="SecureStringLength"/></returns>
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();
}
}
/// <summary>
/// The <see cref="IPasswordHasher{TUser}"/> for the <see cref="CryptographySuite"/>
/// </summary>
readonly IPasswordHasher<User> passwordHasher;
/// <summary>
/// Construct a <see cref="CryptographySuite"/>
/// </summary>
/// <param name="passwordHasher">The value of <see cref="passwordHasher"/></param>
public CryptographySuite(IPasswordHasher<User> passwordHasher) => this.passwordHasher = passwordHasher ?? throw new ArgumentNullException(nameof(passwordHasher));
/// <inheritdoc />
public void RegenerateUserToken(User user)
{
if (user == null)
throw new ArgumentNullException(nameof(user));
user.TokenSecret = GenerateSecureString();
}
/// <inheritdoc />
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);
}
}
}
@@ -0,0 +1,23 @@
using Tgstation.Server.Host.Models;
namespace Tgstation.Server.Host.Core
{
/// <summary>
/// Contains various cryptographic functions
/// </summary>
interface ICryptographySuite
{
/// <summary>
/// Sets a <see cref="User.PasswordHash"/> for a given <paramref name="user"/>
/// </summary>
/// <param name="user">The <see cref="User"/> whos <see cref="User.PasswordHash"/> is to be set</param>
/// <param name="newPassword">The new password for the <see cref="User"/></param>
void SetUserPassword(User user, string newPassword);
/// <summary>
/// Sets a new <see cref="User.TokenSecret"/> for a givent <paramref name="user"/>
/// </summary>
/// <param name="user">The <see cref="User"/> whos <see cref="User.TokenSecret"/> is to be set</param>
void RegenerateUserToken(User user);
}
}
@@ -0,0 +1,10 @@
namespace Tgstation.Server.Host.Models
{
sealed class ChatChannel : Api.Models.ChatChannel
{
/// <summary>
/// The column ID
/// </summary>
public long Id { get; set; }
}
}
@@ -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<ChatChannel> AdminChannels { get; set; }
public List<ChatChannel> GeneralChannels { get; set; }
}
}
@@ -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 <see cref="IHostingEnvironment"/> for the <see cref="DatabaseContext"/>
/// </summary>
readonly IHostingEnvironment hostingEnvironment;
/// <summary>
/// The <see cref="IDatabaseSeeder"/> for the <see cref="DatabaseContext"/>
/// </summary>
readonly IDatabaseSeeder databaseSeeder;
/// <summary>
/// Construct a <see cref="DatabaseContext"/>
@@ -57,11 +65,13 @@ namespace Tgstation.Server.Host.Models
/// <param name="databaseConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="databaseConfiguration"/></param>
/// <param name="loggerFactory">The value of <see cref="loggerFactory"/></param>
/// <param name="hostingEnvironment">The value of <see cref="hostingEnvironment"/></param>
public DatabaseContext(DbContextOptions<DatabaseContext> dbContextOptions, IOptions<DatabaseConfiguration> databaseConfigurationOptions, ILoggerFactory loggerFactory, IHostingEnvironment hostingEnvironment) : base(dbContextOptions)
/// <param name="databaseSeeder">The value of <see cref="databaseSeeder"/></param>
public DatabaseContext(DbContextOptions<DatabaseContext> dbContextOptions, IOptions<DatabaseConfiguration> 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));
}
/// <inheritdoc />
@@ -98,13 +108,25 @@ namespace Tgstation.Server.Host.Models
}
/// <inheritdoc />
public Task<ServerSettings> GetServerSettings(CancellationToken cancellationToken) => ServerSettings.FirstOrDefaultAsync(cancellationToken);
public async Task<ServerSettings> GetServerSettings(CancellationToken cancellationToken)
{
var settings = await ServerSettings.FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
if (settings == default(ServerSettings))
{
settings = new ServerSettings();
ServerSettings.Add(settings);
}
return settings;
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
@@ -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
{
/// <inheritdoc />
sealed class DatabaseSeeder : IDatabaseSeeder
{
/// <summary>
/// The default password mode admin password
/// </summary>
const string DefaultAdminPassword = "ISolemlySwearToDeleteTheDataDirectory";
/// <summary>
/// The default git repository to pull server updates from
/// </summary>
const string DefaultUpstreamRepository = "https://github.com/tgstation/tgstation-server";
/// <summary>
/// The <see cref="ICryptographySuite"/> for the <see cref="DatabaseContext"/>
/// </summary>
readonly ICryptographySuite cryptographySuite;
/// <summary>
/// Construct a <see cref="DatabaseSeeder"/>
/// </summary>
/// <param name="cryptographySuite">The value of <see cref="cryptographySuite"/></param>
public DatabaseSeeder(ICryptographySuite cryptographySuite) => this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite));
/// <inheritdoc />
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);
}
}
}
@@ -0,0 +1,19 @@
using System.Threading;
using System.Threading.Tasks;
namespace Tgstation.Server.Host.Models
{
/// <summary>
/// For initially seeding a database
/// </summary>
interface IDatabaseSeeder
{
/// <summary>
/// Initially seed a given <paramref name="databaseContext"/>
/// </summary>
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> to seed</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task SeedDatabase(IDatabaseContext databaseContext, CancellationToken cancellationToken);
}
}
@@ -5,11 +5,5 @@ namespace Tgstation.Server.Host.Models
sealed class ServerSettings : Api.Models.Internal.ServerSettings
{
public long Id { get; set; }
/// <summary>
/// The value used for the <see cref="SymmetricSecurityKey"/> to encrypt JWTs
/// </summary>
[Required]
public byte[] TokenSecret { get; set; }
}
}
+8 -2
View File
@@ -1,15 +1,21 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Tgstation.Server.Host.Core;
namespace Tgstation.Server.Host.Models
{
/// <summary>
/// Represents a <see cref="Api.Models.User"/> in the database
/// </summary>
sealed class User : Api.Models.User
sealed class User : Api.Models.Internal.User
{
public string PasswordHash { get; set; }
public string PasswordSalt { get; set; }
/// <summary>
/// The value used for the <see cref="SymmetricSecurityKey"/> to encrypt JWTs
/// </summary>
[StringLength(CryptographySuite.SecureStringLength)]
public string TokenSecret { get; set; }
/// <summary>
/// The <see cref="InstanceUser"/>s for the <see cref="User"/>
@@ -38,6 +38,7 @@
<PackageReference Include="Byond.TopicSender" Version="1.1.0.1" />
<PackageReference Include="Cyberboss.AspNetCore.AsyncInitializer" Version="1.1.0" />
<PackageReference Include="Microsoft.AspNetCore" Version="2.1.0-preview2-final" />
<PackageReference Include="Microsoft.AspNetCore.Identity" Version="2.1.0-preview2-final" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.1.0-preview2-final" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="2.1.0-preview2-final" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.0-preview2-final" />