public void TestGetRightsGeneric() { var user = new User(); var instanceUser = new InstanceUser(); var authContext = new AuthenticationContext(null, user, instanceUser); user.AdministrationRights = AdministrationRights.EditUsers; instanceUser.ByondRights = ByondRights.ChangeVersion | ByondRights.ReadActive; Assert.AreEqual((ulong)user.AdministrationRights, authContext.GetRight(RightsType.Administration)); Assert.AreEqual((ulong)instanceUser.ByondRights, authContext.GetRight(RightsType.Byond)); }
public void TestGetRightsGeneric() { var user = new User() { PermissionSet = new PermissionSet() }; var instanceUser = new InstancePermissionSet(); var authContext = new AuthenticationContext(null, user, instanceUser); user.PermissionSet.AdministrationRights = AdministrationRights.WriteUsers; instanceUser.ByondRights = ByondRights.InstallOfficialOrChangeActiveVersion | ByondRights.ReadActive; Assert.AreEqual((ulong)user.PermissionSet.AdministrationRights, authContext.GetRight(RightsType.Administration)); Assert.AreEqual((ulong)instanceUser.ByondRights, authContext.GetRight(RightsType.Byond)); }
public async Task <IActionResult> Delete(long id, CancellationToken cancellationToken) { // don't care if an instance post or not at this point var job = await DatabaseContext .Jobs .AsQueryable() .Where(x => x.Id == id && x.Instance.Id == Instance.Id) .FirstOrDefaultAsync(cancellationToken) .ConfigureAwait(false); if (job == default) { return(NotFound()); } if (job.StoppedAt != null) { return(Conflict(new ErrorMessage(ErrorCode.JobStopped))); } if (job.CancelRight.HasValue && job.CancelRightsType.HasValue && (AuthenticationContext.GetRight(job.CancelRightsType.Value) & job.CancelRight.Value) == 0) { return(Forbid()); } var updatedJob = await jobManager.CancelJob(job, AuthenticationContext.User, false, cancellationToken).ConfigureAwait(false); return(updatedJob != null?Accepted(updatedJob.ToApi()) : Gone()); }
#pragma warning disable CA1506 // TODO: Decomplexify public async Task <IActionResult> 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.RepositoryRights = model.RepositoryRights ?? originalUser.RepositoryRights; originalUser.InstanceUserRights = model.InstanceUserRights ?? originalUser.InstanceUserRights; originalUser.ChatBotRights = model.ChatBotRights ?? originalUser.ChatBotRights; originalUser.ConfigurationRights = model.ConfigurationRights ?? originalUser.ConfigurationRights; originalUser.DreamDaemonRights = model.DreamDaemonRights ?? originalUser.DreamDaemonRights; originalUser.DreamMakerRights = model.DreamMakerRights ?? originalUser.DreamMakerRights; await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); return(Json(originalUser.UserId == AuthenticationContext.User.Id || (AuthenticationContext.GetRight(RightsType.InstanceUser) & (ulong)InstanceUserRights.ReadUsers) != 0 ? originalUser.ToApi() : new Api.Models.InstanceUser { UserId = originalUser.UserId })); }
public async Task <IActionResult> GetId(long id, CancellationToken cancellationToken) { if (id == AuthenticationContext.User.Id) { return(Read()); } if (!((AdministrationRights)AuthenticationContext.GetRight(RightsType.Administration)).HasFlag(AdministrationRights.ReadUsers)) { return(Forbid()); } var user = await DatabaseContext.Users .AsQueryable() .Where(x => x.Id == id) .Include(x => x.CreatedBy) .Include(x => x.OAuthConnections) .Include(x => x.Group) .ThenInclude(x => x.PermissionSet) .Include(x => x.PermissionSet) .FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); if (user == default) { return(NotFound()); } if (user.CanonicalName == Models.User.CanonicalizeName(Models.User.TgsSystemUserName)) { return(Forbid()); } return(Json(user.ToApi())); }
public override async Task <IActionResult> Update([FromBody] UserUpdate model, CancellationToken cancellationToken) { if (model == null) { throw new ArgumentNullException(nameof(model)); } var passwordEditOnly = !AuthenticationContext.User.AdministrationRights.Value.HasFlag(AdministrationRights.WriteUsers); var originalUser = passwordEditOnly ? AuthenticationContext.User : await DatabaseContext.Users.Where(x => x.Id == model.Id) .Include(x => x.CreatedBy) .FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); if (originalUser == default) { return(NotFound()); } if (passwordEditOnly && (model.Id != originalUser.Id || model.InstanceManagerRights.HasValue || model.AdministrationRights.HasValue || model.Enabled.HasValue || model.SystemIdentifier != null || model.Name != null)) { return(Forbid()); } if (model.Password != null) { if (originalUser.PasswordHash == null) { return(BadRequest(new ErrorMessage { Message = "Cannot convert a system user to a password user!" })); } cryptographySuite.SetUserPassword(originalUser, model.Password, false); } else if (model.SystemIdentifier != null && model.SystemIdentifier != originalUser.SystemIdentifier) { return(BadRequest(new ErrorMessage { Message = "Cannot change a user's system identifier!" })); } if (model.Name != null && model.Name.ToUpperInvariant() != originalUser.CanonicalName) { return(BadRequest(new ErrorMessage { Message = "Can only change capitalization of a user's name!" })); } originalUser.InstanceManagerRights = model.InstanceManagerRights ?? originalUser.InstanceManagerRights; originalUser.AdministrationRights = model.AdministrationRights ?? originalUser.AdministrationRights; originalUser.Enabled = model.Enabled ?? originalUser.Enabled; originalUser.Name = model.Name ?? originalUser.Name; await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); return(Json(model.Id == originalUser.Id || (AuthenticationContext.GetRight(RightsType.Administration) & (ulong)AdministrationRights.ReadUsers) != 0 ? originalUser.ToApi(true) : new Api.Models.User { Id = originalUser.Id })); }
/// <summary> /// Implementation of <see cref="Read(CancellationToken)"/> /// </summary> /// <param name="settings">The <see cref="DreamDaemonSettings"/> to operate on if any</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 operation</returns> async Task <IActionResult> ReadImpl(DreamDaemonSettings settings, CancellationToken cancellationToken) { var instance = instanceManager.GetInstance(Instance); var dd = instance.Watchdog; var metadata = (AuthenticationContext.GetRight(RightsType.DreamDaemon) & (ulong)DreamDaemonRights.ReadMetadata) != 0; var revision = (AuthenticationContext.GetRight(RightsType.DreamDaemon) & (ulong)DreamDaemonRights.ReadRevision) != 0; if (settings == null) { settings = await DatabaseContext .Instances .AsQueryable() .Where(x => x.Id == Instance.Id) .Select(x => x.DreamDaemonSettings) .FirstOrDefaultAsync(cancellationToken) .ConfigureAwait(false); if (settings == default) { return(StatusCode((int)HttpStatusCode.Gone)); } } var result = new DreamDaemon(); if (metadata) { var alphaActive = dd.AlphaIsActive; var llp = dd.LastLaunchParameters; var rstate = dd.RebootState; result.AutoStart = settings.AutoStart.Value; result.CurrentPort = alphaActive ? llp?.PrimaryPort.Value : llp?.SecondaryPort.Value; result.CurrentSecurity = llp?.SecurityLevel.Value; result.CurrentAllowWebclient = llp?.AllowWebClient.Value; result.PrimaryPort = settings.PrimaryPort.Value; result.AllowWebClient = settings.AllowWebClient.Value; result.Running = dd.Running; result.SecondaryPort = settings.SecondaryPort.Value; result.SecurityLevel = settings.SecurityLevel.Value; result.SoftRestart = rstate == RebootState.Restart; result.SoftShutdown = rstate == RebootState.Shutdown; result.StartupTimeout = settings.StartupTimeout.Value; result.HeartbeatSeconds = settings.HeartbeatSeconds.Value; } if (revision) { var latestCompileJob = instance.LatestCompileJob(); result.ActiveCompileJob = ((dd.Running ? dd.ActiveCompileJob : latestCompileJob) ?? latestCompileJob)?.ToApi(); if (latestCompileJob?.Id != result.ActiveCompileJob?.Id) { result.StagedCompileJob = latestCompileJob?.ToApi(); } } return(Json(result)); }
public override async Task <IActionResult> Update([FromBody] Api.Models.Byond model, CancellationToken cancellationToken) { if (model == null) { throw new ArgumentNullException(nameof(model)); } if (model.Version == null) { return(BadRequest(new ErrorMessage { Message = "Missing version!" })); } var byondManager = instanceManager.GetInstance(Instance).ByondManager; // remove cruff fields var installingVersion = new Version(model.Version.Major, model.Version.Minor); var result = new Api.Models.Byond(); if (byondManager.InstalledVersions.Any(x => x == model.Version)) { Logger.LogInformation("User ID {0} changing instance ID {1} BYOND version to {2}", AuthenticationContext.User.Id, Instance.Id, installingVersion); await byondManager.ChangeVersion(model.Version, cancellationToken).ConfigureAwait(false); } else { Logger.LogInformation("User ID {0} installing BYOND version to {2} on instance ID {1}", AuthenticationContext.User.Id, Instance.Id, installingVersion); // run the install through the job manager var job = new Models.Job { Description = String.Format(CultureInfo.InvariantCulture, "Install BYOND version {0}", installingVersion), StartedBy = AuthenticationContext.User, CancelRightsType = RightsType.Byond, CancelRight = (ulong)ByondRights.CancelInstall, Instance = Instance }; await jobManager.RegisterOperation(job, (paramJob, databaseContext, progressHandler, ct) => byondManager.ChangeVersion(installingVersion, ct), cancellationToken).ConfigureAwait(false); result.InstallJob = job.ToApi(); } if ((AuthenticationContext.GetRight(RightsType.Byond) & (ulong)ByondRights.ReadActive) != 0) { result.Version = byondManager.ActiveVersion; } return(result.InstallJob != null ? (IActionResult)Accepted(result) : Json(result)); }
#pragma warning disable CA1506 // TODO: Decomplexify public async Task <IActionResult> Update([FromBody] InstancePermissionSetRequest model, CancellationToken cancellationToken) { if (model == null) { throw new ArgumentNullException(nameof(model)); } var originalPermissionSet = await DatabaseContext .Instances .AsQueryable() .Where(x => x.Id == Instance.Id) .SelectMany(x => x.InstancePermissionSets) .Where(x => x.PermissionSetId == model.PermissionSetId) .FirstOrDefaultAsync(cancellationToken) .ConfigureAwait(false); if (originalPermissionSet == null) { return(Gone()); } originalPermissionSet.ByondRights = RightsHelper.Clamp(model.ByondRights ?? originalPermissionSet.ByondRights.Value); originalPermissionSet.RepositoryRights = RightsHelper.Clamp(model.RepositoryRights ?? originalPermissionSet.RepositoryRights.Value); originalPermissionSet.InstancePermissionSetRights = RightsHelper.Clamp(model.InstancePermissionSetRights ?? originalPermissionSet.InstancePermissionSetRights.Value); originalPermissionSet.ChatBotRights = RightsHelper.Clamp(model.ChatBotRights ?? originalPermissionSet.ChatBotRights.Value); originalPermissionSet.ConfigurationRights = RightsHelper.Clamp(model.ConfigurationRights ?? originalPermissionSet.ConfigurationRights.Value); originalPermissionSet.DreamDaemonRights = RightsHelper.Clamp(model.DreamDaemonRights ?? originalPermissionSet.DreamDaemonRights.Value); originalPermissionSet.DreamMakerRights = RightsHelper.Clamp(model.DreamMakerRights ?? originalPermissionSet.DreamMakerRights.Value); await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); var showFullPermissionSet = originalPermissionSet.PermissionSetId == AuthenticationContext.PermissionSet.Id.Value || (AuthenticationContext.GetRight(RightsType.InstancePermissionSet) & (ulong)InstancePermissionSetRights.Read) != 0; return(Json( showFullPermissionSet ? originalPermissionSet.ToApi() : new InstancePermissionSetResponse { PermissionSetId = originalPermissionSet.PermissionSetId, })); }
/// <summary> /// Implementation of <see cref="Read(CancellationToken)"/> /// </summary> /// <param name="settings">The <see cref="DreamDaemonSettings"/> to operate on if any</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 operation</returns> async Task <IActionResult> ReadImpl(DreamDaemonSettings settings, CancellationToken cancellationToken) { var instance = instanceManager.GetInstance(Instance); var dd = instance.Watchdog; var metadata = (AuthenticationContext.GetRight(RightsType.DreamDaemon) & (ulong)DreamDaemonRights.ReadMetadata) != 0; var revision = (AuthenticationContext.GetRight(RightsType.DreamDaemon) & (ulong)DreamDaemonRights.ReadRevision) != 0; if (settings == null) { settings = await DatabaseContext.Instances.Where(x => x.Id == Instance.Id).Select(x => x.DreamDaemonSettings).FirstAsync(cancellationToken).ConfigureAwait(false); } var result = new DreamDaemon(); if (metadata) { var alphaActive = dd.AlphaIsActive; var llp = dd.LastLaunchParameters; var rstate = dd.RebootState; result.AutoStart = settings.AutoStart; result.CurrentPort = alphaActive ? llp?.PrimaryPort : llp?.SecondaryPort; result.CurrentSecurity = llp?.SecurityLevel; result.CurrentAllowWebclient = llp?.AllowWebClient; result.PrimaryPort = settings.PrimaryPort; result.AllowWebClient = settings.AllowWebClient; result.Running = dd.Running; result.SecondaryPort = settings.SecondaryPort; result.SecurityLevel = settings.SecurityLevel; result.SoftRestart = rstate == RebootState.Restart; result.SoftShutdown = rstate == RebootState.Shutdown; } ; if (revision) { result.ActiveCompileJob = dd.ActiveCompileJob?.ToApi(); var compileJob = instance.LatestCompileJob(); result.StagedCompileJob = compileJob?.ToApi(); } return(Json(result)); }
public async Task <IActionResult> GetId(long id, CancellationToken cancellationToken) { if (id == AuthenticationContext.User.Id) { return(Read()); } if (!((AdministrationRights)AuthenticationContext.GetRight(RightsType.Administration)).HasFlag(AdministrationRights.ReadUsers)) { return(Forbid()); } var user = await DatabaseContext.Users .Where(x => x.Id == id) .Include(x => x.CreatedBy) .FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); if (user == default) { return(NotFound()); } return(Json(user.ToApi(true))); }
#pragma warning disable CA1506 // TODO: Decomplexify public async Task <IActionResult> Update([FromBody] Api.Models.InstanceUser model, CancellationToken cancellationToken) { if (model == null) { throw new ArgumentNullException(nameof(model)); } var originalUser = await DatabaseContext .Instances .AsQueryable() .Where(x => x.Id == Instance.Id) .SelectMany(x => x.InstanceUsers) .Where(x => x.UserId == model.UserId) .FirstOrDefaultAsync(cancellationToken) .ConfigureAwait(false); if (originalUser == null) { return(Gone()); } originalUser.ByondRights = RightsHelper.Clamp(model.ByondRights ?? originalUser.ByondRights.Value); originalUser.RepositoryRights = RightsHelper.Clamp(model.RepositoryRights ?? originalUser.RepositoryRights.Value); originalUser.InstanceUserRights = RightsHelper.Clamp(model.InstanceUserRights ?? originalUser.InstanceUserRights.Value); originalUser.ChatBotRights = RightsHelper.Clamp(model.ChatBotRights ?? originalUser.ChatBotRights.Value); originalUser.ConfigurationRights = RightsHelper.Clamp(model.ConfigurationRights ?? originalUser.ConfigurationRights.Value); originalUser.DreamDaemonRights = RightsHelper.Clamp(model.DreamDaemonRights ?? originalUser.DreamDaemonRights.Value); originalUser.DreamMakerRights = RightsHelper.Clamp(model.DreamMakerRights ?? originalUser.DreamMakerRights.Value); await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); return(Json(originalUser.UserId == AuthenticationContext.User.Id || (AuthenticationContext.GetRight(RightsType.InstanceUser) & (ulong)InstanceUserRights.ReadUsers) != 0 ? originalUser.ToApi() : new Api.Models.InstanceUser { UserId = originalUser.UserId })); }
public async Task <IActionResult> Delete(long id, CancellationToken cancellationToken) { // don't care if an instance post or not at this point var job = await DatabaseContext.Jobs.Where(x => x.Id == id && x.Instance.Id == Instance.Id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); if (job == default(Job)) { return(NotFound()); } if (job.StoppedAt != null) { return(StatusCode((int)HttpStatusCode.Gone)); } if (job.CancelRight.HasValue && job.CancelRightsType.HasValue && (AuthenticationContext.GetRight(job.CancelRightsType.Value) & job.CancelRight.Value) == 0) { return(Forbid()); } var cancelled = await jobManager.CancelJob(job, AuthenticationContext.User, false, cancellationToken).ConfigureAwait(false); return(cancelled ? (IActionResult)Accepted() : StatusCode((int)HttpStatusCode.Gone)); }
public async Task <IActionResult> Update([FromBody] UserUpdate model, CancellationToken cancellationToken) { if (model == null) { throw new ArgumentNullException(nameof(model)); } var callerAdministrationRights = (AdministrationRights)AuthenticationContext.GetRight(RightsType.Administration); var passwordEditOnly = !callerAdministrationRights.HasFlag(AdministrationRights.WriteUsers); var originalUser = passwordEditOnly ? AuthenticationContext.User : await DatabaseContext.Users.Where(x => x.Id == model.Id) .Include(x => x.CreatedBy) .FirstOrDefaultAsync(cancellationToken) .ConfigureAwait(false); if (originalUser == default) { return(NotFound()); } // Ensure they are only trying to edit password (system identity change will trigger a bad request) if (passwordEditOnly && (model.Id != originalUser.Id || model.InstanceManagerRights.HasValue || model.AdministrationRights.HasValue || model.Enabled.HasValue || model.Name != null)) { return(Forbid()); } if (model.SystemIdentifier != null && model.SystemIdentifier != originalUser.SystemIdentifier) { return(BadRequest(new ErrorMessage { Message = "Cannot change a user's system identifier!" })); } if (model.Password != null) { var result = TrySetPassword(originalUser, model.Password); if (result != null) { return(result); } } if (model.Name != null && Models.User.CanonicalizeName(model.Name) != originalUser.CanonicalName) { return(BadRequest(new ErrorMessage { Message = "Can only change capitalization of a user's name!" })); } originalUser.InstanceManagerRights = model.InstanceManagerRights ?? originalUser.InstanceManagerRights; originalUser.AdministrationRights = model.AdministrationRights ?? originalUser.AdministrationRights; originalUser.Enabled = model.Enabled ?? originalUser.Enabled; var fail = CheckValidName(model); if (fail != null) { return(fail); } originalUser.Name = model.Name ?? originalUser.Name; await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); // return id only if not a self update or and cannot read users return(Json( model.Id == originalUser.Id || callerAdministrationRights.HasFlag(AdministrationRights.ReadUsers) ? originalUser.ToApi(true) : new Api.Models.User { Id = originalUser.Id })); }
public override async Task <IActionResult> Update([FromBody] DreamMaker model, CancellationToken cancellationToken) { if (model.ApiValidationPort == 0) { return(BadRequest(new ErrorMessage { Message = "API Validation port cannot be 0!" })); } if (model.ApiValidationSecurityLevel == DreamDaemonSecurity.Ultrasafe) { return(BadRequest(new ErrorMessage { Message = "This version of TGS does not support the ultrasafe DreamDaemon configuration!" })); } var hostModel = await DatabaseContext.DreamMakerSettings.Where(x => x.InstanceId == Instance.Id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); if (hostModel == null) { return(StatusCode((int)HttpStatusCode.Gone)); } if (model.ProjectName != null) { if (!AuthenticationContext.InstanceUser.DreamMakerRights.Value.HasFlag(DreamMakerRights.SetDme)) { return(Forbid()); } if (model.ProjectName.Length == 0) { hostModel.ProjectName = null; } else { hostModel.ProjectName = model.ProjectName; } } if (model.ApiValidationPort.HasValue) { if (!AuthenticationContext.InstanceUser.DreamMakerRights.Value.HasFlag(DreamMakerRights.SetApiValidationPort)) { return(Forbid()); } hostModel.ApiValidationPort = model.ApiValidationPort; } if (model.ApiValidationSecurityLevel.HasValue) { if (!AuthenticationContext.InstanceUser.DreamMakerRights.Value.HasFlag(DreamMakerRights.SetSecurityLevel)) { return(Forbid()); } hostModel.ApiValidationSecurityLevel = model.ApiValidationSecurityLevel; } await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); if ((AuthenticationContext.GetRight(RightsType.DreamMaker) & (ulong)DreamMakerRights.Read) == 0) { return(Ok()); } return(await Read(cancellationToken).ConfigureAwait(false)); }
#pragma warning disable CA1506 // TODO: Decomplexify public async Task <IActionResult> Update([FromBody] Api.Models.Byond model, CancellationToken cancellationToken) #pragma warning restore CA1506 { if (model == null) { throw new ArgumentNullException(nameof(model)); } if (model.Version == null || model.Version.Revision != -1 || (model.Content != null && model.Version.Build > 0)) { return(BadRequest(new ErrorMessage(ErrorCode.ModelValidationFailure))); } var userByondRights = AuthenticationContext.InstanceUser.ByondRights.Value; if ((!userByondRights.HasFlag(ByondRights.InstallOfficialOrChangeActiveVersion) && model.Content == null) || (!userByondRights.HasFlag(ByondRights.InstallCustomVersion) && model.Content != null)) { return(Forbid()); } // remove cruff fields var result = new Api.Models.Byond(); return(await WithComponentInstance( async instance => { var byondManager = instance.ByondManager; if (model.Content == null && byondManager.InstalledVersions.Any(x => x == model.Version)) { Logger.LogInformation( "User ID {0} changing instance ID {1} BYOND version to {2}", AuthenticationContext.User.Id, Instance.Id, model.Version); await byondManager.ChangeVersion(model.Version, null, cancellationToken).ConfigureAwait(false); } else if (model.Version.Build > 0) { return BadRequest(new ErrorMessage(ErrorCode.ByondNonExistentCustomVersion)); } else { var installingVersion = model.Version.Build <= 0 ? new Version(model.Version.Major, model.Version.Minor) : model.Version; Logger.LogInformation( "User ID {0} installing BYOND version to {1} on instance ID {2}", AuthenticationContext.User.Id, installingVersion, Instance.Id); // run the install through the job manager var job = new Models.Job { Description = $"Install {(model.Content == null ? String.Empty : "custom ")}BYOND version {model.Version.Major}.{model.Version.Minor}", StartedBy = AuthenticationContext.User, CancelRightsType = RightsType.Byond, CancelRight = (ulong)ByondRights.CancelInstall, Instance = Instance }; await jobManager.RegisterOperation( job, (core, databaseContextFactory, paramJob, progressHandler, jobCancellationToken) => core.ByondManager.ChangeVersion( model.Version, model.Content, jobCancellationToken), cancellationToken) .ConfigureAwait(false); result.InstallJob = job.ToApi(); } if ((AuthenticationContext.GetRight(RightsType.Byond) & (ulong)ByondRights.ReadActive) != 0) { result.Version = byondManager.ActiveVersion; } return result.InstallJob != null ? (IActionResult)Accepted(result) : Json(result); }) .ConfigureAwait(false)); }
public async Task <IActionResult> Update([FromBody] DreamMaker model, CancellationToken cancellationToken) { if (model == null) { throw new ArgumentNullException(nameof(model)); } if (model.ApiValidationPort == 0) { throw new InvalidOperationException("ApiValidationPort cannot be 0!"); } var hostModel = await DatabaseContext .DreamMakerSettings .AsQueryable() .Where(x => x.InstanceId == Instance.Id) .FirstOrDefaultAsync(cancellationToken) .ConfigureAwait(false); if (hostModel == null) { return(Gone()); } if (model.ProjectName != null) { if (!AuthenticationContext.InstanceUser.DreamMakerRights.Value.HasFlag(DreamMakerRights.SetDme)) { return(Forbid()); } if (model.ProjectName.Length == 0) { hostModel.ProjectName = null; } else { hostModel.ProjectName = model.ProjectName; } } if (model.ApiValidationPort.HasValue) { if (!AuthenticationContext.InstanceUser.DreamMakerRights.Value.HasFlag(DreamMakerRights.SetApiValidationPort)) { return(Forbid()); } hostModel.ApiValidationPort = model.ApiValidationPort; } if (model.ApiValidationSecurityLevel.HasValue) { if (!AuthenticationContext.InstanceUser.DreamMakerRights.Value.HasFlag(DreamMakerRights.SetSecurityLevel)) { return(Forbid()); } hostModel.ApiValidationSecurityLevel = model.ApiValidationSecurityLevel; } if (model.RequireDMApiValidation.HasValue) { if (!AuthenticationContext.InstanceUser.DreamMakerRights.Value.HasFlag(DreamMakerRights.SetApiValidationRequirement)) { return(Forbid()); } hostModel.RequireDMApiValidation = model.RequireDMApiValidation; } await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); if ((AuthenticationContext.GetRight(RightsType.DreamMaker) & (ulong)DreamMakerRights.Read) == 0) { return(NoContent()); } return(await Read(cancellationToken).ConfigureAwait(false)); }
#pragma warning disable CA1506 // TODO: Decomplexify public async Task <IActionResult> Update([FromBody] DreamDaemon model, CancellationToken cancellationToken) { if (model == null) { throw new ArgumentNullException(nameof(model)); } if (model.PrimaryPort == 0) { return(BadRequest(new ErrorMessage { Message = "Primary port cannot be 0!" })); } if (model.SecurityLevel == DreamDaemonSecurity.Ultrasafe) { return(BadRequest(new ErrorMessage { Message = "This version of TGS does not support the ultrasafe DreamDaemon configuration!" })); } // alias for changing DD settings var current = await DatabaseContext.Instances.Where(x => x.Id == Instance.Id).Select(x => x.DreamDaemonSettings).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); if (current == default) { return(StatusCode((int)HttpStatusCode.Gone)); } var userRights = (DreamDaemonRights)AuthenticationContext.GetRight(RightsType.DreamDaemon); bool CheckModified <T>(Expression <Func <Api.Models.Internal.DreamDaemonSettings, T> > expression, DreamDaemonRights requiredRight) { var memberSelectorExpression = (MemberExpression)expression.Body; var property = (PropertyInfo)memberSelectorExpression.Member; var newVal = property.GetValue(model); if (newVal == null) { return(false); } if (!userRights.HasFlag(requiredRight) && property.GetValue(current) != newVal) { return(true); } property.SetValue(current, newVal); return(false); } var oldSoftRestart = current.SoftRestart; var oldSoftShutdown = current.SoftShutdown; if (CheckModified(x => x.AllowWebClient, DreamDaemonRights.SetWebClient) || CheckModified(x => x.AutoStart, DreamDaemonRights.SetAutoStart) || CheckModified(x => x.PrimaryPort, DreamDaemonRights.SetPorts) || CheckModified(x => x.SecondaryPort, DreamDaemonRights.SetPorts) || CheckModified(x => x.SecurityLevel, DreamDaemonRights.SetSecurity) || CheckModified(x => x.SoftRestart, DreamDaemonRights.SoftRestart) || CheckModified(x => x.SoftShutdown, DreamDaemonRights.SoftShutdown) || CheckModified(x => x.StartupTimeout, DreamDaemonRights.SetStartupTimeout)) { return(Forbid()); } if (current.PrimaryPort == current.SecondaryPort) { return(BadRequest(new ErrorMessage { Message = "Primary port and secondary port cannot be the same!" })); } var wd = instanceManager.GetInstance(Instance).Watchdog; await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); // run this second because current may be modified by it await wd.ChangeSettings(current, cancellationToken).ConfigureAwait(false); if (!oldSoftRestart.Value && current.SoftRestart.Value) { await wd.Restart(true, cancellationToken).ConfigureAwait(false); } else if (!oldSoftShutdown.Value && current.SoftShutdown.Value) { await wd.Terminate(true, cancellationToken).ConfigureAwait(false); } else if ((oldSoftRestart.Value && !current.SoftRestart.Value) || (oldSoftShutdown.Value && !current.SoftShutdown.Value)) { await wd.ResetRebootState(cancellationToken).ConfigureAwait(false); } return(await ReadImpl(current, cancellationToken).ConfigureAwait(false)); }
#pragma warning disable CA1506 // TODO: Decomplexify public async Task <IActionResult> Update([FromBody] Api.Models.Byond model, CancellationToken cancellationToken) #pragma warning restore CA1506 { if (model == null) { throw new ArgumentNullException(nameof(model)); } var uploadingZip = model.UploadCustomZip == true; if (model.Version == null || model.Version.Revision != -1 || (uploadingZip && model.Version.Build > 0)) { return(BadRequest(new ErrorMessage(ErrorCode.ModelValidationFailure))); } var userByondRights = AuthenticationContext.InstancePermissionSet.ByondRights.Value; if ((!userByondRights.HasFlag(ByondRights.InstallOfficialOrChangeActiveVersion) && !uploadingZip) || (!userByondRights.HasFlag(ByondRights.InstallCustomVersion) && uploadingZip)) { return(Forbid()); } // remove cruff fields var result = new Api.Models.Byond(); return(await WithComponentInstance( async instance => { var byondManager = instance.ByondManager; if (!uploadingZip && byondManager.InstalledVersions.Any(x => x == model.Version)) { Logger.LogInformation( "User ID {0} changing instance ID {1} BYOND version to {2}", AuthenticationContext.User.Id, Instance.Id, model.Version); await byondManager.ChangeVersion(model.Version, null, cancellationToken).ConfigureAwait(false); } else if (model.Version.Build > 0) { return BadRequest(new ErrorMessage(ErrorCode.ByondNonExistentCustomVersion)); } else { var installingVersion = model.Version.Build <= 0 ? new Version(model.Version.Major, model.Version.Minor) : model.Version; Logger.LogInformation( "User ID {0} installing BYOND version to {1} on instance ID {2}", AuthenticationContext.User.Id, installingVersion, Instance.Id); // run the install through the job manager var job = new Models.Job { Description = $"Install {(!uploadingZip ? String.Empty : "custom ")}BYOND version {model.Version.Major}.{model.Version.Minor}", StartedBy = AuthenticationContext.User, CancelRightsType = RightsType.Byond, CancelRight = (ulong)ByondRights.CancelInstall, Instance = Instance }; IFileUploadTicket fileUploadTicket = null; if (uploadingZip) { fileUploadTicket = fileTransferService.CreateUpload(false); } try { await jobManager.RegisterOperation( job, async(core, databaseContextFactory, paramJob, progressHandler, jobCancellationToken) => { Stream zipFileStream = null; if (fileUploadTicket != null) { using (fileUploadTicket) { var uploadStream = await fileUploadTicket.GetResult(jobCancellationToken).ConfigureAwait(false); if (uploadStream == null) { throw new JobException(ErrorCode.FileUploadExpired); } zipFileStream = new MemoryStream(); try { await uploadStream.CopyToAsync(zipFileStream, jobCancellationToken).ConfigureAwait(false); } catch { await zipFileStream.DisposeAsync().ConfigureAwait(false); throw; } } } using (zipFileStream) await core.ByondManager.ChangeVersion( model.Version, zipFileStream, jobCancellationToken) .ConfigureAwait(false); }, cancellationToken) .ConfigureAwait(false); result.InstallJob = job.ToApi(); result.FileTicket = fileUploadTicket?.Ticket.FileTicket; } catch { fileUploadTicket?.Dispose(); throw; } } if ((AuthenticationContext.GetRight(RightsType.Byond) & (ulong)ByondRights.ReadActive) != 0) { result.Version = byondManager.ActiveVersion; } return result.InstallJob != null ? (IActionResult)Accepted(result) : Json(result); }) .ConfigureAwait(false)); }
#pragma warning disable CA1502 // TODO: Decomplexify #pragma warning disable CA1505 public async Task <IActionResult> Update([FromBody] Repository model, CancellationToken cancellationToken) { if (model == null) { throw new ArgumentNullException(nameof(model)); } if (model.AccessUser == null ^ model.AccessToken == null) { return(BadRequest(new ErrorMessage { Message = "Either both accessToken and accessUser must be present or neither!" })); } if (model.CheckoutSha != null && model.Reference != null) { return(BadRequest(new ErrorMessage { Message = "Only one of sha or reference may be specified!" })); } if (model.CheckoutSha != null && model.UpdateFromOrigin == true) { return(BadRequest(new ErrorMessage { Message = "Cannot update a reference when checking out a sha!" })); } if (model.Origin != null) { return(BadRequest(new ErrorMessage { Message = "origin cannot be modified without deleting the repository!" })); } if (model.NewTestMerges?.Any(x => !x.Number.HasValue) == true) { return(BadRequest(new ErrorMessage { Message = "All new test merges must provide a number!" })); } if (model.NewTestMerges?.Any(x => model.NewTestMerges.Any(y => x != y && x.Number == y.Number)) == true) { return(BadRequest(new ErrorMessage { Message = "Cannot test merge the same PR twice in one job!" })); } if (model.CommitterName?.Length == 0) { return(BadRequest(new ErrorMessage { Message = "Cannot set empty committer name!" })); } if (model.CommitterEmail?.Length == 0) { return(BadRequest(new ErrorMessage { Message = "Cannot set empty committer e=mail!" })); } var newTestMerges = model.NewTestMerges != null && model.NewTestMerges.Count > 0; var userRights = (RepositoryRights)AuthenticationContext.GetRight(RightsType.Repository); if (newTestMerges && !userRights.HasFlag(RepositoryRights.MergePullRequest)) { return(Forbid()); } var currentModel = await DatabaseContext.RepositorySettings.Where(x => x.InstanceId == Instance.Id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); if (currentModel == default) { return(StatusCode((int)HttpStatusCode.Gone)); } bool CheckModified <T>(Expression <Func <Api.Models.Internal.RepositorySettings, T> > expression, RepositoryRights requiredRight) { var memberSelectorExpression = (MemberExpression)expression.Body; var property = (PropertyInfo)memberSelectorExpression.Member; var newVal = property.GetValue(model); if (newVal == null) { return(false); } if (!userRights.HasFlag(requiredRight) && property.GetValue(currentModel) != newVal) { return(true); } property.SetValue(currentModel, newVal); return(false); } if (CheckModified(x => x.AccessToken, RepositoryRights.ChangeCredentials) || CheckModified(x => x.AccessUser, RepositoryRights.ChangeCredentials) || CheckModified(x => x.AutoUpdatesKeepTestMerges, RepositoryRights.ChangeAutoUpdateSettings) || CheckModified(x => x.AutoUpdatesSynchronize, RepositoryRights.ChangeAutoUpdateSettings) || CheckModified(x => x.CommitterEmail, RepositoryRights.ChangeCommitter) || CheckModified(x => x.CommitterName, RepositoryRights.ChangeCommitter) || CheckModified(x => x.PushTestMergeCommits, RepositoryRights.ChangeTestMergeCommits) || CheckModified(x => x.ShowTestMergeCommitters, RepositoryRights.ChangeTestMergeCommits) || CheckModified(x => x.PostTestMergeComment, RepositoryRights.ChangeTestMergeCommits) || (model.UpdateFromOrigin == true && !userRights.HasFlag(RepositoryRights.UpdateBranch))) { return(Forbid()); } if (currentModel.AccessToken?.Length == 0 && currentModel.AccessUser?.Length == 0) { // setting an empty string clears everything currentModel.AccessUser = null; currentModel.AccessToken = null; } var canRead = userRights.HasFlag(RepositoryRights.Read); var api = canRead ? currentModel.ToApi() : new Repository(); var repoManager = instanceManager.GetInstance(Instance).RepositoryManager; if (canRead) { if (repoManager.CloneInProgress) { return(Conflict(new ErrorMessage { Message = "A clone operation is in progress!" })); } if (repoManager.InUse) { return(Conflict(new ErrorMessage { Message = "The repo is busy!" })); } using (var repo = await repoManager.LoadRepository(cancellationToken).ConfigureAwait(false)) { if (repo == null) { return(Conflict(new ErrorMessage { Message = "Repository could not be loaded!" })); } await PopulateApi(api, repo, DatabaseContext, Instance, cancellationToken).ConfigureAwait(false); } } // this is just db stuf so stow it away await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); // format the job description string description = null; if (model.UpdateFromOrigin == true) { if (model.Reference != null) { description = String.Format(CultureInfo.InvariantCulture, "Fetch and hard reset repository to origin/{0}", model.Reference); } else if (model.CheckoutSha != null) { description = String.Format(CultureInfo.InvariantCulture, "Fetch and checkout {0} in repository", model.CheckoutSha); } else { description = "Pull current repository reference"; } } else if (model.Reference != null || model.CheckoutSha != null) { description = String.Format(CultureInfo.InvariantCulture, "Checkout repository {0} {1}", model.Reference != null ? "reference" : "SHA", model.Reference ?? model.CheckoutSha); } if (newTestMerges) { description = String.Format(CultureInfo.InvariantCulture, "{0}est merge pull request(s) {1}{2}", description != null ? String.Format(CultureInfo.InvariantCulture, "{0} and t", description) : "T", String.Join(", ", model.NewTestMerges.Select(x => String.Format(CultureInfo.InvariantCulture, "#{0}{1}", x.Number, x.PullRequestRevision != null ? String.Format(CultureInfo.InvariantCulture, " at {0}", x.PullRequestRevision.Substring(0, 7)) : String.Empty))), description != null ? String.Empty : " in repository"); } if (description == null) { return(Json(api)); // no git changes } var job = new Models.Job { Description = description, StartedBy = AuthenticationContext.User, Instance = Instance, CancelRightsType = RightsType.Repository, CancelRight = (ulong)RepositoryRights.CancelPendingChanges, }; await jobManager.RegisterOperation(job, async (paramJob, databaseContext, progressReporter, ct) => { using (var repo = await repoManager.LoadRepository(ct).ConfigureAwait(false)) { if (repo == null) { throw new JobException("Repository could not be loaded!"); } var modelHasShaOrReference = model.CheckoutSha != null || model.Reference != null; var startReference = repo.Reference; var startSha = repo.Head; string postUpdateSha = null; if (newTestMerges && !repo.IsGitHubRepository) { throw new JobException("Cannot test merge on a non GitHub based repository!"); } var committerName = currentModel.ShowTestMergeCommitters.Value ? AuthenticationContext.User.Name : currentModel.CommitterName; var hardResettingToOriginReference = model.UpdateFromOrigin == true && model.Reference != null; var numSteps = (model.NewTestMerges?.Count ?? 0) + (model.UpdateFromOrigin == true ? 1 : 0) + (!modelHasShaOrReference ? 2 : (hardResettingToOriginReference ? 3 : 1)); var doneSteps = 0; Action <int> NextProgressReporter() { var tmpDoneSteps = doneSteps; ++doneSteps; return(progress => progressReporter((progress + (100 * tmpDoneSteps)) / numSteps)); } progressReporter(0); // get a base line for where we are Models.RevisionInformation lastRevisionInfo = null; var attachedInstance = new Models.Instance { Id = Instance.Id }; databaseContext.Instances.Attach(attachedInstance); await LoadRevisionInformation(repo, databaseContext, attachedInstance, null, x => lastRevisionInfo = x, ct).ConfigureAwait(false); // apply new rev info, tracking applied test merges async Task UpdateRevInfo() { var last = lastRevisionInfo; await LoadRevisionInformation(repo, databaseContext, attachedInstance, last.OriginCommitSha, x => lastRevisionInfo = x, ct).ConfigureAwait(false); lastRevisionInfo.ActiveTestMerges.AddRange(last.ActiveTestMerges); } try { // fetch/pull if (model.UpdateFromOrigin == true) { if (!repo.Tracking) { throw new JobException("Not on an updatable reference!"); } await repo.FetchOrigin(currentModel.AccessUser, currentModel.AccessToken, NextProgressReporter(), ct).ConfigureAwait(false); doneSteps = 1; if (!modelHasShaOrReference) { var fastForward = await repo.MergeOrigin(committerName, currentModel.CommitterEmail, NextProgressReporter(), ct).ConfigureAwait(false); if (!fastForward.HasValue) { throw new JobException("Merge conflict occurred during origin update!"); } await UpdateRevInfo().ConfigureAwait(false); if (fastForward.Value) { lastRevisionInfo.OriginCommitSha = repo.Head; await repo.Sychronize(currentModel.AccessUser, currentModel.AccessToken, currentModel.CommitterName, currentModel.CommitterEmail, NextProgressReporter(), true, ct).ConfigureAwait(false); postUpdateSha = repo.Head; } else { NextProgressReporter()(100); } } } // checkout/hard reset if (modelHasShaOrReference) { if ((model.CheckoutSha != null && repo.Head.ToUpperInvariant().StartsWith(model.CheckoutSha.ToUpperInvariant(), StringComparison.Ordinal)) || (model.Reference != null && repo.Reference.ToUpperInvariant() != model.Reference.ToUpperInvariant())) { var committish = model.CheckoutSha ?? model.Reference; var isSha = await repo.IsSha(committish, cancellationToken).ConfigureAwait(false); if ((isSha && model.Reference != null) || (!isSha && model.CheckoutSha != null)) { throw new JobException("Attempted to checkout a SHA or reference that was actually the opposite!"); } await repo.CheckoutObject(committish, NextProgressReporter(), ct).ConfigureAwait(false); await LoadRevisionInformation(repo, databaseContext, attachedInstance, null, x => lastRevisionInfo = x, ct).ConfigureAwait(false); // we've either seen origin before or what we're checking out is on origin } else { NextProgressReporter()(100); } if (hardResettingToOriginReference) { if (!repo.Tracking) { throw new JobException("Checked out reference does not track a remote object!"); } await repo.ResetToOrigin(NextProgressReporter(), ct).ConfigureAwait(false); await repo.Sychronize(currentModel.AccessUser, currentModel.AccessToken, currentModel.CommitterName, currentModel.CommitterEmail, NextProgressReporter(), true, ct).ConfigureAwait(false); await LoadRevisionInformation(repo, databaseContext, attachedInstance, null, x => lastRevisionInfo = x, ct).ConfigureAwait(false); // repo head is on origin so force this // will update the db if necessary lastRevisionInfo.OriginCommitSha = repo.Head; } } // test merging Dictionary <int, Octokit.PullRequest> prMap = null; if (newTestMerges) { // bit of sanitization foreach (var I in model.NewTestMerges.Where(x => String.IsNullOrWhiteSpace(x.PullRequestRevision))) { I.PullRequestRevision = null; } var gitHubClient = currentModel.AccessToken != null ? gitHubClientFactory.CreateClient(currentModel.AccessToken) : (String.IsNullOrEmpty(generalConfiguration.GitHubAccessToken) ? gitHubClientFactory.CreateClient() : gitHubClientFactory.CreateClient(generalConfiguration.GitHubAccessToken)); var repoOwner = repo.GitHubOwner; var repoName = repo.GitHubRepoName; // optimization: if we've already merged these exact same commits in this fashion before, just find the rev info for it and check it out Models.RevisionInformation revInfoWereLookingFor = null; bool needToApplyRemainingPrs = true; if (lastRevisionInfo.OriginCommitSha == lastRevisionInfo.CommitSha) { // In order for this to work though we need the shas of all the commits if (model.NewTestMerges.Any(x => x.PullRequestRevision == null)) { prMap = new Dictionary <int, Octokit.PullRequest>(); } bool cantSearch = false; foreach (var I in model.NewTestMerges) { if (I.PullRequestRevision != null) #pragma warning disable CA1308 // Normalize strings to uppercase { I.PullRequestRevision = I.PullRequestRevision?.ToLowerInvariant(); // ala libgit2 } #pragma warning restore CA1308 // Normalize strings to uppercase else { try { // retrieve the latest sha var pr = await gitHubClient.PullRequest.Get(repoOwner, repoName, I.Number.Value).ConfigureAwait(false); prMap.Add(I.Number.Value, pr); I.PullRequestRevision = pr.Head.Sha; } catch { cantSearch = true; break; } } } if (!cantSearch) { var dbPull = await databaseContext.RevisionInformations .Where(x => x.Instance.Id == Instance.Id && x.OriginCommitSha == lastRevisionInfo.OriginCommitSha && x.ActiveTestMerges.Count <= model.NewTestMerges.Count && x.ActiveTestMerges.Count > 0) .Include(x => x.ActiveTestMerges) .ThenInclude(x => x.TestMerge) .ToListAsync(cancellationToken).ConfigureAwait(false); // split here cause this bit has to be done locally revInfoWereLookingFor = dbPull .Where(x => x.ActiveTestMerges.Count == model.NewTestMerges.Count && x.ActiveTestMerges.Select(y => y.TestMerge) .All(y => model.NewTestMerges.Any(z => y.Number == z.Number && y.PullRequestRevision.StartsWith(z.PullRequestRevision, StringComparison.Ordinal) && (y.Comment?.Trim().ToUpperInvariant() == z.Comment?.Trim().ToUpperInvariant() || z.Comment == null)))) .FirstOrDefault(); if (revInfoWereLookingFor == null && model.NewTestMerges.Count > 1) { // okay try to add at least SOME prs we've seen before var search = model.NewTestMerges.ToList(); var appliedTestMergeIds = new List <long>(); Models.RevisionInformation lastGoodRevInfo = null; do { foreach (var I in search) { revInfoWereLookingFor = dbPull .Where(x => model.NewTestMerges.Any(z => x.PrimaryTestMerge.Number == z.Number && x.PrimaryTestMerge.PullRequestRevision.StartsWith(z.PullRequestRevision, StringComparison.Ordinal) && (x.PrimaryTestMerge.Comment?.Trim().ToUpperInvariant() == z.Comment?.Trim().ToUpperInvariant() || z.Comment == null)) && x.ActiveTestMerges.Select(y => y.TestMerge).All(y => appliedTestMergeIds.Contains(y.Id))) .FirstOrDefault(); if (revInfoWereLookingFor != null) { lastGoodRevInfo = revInfoWereLookingFor; appliedTestMergeIds.Add(revInfoWereLookingFor.PrimaryTestMerge.Id); search.Remove(I); break; } } }while (revInfoWereLookingFor != null && search.Count > 0); revInfoWereLookingFor = lastGoodRevInfo; needToApplyRemainingPrs = search.Count != 0; if (needToApplyRemainingPrs) { model.NewTestMerges = search; } } else if (revInfoWereLookingFor != null) { needToApplyRemainingPrs = false; } } } if (revInfoWereLookingFor != null) { // goteem await repo.ResetToSha(revInfoWereLookingFor.CommitSha, NextProgressReporter(), cancellationToken).ConfigureAwait(false); lastRevisionInfo = revInfoWereLookingFor; } if (needToApplyRemainingPrs) { // an invocation of LoadRevisionInformation could have already loaded this user var contextUser = databaseContext.Users.Local.Where(x => x.Id == AuthenticationContext.User.Id).FirstOrDefault(); if (contextUser == default) { contextUser = new Models.User { Id = AuthenticationContext.User.Id }; databaseContext.Users.Attach(contextUser); } else { Logger.LogTrace("Skipping attaching the user to the database context as it is already loaded!"); } foreach (var I in model.NewTestMerges) { Octokit.PullRequest pr = null; string errorMessage = null; if (lastRevisionInfo.ActiveTestMerges.Any(x => x.TestMerge.Number == I.Number.Value)) { throw new JobException("Cannot test merge the same PR twice in one HEAD!"); } try { // load from cache if possible if (prMap == null || !prMap.TryGetValue(I.Number.Value, out pr)) { pr = await gitHubClient.PullRequest.Get(repoOwner, repoName, I.Number.Value).ConfigureAwait(false); } } catch (Octokit.RateLimitExceededException) { // you look at your anonymous access and sigh errorMessage = "P.R.E. RATE LIMITED"; } catch (Octokit.AuthorizationException) { errorMessage = "P.R.E. BAD CREDENTIALS"; } catch (Octokit.NotFoundException) { // you look at your shithub and sigh errorMessage = "P.R.E. NOT FOUND"; } // we want to take the earliest truth possible to prevent RCEs, if this fails AddTestMerge will set it if (I.PullRequestRevision == null && pr != null) { I.PullRequestRevision = pr.Head.Sha; } var mergeResult = await repo.AddTestMerge(I, committerName, currentModel.CommitterEmail, currentModel.AccessUser, currentModel.AccessToken, NextProgressReporter(), ct).ConfigureAwait(false); if (!mergeResult.HasValue) { throw new JobException(String.Format(CultureInfo.InvariantCulture, "Merge of PR #{0} at {1} conflicted!", I.Number, I.PullRequestRevision.Substring(0, 7))); } ++doneSteps; var revInfoUpdateTask = UpdateRevInfo(); var tm = new Models.TestMerge { Author = pr?.User.Login ?? errorMessage, BodyAtMerge = pr?.Body ?? errorMessage ?? String.Empty, MergedAt = DateTimeOffset.Now, TitleAtMerge = pr?.Title ?? errorMessage ?? String.Empty, Comment = I.Comment, Number = I.Number, MergedBy = contextUser, PullRequestRevision = I.PullRequestRevision, Url = pr?.HtmlUrl ?? errorMessage }; await revInfoUpdateTask.ConfigureAwait(false); lastRevisionInfo.PrimaryTestMerge = tm; lastRevisionInfo.ActiveTestMerges.Add(new RevInfoTestMerge { TestMerge = tm }); } } } var currentHead = repo.Head; if (startSha != currentHead || (postUpdateSha != null && postUpdateSha != currentHead)) { await repo.Sychronize(currentModel.AccessUser, currentModel.AccessToken, currentModel.CommitterName, currentModel.CommitterEmail, NextProgressReporter(), false, ct).ConfigureAwait(false); await UpdateRevInfo().ConfigureAwait(false); } await databaseContext.Save(ct).ConfigureAwait(false); } catch { doneSteps = 0; numSteps = 2; // the stuff didn't make it into the db, forget what we've done and abort await repo.CheckoutObject(startReference ?? startSha, NextProgressReporter(), default).ConfigureAwait(false); if (startReference != null && repo.Head != startSha) { await repo.ResetToSha(startSha, NextProgressReporter(), default).ConfigureAwait(false); } else { progressReporter(100); } throw; } } }, cancellationToken).ConfigureAwait(false); api.ActiveJob = job.ToApi(); return(Accepted(api)); }
#pragma warning disable CA1502 // TODO: Decomplexify #pragma warning disable CA1506 public async Task <IActionResult> Update([FromBody] DreamDaemonResponse model, CancellationToken cancellationToken) { if (model == null) { throw new ArgumentNullException(nameof(model)); } if (model.SoftShutdown == true && model.SoftRestart == true) { return(BadRequest(new ErrorMessageResponse(ErrorCode.DreamDaemonDoubleSoft))); } // alias for changing DD settings var current = await DatabaseContext .Instances .AsQueryable() .Where(x => x.Id == Instance.Id) .Select(x => x.DreamDaemonSettings) .FirstOrDefaultAsync(cancellationToken) .ConfigureAwait(false); if (current == default) { return(Gone()); } if (model.Port.HasValue && model.Port.Value != current.Port.Value) { var verifiedPort = await portAllocator .GetAvailablePort( model.Port.Value, true, cancellationToken) .ConfigureAwait(false); if (verifiedPort != model.Port) { return(Conflict(new ErrorMessageResponse(ErrorCode.PortNotAvailable))); } } var userRights = (DreamDaemonRights)AuthenticationContext.GetRight(RightsType.DreamDaemon); bool CheckModified <T>(Expression <Func <Api.Models.Internal.DreamDaemonSettings, T> > expression, DreamDaemonRights requiredRight) { var memberSelectorExpression = (MemberExpression)expression.Body; var property = (PropertyInfo)memberSelectorExpression.Member; var newVal = property.GetValue(model); if (newVal == null) { return(false); } if (!userRights.HasFlag(requiredRight) && property.GetValue(current) != newVal) { return(true); } property.SetValue(current, newVal); return(false); } return(await WithComponentInstance( async instance => { var watchdog = instance.Watchdog; var rebootState = watchdog.RebootState; var oldSoftRestart = rebootState == RebootState.Restart; var oldSoftShutdown = rebootState == RebootState.Shutdown; if (CheckModified(x => x.AllowWebClient, DreamDaemonRights.SetWebClient) || CheckModified(x => x.AutoStart, DreamDaemonRights.SetAutoStart) || CheckModified(x => x.Port, DreamDaemonRights.SetPort) || CheckModified(x => x.SecurityLevel, DreamDaemonRights.SetSecurity) || (model.SoftRestart.HasValue && !AuthenticationContext.InstancePermissionSet.DreamDaemonRights.Value.HasFlag(DreamDaemonRights.SoftRestart)) || (model.SoftShutdown.HasValue && !AuthenticationContext.InstancePermissionSet.DreamDaemonRights.Value.HasFlag(DreamDaemonRights.SoftShutdown)) || CheckModified(x => x.StartupTimeout, DreamDaemonRights.SetStartupTimeout) || CheckModified(x => x.HeartbeatSeconds, DreamDaemonRights.SetHeartbeatInterval) || CheckModified(x => x.TopicRequestTimeout, DreamDaemonRights.SetTopicTimeout) || CheckModified(x => x.AdditionalParameters, DreamDaemonRights.SetAdditionalParameters)) { return Forbid(); } await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); // run this second because current may be modified by it await watchdog.ChangeSettings(current, cancellationToken).ConfigureAwait(false); if (!oldSoftRestart && model.SoftRestart == true && watchdog.Status == WatchdogStatus.Online) { await watchdog.Restart(true, cancellationToken).ConfigureAwait(false); } else if (!oldSoftShutdown && model.SoftShutdown == true) { await watchdog.Terminate(true, cancellationToken).ConfigureAwait(false); } else if ((oldSoftRestart && model.SoftRestart == false) || (oldSoftShutdown && model.SoftShutdown == false)) { await watchdog.ResetRebootState(cancellationToken).ConfigureAwait(false); } return await ReadImpl(current, cancellationToken).ConfigureAwait(false); }) .ConfigureAwait(false)); }
#pragma warning disable CA1502 // TODO: Decomplexify #pragma warning disable CA1506 public async Task <IActionResult> Update([FromBody] UserUpdateRequest model, CancellationToken cancellationToken) { if (model == null) { throw new ArgumentNullException(nameof(model)); } if (!model.Id.HasValue || model.OAuthConnections?.Any(x => x == null) == true) { return(BadRequest(new ErrorMessageResponse(ErrorCode.ModelValidationFailure))); } if (model.Group != null && model.PermissionSet != null) { return(BadRequest(new ErrorMessageResponse(ErrorCode.UserGroupAndPermissionSet))); } var callerAdministrationRights = (AdministrationRights)AuthenticationContext.GetRight(RightsType.Administration); var canEditAllUsers = callerAdministrationRights.HasFlag(AdministrationRights.WriteUsers); var passwordEdit = canEditAllUsers || callerAdministrationRights.HasFlag(AdministrationRights.EditOwnPassword); var oAuthEdit = canEditAllUsers || callerAdministrationRights.HasFlag(AdministrationRights.EditOwnOAuthConnections); var originalUser = !canEditAllUsers ? AuthenticationContext.User : await DatabaseContext .Users .AsQueryable() .Where(x => x.Id == model.Id) .Include(x => x.CreatedBy) .Include(x => x.OAuthConnections) .Include(x => x.Group) .ThenInclude(x => x.PermissionSet) .Include(x => x.PermissionSet) .FirstOrDefaultAsync(cancellationToken) .ConfigureAwait(false); if (originalUser == default) { return(NotFound()); } if (originalUser.CanonicalName == Models.User.CanonicalizeName(Models.User.TgsSystemUserName)) { return(Forbid()); } // Ensure they are only trying to edit things they have perms for (system identity change will trigger a bad request) if ((!canEditAllUsers && (model.Id != originalUser.Id || model.Enabled.HasValue || model.Group != null || model.PermissionSet != null || model.Name != null)) || (!passwordEdit && model.Password != null) || (!oAuthEdit && model.OAuthConnections != null)) { return(Forbid()); } if (model.SystemIdentifier != null && model.SystemIdentifier != originalUser.SystemIdentifier) { return(BadRequest(new ErrorMessageResponse(ErrorCode.UserSidChange))); } if (model.Password != null) { var result = TrySetPassword(originalUser, model.Password, false); if (result != null) { return(result); } } if (model.Name != null && Models.User.CanonicalizeName(model.Name) != originalUser.CanonicalName) { return(BadRequest(new ErrorMessageResponse(ErrorCode.UserNameChange))); } if (model.Enabled.HasValue) { if (originalUser.Enabled.Value && !model.Enabled.Value) { originalUser.LastPasswordUpdate = DateTimeOffset.UtcNow; } originalUser.Enabled = model.Enabled.Value; } if (model.OAuthConnections != null && (model.OAuthConnections.Count != originalUser.OAuthConnections.Count || !model.OAuthConnections.All(x => originalUser.OAuthConnections.Any(y => y.Provider == x.Provider && y.ExternalUserId == x.ExternalUserId)))) { if (originalUser.CanonicalName == Models.User.CanonicalizeName(DefaultCredentials.AdminUserName)) { return(BadRequest(new ErrorMessageResponse(ErrorCode.AdminUserCannotOAuth))); } if (model.OAuthConnections.Count == 0 && originalUser.PasswordHash == null && originalUser.SystemIdentifier == null) { return(BadRequest(new ErrorMessageResponse(ErrorCode.CannotRemoveLastAuthenticationOption))); } originalUser.OAuthConnections.Clear(); foreach (var updatedConnection in model.OAuthConnections) { originalUser.OAuthConnections.Add(new Models.OAuthConnection { Provider = updatedConnection.Provider, ExternalUserId = updatedConnection.ExternalUserId }); } } if (model.Group != null) { originalUser.Group = await DatabaseContext .Groups .AsQueryable() .Where(x => x.Id == model.Group.Id) .Include(x => x.PermissionSet) .FirstOrDefaultAsync(cancellationToken) .ConfigureAwait(false); if (originalUser.Group == default) { return(Gone()); } DatabaseContext.Groups.Attach(originalUser.Group); if (originalUser.PermissionSet != null) { Logger.LogInformation("Deleting permission set {0}...", originalUser.PermissionSet.Id); DatabaseContext.PermissionSets.Remove(originalUser.PermissionSet); originalUser.PermissionSet = null; } } else if (model.PermissionSet != null) { if (originalUser.PermissionSet == null) { Logger.LogTrace("Creating new permission set..."); originalUser.PermissionSet = new Models.PermissionSet(); } originalUser.PermissionSet.AdministrationRights = model.PermissionSet.AdministrationRights ?? AdministrationRights.None; originalUser.PermissionSet.InstanceManagerRights = model.PermissionSet.InstanceManagerRights ?? InstanceManagerRights.None; originalUser.Group = null; originalUser.GroupId = null; } var fail = CheckValidName(model, false); if (fail != null) { return(fail); } originalUser.Name = model.Name ?? originalUser.Name; await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); Logger.LogInformation("Updated user {0} ({1})", originalUser.Name, originalUser.Id); // return id only if not a self update and cannot read users var canReadBack = AuthenticationContext.User.Id == originalUser.Id || callerAdministrationRights.HasFlag(AdministrationRights.ReadUsers); return(canReadBack ? (IActionResult)Json(originalUser.ToApi()) : NoContent()); }
#pragma warning disable CA1502, CA1505 // TODO: Decomplexify public async Task <IActionResult> Update([FromBody] Repository model, CancellationToken cancellationToken) { if (model == null) { throw new ArgumentNullException(nameof(model)); } if (model.AccessUser == null ^ model.AccessToken == null) { return(BadRequest(new ErrorMessage(ErrorCode.RepoMismatchUserAndAccessToken))); } if (model.CheckoutSha != null && model.Reference != null) { return(BadRequest(new ErrorMessage(ErrorCode.RepoMismatchShaAndReference))); } if (model.CheckoutSha != null && model.UpdateFromOrigin == true) { return(BadRequest(new ErrorMessage(ErrorCode.RepoMismatchShaAndUpdate))); } if (model.NewTestMerges?.Any(x => model.NewTestMerges.Any(y => x != y && x.Number == y.Number)) == true) { return(BadRequest(new ErrorMessage(ErrorCode.RepoDuplicateTestMerge))); } if (model.CommitterName?.Length == 0) { return(BadRequest(new ErrorMessage(ErrorCode.RepoWhitespaceCommitterName))); } if (model.CommitterEmail?.Length == 0) { return(BadRequest(new ErrorMessage(ErrorCode.RepoWhitespaceCommitterEmail))); } var newTestMerges = model.NewTestMerges != null && model.NewTestMerges.Count > 0; var userRights = (RepositoryRights)AuthenticationContext.GetRight(RightsType.Repository); if (newTestMerges && !userRights.HasFlag(RepositoryRights.MergePullRequest)) { return(Forbid()); } var currentModel = await DatabaseContext .RepositorySettings .AsQueryable() .Where(x => x.InstanceId == Instance.Id) .FirstOrDefaultAsync(cancellationToken) .ConfigureAwait(false); if (currentModel == default) { return(Gone()); } bool CheckModified <T>(Expression <Func <Api.Models.Internal.RepositorySettings, T> > expression, RepositoryRights requiredRight) { var memberSelectorExpression = (MemberExpression)expression.Body; var property = (PropertyInfo)memberSelectorExpression.Member; var newVal = property.GetValue(model); if (newVal == null) { return(false); } if (!userRights.HasFlag(requiredRight) && property.GetValue(currentModel) != newVal) { return(true); } property.SetValue(currentModel, newVal); return(false); } if (CheckModified(x => x.AccessToken, RepositoryRights.ChangeCredentials) || CheckModified(x => x.AccessUser, RepositoryRights.ChangeCredentials) || CheckModified(x => x.AutoUpdatesKeepTestMerges, RepositoryRights.ChangeAutoUpdateSettings) || CheckModified(x => x.AutoUpdatesSynchronize, RepositoryRights.ChangeAutoUpdateSettings) || CheckModified(x => x.CommitterEmail, RepositoryRights.ChangeCommitter) || CheckModified(x => x.CommitterName, RepositoryRights.ChangeCommitter) || CheckModified(x => x.PushTestMergeCommits, RepositoryRights.ChangeTestMergeCommits) || CheckModified(x => x.CreateGitHubDeployments, RepositoryRights.ChangeTestMergeCommits) || CheckModified(x => x.ShowTestMergeCommitters, RepositoryRights.ChangeTestMergeCommits) || CheckModified(x => x.PostTestMergeComment, RepositoryRights.ChangeTestMergeCommits) || (model.UpdateFromOrigin == true && !userRights.HasFlag(RepositoryRights.UpdateBranch))) { return(Forbid()); } if (model.AccessToken?.Length == 0 && model.AccessUser?.Length == 0) { // setting an empty string clears everything currentModel.AccessUser = null; currentModel.AccessToken = null; } var canRead = userRights.HasFlag(RepositoryRights.Read); var api = canRead ? currentModel.ToApi() : new Repository(); if (canRead) { var earlyOut = await WithComponentInstance( async instance => { var repoManager = instance.RepositoryManager; if (repoManager.CloneInProgress) { return(Conflict(new ErrorMessage(ErrorCode.RepoCloning))); } if (repoManager.InUse) { return(Conflict(new ErrorMessage(ErrorCode.RepoBusy))); } using var repo = await repoManager.LoadRepository(cancellationToken).ConfigureAwait(false); if (repo == null) { return(Conflict(new ErrorMessage(ErrorCode.RepoMissing))); } await PopulateApi(api, repo, DatabaseContext, Instance, cancellationToken).ConfigureAwait(false); if (model.Origin != null && model.Origin != repo.Origin) { return(BadRequest(new ErrorMessage(ErrorCode.RepoCantChangeOrigin))); } return(null); }) .ConfigureAwait(false); if (earlyOut != null) { return(earlyOut); } } // this is just db stuf so stow it away await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); // format the job description string description = null; if (model.UpdateFromOrigin == true) { if (model.Reference != null) { description = String.Format(CultureInfo.InvariantCulture, "Fetch and hard reset repository to origin/{0}", model.Reference); } else if (model.CheckoutSha != null) { description = String.Format(CultureInfo.InvariantCulture, "Fetch and checkout {0} in repository", model.CheckoutSha); } else { description = "Pull current repository reference"; } } else if (model.Reference != null || model.CheckoutSha != null) { description = String.Format(CultureInfo.InvariantCulture, "Checkout repository {0} {1}", model.Reference != null ? "reference" : "SHA", model.Reference ?? model.CheckoutSha); } if (newTestMerges) { description = String.Format(CultureInfo.InvariantCulture, "{0}est merge(s) {1}{2}", description != null ? String.Format(CultureInfo.InvariantCulture, "{0} and t", description) : "T", String.Join(", ", model.NewTestMerges.Select(x => String.Format(CultureInfo.InvariantCulture, "#{0}{1}", x.Number, x.TargetCommitSha != null ? String.Format(CultureInfo.InvariantCulture, " at {0}", x.TargetCommitSha.Substring(0, 7)) : String.Empty))), description != null ? String.Empty : " in repository"); } if (description == null) { return(Json(api)); // no git changes } async Task <IActionResult> UpdateCallbackThatDesperatelyNeedsRefactoring( IInstanceCore instance, IDatabaseContextFactory databaseContextFactory, Action <int> progressReporter, CancellationToken ct) { var repoManager = instance.RepositoryManager; using var repo = await repoManager.LoadRepository(ct).ConfigureAwait(false); if (repo == null) { throw new JobException(ErrorCode.RepoMissing); } var modelHasShaOrReference = model.CheckoutSha != null || model.Reference != null; var startReference = repo.Reference; var startSha = repo.Head; string postUpdateSha = null; if (newTestMerges && repo.RemoteGitProvider == RemoteGitProvider.Unknown) { throw new JobException(ErrorCode.RepoUnsupportedTestMergeRemote); } var committerName = currentModel.ShowTestMergeCommitters.Value ? AuthenticationContext.User.Name : currentModel.CommitterName; var hardResettingToOriginReference = model.UpdateFromOrigin == true && model.Reference != null; var numSteps = (model.NewTestMerges?.Count ?? 0) + (model.UpdateFromOrigin == true ? 1 : 0) + (!modelHasShaOrReference ? 2 : (hardResettingToOriginReference ? 3 : 1)); var doneSteps = 0; Action <int> NextProgressReporter() { var tmpDoneSteps = doneSteps; ++doneSteps; return(progress => progressReporter((progress + (100 * tmpDoneSteps)) / numSteps)); } progressReporter(0); // get a base line for where we are Models.RevisionInformation lastRevisionInfo = null; var attachedInstance = new Models.Instance { Id = Instance.Id }; Task CallLoadRevInfo(Models.TestMerge testMergeToAdd = null, string lastOriginCommitSha = null) => databaseContextFactory .UseContext( async databaseContext => { databaseContext.Instances.Attach(attachedInstance); var previousRevInfo = lastRevisionInfo; var needsUpdate = await LoadRevisionInformation( repo, databaseContext, attachedInstance, lastOriginCommitSha, x => lastRevisionInfo = x, ct) .ConfigureAwait(false); if (testMergeToAdd != null) { // rev info may have already loaded the user var mergedBy = databaseContext.Users.Local.FirstOrDefault(x => x.Id == AuthenticationContext.User.Id); if (mergedBy == default) { mergedBy = new Models.User { Id = AuthenticationContext.User.Id }; databaseContext.Users.Attach(mergedBy); } testMergeToAdd.MergedBy = mergedBy; foreach (var activeTestMerge in previousRevInfo.ActiveTestMerges) { lastRevisionInfo.ActiveTestMerges.Add(activeTestMerge); } lastRevisionInfo.ActiveTestMerges.Add(new RevInfoTestMerge { TestMerge = testMergeToAdd }); lastRevisionInfo.PrimaryTestMerge = testMergeToAdd; needsUpdate = true; } if (needsUpdate) { await databaseContext.Save(cancellationToken).ConfigureAwait(false); } }); await CallLoadRevInfo().ConfigureAwait(false); // apply new rev info, tracking applied test merges Task UpdateRevInfo(Models.TestMerge testMergeToAdd = null) => CallLoadRevInfo(testMergeToAdd, lastRevisionInfo.OriginCommitSha); try { // fetch/pull if (model.UpdateFromOrigin == true) { if (!repo.Tracking) { throw new JobException(ErrorCode.RepoReferenceRequired); } await repo.FetchOrigin(currentModel.AccessUser, currentModel.AccessToken, NextProgressReporter(), ct).ConfigureAwait(false); doneSteps = 1; if (!modelHasShaOrReference) { var fastForward = await repo.MergeOrigin(committerName, currentModel.CommitterEmail, NextProgressReporter(), ct).ConfigureAwait(false); if (!fastForward.HasValue) { throw new JobException(ErrorCode.RepoMergeConflict); } lastRevisionInfo.OriginCommitSha = await repo.GetOriginSha(cancellationToken).ConfigureAwait(false); await UpdateRevInfo().ConfigureAwait(false); if (fastForward.Value) { await repo.Sychronize(currentModel.AccessUser, currentModel.AccessToken, currentModel.CommitterName, currentModel.CommitterEmail, NextProgressReporter(), true, ct).ConfigureAwait(false); postUpdateSha = repo.Head; } else { NextProgressReporter()(100); } } } // checkout/hard reset if (modelHasShaOrReference) { var validCheckoutSha = model.CheckoutSha != null && !repo.Head.StartsWith(model.CheckoutSha, StringComparison.OrdinalIgnoreCase); var validCheckoutReference = model.Reference != null && !repo.Reference.Equals(model.Reference, StringComparison.OrdinalIgnoreCase); if (validCheckoutSha || validCheckoutReference) { var committish = model.CheckoutSha ?? model.Reference; var isSha = await repo.IsSha(committish, cancellationToken).ConfigureAwait(false); if ((isSha && model.Reference != null) || (!isSha && model.CheckoutSha != null)) { throw new JobException(ErrorCode.RepoSwappedShaOrReference); } await repo.CheckoutObject(committish, NextProgressReporter(), ct).ConfigureAwait(false); await CallLoadRevInfo().ConfigureAwait(false); // we've either seen origin before or what we're checking out is on origin } else { NextProgressReporter()(100); } if (hardResettingToOriginReference) { if (!repo.Tracking) { throw new JobException(ErrorCode.RepoReferenceNotTracking); } await repo.ResetToOrigin(NextProgressReporter(), ct).ConfigureAwait(false); await repo.Sychronize(currentModel.AccessUser, currentModel.AccessToken, currentModel.CommitterName, currentModel.CommitterEmail, NextProgressReporter(), true, ct).ConfigureAwait(false); await CallLoadRevInfo().ConfigureAwait(false); // repo head is on origin so force this // will update the db if necessary lastRevisionInfo.OriginCommitSha = repo.Head; } } // test merging if (newTestMerges) { if (repo.RemoteGitProvider == RemoteGitProvider.Unknown) { throw new JobException(ErrorCode.RepoTestMergeInvalidRemote); } // bit of sanitization foreach (var I in model.NewTestMerges.Where(x => String.IsNullOrWhiteSpace(x.TargetCommitSha))) { I.TargetCommitSha = null; } var gitHubClient = currentModel.AccessToken != null ? gitHubClientFactory.CreateClient(currentModel.AccessToken) : gitHubClientFactory.CreateClient(); var repoOwner = repo.RemoteRepositoryOwner; var repoName = repo.RemoteRepositoryName; // optimization: if we've already merged these exact same commits in this fashion before, just find the rev info for it and check it out Models.RevisionInformation revInfoWereLookingFor = null; bool needToApplyRemainingPrs = true; if (lastRevisionInfo.OriginCommitSha == lastRevisionInfo.CommitSha) { bool cantSearch = false; foreach (var I in model.NewTestMerges) { if (I.TargetCommitSha != null) #pragma warning disable CA1308 // Normalize strings to uppercase { I.TargetCommitSha = I.TargetCommitSha?.ToLowerInvariant(); // ala libgit2 } #pragma warning restore CA1308 // Normalize strings to uppercase else { try { // retrieve the latest sha var pr = await repo.GetTestMerge(I, currentModel, ct).ConfigureAwait(false); // we want to take the earliest truth possible to prevent RCEs, if this fails AddTestMerge will set it I.TargetCommitSha = pr.TargetCommitSha; } catch { cantSearch = true; break; } } } if (!cantSearch) { List <Models.RevisionInformation> dbPull = null; await databaseContextFactory.UseContext( async databaseContext => dbPull = await databaseContext.RevisionInformations .AsQueryable() .Where(x => x.Instance.Id == Instance.Id && x.OriginCommitSha == lastRevisionInfo.OriginCommitSha && x.ActiveTestMerges.Count <= model.NewTestMerges.Count && x.ActiveTestMerges.Count > 0) .Include(x => x.ActiveTestMerges) .ThenInclude(x => x.TestMerge) .ToListAsync(cancellationToken) .ConfigureAwait(false)) .ConfigureAwait(false); // split here cause this bit has to be done locally revInfoWereLookingFor = dbPull .Where(x => x.ActiveTestMerges.Count == model.NewTestMerges.Count && x.ActiveTestMerges.Select(y => y.TestMerge) .All(y => model.NewTestMerges.Any(z => y.Number == z.Number && y.TargetCommitSha.StartsWith(z.TargetCommitSha, StringComparison.Ordinal) && (y.Comment?.Trim().ToUpperInvariant() == z.Comment?.Trim().ToUpperInvariant() || z.Comment == null)))) .FirstOrDefault(); if (revInfoWereLookingFor == default && model.NewTestMerges.Count > 1) { // okay try to add at least SOME prs we've seen before var search = model.NewTestMerges.ToList(); var appliedTestMergeIds = new List <long>(); Models.RevisionInformation lastGoodRevInfo = null; do { foreach (var I in search) { revInfoWereLookingFor = dbPull .Where(testRevInfo => { if (testRevInfo.PrimaryTestMerge == null) { return(false); } var testMergeMatch = model.NewTestMerges.Any(testTestMerge => { var numberMatch = testRevInfo.PrimaryTestMerge.Number == testTestMerge.Number; if (!numberMatch) { return(false); } var shaMatch = testRevInfo.PrimaryTestMerge.TargetCommitSha.StartsWith( testTestMerge.TargetCommitSha, StringComparison.Ordinal); if (!shaMatch) { return(false); } var commentMatch = testRevInfo.PrimaryTestMerge.Comment == testTestMerge.Comment; return(commentMatch); }); if (!testMergeMatch) { return(false); } var previousTestMergesMatch = testRevInfo .ActiveTestMerges .Select(previousRevInfoTestMerge => previousRevInfoTestMerge.TestMerge) .All(previousTestMerge => appliedTestMergeIds.Contains(previousTestMerge.Id)); return(previousTestMergesMatch); }) .FirstOrDefault(); if (revInfoWereLookingFor != null) { lastGoodRevInfo = revInfoWereLookingFor; appliedTestMergeIds.Add(revInfoWereLookingFor.PrimaryTestMerge.Id); search.Remove(I); break; } } }while (revInfoWereLookingFor != null && search.Count > 0); revInfoWereLookingFor = lastGoodRevInfo; needToApplyRemainingPrs = search.Count != 0; if (needToApplyRemainingPrs) { model.NewTestMerges = search; } } else if (revInfoWereLookingFor != null) { needToApplyRemainingPrs = false; } } } if (revInfoWereLookingFor != null) { // goteem Logger.LogDebug("Reusing existing SHA {0}...", revInfoWereLookingFor.CommitSha); await repo.ResetToSha(revInfoWereLookingFor.CommitSha, NextProgressReporter(), cancellationToken).ConfigureAwait(false); lastRevisionInfo = revInfoWereLookingFor; } if (needToApplyRemainingPrs) { foreach (var I in model.NewTestMerges) { if (lastRevisionInfo.ActiveTestMerges.Any(x => x.TestMerge.Number == I.Number)) { throw new JobException(ErrorCode.RepoDuplicateTestMerge); } var fullTestMergeTask = repo.GetTestMerge(I, currentModel, ct); var mergeResult = await repo.AddTestMerge( I, committerName, currentModel.CommitterEmail, currentModel.AccessUser, currentModel.AccessToken, NextProgressReporter(), ct).ConfigureAwait(false); if (mergeResult == null) { throw new JobException( ErrorCode.RepoTestMergeConflict, new JobException( $"Test Merge #{I.Number} at {I.TargetCommitSha.Substring(0, 7)} conflicted!")); } Models.TestMerge fullTestMerge; try { fullTestMerge = await fullTestMergeTask.ConfigureAwait(false); } catch (Exception ex) { Logger.LogWarning("Error retrieving metadata for test merge #{0}!", I.Number); fullTestMerge = new Models.TestMerge { Author = ex.Message, BodyAtMerge = ex.Message, MergedAt = DateTimeOffset.UtcNow, TitleAtMerge = ex.Message, Comment = I.Comment, Number = I.Number, Url = ex.Message }; } // Ensure we're getting the full sha from git itself fullTestMerge.TargetCommitSha = I.TargetCommitSha; // MergedBy will be set later ++doneSteps; await UpdateRevInfo(fullTestMerge).ConfigureAwait(false); } } } var currentHead = repo.Head; if (startSha != currentHead || (postUpdateSha != null && postUpdateSha != currentHead)) { await repo.Sychronize(currentModel.AccessUser, currentModel.AccessToken, currentModel.CommitterName, currentModel.CommitterEmail, NextProgressReporter(), false, ct).ConfigureAwait(false); await UpdateRevInfo().ConfigureAwait(false); } return(null); } catch { doneSteps = 0; numSteps = 2; // Forget what we've done and abort // DCTx2: Cancellation token is for job, operations should always run await repo.CheckoutObject(startReference ?? startSha, NextProgressReporter(), default).ConfigureAwait(false); if (startReference != null && repo.Head != startSha) { await repo.ResetToSha(startSha, NextProgressReporter(), default).ConfigureAwait(false); } else { progressReporter(100); } throw; } } var job = new Models.Job { Description = description, StartedBy = AuthenticationContext.User, Instance = Instance, CancelRightsType = RightsType.Repository, CancelRight = (ulong)RepositoryRights.CancelPendingChanges, }; // Time to access git, do it in a job await jobManager.RegisterOperation( job, (core, databaseContextFactory, paramJob, progressReporter, ct) => UpdateCallbackThatDesperatelyNeedsRefactoring( core, databaseContextFactory, progressReporter, ct), cancellationToken) .ConfigureAwait(false); api.ActiveJob = job.ToApi(); return(Accepted(api)); }