Files
UnrealEngine/Engine/Source/Programs/Horde/HordeServer/Dashboard/DashboardController.cs
2025-05-18 13:04:45 +08:00

272 lines
9.4 KiB
C#

// 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
{
/// <summary>
/// Dashboard authorization challenge controller
/// </summary>
[ApiController]
[Route("[controller]")]
public class DashboardController : Controller
{
readonly string _authenticationScheme;
readonly ServerSettings _settings;
readonly IDashboardPreviewCollection _previewCollection;
readonly IAccountCollection _hordeAccounts;
readonly IEnumerable<IPluginResponseFilter> _responseFilters;
readonly IOptionsSnapshot<GlobalConfig> _globalConfig;
/// <summary>
/// Constructor
/// </summary>
public DashboardController(IDashboardPreviewCollection previewCollection, IAccountCollection hordeAccounts, IEnumerable<IPluginResponseFilter> responseFilters, IOptionsMonitor<ServerSettings> serverSettings, IOptionsSnapshot<GlobalConfig> globalConfig)
{
_authenticationScheme = AccountController.GetAuthScheme(serverSettings.CurrentValue.AuthMethod);
_previewCollection = previewCollection;
_hordeAccounts = hordeAccounts;
_responseFilters = responseFilters;
_settings = serverSettings.CurrentValue;
_globalConfig = globalConfig;
}
/// <summary>
/// Challenge endpoint for the dashboard, using cookie authentication scheme
/// </summary>
/// <returns>Ok on authorized, otherwise will 401</returns>
[HttpGet]
[Route("/api/v1/dashboard/challenge")]
public async Task<IActionResult> 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 });
}
/// <summary>
/// Login to server, redirecting to the specified URL on success
/// </summary>
/// <param name="redirect"></param>
/// <returns></returns>
[HttpGet]
[Route("/api/v1/dashboard/login")]
public IActionResult Login([FromQuery] string? redirect)
{
return new ChallengeResult(_authenticationScheme, new AuthenticationProperties { RedirectUri = redirect ?? "/" });
}
/// <summary>
/// Login to server, redirecting to the specified Base64 encoded URL, which fixes some escaping issues on some auth providers, on success
/// </summary>
/// <param name="redirect"></param>
/// <returns></returns>
[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" });
}
/// <summary>
/// Logout of the current account
/// </summary>
/// /// <param name="dashboard"></param>
/// <returns></returns>
[HttpGet]
[Route("/api/v1/dashboard/logout")]
public async Task<ActionResult> 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();
}
/// <summary>
/// Query all the projects
/// </summary>
/// <returns>Config information needed by the dashboard</returns>
[HttpGet]
[Authorize]
[Route("/api/v1/dashboard/config")]
public ActionResult<GetDashboardConfigResponse> 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;
}
/// <summary>
/// Create a new dashboard preview item
/// </summary>
/// <returns>Config information needed by the dashboard</returns>
[HttpPost]
[Authorize]
[Route("/api/v1/dashboard/preview")]
public async Task<ActionResult<GetDashboardPreviewResponse>> 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);
}
/// <summary>
/// Update a dashboard preview item
/// </summary>
/// <returns>Config information needed by the dashboard</returns>
[HttpPut]
[Authorize]
[Route("/api/v1/dashboard/preview")]
public async Task<ActionResult<GetDashboardPreviewResponse>> 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);
}
/// <summary>
/// Query dashboard preview items
/// </summary>
/// <returns>Config information needed by the dashboard</returns>
[HttpGet]
[Authorize]
[Route("/api/v1/dashboard/previews")]
public async Task<ActionResult<List<GetDashboardPreviewResponse>>> GetDashbordPreviewsAsync([FromQuery] bool open = true, CancellationToken cancellationToken = default)
{
List<IDashboardPreview> 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;
}
/// <summary>
/// Returns a list of valid user-defined groups from the current server config. These are any claims with
/// the <see cref="HordeClaimTypes.Group"/> type.
/// </summary>
[HttpGet]
[Authorize]
[Route("/api/v1/dashboard/accountgroups")]
public ActionResult<List<AccountClaimMessage>> 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());
}
}
}