コード例 #1
0
        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));
        }
コード例 #2
0
        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));
        }
コード例 #3
0
        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());
        }
コード例 #4
0
                #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
            }));
        }
コード例 #5
0
        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()));
        }
コード例 #6
0
        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
            }));
        }
コード例 #7
0
        /// <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));
        }
コード例 #8
0
        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));
        }
コード例 #9
0
#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,
            }));
        }
コード例 #10
0
        /// <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));
        }
コード例 #11
0
        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)));
        }
コード例 #12
0
                #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
            }));
        }
コード例 #13
0
        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));
        }
コード例 #14
0
        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
            }));
        }
コード例 #15
0
        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));
        }
コード例 #16
0
#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));
        }
コード例 #17
0
        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));
        }
コード例 #18
0
                #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));
        }
コード例 #19
0
#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));
        }
コード例 #20
0
                #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));
        }
コード例 #21
0
#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));
        }
コード例 #22
0
#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());
        }
コード例 #23
0
#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));
        }