// Copyright Epic Games, Inc. All Rights Reserved. using EpicGames.Core; using HordeServer.Api; using HordeServer.Models; using HordeServer.Services; using HordeServer.Utilities; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using MongoDB.Bson; using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading.Tasks; using ProjectId = HordeServer.Utilities.StringId; using StreamId = HordeServer.Utilities.StringId; using TemplateRefId = HordeServer.Utilities.StringId; using HordeServer.Collections; using System.Security.Claims; using System.IO; namespace HordeServer.Controllers { /// /// Controller for the /api/v1/streams endpoint /// [ApiController] [Authorize] [Route("[controller]")] public class StreamsController : ControllerBase { /// /// Singleton instance of the ACL service /// private readonly AclService AclService; /// /// Singleton instance of the stream service /// private readonly ProjectService ProjectService; /// /// Singleton instance of the stream service /// private readonly StreamService StreamService; /// /// Singleton instance of the template service /// private readonly TemplateService TemplateService; /// /// Singleton instance of the job service /// private readonly JobService JobService; /// /// Collection of jobstep refs /// private readonly IJobStepRefCollection JobStepRefCollection; /// /// Singleton instance of the perforce service /// private readonly IPerforceService PerforceService; /// /// Constructor /// /// The ACL service /// The project service /// The stream service /// The template service /// The job service /// The jobstep ref collection /// The perforce service public StreamsController(AclService AclService, ProjectService ProjectService, StreamService StreamService, TemplateService TemplateService, JobService JobService, IJobStepRefCollection JobStepRefCollection, IPerforceService PerforceService) { this.AclService = AclService; this.ProjectService = ProjectService; this.StreamService = StreamService; this.TemplateService = TemplateService; this.JobService = JobService; this.JobStepRefCollection = JobStepRefCollection; this.PerforceService = PerforceService; } /// /// Query all the streams for a particular project. /// /// Unique id of the project to query /// Filter for the properties to return /// Information about all the projects [HttpGet] [Route("/api/v1/streams")] [ProducesResponseType(typeof(List), 200)] public async Task>> GetStreamsAsync([FromQuery(Name = "ProjectId")] string[] ProjectIds, [FromQuery] PropertyFilter? Filter = null) { ProjectId[] ProjectIdValues = Array.ConvertAll(ProjectIds, x => new ProjectId(x)); List Streams = await StreamService.GetStreamsAsync(ProjectIdValues); ProjectPermissionsCache PermissionsCache = new ProjectPermissionsCache(); List Responses = new List(); foreach (IStream Stream in Streams) { if (await StreamService.AuthorizeAsync(Stream, AclAction.ViewStream, User, PermissionsCache)) { GetStreamResponse Response = await CreateGetStreamResponse(Stream, PermissionsCache); Responses.Add(Response); } } return Responses.OrderBy(x => x.Order).ThenBy(x => x.Name).Select(x => PropertyFilter.Apply(x, Filter)).ToList(); } /// /// Retrieve information about a specific stream. /// /// Id of the stream to get information about /// Filter for the properties to return /// Information about the requested project [HttpGet] [Route("/api/v1/streams/{StreamId}")] [ProducesResponseType(typeof(GetStreamResponse), 200)] public async Task> GetStreamAsync(string StreamId, [FromQuery] PropertyFilter? Filter = null) { StreamId StreamIdValue = new StreamId(StreamId); IStream? Stream = await StreamService.GetStreamAsync(StreamIdValue); if (Stream == null) { return NotFound(); } ProjectPermissionsCache PermissionsCache = new ProjectPermissionsCache(); if (!await StreamService.AuthorizeAsync(Stream, AclAction.ViewStream, User, PermissionsCache)) { return Forbid(); } bool bIncludeAcl = await StreamService.AuthorizeAsync(Stream, AclAction.ViewPermissions, User, PermissionsCache); return PropertyFilter.Apply(await CreateGetStreamResponse(Stream, PermissionsCache), Filter); } /// /// Create a stream response object, including all the templates /// /// Stream to create response for /// Permissions cache /// Response object async Task CreateGetStreamResponse(IStream Stream, ProjectPermissionsCache Cache) { bool bIncludeAcl = Stream.Acl != null && await StreamService.AuthorizeAsync(Stream, AclAction.ViewPermissions, User, Cache); List ApiTemplateRefs = new List(); foreach (KeyValuePair Pair in Stream.Templates) { if (await StreamService.AuthorizeAsync(Stream, Pair.Value, AclAction.ViewTemplate, User, Cache)) { ITemplate? Template = await TemplateService.GetTemplateAsync(Pair.Value.Hash); if (Template != null) { bool bIncludeTemplateAcl = Pair.Value.Acl != null && await StreamService.AuthorizeAsync(Stream, Pair.Value, AclAction.ViewPermissions, User, Cache); ApiTemplateRefs.Add(new GetTemplateRefResponse(Pair.Key, Pair.Value, Template, bIncludeTemplateAcl)); } } } return Stream.ToApiResponse(bIncludeAcl, ApiTemplateRefs); } /// /// Gets a list of changes for a stream /// /// The stream id /// The starting changelist number /// The ending changelist number /// Number of results to return /// The filter to apply to the results /// Http result code [HttpGet] [Route("/api/v1/streams/{StreamId}/changes")] [ProducesResponseType(typeof(List), 200)] public async Task>> GetChangesAsync(string StreamId, [FromQuery] int? Min = null, [FromQuery] int? Max = null, [FromQuery] int Results = 50, PropertyFilter? Filter = null) { StreamId StreamIdValue = new StreamId(StreamId); IStream? Stream = await StreamService.GetStreamAsync(StreamIdValue); if (Stream == null) { return NotFound(); } if (!await StreamService.AuthorizeAsync(Stream, AclAction.ViewChanges, User, null)) { return Forbid(); } string? PerforceUser = User.GetPerforceUser(); if(PerforceUser == null) { return Forbid(); } List Commits = await PerforceService.GetChangesAsync(Stream.Name, Min, Max, Results, PerforceUser); return Commits.ConvertAll(x => PropertyFilter.Apply(new GetChangeSummaryResponse(x), Filter)); } /// /// Gets a list of changes for a stream /// /// The stream id /// The changelist number /// The filter to apply to the results /// Http result code [HttpGet] [Route("/api/v1/streams/{StreamId}/changes/{Number}")] [ProducesResponseType(typeof(GetChangeDetailsResponse), 200)] public async Task> GetChangeDetailsAsync(string StreamId, int Number, PropertyFilter? Filter = null) { StreamId StreamIdValue = new StreamId(StreamId); IStream? Stream = await StreamService.GetStreamAsync(StreamIdValue); if (Stream == null) { return NotFound(); } if (!await StreamService.AuthorizeAsync(Stream, AclAction.ViewChanges, User, null)) { return Forbid(); } string? PerforceUser = User.GetPerforceUser(); if(PerforceUser == null) { return Forbid(); } ChangeDetails? ChangeDetails = await PerforceService.GetChangeDetailsAsync(Stream.Name, Number, PerforceUser); if(ChangeDetails == null) { return NotFound(); } return PropertyFilter.Apply(new GetChangeDetailsResponse(ChangeDetails), Filter); } /// /// Gets the history of a step in the stream /// /// The stream id /// /// Name of the step to search for /// Maximum changelist number to return /// Number of results to return /// The filter to apply to the results /// Http result code [HttpGet] [Route("/api/v1/streams/{StreamId}/history")] [ProducesResponseType(typeof(List), 200)] public async Task>> GetStepHistoryAsync(string StreamId, [FromQuery] string TemplateId, [FromQuery] string Step, [FromQuery] int? Change = null, [FromQuery] int Count = 10, [FromQuery] PropertyFilter? Filter = null) { StreamId StreamIdValue = new StreamId(StreamId); IStream? Stream = await StreamService.GetStreamAsync(StreamIdValue); if (Stream == null) { return NotFound(); } if (!await StreamService.AuthorizeAsync(Stream, AclAction.ViewJob, User, null)) { return Forbid(); } TemplateRefId TemplateIdValue = new TemplateRefId(TemplateId); List Steps = await JobStepRefCollection.GetStepsForNodeAsync(StreamIdValue, TemplateIdValue, Step, Change, true, Count); return Steps.ConvertAll(x => PropertyFilter.Apply(new GetJobStepRefResponse(x), Filter)); } /// /// Deletes a stream /// /// Id of the stream to update. /// Http result code [HttpDelete] [Route("/api/v1/streams/{StreamId}")] public async Task DeleteStreamAsync(string StreamId) { StreamId StreamIdValue = new StreamId(StreamId); IStream? Stream = await StreamService.GetStreamAsync(StreamIdValue); if (Stream == null) { return NotFound(); } if (!await StreamService.AuthorizeAsync(Stream, AclAction.DeleteStream, User, null)) { return Forbid(); } await StreamService.DeleteStreamAsync(StreamIdValue); return new OkResult(); } /// /// Validates that the user is allowed to specify the given template refs /// /// List of template refs /// On failure, receives information about the missing claim /// True if the refs are valid, false otherwise bool TryAuthorizeTemplateClaims(List? Requests, [NotNullWhen(false)] out ActionResult? OutResult) { if (Requests != null) { foreach (CreateTemplateRefRequest Request in Requests) { if (Request.Schedule != null && Request.Schedule.Claims != null) { foreach (CreateAclClaimRequest Claim in Request.Schedule.Claims) { if (!User.HasClaim(Claim.Type, Claim.Value)) { OutResult = BadRequest("User does not have the {Claim.Type}: {Claim.Value} claim"); return false; } } } } } OutResult = null; return true; } /// /// Creates a list of template refs from a set of request objects /// /// Request objects /// The current stream state /// The template service /// List of new template references static async Task> CreateTemplateRefs(List Requests, IStream? Stream, TemplateService TemplateService) { Dictionary NewTemplateRefs = new Dictionary(); foreach (CreateTemplateRefRequest Request in Requests) { // Create the template ITemplate NewTemplate = await TemplateService.CreateTemplateAsync(Request.Name, Request.Priority, Request.AllowPreflights, Request.InitialAgentType, Request.SubmitNewChange, Request.Counters, Request.Arguments, Request.Parameters.ConvertAll(x => x.ToModel())); // Get an identifier for the new template ref TemplateRefId NewTemplateRefId; if (Request.Id != null) { NewTemplateRefId = new TemplateRefId(Request.Id); } else { NewTemplateRefId = TemplateRefId.Sanitize(Request.Name); } // Add it to the list TemplateRef NewTemplateRef = new TemplateRef(NewTemplate, Request.ShowUgsBadges, Request.ShowUgsAlerts, Request.NotificationChannel, Request.NotificationChannelFilter, Request.TriageChannel, Request.Schedule?.ToModel(), Request.ChainedJobs?.ConvertAll(x => new ChainedJobTemplate(x)), Acl.Merge(null, Request.Acl)); if (Stream != null && Stream.Templates.TryGetValue(NewTemplateRefId, out TemplateRef? OldTemplateRef)) { if (OldTemplateRef.Schedule != null && NewTemplateRef.Schedule != null) { NewTemplateRef.Schedule.CopyState(OldTemplateRef.Schedule); } } NewTemplateRefs.Add(NewTemplateRefId, NewTemplateRef); } foreach (TemplateRef TemplateRef in NewTemplateRefs.Values) { if (TemplateRef.ChainedJobs != null) { foreach (ChainedJobTemplate ChainedJob in TemplateRef.ChainedJobs) { if (!NewTemplateRefs.ContainsKey(ChainedJob.TemplateRefId)) { throw new InvalidDataException($"Invalid template ref id '{ChainedJob.TemplateRefId}"); } } } } return NewTemplateRefs; } /// /// Gets all the templates for a stream /// /// Unique id of the stream to query /// Unique id of the template to query /// Filter for properties to return /// Information about all the templates [HttpGet] [Route("/api/v1/streams/{StreamId}/templates/{TemplateRefId}")] [ProducesResponseType(typeof(List), 200)] public async Task> GetTemplateAsync(string StreamId, string TemplateRefId, [FromQuery] PropertyFilter? Filter = null) { StreamId StreamIdValue = new StreamId(StreamId); TemplateRefId TemplateRefIdValue = new TemplateRefId(TemplateRefId); IStream? Stream = await StreamService.GetStreamAsync(StreamIdValue); if (Stream == null) { return NotFound(); } TemplateRef? TemplateRef; if (!Stream.Templates.TryGetValue(TemplateRefIdValue, out TemplateRef)) { return NotFound(); } if (!await StreamService.AuthorizeAsync(Stream, TemplateRef, AclAction.ViewTemplate, User, null)) { return Forbid(); } ITemplate? Template = await TemplateService.GetTemplateAsync(TemplateRef.Hash); if(Template == null) { return NotFound(); } return new GetTemplateResponse(Template).ApplyFilter(Filter); } } }