Merge pull request #1052 from tgstation/1009-FixInstanceRestore

Add instance user restore permission
This commit is contained in:
Jordan Brown
2020-06-16 13:21:31 -04:00
committed by GitHub
12 changed files with 104 additions and 40 deletions
@@ -531,5 +531,11 @@ namespace Tgstation.Server.Api.Models
/// </summary>
[Description("Could not create dump as gcore exited with a non-zero exit code!")]
GCoreFailure,
/// <summary>
/// Attempted to test merge with an invalid remote repository.
/// </summary>
[Description("Test merging cannot be performed with this remote!")]
RepoTestMergeInvalidRemote,
}
}
@@ -61,6 +61,11 @@ namespace Tgstation.Server.Api.Rights
/// <summary>
/// User can change <see cref="Models.Instance.ChatBotLimit"/>.
/// </summary>
SetChatBotLimit = 512
SetChatBotLimit = 512,
/// <summary>
/// User can give themselves full <see cref="Models.InstanceUser"/> rights on instances.
/// </summary>
GrantPermissions = 1024,
}
}
+3
View File
@@ -235,6 +235,9 @@ namespace Tgstation.Server.Client
/// <inheritdoc />
public Task<TResult> Update<TBody, TResult>(string route, TBody body, CancellationToken cancellationToken) => RunRequest<TResult>(route, body, HttpMethod.Post, null, false, cancellationToken);
/// <inheritdoc />
public Task Patch(string route, CancellationToken cancellationToken) => RunRequest<object>(route, null, HttpMethod.Patch, null, false, cancellationToken);
/// <inheritdoc />
public Task Update<TBody>(string route, TBody body, CancellationToken cancellationToken) => RunRequest<object>(route, body, HttpMethod.Post, null, false, cancellationToken);
@@ -90,6 +90,14 @@ namespace Tgstation.Server.Client
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task Update<TBody>(string route, TBody body, CancellationToken cancellationToken);
/// <summary>
/// Run an HTTP PATCH request.
/// </summary>
/// <param name="route">The server route to make the request to.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
Task Patch(string route, CancellationToken cancellationToken);
/// <summary>
/// Run an HTTP DELETE request
/// </summary>
@@ -50,6 +50,14 @@ namespace Tgstation.Server.Client
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task Detach(Instance instance, CancellationToken cancellationToken);
/// <summary>
/// Gives the user full permissions on an <paramref name="instance"/>.
/// </summary>
/// <param name="instance">The <see cref="Instance"/> to grant permissions on.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
Task GrantPermissions(Instance instance, CancellationToken cancellationToken);
/// <summary>
/// Create an <see cref="IInstanceClient"/> for a given <see cref="Instance"/>
/// </summary>
@@ -47,6 +47,9 @@ namespace Tgstation.Server.Client
/// <inheritdoc />
public Task<Instance> GetId(Instance instance, CancellationToken cancellationToken) => apiClient.Read<Instance>(Routes.SetID(Routes.InstanceManager, instance?.Id ?? throw new ArgumentNullException(nameof(instance))), cancellationToken);
/// <inheritdoc />
public Task GrantPermissions(Instance instance, CancellationToken cancellationToken) => apiClient.Patch(Routes.SetID(Routes.InstanceManager, instance?.Id ?? throw new ArgumentNullException(nameof(instance))), cancellationToken);
/// <inheritdoc />
public IInstanceClient CreateClient(Instance instance)
{
@@ -274,7 +274,7 @@ namespace Tgstation.Server.Host.Components.Repository
committerEmail);
if (!IsGitHubRepository)
throw new InvalidOperationException("Test merging is only available on GitHub hosted origin repositories!");
throw new JobException(ErrorCode.RepoTestMergeInvalidRemote);
var commitMessage = String.Format(
CultureInfo.InvariantCulture,
@@ -114,17 +114,22 @@ namespace Tgstation.Server.Host.Controllers
return path;
}
Models.InstanceUser InstanceAdminUser() => new Models.InstanceUser
Models.InstanceUser InstanceAdminUser(Models.InstanceUser userToModify)
{
ByondRights = (ByondRights)~0U,
ChatBotRights = (ChatBotRights)~0U,
ConfigurationRights = (ConfigurationRights)~0U,
DreamDaemonRights = (DreamDaemonRights)~0U,
DreamMakerRights = (DreamMakerRights)~0U,
RepositoryRights = (RepositoryRights)~0U,
InstanceUserRights = (InstanceUserRights)~0U,
UserId = AuthenticationContext.User.Id
};
if (userToModify == null)
userToModify = new Models.InstanceUser()
{
UserId = AuthenticationContext.User.Id
};
userToModify.ByondRights = RightsHelper.AllRights<ByondRights>();
userToModify.ChatBotRights = RightsHelper.AllRights<ChatBotRights>();
userToModify.ConfigurationRights = RightsHelper.AllRights<ConfigurationRights>();
userToModify.DreamDaemonRights = RightsHelper.AllRights<DreamDaemonRights>();
userToModify.DreamMakerRights = RightsHelper.AllRights<DreamMakerRights>();
userToModify.RepositoryRights = RightsHelper.AllRights<RepositoryRights>();
userToModify.InstanceUserRights = RightsHelper.AllRights<InstanceUserRights>();
return userToModify;
}
/// <summary>
/// Create or attach an <see cref="Api.Models.Instance"/>.
@@ -267,7 +272,7 @@ namespace Tgstation.Server.Host.Controllers
},
InstanceUsers = new List<Models.InstanceUser> // give this user full privileges on the instance
{
InstanceAdminUser()
InstanceAdminUser(null)
}
};
@@ -458,21 +463,6 @@ namespace Tgstation.Server.Host.Controllers
return Conflict(new ErrorMessage(ErrorCode.ChatBotMax));
}
// ensure the current user has write privilege on the instance
var usersInstanceUser = await InstanceQuery()
.SelectMany(x => x.InstanceUsers)
.Where(x => x.UserId == AuthenticationContext.User.Id)
.FirstOrDefaultAsync(cancellationToken)
.ConfigureAwait(false);
if (usersInstanceUser == default)
{
var instanceAdminUser = InstanceAdminUser();
instanceAdminUser.InstanceId = originalModel.Id;
DatabaseContext.InstanceUsers.Add(instanceAdminUser);
}
else
usersInstanceUser.InstanceUserRights |= InstanceUserRights.WriteUsers;
await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
if (renamed)
@@ -634,5 +624,40 @@ namespace Tgstation.Server.Host.Controllers
api.MoveJob = moveJob?.ToApi();
return Json(api);
}
/// <summary>
/// Gives the current user full permissions on a given instance <paramref name="id"/>.
/// </summary>
/// <param name="id">The instance <see cref="EntityId.Id"/> to give permissions on.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IActionResult"/> of the request.</returns>
/// <response code="204">Granted permissions successfully.</response>
[HttpPatch("{id}")]
[TgsAuthorize(InstanceManagerRights.GrantPermissions)]
[ProducesResponseType(204)]
public async Task<IActionResult> GrantPermissions(long id, CancellationToken cancellationToken)
{
// ensure the current user has write privilege on the instance
var usersInstanceUser = await DatabaseContext
.Instances
.AsQueryable()
.Where(x => x.Id == id)
.SelectMany(x => x.InstanceUsers)
.Where(x => x.UserId == AuthenticationContext.User.Id)
.FirstOrDefaultAsync(cancellationToken)
.ConfigureAwait(false);
if (usersInstanceUser == default)
{
var instanceAdminUser = InstanceAdminUser(null);
instanceAdminUser.InstanceId = id;
DatabaseContext.InstanceUsers.Add(instanceAdminUser);
}
else
InstanceAdminUser(usersInstanceUser);
await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
return NoContent();
}
}
}
@@ -211,7 +211,7 @@ namespace Tgstation.Server.Host.Core
// HACK HACK HACK HACK HACK
const string ConfigureMethodName = nameof(SqlServerDatabaseContext.ConfigureWith);
var configureFunction = typeof(TContext).GetMethod(
nameof(SqlServerDatabaseContext.ConfigureWith),
ConfigureMethodName,
BindingFlags.Public | BindingFlags.Static);
if (configureFunction == null)
+7 -1
View File
@@ -1,6 +1,5 @@
{
"General": {
"ApiPort": 5000,
"MinimumPasswordLength": 15,
"GitHubAccessToken": null,
"SetupWizardMode": "AutoDetect",
@@ -33,6 +32,13 @@
}
}
},
"Kestrel": {
"EndPoints": {
"Http": {
"Url": "http://0.0.0.0:80"
}
}
},
"ControlPanel": {
"Enable": false,
"AllowAnyOrigin": false,
@@ -80,26 +80,26 @@ namespace Tgstation.Server.Tests.Instance
// checkout V3 and back
cloned.Reference = "V3";
var updated = await Checkout(cloned, false, true, true, cancellationToken);
var updated = await Checkout(cloned, false, true, cancellationToken);
// Specific SHA
updated.CheckoutSha = "f43f5bd";
await ApiAssert.ThrowsException<ApiConflictException>(() => Checkout(updated, false, false, false, cancellationToken), ErrorCode.RepoMismatchShaAndReference);
await ApiAssert.ThrowsException<ApiConflictException>(() => Checkout(updated, false, false, cancellationToken), ErrorCode.RepoMismatchShaAndReference);
updated.Reference = null;
updated = await Checkout(updated, false, false, false, cancellationToken);
updated = await Checkout(updated, false, false, cancellationToken);
// Fake SHA
updated.Reference = null;
updated.CheckoutSha = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
updated = await Checkout(updated, true, false, false, cancellationToken);
updated = await Checkout(updated, true, false, cancellationToken);
// Fake ref
updated.Reference = "Tgs4IntegrationTestFakeBranchNeverNameABranchThis";
updated = await Checkout(updated, true, true, false, cancellationToken);
updated = await Checkout(updated, true, true, cancellationToken);
// Back
updated.Reference = workingBranch;
updated = await Checkout(updated, false, true, false, cancellationToken);
updated = await Checkout(updated, false, true, cancellationToken);
var testPRString = Environment.GetEnvironmentVariable("TGS4_TEST_PULL_REQUEST_NUMBER");
if (String.IsNullOrWhiteSpace(testPRString))
@@ -118,13 +118,11 @@ namespace Tgstation.Server.Tests.Instance
await TestMergeTests(updated, prNumber, cancellationToken);
}
async Task<Repository> Checkout(Repository updated, bool expectFailure, bool isRef, bool checkBusy, CancellationToken cancellationToken)
async Task<Repository> Checkout(Repository updated, bool expectFailure, bool isRef, CancellationToken cancellationToken)
{
var newRef = isRef ? updated.Reference : updated.CheckoutSha;
var checkingOut = await repositoryClient.Update(updated, cancellationToken);
Assert.IsNotNull(checkingOut.ActiveJob);
if(checkBusy)
await ApiAssert.ThrowsException<ConflictException>(() => repositoryClient.Read(cancellationToken), ErrorCode.RepoBusy);
await WaitForJob(checkingOut.ActiveJob, 30, expectFailure, cancellationToken);
var result = await repositoryClient.Read(cancellationToken);
@@ -143,12 +143,14 @@ namespace Tgstation.Server.Tests
await Assert.ThrowsExceptionAsync<InsufficientPermissionsException>(() => instanceClient.Users.Read(cancellationToken)).ConfigureAwait(false);
await instanceManagerClient.Update(new Api.Models.Instance
await instanceManagerClient.GrantPermissions(new Api.Models.Instance
{
Id = firstTest.Id
}, cancellationToken).ConfigureAwait(false);
ourInstanceUser = await instanceClient.Users.Read(cancellationToken).ConfigureAwait(false);
Assert.AreEqual(RightsHelper.AllRights<DreamDaemonRights>(), ourInstanceUser.DreamDaemonRights.Value);
//can't detach online instance
await ApiAssert.ThrowsException<ConflictException>(() => instanceManagerClient.Detach(firstTest, cancellationToken), ErrorCode.InstanceDetachOnline).ConfigureAwait(false);