diff --git a/src/Tgstation.Server.Api/Models/InstanceUser.cs b/src/Tgstation.Server.Api/Models/InstanceUser.cs
index b198ceff64..4e25521934 100644
--- a/src/Tgstation.Server.Api/Models/InstanceUser.cs
+++ b/src/Tgstation.Server.Api/Models/InstanceUser.cs
@@ -8,13 +8,19 @@ namespace Tgstation.Server.Api.Models
///
[Model(RightsType.InstanceUser, WriteRight = InstanceUserRights.WriteUsers, CanList = true, RequiresInstance = true)]
public class InstanceUser
- {
+ {
///
/// The of the the belongs to
///
[Permissions(DenyWrite = true)]
public long UserId { get; set; }
+ ///
+ /// The of the the belongs to
+ ///
+ [Permissions(DenyWrite = true)]
+ public long InstanceId { get; set; }
+
///
/// The of the
///
diff --git a/src/Tgstation.Server.Api/Models/Internal/ServerSettings.cs b/src/Tgstation.Server.Api/Models/Internal/ServerSettings.cs
index f044801219..3ab52adef5 100644
--- a/src/Tgstation.Server.Api/Models/Internal/ServerSettings.cs
+++ b/src/Tgstation.Server.Api/Models/Internal/ServerSettings.cs
@@ -8,12 +8,6 @@ namespace Tgstation.Server.Api.Models.Internal
[Model(RightsType.Administration)]
public class ServerSettings
{
- ///
- /// Use the specified Windows/POSIX authentication group to authorize users. Changing this may enable or disable s depending on how they were configured. Setting this to changes the authentication mode to database.
- ///
- [Permissions(ReadRight = AdministrationRights.ChangeAuthenticationGroup, WriteRight = AdministrationRights.ChangeAuthenticationGroup)]
- public string SystemAuthenticationGroup { get; set; }
-
///
/// Automatically send unhandled exception data to a public collection service. This will be limited to system information, path data, and game code compilation information.
///
@@ -21,7 +15,7 @@ namespace Tgstation.Server.Api.Models.Internal
public bool EnableTelemetry { get; set; }
///
- /// The git repository to recieve updates to Tgstation.Server.Host from, must include credentials if necessary. If set to upstream pulls will be disabled entirely
+ /// The git repository URL to recieve updates to Tgstation.Server.Host from, must include credentials if necessary. If set to upstream pulls will be disabled entirely. Should be https://github.com/tgstation/tgstation-server or a fork of it
///
[Permissions(ReadRight = AdministrationRights.SetUpstreamRepository, WriteRight = AdministrationRights.SetUpstreamRepository)]
public string UpstreamRepository { get; set; }
diff --git a/src/Tgstation.Server.Host/Configuration/DatabaseConfiguration.cs b/src/Tgstation.Server.Host/Configuration/DatabaseConfiguration.cs
index 4afebe14a5..a75d463a48 100644
--- a/src/Tgstation.Server.Host/Configuration/DatabaseConfiguration.cs
+++ b/src/Tgstation.Server.Host/Configuration/DatabaseConfiguration.cs
@@ -15,6 +15,11 @@
///
public DatabaseType DatabaseType { get; set; }
+ ///
+ /// If the admin user should be enabled and have it's password reset
+ ///
+ public bool ResetAdminPassword { get; set; }
+
///
/// The connection string for the database
///
diff --git a/src/Tgstation.Server.Host/Controllers/HomeController.cs b/src/Tgstation.Server.Host/Controllers/HomeController.cs
index 40ea8a1e91..79d6f528c3 100644
--- a/src/Tgstation.Server.Host/Controllers/HomeController.cs
+++ b/src/Tgstation.Server.Host/Controllers/HomeController.cs
@@ -100,7 +100,7 @@ namespace Tgstation.Server.Host.Controllers
else
try
{
- var systemIdentity = systemIdentityFactory.CreateSystemIdentity(ApiHeaders.Username, ApiHeaders.Password);
+ using (await systemIdentityFactory.CreateSystemIdentity(ApiHeaders.Username, ApiHeaders.Password, cancellationToken).ConfigureAwait(false)) { }
}
catch
{
diff --git a/src/Tgstation.Server.Host/Controllers/UsersController.cs b/src/Tgstation.Server.Host/Controllers/UsersController.cs
index f66ffe1e5c..4eaaa01e6b 100644
--- a/src/Tgstation.Server.Host/Controllers/UsersController.cs
+++ b/src/Tgstation.Server.Host/Controllers/UsersController.cs
@@ -2,6 +2,7 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System;
+using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading;
@@ -59,8 +60,8 @@ namespace Tgstation.Server.Host.Controllers
if (model.Name == null)
return BadRequest(new { message = "Missing user name!" });
- if (model.Password == null && model.SystemIdentifier == null)
- return BadRequest(new { message = "User must have either a password or system identifier!" });
+ if (!(model.Password == null ^ model.SystemIdentifier == null))
+ return BadRequest(new { message = "User must have exactly one of either a password or system identifier!" });
var dbUser = new Models.User
{
@@ -69,15 +70,22 @@ namespace Tgstation.Server.Host.Controllers
CreatedBy = AuthenticationContext.User,
Enabled = model.Enabled ?? false,
InstanceManagerRights = model.InstanceManagerRights ?? InstanceManagerRights.None,
- Name = model.Name,
- SystemIdentifier = model.SystemIdentifier
+#pragma warning disable CA1308 // Normalize strings to uppercase
+ Name = model.Name.ToLowerInvariant(),
+#pragma warning restore CA1308 // Normalize strings to uppercase
+ SystemIdentifier = model.SystemIdentifier,
+ InstanceUsers = new List()
};
if (model.SystemIdentifier != null)
- using (var systemIdentity = systemIdentityFactory.CreateSystemIdentity(dbUser))
+ try
{
- if (systemIdentity == null)
- return Forbid();
+ using (await systemIdentityFactory.CreateSystemIdentity(dbUser, cancellationToken).ConfigureAwait(false)) { }
+ }
+ catch(Exception e)
+ {
+ logger.LogInformation("System identifier user creation failure for {0}. Exception: {1}", model.SystemIdentifier, e);
+ return Forbid();
}
else
cryptographySuite.SetUserPassword(dbUser, model.Password);
@@ -121,10 +129,12 @@ namespace Tgstation.Server.Host.Controllers
return BadRequest(new { message = "Cannot convert a system user to a password user!" });
cryptographySuite.SetUserPassword(originalUser, model.Password);
}
- else if(model.SystemIdentifier != originalUser.SystemIdentifier)
+ else if(model.SystemIdentifier != null && model.SystemIdentifier != originalUser.SystemIdentifier)
return BadRequest(new { message = "Cannot change a user's system identifier!" });
- originalUser.Name = model.Name ?? originalUser.Name;
+ if (model.Name != null && model.Name != originalUser.SystemIdentifier)
+ return BadRequest(new { message = "Cannot change a user's name!" });
+
originalUser.InstanceManagerRights = model.InstanceManagerRights ?? originalUser.InstanceManagerRights;
originalUser.AdministrationRights = model.AdministrationRights ?? originalUser.AdministrationRights;
originalUser.Enabled = model.Enabled ?? originalUser.Enabled;
@@ -133,5 +143,17 @@ namespace Tgstation.Server.Host.Controllers
return Json(originalUser.ToApi());
}
+
+ ///
+ [TgsAuthorize]
+ public override Task Read(CancellationToken cancellationToken) => Task.FromResult(Json(AuthenticationContext.User.ToApi()));
+
+ ///
+ [TgsAuthorize(AdministrationRights.EditUsers)]
+ public override async Task List(CancellationToken cancellationToken)
+ {
+ var users = await DatabaseContext.Users.ToListAsync(cancellationToken).ConfigureAwait(false);
+ return Json(users);
+ }
}
}
diff --git a/src/Tgstation.Server.Host/Models/DatabaseContext.cs b/src/Tgstation.Server.Host/Models/DatabaseContext.cs
index 25caf31b19..04befa41fa 100644
--- a/src/Tgstation.Server.Host/Models/DatabaseContext.cs
+++ b/src/Tgstation.Server.Host/Models/DatabaseContext.cs
@@ -151,6 +151,8 @@ namespace Tgstation.Server.Host.Models
#endif
if (wasEmpty)
await databaseSeeder.SeedDatabase(this, cancellationToken).ConfigureAwait(false);
+ else if(databaseConfiguration.ResetAdminPassword)
+ await databaseSeeder.ResetAdminPassword(this, cancellationToken).ConfigureAwait(false);
}
///
diff --git a/src/Tgstation.Server.Host/Models/DatabaseSeeder.cs b/src/Tgstation.Server.Host/Models/DatabaseSeeder.cs
index 7ccf4f33ca..ef57b6f6b6 100644
--- a/src/Tgstation.Server.Host/Models/DatabaseSeeder.cs
+++ b/src/Tgstation.Server.Host/Models/DatabaseSeeder.cs
@@ -1,4 +1,6 @@
-using System;
+using Microsoft.EntityFrameworkCore;
+using System;
+using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api.Rights;
@@ -10,7 +12,14 @@ namespace Tgstation.Server.Host.Models
sealed class DatabaseSeeder : IDatabaseSeeder
{
///
- /// The default password mode admin password
+ /// The name of the default admin user
+ ///
+#pragma warning disable CA1308 // Normalize strings to uppercase
+ static readonly string AdminName = "admin".ToLowerInvariant();
+#pragma warning restore CA1308 // Normalize strings to uppercase
+
+ ///
+ /// The default admin password
///
const string DefaultAdminPassword = "ISolemlySwearToDeleteTheDataDirectory";
@@ -30,19 +39,28 @@ namespace Tgstation.Server.Host.Models
/// The value of
public DatabaseSeeder(ICryptographySuite cryptographySuite) => this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite));
- ///
- public async Task SeedDatabase(IDatabaseContext databaseContext, CancellationToken cancellationToken)
+ ///
+ /// Add a default admin to a given
+ ///
+ /// The to add an admin to
+ void SeedAdminUser(IDatabaseContext databaseContext)
{
var admin = new User
{
AdministrationRights = (AdministrationRights)~0,
CreatedAt = DateTimeOffset.Now,
InstanceManagerRights = (InstanceManagerRights)~0,
- Name = "Admin",
+ Name = AdminName,
Enabled = true,
};
cryptographySuite.SetUserPassword(admin, DefaultAdminPassword);
databaseContext.Users.Add(admin);
+ }
+
+ ///
+ public async Task SeedDatabase(IDatabaseContext databaseContext, CancellationToken cancellationToken)
+ {
+ SeedAdminUser(databaseContext);
var serverSettings = await databaseContext.GetServerSettings(cancellationToken).ConfigureAwait(false);
@@ -51,5 +69,20 @@ namespace Tgstation.Server.Host.Models
await databaseContext.Save(cancellationToken).ConfigureAwait(false);
}
+
+ ///
+ public async Task ResetAdminPassword(IDatabaseContext databaseContext, CancellationToken cancellationToken)
+ {
+ var admin = await databaseContext.Users.Where(x => x.Name == AdminName).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
+ if (admin == default)
+ SeedAdminUser(databaseContext);
+ else
+ {
+ admin.Enabled = true;
+ cryptographySuite.SetUserPassword(admin, DefaultAdminPassword);
+ }
+
+ await databaseContext.Save(cancellationToken).ConfigureAwait(false);
+ }
}
}
diff --git a/src/Tgstation.Server.Host/Models/IDatabaseSeeder.cs b/src/Tgstation.Server.Host/Models/IDatabaseSeeder.cs
index f57620fd0a..354b3eee7b 100644
--- a/src/Tgstation.Server.Host/Models/IDatabaseSeeder.cs
+++ b/src/Tgstation.Server.Host/Models/IDatabaseSeeder.cs
@@ -7,7 +7,7 @@ namespace Tgstation.Server.Host.Models
/// For initially seeding a database
///
interface IDatabaseSeeder
- {
+ {
///
/// Initially seed a given
///
@@ -15,5 +15,13 @@ namespace Tgstation.Server.Host.Models
/// The for the operation
/// A representing the running operation
Task SeedDatabase(IDatabaseContext databaseContext, CancellationToken cancellationToken);
- }
+
+ ///
+ /// Changes the admin password in back to it's default and enables the account
+ ///
+ /// The to reset the admin password for
+ /// The for the operation
+ /// A representing the running operation
+ Task ResetAdminPassword(IDatabaseContext databaseContext, CancellationToken cancellationToken);
+ }
}
diff --git a/src/Tgstation.Server.Host/Security/AuthenticationContext.cs b/src/Tgstation.Server.Host/Security/AuthenticationContext.cs
index 5e03fbff4b..47d3cac17e 100644
--- a/src/Tgstation.Server.Host/Security/AuthenticationContext.cs
+++ b/src/Tgstation.Server.Host/Security/AuthenticationContext.cs
@@ -35,7 +35,7 @@ namespace Tgstation.Server.Host.Security
}
///
- public void Dispose() => SystemIdentity.Dispose();
+ public void Dispose() => SystemIdentity?.Dispose();
///
public IAuthenticationContext Clone() => new AuthenticationContext(SystemIdentity.Clone(), User, InstanceUser);
diff --git a/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs b/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs
index 7c7e3465c7..1b73c7afc5 100644
--- a/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs
+++ b/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs
@@ -45,13 +45,14 @@ namespace Tgstation.Server.Host.Security
if (instanceId.HasValue)
userQuery = userQuery.Include(x => x.InstanceUsers.Where(y => y.Id == instanceId));
- var user = await userQuery.FirstAsync(cancellationToken).ConfigureAwait(false);
+ var user = await userQuery.Include(x => x.InstanceUsers).FirstAsync(cancellationToken).ConfigureAwait(false);
InstanceUser instanceUser = null;
if (instanceId.HasValue)
- instanceUser = user.InstanceUsers.First();
+ instanceUser = user.InstanceUsers.Where(x => x.InstanceId == instanceId).First();
- CurrentAuthenticationContext = new AuthenticationContext(user.SystemIdentifier != null ? systemIdentityFactory.CreateSystemIdentity(user) : null, user, instanceUser);
+ var systemIdentity = user.SystemIdentifier != null ? await systemIdentityFactory.CreateSystemIdentity(user, cancellationToken).ConfigureAwait(false) : null;
+ CurrentAuthenticationContext = new AuthenticationContext(systemIdentity, user, instanceUser);
}
}
}
diff --git a/src/Tgstation.Server.Host/Security/ISystemIdentityFactory.cs b/src/Tgstation.Server.Host/Security/ISystemIdentityFactory.cs
index 86f4f9b7e2..11ba06ebe7 100644
--- a/src/Tgstation.Server.Host/Security/ISystemIdentityFactory.cs
+++ b/src/Tgstation.Server.Host/Security/ISystemIdentityFactory.cs
@@ -1,4 +1,6 @@
-using Tgstation.Server.Host.Models;
+using System.Threading;
+using System.Threading.Tasks;
+using Tgstation.Server.Host.Models;
namespace Tgstation.Server.Host.Security
{
@@ -11,15 +13,17 @@ namespace Tgstation.Server.Host.Security
/// Create a for a given
///
/// The user to create a for
+ /// The for the operation
/// A new or if the has no
- ISystemIdentity CreateSystemIdentity(User user);
+ Task CreateSystemIdentity(User user, CancellationToken cancellationToken);
///
/// Create a for a given username and password
///
/// The username of the user
/// The password of the user
+ /// The for the operation
/// A new
- ISystemIdentity CreateSystemIdentity(string username, string password);
+ Task CreateSystemIdentity(string username, string password, CancellationToken cancellationToken);
}
}
diff --git a/src/Tgstation.Server.Host/Security/SystemIdentityFactory.cs b/src/Tgstation.Server.Host/Security/SystemIdentityFactory.cs
index f9e9c15a87..4f384ff4b4 100644
--- a/src/Tgstation.Server.Host/Security/SystemIdentityFactory.cs
+++ b/src/Tgstation.Server.Host/Security/SystemIdentityFactory.cs
@@ -1,4 +1,6 @@
using System;
+using System.Threading;
+using System.Threading.Tasks;
using Tgstation.Server.Host.Models;
namespace Tgstation.Server.Host.Security
@@ -7,13 +9,13 @@ namespace Tgstation.Server.Host.Security
sealed class SystemIdentityFactory : ISystemIdentityFactory
{
///
- public ISystemIdentity CreateSystemIdentity(User user)
+ public Task CreateSystemIdentity(User user, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
///
- public ISystemIdentity CreateSystemIdentity(string username, string password)
+ public Task CreateSystemIdentity(string username, string password, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
diff --git a/src/Tgstation.Server.Host/appsettings.json b/src/Tgstation.Server.Host/appsettings.json
index 233a6dd6fa..8abbd1eb6a 100644
--- a/src/Tgstation.Server.Host/appsettings.json
+++ b/src/Tgstation.Server.Host/appsettings.json
@@ -20,6 +20,7 @@
},
"Database": {
"DatabaseType": "SqlServer",
+ "ResetAdminPassword": false,
"ConnectionString": "Data Source=(local);Initial Catalog=TGS;Integrated Security=True"
}
}