// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using EpicGames.Horde.Accounts; using EpicGames.Horde.Dashboard; using EpicGames.Horde.Server; using HordeServer.Accounts; using HordeServer.Plugins; using HordeServer.Server; using HordeServer.Utilities; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; namespace HordeServer.Dashboard { /// /// Dashboard authorization challenge controller /// [ApiController] [Route("[controller]")] public class DashboardController : Controller { readonly string _authenticationScheme; readonly ServerSettings _settings; readonly IDashboardPreviewCollection _previewCollection; readonly IAccountCollection _hordeAccounts; readonly IEnumerable _responseFilters; readonly IOptionsSnapshot _globalConfig; /// /// Constructor /// public DashboardController(IDashboardPreviewCollection previewCollection, IAccountCollection hordeAccounts, IEnumerable responseFilters, IOptionsMonitor serverSettings, IOptionsSnapshot globalConfig) { _authenticationScheme = AccountController.GetAuthScheme(serverSettings.CurrentValue.AuthMethod); _previewCollection = previewCollection; _hordeAccounts = hordeAccounts; _responseFilters = responseFilters; _settings = serverSettings.CurrentValue; _globalConfig = globalConfig; } /// /// Challenge endpoint for the dashboard, using cookie authentication scheme /// /// Ok on authorized, otherwise will 401 [HttpGet] [Route("/api/v1/dashboard/challenge")] public async Task GetChallengeAsync() { bool needsFirstTimeSetup = false; if (_settings.AuthMethod == AuthMethod.Horde) { IAccount? defaultAdminAccount = await _hordeAccounts.GetAsync(AccountId.Parse("65d4f282ff286703e0609ccd")); if (defaultAdminAccount == null) { needsFirstTimeSetup = true; } } return Ok(new GetDashboardChallengeResponse { NeedsFirstTimeSetup = needsFirstTimeSetup, NeedsAuthorization = User.Identity == null || !User.Identity.IsAuthenticated }); } /// /// Login to server, redirecting to the specified URL on success /// /// /// [HttpGet] [Route("/api/v1/dashboard/login")] public IActionResult Login([FromQuery] string? redirect) { return new ChallengeResult(_authenticationScheme, new AuthenticationProperties { RedirectUri = redirect ?? "/" }); } /// /// Login to server, redirecting to the specified Base64 encoded URL, which fixes some escaping issues on some auth providers, on success /// /// /// [HttpGet] [Route("/api/v2/dashboard/login")] public IActionResult LoginV2([FromQuery] string? redirect) { if (_settings.AuthMethod == AuthMethod.Horde) { return Redirect("/login" + (redirect == null ? String.Empty : $"?redirect={redirect}")); } string? redirectUri = null; if (redirect != null) { byte[] data = Convert.FromBase64String(redirect); redirectUri = Encoding.UTF8.GetString(data); } return new ChallengeResult(_authenticationScheme, new AuthenticationProperties { RedirectUri = redirectUri ?? "/index" }); } /// /// Logout of the current account /// /// /// /// [HttpGet] [Route("/api/v1/dashboard/logout")] public async Task LogoutAsync([FromQuery] bool? dashboard) { await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); try { await HttpContext.SignOutAsync(_authenticationScheme); } catch { } if (dashboard == false && _settings.AuthMethod == AuthMethod.Horde) { return Redirect("/"); } return Ok(); } /// /// Query all the projects /// /// Config information needed by the dashboard [HttpGet] [Authorize] [Route("/api/v1/dashboard/config")] public ActionResult GetDashbordConfig() { GetDashboardConfigResponse dashboardConfigResponse = new GetDashboardConfigResponse(); dashboardConfigResponse.AuthMethod = _settings.AuthMethod; dashboardConfigResponse.HelpEmailAddress = _settings.HelpEmailAddress; dashboardConfigResponse.HelpSlackChannel = _settings.HelpSlackChannel; foreach (DashboardAgentCategoryConfig category in _globalConfig.Value.Dashboard.AgentCategories) { dashboardConfigResponse.AgentCategories.Add(new GetDashboardAgentCategoryResponse { Name = category.Name, Condition = category.Condition }); } foreach (DashboardPoolCategoryConfig category in _globalConfig.Value.Dashboard.PoolCategories) { dashboardConfigResponse.PoolCategories.Add(new GetDashboardPoolCategoryResponse { Name = category.Name, Condition = category.Condition }); } dashboardConfigResponse.ArtifactTypes = _globalConfig.Value.Plugins.GetBuildConfig().ArtifactTypes.Select(a => a.Type.ToString()).ToList(); dashboardConfigResponse.ArtifactTypeInfo = _globalConfig.Value.Plugins.GetBuildConfig().ArtifactTypes.Select(a => new GetDashboardArtifactTypeResponse { Type = a.Type.ToString(), KeepDays = a.KeepDays}).ToList(); foreach (IPluginResponseFilter responseFilter in _responseFilters) { responseFilter.Apply(HttpContext, dashboardConfigResponse); } return dashboardConfigResponse; } /// /// Create a new dashboard preview item /// /// Config information needed by the dashboard [HttpPost] [Authorize] [Route("/api/v1/dashboard/preview")] public async Task> CreateDashbordPreviewAsync([FromBody] CreateDashboardPreviewRequest request, CancellationToken cancellationToken = default) { if (!_globalConfig.Value.Authorize(AdminAclAction.AdminWrite, User)) { return Forbid(); } IDashboardPreview preview = await _previewCollection.AddPreviewAsync(request.Summary, cancellationToken); if (!String.IsNullOrEmpty(request.ExampleLink) || !String.IsNullOrEmpty(request.DiscussionLink) || !String.IsNullOrEmpty(request.TrackingLink)) { IDashboardPreview? updated = await _previewCollection.UpdatePreviewAsync(preview.Id, null, null, null, request.ExampleLink, request.DiscussionLink, request.TrackingLink, cancellationToken); if (updated == null) { return NotFound(preview.Id); } return CreatePreviewResponse(updated); } return CreatePreviewResponse(preview); } /// /// Update a dashboard preview item /// /// Config information needed by the dashboard [HttpPut] [Authorize] [Route("/api/v1/dashboard/preview")] public async Task> UpdateDashbordPreviewAsync([FromBody] UpdateDashboardPreviewRequest request, CancellationToken cancellationToken = default) { if (!_globalConfig.Value.Authorize(AdminAclAction.AdminWrite, User)) { return Forbid(); } IDashboardPreview? preview = await _previewCollection.UpdatePreviewAsync(request.Id, request.Summary, request.DeployedCL, request.Open, request.ExampleLink, request.DiscussionLink, request.TrackingLink, cancellationToken); if (preview == null) { return NotFound(request.Id); } return CreatePreviewResponse(preview); } /// /// Query dashboard preview items /// /// Config information needed by the dashboard [HttpGet] [Authorize] [Route("/api/v1/dashboard/previews")] public async Task>> GetDashbordPreviewsAsync([FromQuery] bool open = true, CancellationToken cancellationToken = default) { List previews = await _previewCollection.FindPreviewsAsync(open, cancellationToken); return previews.Select(CreatePreviewResponse).ToList(); } static GetDashboardPreviewResponse CreatePreviewResponse(IDashboardPreview preview) { GetDashboardPreviewResponse response = new GetDashboardPreviewResponse(); response.Id = preview.Id; response.CreatedAt = preview.CreatedAt; response.Summary = preview.Summary; response.DeployedCL = preview.DeployedCL; response.Open = preview.Open; response.ExampleLink = preview.ExampleLink; response.DiscussionLink = preview.DiscussionLink; response.TrackingLink = preview.TrackingLink; return response; } /// /// Returns a list of valid user-defined groups from the current server config. These are any claims with /// the type. /// [HttpGet] [Authorize] [Route("/api/v1/dashboard/accountgroups")] public ActionResult> GetAccountGroupClaims() { if (!_globalConfig.Value.Authorize(AccountAclAction.CreateAccount, User) && !_globalConfig.Value.Authorize(AccountAclAction.UpdateAccount, User)) { return Forbid(); } return Ok(_globalConfig.Value.GetValidAccountGroupClaims().Select(x => new AccountClaimMessage(HordeClaimTypes.Group, x)).ToList()); } } }