// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.IO; using System.Linq; using System.Net.Mime; using System.Security.Claims; using System.Security.Cryptography; using System.Text; using System.Text.Json; using System.Threading.Tasks; using EpicGames.Core; using EpicGames.Horde.Storage; using EpicGames.Serialization; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace Jupiter.Controllers { [ApiController] [Route("api/v1/auth")] [Authorize] public class AuthController : ControllerBase { private readonly IRequestHelper _requestHelper; private readonly IOptionsMonitor _authSettings; public AuthController(IRequestHelper requestHelper, IOptionsMonitor authSettings) { _requestHelper = requestHelper; _authSettings = authSettings; } [HttpGet("oidc-configuration")] // disable authentication on this endpoint [AllowAnonymous] // this endpoint always produces encrypted json [Produces(MediaTypeNames.Application.Octet)] public async Task GetConfigAsync() { if (_authSettings.CurrentValue.ClientOidcConfiguration == null) { return BadRequest(); } byte[] b = JsonSerializer.SerializeToUtf8Bytes(_authSettings.CurrentValue.ClientOidcConfiguration); using Aes aes = Aes.Create(); byte[] key = Convert.FromHexString(_authSettings.CurrentValue.ClientOidcEncryptionKey); aes.Key = key; aes.GenerateIV(); // write the IV into the stream before the encrypted content Stream responseStream = new MemoryStream(); await responseStream.WriteAsync(aes.IV); await using CryptoStream cryptoStream = new(responseStream, aes.CreateEncryptor(), CryptoStreamMode.Write); await cryptoStream.WriteAsync(b, 0, b.Length); await cryptoStream.FlushFinalBlockAsync(); Response.ContentType = MediaTypeNames.Application.Octet; Response.StatusCode = 200; Response.ContentLength = responseStream.Length; responseStream.Position = 0; await responseStream.CopyToAsync(Response.Body); return new EmptyResult(); } [HttpGet("{ns}")] public async Task VerifyAsync([FromRoute][Required] NamespaceId ns) { ActionResult? result = await _requestHelper.HasAccessToNamespaceAsync(User, Request, ns, new[] { JupiterAclAction.ReadObject }); if (result != null) { return result; } return Ok(); } [HttpGet("{ns}/actions")] public IActionResult Actions([FromRoute][Required] NamespaceId ns) { List allowedActions = new List(); CaptureLogger captureLogger = new CaptureLogger(); bool policiesFound = false; List matchedPolicies = new(); foreach (AclPolicy acl in _authSettings.CurrentValue.Policies) { policiesFound = true; List grantedActions = acl.Resolve(User, new AccessScope(ns), captureLogger, out List matchedClaims).ToList(); allowedActions.AddRange(grantedActions); MatchedPolicy matchedPolicy = new MatchedPolicy { Actions = grantedActions, Claims = matchedClaims }; matchedPolicies.Add(matchedPolicy); } if (!policiesFound) { captureLogger.LogWarning("No policies set so no actions generated"); return BadRequest(new ActionsResult {Actions = allowedActions, LogOutput = captureLogger.RenderLines(), MatchedPolicies = matchedPolicies }); } return Ok(new ActionsResult { Actions = allowedActions , LogOutput = captureLogger.RenderLines(), MatchedPolicies = matchedPolicies}); } [HttpGet("{ns}/{bucket}/actions")] public IActionResult Actions([FromRoute][Required] NamespaceId ns, [FromRoute][Required] BucketId bucket, [FromQuery] string? claims = null) { List allowedActions = new List(); ClaimsPrincipal principal = User; if (claims != null) { string s = Encoding.ASCII.GetString(Convert.FromBase64String(claims)); JsonDocument doc = JsonDocument.Parse(s); List claimList = new(); foreach(JsonProperty prop in doc.RootElement.EnumerateObject()) { if (prop.Value.ValueKind == JsonValueKind.String) { claimList.Add(new Claim(prop.Name, prop.Value.GetString()!)); } else if (prop.Value.ValueKind == JsonValueKind.Array) { foreach (JsonElement element in prop.Value.EnumerateArray()) { claimList.Add(new Claim(prop.Name, element.GetString()!)); } } else { throw new NotImplementedException("Unknown json property type when attempting to convert it into a claim"); } } principal = new ClaimsPrincipal(new ClaimsIdentity(claimList)); } List matchedPolicies = new(); CaptureLogger captureLogger = new CaptureLogger(); bool policiesFound = false; foreach (AclPolicy acl in _authSettings.CurrentValue.Policies) { policiesFound = true; List grantedActions = acl.Resolve(principal, new AccessScope(ns, bucket), captureLogger, out List matchedClaims).ToList(); allowedActions.AddRange(grantedActions); MatchedPolicy matchedPolicy = new MatchedPolicy { Name = acl.Name, Actions = grantedActions, Claims = matchedClaims }; matchedPolicies.Add(matchedPolicy); } if (!policiesFound) { captureLogger.LogWarning("No policies set so no actions generated"); return BadRequest(new ActionsResult { Actions = allowedActions, LogOutput = captureLogger.RenderLines(), MatchedPolicies = matchedPolicies }); } return Ok(new ActionsResult { Actions = allowedActions , LogOutput = captureLogger.RenderLines(), MatchedPolicies = matchedPolicies}); } } public class ActionsResult { [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "Used by serialization")] [CbField("actions")] public List Actions { get; set; } = new List(); [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "Used by serialization")] [CbField("logOutput")] public List LogOutput { get; set; } = new List(); [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "Used by serialization")] [CbField("matchedPolicies")] public List MatchedPolicies { get; set; } = new List(); } public class MatchedPolicy { [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "Used by serialization")] [CbField("actions")] public List Actions { get; set; } = new List(); [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "Used by serialization")] [CbField("claims")] public List Claims { get; set; } = new List(); [CbField("name")] public string Name { get; set; } = null!; } }