예제 #1
0
        public async Task <IActionResult> RefreshFileTree([Required] long id)
        {
            var item = await FindAndCheckAccess(id);

            if (item == null || item.Deleted)
            {
                return(NotFound());
            }

            if (item.FileTreeUpdated != null && DateTime.UtcNow - item.FileTreeUpdated.Value < TimeSpan.FromMinutes(3))
            {
                return(BadRequest("This file tree was refreshed very recently"));
            }

            var user = HttpContext.AuthenticatedUserOrThrow();

            await database.ActionLogEntries.AddAsync(new ActionLogEntry()
            {
                Message       = $"LFS project file tree refresh requested for {item.Id}",
                PerformedById = user.Id,
            });

            await database.SaveChangesAsync();

            jobClient.Enqueue <RefreshLFSProjectFilesJob>(x => x.Execute(item.Id, CancellationToken.None));

            logger.LogInformation("LFS project {Id} file tree refreshed by {Email}", item.Id, user.Email);
            return(Ok());
        }
        public async Task <IActionResult> CreateNew([Required][FromBody] AccessKeyDTO newKey)
        {
            if (string.IsNullOrWhiteSpace(newKey.Description) || await database.AccessKeys
                .FirstOrDefaultAsync(a => a.Description == newKey.Description) != null)
            {
                return(BadRequest("Description is empty or a key with that description already exists"));
            }

            var key = new AccessKey()
            {
                Description = newKey.Description,
                KeyCode     = Guid.NewGuid().ToString(),
                KeyType     = newKey.KeyType
            };

            var action = new AdminAction()
            {
                Message       = $"New access key ({key.Description}) created with scope: {key.KeyType}",
                PerformedById = HttpContext.AuthenticatedUser() !.Id
            };

            await database.AccessKeys.AddAsync(key);

            await database.AdminActions.AddAsync(action);

            await database.SaveChangesAsync();

            return(Ok($"Created new key \"{key.Id}\" with code: {key.KeyCode}"));
        }
        public async Task <IActionResult> ConnectLauncher([Required] LauncherLinkCodeCheckForm request)
        {
            Response.ContentType = "application/json";
            var user = await GetUserForNewLink(request.Code);

            // Update user to consume the code
            user.LauncherCodeExpires = DateTime.UtcNow - TimeSpan.FromSeconds(1);
            user.LauncherLinkCode    = null;
            user.TotalLauncherLinks += 1;

            // Create a new code, which the user doesn't directly see to avoid it leaking as easily
            var code = NonceGenerator.GenerateNonce(42);

            var remoteAddress = HttpContext.Connection.RemoteIpAddress;

            await database.LauncherLinks.AddAsync(new LauncherLink()
            {
                User           = user,
                LinkCode       = code,
                LastIp         = remoteAddress?.ToString(),
                LastConnection = DateTime.UtcNow
            });

            await database.LogEntries.AddAsync(new LogEntry()
            {
                Message      = $"New launcher link created from: {remoteAddress}",
                TargetUserId = user.Id
            });

            await database.SaveChangesAsync();

            logger.LogInformation("New launcher linked to user {Id} from {RemoteAddress}", user.Id, remoteAddress);

            return(Created(string.Empty, new LauncherLinkResult(true, code)));
        }
        public async Task Execute(CancellationToken cancellationToken)
        {
            var cutoff = DateTime.UtcNow - AppInfo.KeepStackwalkResultsFor;

            var finishedTasks = await database.StackwalkTasks.Where(s => s.FinishedAt != null && s.FinishedAt < cutoff)
                                .ToListAsync(cancellationToken);

            if (finishedTasks.Count > 0)
            {
                logger.LogInformation("Cleaning finished stackwalk tasks, count: {Count}", finishedTasks.Count);

                database.StackwalkTasks.RemoveRange(finishedTasks);
                await database.SaveChangesAsync(cancellationToken);
            }

            var cutoff2 = DateTime.UtcNow - AppInfo.DeleteFailedStackwalkAttemptsAfter;

            var failedTasks = await database.StackwalkTasks.Where(s => s.CreatedAt < cutoff2)
                              .ToListAsync(cancellationToken);

            if (failedTasks.Count > 0)
            {
                logger.LogWarning("Cleaning entirely failed stackwalk tasks, count: {Count}", failedTasks.Count);

                database.StackwalkTasks.RemoveRange(failedTasks);

                await database.LogEntries.AddAsync(new LogEntry()
                {
                    Message = $"Cleared {failedTasks.Count} stackwalk tasks that failed to run entirely",
                }, cancellationToken);

                await database.SaveChangesAsync(cancellationToken);
            }
        }
예제 #5
0
        public async Task <IActionResult> VerifyBuild([Required] long id, bool siblingsAsWell = true)
        {
            var build = await database.DevBuilds.FindAsync(id);

            if (build == null)
            {
                return(NotFound());
            }

            var user = HttpContext.AuthenticatedUser() !;

            bool didSomething = false;

            if (siblingsAsWell)
            {
                foreach (var sibling in await GetSiblingBuilds(build))
                {
                    if (sibling.Verified)
                    {
                        continue;
                    }

                    logger.LogInformation("Marking sibling devbuild {Id} as verified as well", sibling.Id);
                    sibling.Verified     = true;
                    sibling.VerifiedById = user.Id;

                    didSomething = true;
                }
            }

            if (!build.Verified)
            {
                logger.LogInformation("Marking devbuild {Id} as verified by {Email}", build.Id, user.Email);
                build.Verified     = true;
                build.VerifiedById = user.Id;

                didSomething = true;
            }

            if (!didSomething)
            {
                return(Ok("Nothing needed to be marked as verified"));
            }

            await database.ActionLogEntries.AddAsync(new ActionLogEntry()
            {
                Message       = $"Build {id} marked verified",
                PerformedById = user.Id
            });

            await database.SaveChangesAsync();

            return(Ok());
        }
        public async Task <ActionResult <PagedResult <SessionDTO> > > DeleteAllUserSessions([Required] long id)
        {
            bool admin =
                HttpContext.HasAuthenticatedUserWithAccess(UserAccessLevel.Admin, AuthenticationScopeRestriction.None);

            var user = await database.Users.FindAsync(id);

            var actingUser = HttpContext.AuthenticatedUser() !;

            if (user == null)
            {
                return(NotFound());
            }

            // Has to be an admin or performing an action on their own data
            if (!admin && actingUser.Id != user.Id)
            {
                return(NotFound());
            }

            var sessions = await database.Sessions.Where(s => s.UserId == id).ToListAsync();

            if (sessions.Count < 1)
            {
                return(Ok());
            }

            if (admin && actingUser.Id != user.Id)
            {
                await database.AdminActions.AddAsync(new AdminAction()
                {
                    Message       = "Forced logout",
                    PerformedById = actingUser.Id,
                    TargetUserId  = user.Id,
                });
            }
            else
            {
                logger.LogInformation("User ({Email}) deleted their sessions from {RemoteIpAddress}", user.Email,
                                      HttpContext.Connection.RemoteIpAddress);
            }

            database.Sessions.RemoveRange(sessions);
            await database.SaveChangesAsync();

            await InvalidateSessions(sessions.Select(s => s.Id));

            return(Ok());
        }
예제 #7
0
        public async Task<IActionResult> CancelRunningJob([Required] long projectId,
            [Required] long buildId, [Required] long jobId)
        {
            var job = await database.CiJobs.Include(j => j.Build!).ThenInclude(b => b.CiProject)
                .Include(j => j.CiJobOutputSections)
                .FirstOrDefaultAsync(j => j.CiProjectId == projectId && j.CiBuildId == buildId && j.CiJobId == jobId);

            if (job == null)
                return NotFound();

            if (job.Build?.CiProject == null)
                throw new NotLoadedModelNavigationException();

            if (job.Build.CiProject.Deleted)
                return NotFound();

            if (job.State == CIJobState.Finished)
                return BadRequest("Can't cancel a finished job");

            long cancelSectionId = 0;

            foreach (var section in job.CiJobOutputSections)
            {
                if (section.CiJobOutputSectionId > cancelSectionId)
                    cancelSectionId = section.CiJobOutputSectionId;
            }

            await job.CreateFailureSection(database, "Job canceled by a user", "Canceled", ++cancelSectionId);

            var user = HttpContext.AuthenticatedUser()!;

            logger.LogInformation("CI job {ProjectId}-{BuildId}-{JobId} canceled by {Email}", projectId, buildId, jobId,
                user.Email);

            // TODO: would be nice to have that non-admin action log type
            await database.LogEntries.AddAsync(new LogEntry()
            {
                Message = $"CI job {projectId}-{buildId}-{jobId} canceled by an user",
                TargetUserId = user.Id,
            });

            await database.SaveChangesAsync();

            jobClient.Enqueue<SetFinishedCIJobStatusJob>(x =>
                x.Execute(projectId, buildId, jobId, false, CancellationToken.None));

            return Ok();
        }
예제 #8
0
        public async Task Execute(string githubUsername, CancellationToken cancellationToken)
        {
            var pullRequests = await database.GithubPullRequests
                               .Where(p => p.ClaSigned != true && p.AuthorUsername == githubUsername).ToListAsync(cancellationToken);

            foreach (var pullRequest in pullRequests)
            {
                pullRequest.ClaSigned = null;
            }

            await database.SaveChangesAsync(cancellationToken);

            foreach (var pullRequest in pullRequests)
            {
                // Don't need to run this for closed ones as the open event will cause a re-check anyway
                if (!pullRequest.Open)
                {
                    continue;
                }

                logger.LogInformation(
                    "New CLA signature made that matches pull request: {Repository}/{GithubId} by {GithubUsername}",
                    pullRequest.Repository, pullRequest.GithubId, githubUsername);

                // This shouldn't cause any problems regarding data consistency but:
                // TODO: make separate job that doesn't update PR data other than the signature status
                jobClient.Enqueue <CheckPullRequestStatusJob>(x => x.Execute(pullRequest.Repository,
                                                                             pullRequest.GithubId, pullRequest.LatestCommit, pullRequest.AuthorUsername, pullRequest.Open,
                                                                             CancellationToken.None));
            }
        }
예제 #9
0
        public async Task Execute(CancellationToken cancellationToken)
        {
            var cutoff = DateTime.UtcNow - detectionTime;

            bool jobNeeded = false;

            foreach (var server in await database.ControlledServers.Where(s =>
                                                                          s.UpdatedAt < cutoff && s.Status == ServerStatus.Running).ToListAsync(cancellationToken))
            {
                logger.LogWarning("Server {Id} has been left running, last updated: {UpdatedAt}",
                                  server.Id, server.UpdatedAt);

                jobNeeded = true;
            }

            if (jobNeeded)
            {
                await database.LogEntries.AddAsync(new LogEntry()
                {
                    Message = "Detected servers that are left in running state, trying to fix by re-running handle job"
                }, cancellationToken);

                await database.SaveChangesAsync(cancellationToken);

                jobClient.Enqueue <HandleControlledServerJobsJob>(x => x.Execute(CancellationToken.None));
            }
        }
        public async Task Execute(long itemId, CancellationToken cancellationToken)
        {
            var item = await database.StorageItems.Include(i => i.StorageItemVersions)
                       .FirstOrDefaultAsync(i => i.Id == itemId, cancellationToken);

            if (item == null)
            {
                logger.LogError("Failed to get StorageItem ({ItemId}) for CI image locking", itemId);
                return;
            }

            if (item.WriteAccess == FileAccess.Nobody && item.Special)
            {
                logger.LogInformation("Skipping lock on StorageItem ({ItemId}) as it's already in nobody write mode",
                                      itemId);
                return;
            }

            // Queue jobs to delete other than the first uploaded version
            var lowestVersion = item.StorageItemVersions.Where(v => !v.Uploading).Min(v => v.Version);

            foreach (var version in item.StorageItemVersions)
            {
                if (version.Version == lowestVersion)
                {
                    if (version.Protected != true || version.Keep != true)
                    {
                        version.Protected = true;
                        version.Keep      = true;
                        version.BumpUpdatedAt();
                    }
                }
                else
                {
                    // This version needs to be deleted
                    jobClient.Schedule <DeleteStorageItemVersionJob>(x => x.Execute(version.Id, CancellationToken.None),
                                                                     TimeSpan.FromSeconds(30));
                }
            }

            item.WriteAccess = FileAccess.Nobody;
            item.Special     = true;
            item.BumpUpdatedAt();

            // Also mark all parent folders as special so that they can't be deleted
            foreach (var parent in await item.GetParentsRecursively(database))
            {
                cancellationToken.ThrowIfCancellationRequested();

                if (parent.Special != true)
                {
                    parent.Special = true;
                    logger.LogInformation("CI image parent folder ({Id}) will be marked as special", parent.Id);
                }
            }

            await database.SaveChangesAsync(cancellationToken);

            logger.LogInformation("Use as CI image has locked writing to StorageItem ({ItemId})", itemId);
        }
        protected async Task OnJobEnded(BaseServer server, CiJob job)
        {
            ReleaseServerReservation(server);
            job.RunningOnServerId         = -1;
            job.RunningOnServerIsExternal = null;

            // After running the job, the changes saving should not be skipped
            await Database.SaveChangesAsync();

            // Send status to github
            var    status            = GithubAPI.CommitStatus.Success;
            string statusDescription = "Checks succeeded";

            if (!job.Succeeded)
            {
                status            = GithubAPI.CommitStatus.Failure;
                statusDescription = "Some checks failed";
            }

            if (job.Build?.CiProject == null)
            {
                throw new NotLoadedModelNavigationException();
            }

            if (!await StatusReporter.SetCommitStatus(job.Build.CiProject.RepositoryFullName, job.Build.CommitHash,
                                                      status, StatusReporter.CreateStatusUrlForJob(job), statusDescription,
                                                      job.JobName))
            {
                Logger.LogError("Failed to set commit status for build's job: {JobName}", job.JobName);
            }

            JobClient.Enqueue <CheckOverallBuildStatusJob>(x =>
                                                           x.Execute(job.CiProjectId, job.CiBuildId, CancellationToken.None));
        }
        public async Task Execute(long meetingId, long pollId, CancellationToken cancellationToken)
        {
            var poll = await database.MeetingPolls.FindAsync(meetingId, pollId);

            if (poll == null)
            {
                logger.LogError("Can't compute results for non-existent poll: {MeetingId}-{PollId}", meetingId, pollId);
                return;
            }

            if (poll.ClosedAt == null)
            {
                throw new Exception("Can't calculate results for a poll that is not closed");
            }

            // This will work until we have more than hundreds of thousands of votes per poll
            var votes = await database.MeetingPollVotes.Where(v => v.MeetingId == poll.MeetingId && v.PollId == poll.PollId)
                        .ToListAsync(cancellationToken);

            poll.CalculateResults(votes);

            await database.SaveChangesAsync(cancellationToken);

            logger.LogInformation("Poll results computed for: {MeetingId}-{PollId}", meetingId, pollId);
        }
예제 #13
0
        public async Task <IActionResult> Logout([FromForm] LogoutFormData request)
        {
            var existingSession = await HttpContext.Request.Cookies.GetSession(database);

            if (existingSession?.User == null)
            {
                return(BadRequest("You are not currently logged in"));
            }

            if (!csrfVerifier.IsValidCSRFToken(request.CSRF, existingSession.User))
            {
                return(BadRequest("Invalid CSRF token, please try refreshing and then try again"));
            }

            // Session version doesn't need to be enforced here as logging out a session should always be safe
            // (after the above checks)

            // TODO: if an in-progress signature exists, should the session be just converted to a logged out one?

            database.Sessions.Remove(existingSession);
            await database.SaveChangesAsync();

            logger.LogInformation("Session {Id} logged out", existingSession.Id);

            Response.Cookies.Delete(AppInfo.SessionCookieName);
            return(Redirect("/login"));
        }
        public async Task Execute(long pullRequestId, CancellationToken cancellationToken)
        {
            var pullRequest = await database.GithubPullRequests.Include(p => p.AutoComments)
                              .FirstOrDefaultAsync(p => p.Id == pullRequestId, cancellationToken);

            if (pullRequest == null)
            {
                logger.LogError("No pull request with ID {PullRequestId} found to post comments on", pullRequestId);
                return;
            }

            var possibleComments = await database.GithubAutoComments.Where(c =>
                                                                           c.Enabled && (string.IsNullOrEmpty(c.Repository) || c.Repository == "*" ||
                                                                                         c.Repository == pullRequest.Repository)).ToListAsync(cancellationToken);

            foreach (var possibleComment in possibleComments)
            {
                bool post = false;

                switch (possibleComment.Condition)
                {
                case AutoCommentCondition.Always:
                    post = true;
                    break;

                case AutoCommentCondition.OnceOnPullRequest:
                    // Contains doesn't work here. Has to be an ID check like this
                    post = pullRequest.AutoComments.All(c => c.Id != possibleComment.Id);
                    break;

                case AutoCommentCondition.IfCLANotSigned:
                    post = pullRequest.AutoComments.All(c => c.Id != possibleComment.Id) &&
                           pullRequest.ClaSigned == false;
                    break;

                case AutoCommentCondition.IfCLABecomesInvalid:
                    // Handled elsewhere (InvalidatePullRequestsWithCLASignaturesJob)
                    break;

                default:
                    throw new ArgumentOutOfRangeException();
                }

                if (!post)
                {
                    continue;
                }

                jobClient.Enqueue <PostGithubCommentJob>(x =>
                                                         x.Execute(pullRequest.Repository, pullRequest.GithubId, possibleComment.CommentText,
                                                                   CancellationToken.None));
                pullRequest.AutoComments.Add(possibleComment);
            }

            // Not cancellable here as we have potentially already queued up the comment posts so we don't want to lose
            // that information
            // ReSharper disable once MethodSupportsCancellation
            await database.SaveChangesAsync();
        }
예제 #15
0
        public async Task <IActionResult> JoinMeeting([Required] long id)
        {
            var access  = GetCurrentUserAccess();
            var meeting = await GetMeetingWithReadAccess(id, access);

            if (meeting == null)
            {
                return(NotFound());
            }

            // TODO: should admins be able to always join?
            if (meeting.JoinAccess > access)
            {
                return(this.WorkingForbid("You don't have permission to join this meeting"));
            }

            if (meeting.EndedAt != null)
            {
                return(BadRequest("This meeting has already ended"));
            }

            var user = HttpContext.AuthenticatedUser() !;

            // Fail if already joined
            if (await GetMeetingMember(meeting.Id, user.Id) != null)
            {
                return(BadRequest("You have already joined this meeting"));
            }

            // Don't allow if already started and grace period is over
            if (DateTime.UtcNow > meeting.StartsAt + meeting.JoinGracePeriod)
            {
                return(BadRequest("You are too late to join this meeting"));
            }

            // Allow join
            await database.ActionLogEntries.AddAsync(new ActionLogEntry()
            {
                Message       = $"User joined meeting {meeting.Id}",
                PerformedById = user.Id,
            });

            await database.MeetingMembers.AddAsync(new MeetingMember()
            {
                MeetingId = meeting.Id,
                UserId    = user.Id,
            });

            await database.SaveChangesAsync();

            logger.LogInformation("User {Email} joined meeting {Name} ({Id})", user.Email, meeting.Name,
                                  meeting.Id);

            return(Ok());
        }
        public async Task <ActionResult <EmailVerifyResult> > StartEmailVerifyForCLA(
            [Required][FromBody] EmailVerificationFinishForm request)
        {
            var verifiedToken = emailTokens.ReadAndVerify(request.Token);

            if (verifiedToken == null)
            {
                return(this.WorkingForbid(
                           "Invalid email token given. Please check you used the right link and " +
                           "that it didn't expire yet"));
            }

            const string sameBrowserAdvice =
                "Make sure to use the link in the same browser where you started email verification from.";

            string redirect;

            switch (verifiedToken.Type)
            {
            case EmailVerificationType.CLA:
            {
                var(inProgressSign, session, error) = await GetActiveSignature();

                if (error != null)
                {
                    return(this.WorkingForbid("You don't have an in-progress signature or session. " +
                                              sameBrowserAdvice));
                }

                if (!SecurityHelpers.SlowEquals(session !.HashedId, verifiedToken.VerifiedResourceId))
                {
                    return(this.WorkingForbid("Current session doesn't match one the email token " +
                                              "was sent from. " + sameBrowserAdvice));
                }

                // Valid CLA verified email
                logger.LogInformation("Email verification of {Email} succeeded for CLA in session {Id}",
                                      verifiedToken.SentToEmail, session.Id);

                inProgressSign !.EmailVerified = true;
                inProgressSign.Email           = verifiedToken.SentToEmail;

                redirect = "/cla/sign";
                break;
            }

            default:
                throw new ArgumentOutOfRangeException();
            }

            await database.SaveChangesAsync();

            return(new EmailVerifyResult()
            {
                RedirectTo = redirect,
            });
        }
        public async Task <IActionResult> Create([Required] long projectId,
                                                 [Required][FromBody] CreateCISecretForm request)
        {
            var project = await database.CiProjects.FindAsync(projectId);

            if (project == null)
            {
                return(NotFound());
            }

            if (await database.CiSecrets.FirstOrDefaultAsync(s => s.CiProjectId == project.Id &&
                                                             s.SecretName == request.SecretName && s.UsedForBuildTypes == request.UsedForBuildTypes) != null)
            {
                return(BadRequest("A secret with the given name and type already exists"));
            }

            var previousSecretId = await database.CiSecrets.Where(s => s.CiProjectId == project.Id)
                                   .MaxAsync(s => (long?)s.CiSecretId) ?? 0;

            var user = HttpContext.AuthenticatedUser() !;

            await database.AdminActions.AddAsync(new AdminAction()
            {
                Message       = $"New secret \"{request.SecretName}\" created for project {project.Id}",
                PerformedById = user.Id,
            });

            await database.CiSecrets.AddAsync(new CiSecret()
            {
                CiProjectId       = project.Id,
                CiSecretId        = previousSecretId + 1,
                SecretName        = request.SecretName,
                SecretContent     = request.SecretContent ?? string.Empty,
                UsedForBuildTypes = request.UsedForBuildTypes,
            });

            await database.SaveChangesAsync();

            logger.LogInformation("New secret {SecretName} created by {Email} for {Id}", request.SecretName, user.Email,
                                  project.Id);

            return(Ok());
        }
        public async Task <IActionResult> DeleteAllLinks([Required] long userId)
        {
            var performingUser = HttpContext.AuthenticatedUser() !;

            // Only admins can delete other user's links
            if (userId != performingUser.Id &&
                !HttpContext.HasAuthenticatedUserWithAccess(UserAccessLevel.Admin, AuthenticationScopeRestriction.None))
            {
                return(Forbid());
            }

            var linksToDelete = await database.LauncherLinks.Where(l => l.UserId == userId).ToListAsync();

            // Skip doing anything if there's nothing to delete
            if (linksToDelete.Count < 1)
            {
                return(Ok());
            }

            if (userId == performingUser.Id)
            {
                await database.LogEntries.AddAsync(new LogEntry()
                {
                    Message      = "All launcher links deleted by self",
                    TargetUserId = userId
                });
            }
            else
            {
                await database.AdminActions.AddAsync(new AdminAction()
                {
                    Message       = "All launcher links deleted by an admin",
                    TargetUserId  = userId,
                    PerformedById = performingUser.Id
                });
            }

            database.LauncherLinks.RemoveRange(linksToDelete);

            await database.SaveChangesAsync();

            return(Ok());
        }
예제 #19
0
        public async Task Execute(string repository, long pullRequestNumber, string commit, string githubUsername,
                                  bool open, CancellationToken cancellationToken)
        {
            logger.LogInformation("Update to PR {Repository}/{PullRequestNumber} detected by {GithubUsername}",
                                  repository, pullRequestNumber, githubUsername);

            var pullRequest =
                await database.GithubPullRequests.FirstOrDefaultAsync(p =>
                                                                      p.Repository == repository && p.GithubId == pullRequestNumber, cancellationToken);

            if (pullRequest == null)
            {
                await database.LogEntries.AddAsync(new LogEntry()
                {
                    Message = $"New Github pull request detected {repository}/{pullRequestNumber}",
                }, cancellationToken);

                pullRequest = new GithubPullRequest()
                {
                    Repository     = repository,
                    GithubId       = pullRequestNumber,
                    LatestCommit   = commit,
                    AuthorUsername = githubUsername,
                    Open           = open,
                    ClaSigned      = await CheckNewCLASignedStatus(null, githubUsername),
                };

                await database.GithubPullRequests.AddAsync(pullRequest, cancellationToken);
            }
            else
            {
                pullRequest.LatestCommit = commit;

                if (pullRequest.Open != open)
                {
                    pullRequest.Open = open;

                    await database.LogEntries.AddAsync(new LogEntry()
                    {
                        Message = $"Github pull request open state {repository}/{pullRequestNumber} is now {open}",
                    }, cancellationToken);
                }

                pullRequest.ClaSigned =
                    await CheckNewCLASignedStatus(pullRequest.ClaSigned, pullRequest.AuthorUsername);

                pullRequest.BumpUpdatedAt();
            }

            await database.SaveChangesAsync(cancellationToken);

            jobClient.Enqueue <CheckAutoCommentsToPostJob>(x => x.Execute(pullRequest.Id, CancellationToken.None));
            jobClient.Enqueue <SetCLAGithubCommitStatusJob>(x => x.Execute(pullRequest.Id, CancellationToken.None));
        }
        public async Task Execute(CancellationToken cancellationToken)
        {
            if (!ec2Controller.Configured)
            {
                return;
            }

            var cutoff = DateTime.UtcNow - TimeSpan.FromHours(2);

            foreach (var server in await database.ControlledServers.Where(s =>
                                                                          s.UpdatedAt < cutoff && s.Status != ServerStatus.Stopped &&
                                                                          s.Status != ServerStatus.Terminated).ToListAsync(cancellationToken))
            {
                cancellationToken.ThrowIfCancellationRequested();

                logger.LogError(
                    "Server {Id} is stuck! Last state change: {UpdatedAt} current state: {Status}, terminating it",
                    server.Id, server.UpdatedAt, server.Status);

                await database.LogEntries.AddAsync(new LogEntry()
                {
                    Message =
                        $"Server {server.Id} ({server.InstanceId}) is stuck in state {server.Status} " +
                        $"since {server.UpdatedAt}"
                }, cancellationToken);

                if (string.IsNullOrEmpty(server.InstanceId))
                {
                    throw new Exception("Can't terminate server with no InstanceId");
                }

                await ec2Controller.TerminateInstance(server.InstanceId);

                server.Status = ServerStatus.Terminated;

                if (server.ReservationType != ServerReservationType.None)
                {
                    logger.LogWarning("Stuck server was reserved for type: {ReservationType}", server.ReservationType);
                    server.ReservationType = ServerReservationType.None;
                }

                if (server.RunningSince != null)
                {
                    server.TotalRuntime += (DateTime.UtcNow - server.RunningSince.Value).TotalSeconds;
                }
                server.RunningSince = null;

                logger.LogInformation("Successfully terminated: {InstanceId}", server.InstanceId);

                // Not cancellable done as the state to terminated is very important to save
                // ReSharper disable once MethodSupportsCancellation
                await database.SaveChangesAsync();
            }
        }
        private async Task CheckControlledServers(DateTime cutoff, CancellationToken cancellationToken)
        {
            // Only one server is scheduled for maintenance at once to avoid all being unavailable for job running
            var serverToMaintain = await database.ControlledServers.Where(s =>
                                                                          !s.WantsMaintenance && s.Status != ServerStatus.Terminated && s.LastMaintenance < cutoff)
                                   .OrderBy(s => s.LastMaintenance).FirstOrDefaultAsync(cancellationToken);

            if (serverToMaintain == null)
            {
                return;
            }

            serverToMaintain.WantsMaintenance = true;

            await database.LogEntries.AddAsync(new LogEntry()
            {
                Message = $"Scheduled controlled server {serverToMaintain.Id} for termination due to maintenance",
            }, cancellationToken);

            await database.SaveChangesAsync(cancellationToken);
        }
        public async Task <IActionResult> ReProcessCrashDump([Required] long id)
        {
            var report = await WithoutLogs(database.CrashReports, true).Where(r => r.Id == id)
                         .FirstOrDefaultAsync();

            if (report == null)
            {
                return(NotFound());
            }

            if (report.DumpLocalFileName == null)
            {
                return(BadRequest("Report no longer has a crash dump file"));
            }

            await database.ActionLogEntries.AddAsync(new ActionLogEntry()
            {
                Message       = $"Report {report.Id} crash dump reprocessing requested",
                PerformedById = HttpContext.AuthenticatedUserOrThrow().Id,
            });

            await database.SaveChangesAsync();

            jobClient.Enqueue <StartStackwalkOnReportJob>(x => x.Execute(report.Id, CancellationToken.None));

            return(Ok());
        }
        public async Task Execute(CancellationToken cancellationToken)
        {
            var pullRequests = await database.GithubPullRequests.Include(p => p.AutoComments)
                               .Where(pr => pr.ClaSigned == true)
                               .ToListAsync(cancellationToken);

            var comments = await database.GithubAutoComments.Where(c =>
                                                                   c.Enabled && c.Condition == AutoCommentCondition.IfCLABecomesInvalid).ToListAsync(cancellationToken);

            foreach (var pullRequest in pullRequests)
            {
                logger.LogInformation("Unmarking PR {Repository}/{GithubId} as having CLA signed",
                                      pullRequest.Repository, pullRequest.GithubId);
                pullRequest.ClaSigned = null;
            }

            await database.SaveChangesAsync(cancellationToken);

            // Open PRs need immediately (this way new webhook shouldn't be able to get their new data lost by this)
            // to be checked if they are still good
            foreach (var pullRequest in pullRequests)
            {
                if (pullRequest.Open)
                {
                    // TODO: make separate job that doesn't update PR data other than the signature status
                    jobClient.Enqueue <CheckPullRequestStatusJob>(x => x.Execute(pullRequest.Repository,
                                                                                 pullRequest.GithubId, pullRequest.LatestCommit, pullRequest.AuthorUsername, pullRequest.Open,
                                                                                 CancellationToken.None));
                }
            }

            if (comments.Count > 0)
            {
                await PostComments(pullRequests, comments);
            }
        }
        // TODO: make this async
        protected async Task PerformProvisioningCommands(BaseServer server, string command)
        {
            Logger.LogInformation("Beginning SSH connect to provision server at: {PublicAddress}",
                                  server.PublicAddress);

            // TODO: there should probably be a maximum number of times this is attempted
            IBaseSSHAccess sshAccess;

            try
            {
                sshAccess = ConnectWithSSH(server);
            }
            catch (SocketException)
            {
                Logger.LogWarning("Connection failed (socket exception), server is probably not up yet");
                return;
            }
            catch (SshOperationTimeoutException)
            {
                Logger.LogWarning("Connection failed (ssh timed out), server is probably not up yet");
                return;
            }

            Logger.LogInformation("Connected, running provisioning command...");

            var start  = DateTime.UtcNow;
            var result = sshAccess.RunCommand(command);

            if (!result.Success)
            {
                Logger.LogWarning("Failed provision result ({ExitCode}: {Result}", result.ExitCode, result.Result);
                throw new Exception($"Provisioning commands failed ({result.ExitCode}): {result.Error}");
            }

            // Now fully provisioned
            server.ProvisionedFully  = true;
            server.Status            = ServerStatus.Running;
            server.LastMaintenance   = DateTime.UtcNow;
            server.WantsMaintenance  = false;
            server.StatusLastChecked = DateTime.UtcNow;
            server.ReservationType   = ServerReservationType.None;
            server.BumpUpdatedAt();
            await Database.SaveChangesAsync();

            var elapsed = DateTime.UtcNow - start;

            Logger.LogInformation("Completed provisioning for server {Id}, elapsed: {Elapsed}", server.Id, elapsed);
        }
        public async Task Execute(CancellationToken cancellationToken)
        {
            // TODO: if we ever have more patrons (unlikely) than can be kept in memory, this needs a different
            // approach

            var patrons = await database.Patrons.ToListAsync(cancellationToken);

            patrons.ForEach(p => p.Marked = false);

            foreach (var settings in await database.PatreonSettings.ToListAsync(cancellationToken))
            {
                if (settings.Active == false)
                {
                    continue;
                }

                var api = new PatreonCreatorAPI(settings);

                foreach (var actualPatron in await api.GetPatrons(settings, cancellationToken))
                {
                    await PatreonGroupHandler.HandlePatreonPledgeObject(actualPatron.Pledge, actualPatron.User,
                                                                        actualPatron.Reward?.Id, database, jobClient);

                    if (cancellationToken.IsCancellationRequested)
                    {
                        throw new TaskCanceledException();
                    }
                }

                settings.LastRefreshed = DateTime.UtcNow;
            }

            foreach (var toDelete in patrons.Where(p => p.Marked == false))
            {
                await database.LogEntries.AddAsync(new LogEntry()
                {
                    Message = $"Destroying patron ({toDelete.Id}) because it is unmarked " +
                              "(wasn't found from fresh data from Patreon)"
                }, cancellationToken);

                logger.LogInformation("Deleted unmarked Patron {Id}", toDelete.Id);
                database.Patrons.Remove(toDelete);
            }

            await database.SaveChangesAsync(cancellationToken);

            jobClient.Enqueue <ApplyPatronForumGroupsJob>(x => x.Execute(CancellationToken.None));
        }
        public async Task <IActionResult> Post(RegistrationFormData request)
        {
            if (!csrfVerifier.IsValidCSRFToken(request.CSRF, null, false))
            {
                return(BadRequest("Invalid CSRF"));
            }

            if (!SecurityHelpers.SlowEquals(request.RegistrationCode, configuration.RegistrationCode))
            {
                return(BadRequest("Invalid registration code"));
            }

            if (!request.Email.Contains('@'))
            {
                return(BadRequest("Email is invalid"));
            }

            // Check for conflicting username or email
            if (await database.Users.FirstOrDefaultAsync(u => u.UserName == request.Name) != null ||
                await database.Users.FirstOrDefaultAsync(u => u.Email == request.Email) != null)
            {
                return(BadRequest("There is already an account associated with the given email or name"));
            }

            var password = Passwords.CreateSaltedPasswordHash(request.Password);

            var user = new User()
            {
                Email        = request.Email,
                UserName     = request.Name,
                PasswordHash = password,
                Local        = true
            };

            await database.Users.AddAsync(user);

            Models.User.OnNewUserCreated(user, jobClient);
            await database.SaveChangesAsync();

            logger.LogInformation("New user registered {Name} ({Email})", request.Name, request.Email);

            return(Created($"/users/{user.Id}", user.GetInfo(RecordAccessLevel.Private)));
        }
        public async Task Execute(CancellationToken cancellationToken)
        {
            var ciJobsNeedingActions = await database.CiJobs.Where(j => j.State != CIJobState.Finished)
                                       .ToListAsync(cancellationToken);

            await serverHandler.CheckServerStatuses(cancellationToken);

            cancellationToken.ThrowIfCancellationRequested();

            bool queuedRecheck = false;

            // Cancellation tokens are not used from here on out to avoid not saving changes
            if (!await serverHandler.HandleCIJobs(ciJobsNeedingActions))
            {
                logger.LogInformation(
                    "One or more jobs could not start executing immediately, trying again in 10 seconds");
                queuedRecheck = true;
                jobClient.Schedule <HandleControlledServerJobsJob>(x => x.Execute(CancellationToken.None),
                                                                   TimeSpan.FromSeconds(10));
            }

            // Skip this if we should cancel
            if (!cancellationToken.IsCancellationRequested)
            {
                await serverHandler.ShutdownIdleServers();
            }

            // ReSharper disable once MethodSupportsCancellation
            await database.SaveChangesAsync();

            // If we have active servers, queue a check in 1 minute
            if (!queuedRecheck && (serverHandler.NewServersAdded || (await serverHandler.GetControlledServers()).Any(
                                       s => s.Status == ServerStatus.Provisioning || s.Status == ServerStatus.Running ||
                                       s.Status == ServerStatus.Stopping || s.Status == ServerStatus.WaitingForStartup)))
            {
                jobClient.Schedule <HandleControlledServerJobsJob>(x => x.Execute(CancellationToken.None),
                                                                   TimeSpan.FromSeconds(60));
            }

            // Sleep a tiny amount to ensure that duplicate instances of this job can't hammer a server really hard
            await Task.Delay(TimeSpan.FromMilliseconds(50), cancellationToken);
        }
예제 #28
0
        public async Task Execute(long versionId, CancellationToken cancellationToken)
        {
            var version = await database.StorageItemVersions.Include(v => v.StorageFile)
                          .FirstOrDefaultAsync(i => i.Id == versionId, cancellationToken);

            if (version == null)
            {
                logger.LogWarning(
                    "Failed to get StorageItemVersion ({VersionId}) for deletion, assuming already deleted", versionId);
                return;
            }

            if (version.StorageFile == null)
            {
                throw new NotLoadedModelNavigationException();
            }

            logger.LogInformation("Deleting remote storage object queued for deletion: {StoragePath}",
                                  version.StorageFile.StoragePath);

            await remoteStorage.DeleteObject(version.StorageFile.StoragePath);

            database.StorageItemVersions.Remove(version);
            database.StorageFiles.Remove(version.StorageFile);

            // Not cancellable as we have already deleted the remote item at this point
            // ReSharper disable once MethodSupportsCancellation
            await database.SaveChangesAsync();

            try
            {
                await remoteStorage.DeleteObject(version.StorageFile.UploadPath);

                logger.LogInformation(
                    "Deleted upload path for a remote storage item that was just deleted: {UploadPath}",
                    version.StorageFile.UploadPath);
            }
            catch (Exception e)
            {
                logger.LogTrace("Upload path probably didn't exist for the above file, couldn't delete it: {@E}", e);
            }
        }
        public async Task Execute(long ciProjectId, long ciBuildId, long ciJobId, long serverId,
                                  CancellationToken cancellationToken)
        {
            var job = await database.CiJobs.FirstOrDefaultAsync(
                j => j.CiProjectId == ciProjectId && j.CiBuildId == ciBuildId && j.CiJobId == ciJobId,
                cancellationToken);

            if (job == null)
            {
                logger.LogError("Failed to check if a CI job startup is stuck, can't find the job");
                return;
            }

            if (job.State == CIJobState.Finished)
            {
                return;
            }

            var outputSections = await database.CiJobOutputSections.CountAsync(s =>
                                                                               s.CiProjectId == ciProjectId && s.CiBuildId == ciBuildId && s.CiJobId == ciJobId,
                                                                               cancellationToken);

            if (outputSections > 0)
            {
                logger.LogInformation("CI job {CIProjectId}-{CIBuildId}-{CIJobId} has connected output",
                                      ciProjectId, ciBuildId, ciJobId);
                return;
            }

            logger.LogWarning("Detected CI job {CIProjectId}-{CIBuildId}-{CIJobId} as stuck starting",
                              ciProjectId, ciBuildId, ciJobId);

            if (job.RunningOnServerId != serverId)
            {
                logger.LogError("Wrong RunningOnServerId in job (detected startup is stuck)");
                job.RunningOnServerId = serverId;
                await database.SaveChangesAsync(cancellationToken);
            }

            jobClient.Enqueue <SetFinishedCIJobStatusJob>(x =>
                                                          x.Execute(ciProjectId, ciBuildId, ciJobId, false, CancellationToken.None));
        }
예제 #30
0
        public async Task Execute(long meetingId, long pollId, CancellationToken cancellationToken)
        {
            var poll = await database.MeetingPolls.FindAsync(meetingId, pollId);

            if (poll == null)
            {
                logger.LogError("Can't auto-close a non-existent poll: {MeetingId}-{PollId}", meetingId, pollId);
                return;
            }

            if (poll.AutoCloseAt == null)
            {
                logger.LogInformation("Auto-close poll is no longer an auto-close, skipping doing anything");
                return;
            }

            // If not time yet, reschedule
            if (poll.AutoCloseAt.Value > DateTime.UtcNow)
            {
                logger.LogWarning("Auto-close poll close time is in the future, scheduling a new job to run then");
                jobClient.Schedule <CloseAutoClosePollJob>(x => x.Execute(meetingId, pollId, CancellationToken.None),
                                                           poll.AutoCloseAt.Value);
                return;
            }

            // Don't even log anything if already closed
            if (poll.ClosedAt != null)
            {
                return;
            }

            poll.ClosedAt = DateTime.UtcNow;
            await database.SaveChangesAsync(cancellationToken);

            logger.LogInformation("Auto-closed poll: {MeetingId}-{PollId}", meetingId, pollId);

            // Queue a job to calculate the results
            jobClient.Enqueue <ComputePollResultsJob>(x => x.Execute(meetingId, pollId,
                                                                     CancellationToken.None));
        }