From 2f5e543410411e843f508fb0906237371c52a18f Mon Sep 17 00:00:00 2001 From: Andrew Schlackman <72105194+sei-aschlackman@users.noreply.github.com> Date: Wed, 22 May 2024 10:11:51 -0400 Subject: [PATCH] added file download url endpoint (#44) --- .../Domain/Models/Permissions.cs | 4 +- .../Domain/Services/PermissionsService.cs | 35 ++++---- .../Domain/Vsphere/Services/VsphereService.cs | 83 +++++++++++++++++++ .../Features/Vsphere/BaseHandler.cs | 2 +- .../Features/Vsphere/Commands/GetVmFileUrl.cs | 79 ++++++++++++++++++ .../Features/Vsphere/VsphereController.cs | 12 +++ 6 files changed, 199 insertions(+), 16 deletions(-) create mode 100644 src/Player.Vm.Api/Features/Vsphere/Commands/GetVmFileUrl.cs diff --git a/src/Player.Vm.Api/Domain/Models/Permissions.cs b/src/Player.Vm.Api/Domain/Models/Permissions.cs index 1dc131f..37efd6a 100644 --- a/src/Player.Vm.Api/Domain/Models/Permissions.cs +++ b/src/Player.Vm.Api/Domain/Models/Permissions.cs @@ -5,6 +5,8 @@ namespace Player.Vm.Api.Domain.Models { public enum Permissions { - ReadOnly + ReadOnly, + ViewAdmin, + SystemAdmin } } diff --git a/src/Player.Vm.Api/Domain/Services/PermissionsService.cs b/src/Player.Vm.Api/Domain/Services/PermissionsService.cs index 5eb2f08..49dde35 100644 --- a/src/Player.Vm.Api/Domain/Services/PermissionsService.cs +++ b/src/Player.Vm.Api/Domain/Services/PermissionsService.cs @@ -35,20 +35,13 @@ public PermissionsService( public async Task> GetViewPermissions(Guid viewId, CancellationToken cancellationToken) { - var permissions = new List(); var viewPermissions = await _playerService.GetPermissionsByViewIdAsync(viewId, cancellationToken); - - if (viewPermissions.Any(x => x.Key.ToLower() == Permissions.ReadOnly.ToString().ToLower() && x.Value.ToLower() == "true")) - { - permissions.Add(Permissions.ReadOnly); - } - + var permissions = this.ProcessPermissions(viewPermissions); return permissions; } public async Task> GetPermissions(IEnumerable teamIds, CancellationToken cancellationToken) { - var vmPermissions = new List(); var viewPermissions = new List(); var viewIds = await _viewService.GetViewIdsForTeams(teamIds, cancellationToken); var taskDict = new Dictionary>>(); @@ -80,12 +73,7 @@ public async Task> GetPermissions(IEnumerable tea }); } - if (viewPermissions.Any(x => x.Key.ToLower() == Permissions.ReadOnly.ToString().ToLower() && x.Value.ToLower() == "true")) - { - vmPermissions.Add(Permissions.ReadOnly); - } - - return vmPermissions; + return this.ProcessPermissions(viewPermissions); } public async Task CanWrite(IEnumerable teamIds, CancellationToken cancellationToken) @@ -101,5 +89,24 @@ public async Task CanWrite(IEnumerable teamIds, CancellationToken ca return true; } } + + private IEnumerable ProcessPermissions(IEnumerable playerPermissions) + { + var permissionsList = new List(); + this.ProcessPermission(playerPermissions, ref permissionsList, Permissions.ReadOnly); + this.ProcessPermission(playerPermissions, ref permissionsList, Permissions.ViewAdmin); + this.ProcessPermission(playerPermissions, ref permissionsList, Permissions.SystemAdmin); + return permissionsList; + } + + private IList ProcessPermission(IEnumerable playerPermissions, ref List permissionsList, Permissions permission) + { + if (playerPermissions.Any(x => x.Key.ToLower() == permission.ToString().ToLower() && x.Value.ToLower() == "true")) + { + permissionsList.Add(permission); + } + + return permissionsList; + } } } diff --git a/src/Player.Vm.Api/Domain/Vsphere/Services/VsphereService.cs b/src/Player.Vm.Api/Domain/Vsphere/Services/VsphereService.cs index 04359c7..cc43d73 100644 --- a/src/Player.Vm.Api/Domain/Vsphere/Services/VsphereService.cs +++ b/src/Player.Vm.Api/Domain/Vsphere/Services/VsphereService.cs @@ -16,6 +16,7 @@ using Player.Vm.Api.Domain.Vsphere.Models; using Player.Vm.Api.Domain.Vsphere.Extensions; using Player.Vm.Api.Domain.Models; +using System.Web; namespace Player.Vm.Api.Domain.Vsphere.Services { @@ -31,6 +32,7 @@ public interface IVsphereService Task ReconfigureVm(Guid id, Feature feature, string label, string newvalue); Task GetVmToolsStatus(Guid id); Task UploadFileToVm(Guid id, string username, string password, string filepath, Stream fileStream); + Task GetVmFileUrl(Guid id, string username, string password, string filepath); Task> GetIsos(Guid vmId, string viewId, string subFolder); Task SetResolution(Guid id, int width, int height); Task BulkPowerOperation(Guid[] ids, PowerOperation operation); @@ -746,6 +748,87 @@ public async Task UploadFileToVm(Guid id, string username, string passwo return ""; } + public async Task GetVmFileUrl(Guid id, string username, string password, string filepath) + { + var aggregate = await GetVm(id); + var vmReference = aggregate.MachineReference; + + if (vmReference == null) + { + var errorMessage = $"could not get file url, vmReference is null"; + _logger.LogDebug(errorMessage); + return errorMessage; + } + + //retrieve the properties specificied + RetrievePropertiesResponse response = await aggregate.Connection.Client.RetrievePropertiesAsync( + aggregate.Connection.Props, + VmFilter(vmReference)); + + VimClient.ObjectContent[] oc = response.returnval; + VimClient.ObjectContent obj = oc[0]; + + foreach (DynamicProperty dp in obj.propSet) + { + if (dp.val.GetType() == typeof(VirtualMachineSummary)) + { + VirtualMachineSummary vmSummary = (VirtualMachineSummary)dp.val; + //check vmware tools status + var tools_status = vmSummary.guest.toolsStatus; + if (tools_status == VirtualMachineToolsStatus.toolsNotInstalled || tools_status == VirtualMachineToolsStatus.toolsNotRunning) + { + var errorMessage = $"could not get file url, VM Tools is not running"; + _logger.LogDebug(errorMessage); + return errorMessage; + } + + // user credentials on the VM + NamePasswordAuthentication credentialsAuth = new NamePasswordAuthentication() + { + interactiveSession = false, + username = username, + password = password + }; + ManagedObjectReference fileManager = new ManagedObjectReference() + { + type = "GuestFileManager", + Value = "guestOperationsFileManager" + }; + + var fileTransferInfo = await aggregate.Connection.Client.InitiateFileTransferFromGuestAsync(fileManager, vmReference, credentialsAuth, filepath); + var fileTransferUrl = fileTransferInfo.url; + + // Replace IP address with hostname + RetrievePropertiesResponse hostResponse = await aggregate.Connection.Client.RetrievePropertiesAsync(aggregate.Connection.Props, HostFilter(vmSummary.runtime.host, "name")); + string hostName = hostResponse.returnval[0].propSet[0].val as string; + + if (!fileTransferUrl.Contains(hostName)) + { + fileTransferUrl = fileTransferUrl.Replace("https://", ""); + var s = fileTransferUrl.IndexOf("/"); + fileTransferUrl = "https://" + hostName + fileTransferUrl.Substring(s); + } + + if (_rewriteHostOptions.RewriteHost) + { + var builder = new UriBuilder(fileTransferUrl); + + var query = HttpUtility.ParseQueryString(builder.Query); + query[_rewriteHostOptions.RewriteHostQueryParam] = builder.Host; + var fileName = Path.GetFileName(filepath); + query["fileName"] = fileName; + builder.Query = query.ToString(); + + builder.Host = _rewriteHostOptions.RewriteHostUrl; + fileTransferUrl = builder.ToString(); + } + + return fileTransferUrl; + } + } + return ""; + } + private async Task WaitForVimTask(ManagedObjectReference task, VsphereConnection connection) { int i = 0; diff --git a/src/Player.Vm.Api/Features/Vsphere/BaseHandler.cs b/src/Player.Vm.Api/Features/Vsphere/BaseHandler.cs index 28bfc31..03afbd2 100644 --- a/src/Player.Vm.Api/Features/Vsphere/BaseHandler.cs +++ b/src/Player.Vm.Api/Features/Vsphere/BaseHandler.cs @@ -25,7 +25,7 @@ public class BaseHandler { private readonly IMapper _mapper; private readonly IVsphereService _vsphereService; - private readonly IPlayerService _playerService; + protected readonly IPlayerService _playerService; private readonly Guid _userId; private readonly IPermissionsService _permissionsService; private readonly IVmService _vmService; diff --git a/src/Player.Vm.Api/Features/Vsphere/Commands/GetVmFileUrl.cs b/src/Player.Vm.Api/Features/Vsphere/Commands/GetVmFileUrl.cs new file mode 100644 index 0000000..5f6a88d --- /dev/null +++ b/src/Player.Vm.Api/Features/Vsphere/Commands/GetVmFileUrl.cs @@ -0,0 +1,79 @@ +// Copyright 2024 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. + +using System; +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using System.Runtime.Serialization; +using System.Text.Json.Serialization; +using Player.Vm.Api.Infrastructure.Exceptions; +using Player.Vm.Api.Domain.Vsphere.Services; +using Player.Vm.Api.Features.Vms; +using System.IO; +using AutoMapper; +using Player.Vm.Api.Domain.Services; +using System.Security.Principal; +using Player.Vm.Api.Domain.Models; + +namespace Player.Vm.Api.Features.Vsphere +{ + public class GetVmFileUrl + { + [DataContract(Name = "GetFileUrlVsphereVirtualMachine")] + public class Command : IRequest + { + [JsonIgnore] + public Guid Id { get; set; } + public string Username { get; set; } + public string Password { get; set; } + public string FilePath { get; set; } + } + + [DataContract(Name = "FileVmUrlResponse")] + public class Response + { + public string Url { get; set; } + public string FileName { get; set; } + } + + public class Handler : BaseHandler, IRequestHandler + { + private readonly IVsphereService _vsphereService; + + public Handler( + IVsphereService vsphereService, + IVmService vmService, + IMapper mapper, + IPlayerService playerService, + IPrincipal principal, + IPermissionsService permissionsService) : + base(mapper, vsphereService, playerService, principal, permissionsService, vmService) + { + _vsphereService = vsphereService; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var vm = await base.GetVm(request.Id, Permissions.ReadOnly, cancellationToken); + + if (!await _playerService.CanManageTeamsAsync(vm.TeamIds, false, cancellationToken)) + throw new ForbiddenException("You do not have permission to download files from this vm."); + + var url = await _vsphereService.GetVmFileUrl( + request.Id, + request.Username, + request.Password, + request.FilePath); + + var fileName = Path.GetFileName(request.FilePath); + + return new Response() + { + FileName = fileName, + Url = url + }; + } + } + } +} \ No newline at end of file diff --git a/src/Player.Vm.Api/Features/Vsphere/VsphereController.cs b/src/Player.Vm.Api/Features/Vsphere/VsphereController.cs index 18c6673..c687dc2 100644 --- a/src/Player.Vm.Api/Features/Vsphere/VsphereController.cs +++ b/src/Player.Vm.Api/Features/Vsphere/VsphereController.cs @@ -142,6 +142,18 @@ public async Task UploadFile([FromRoute] Guid id) return Json(result); } + /// + /// Get the url to download a file from a vsphere virtual machine + /// + [HttpPost("vms/vsphere/{id}/actions/file-url")] + [ProducesResponseType(typeof(GetVmFileUrl.Response), (int)HttpStatusCode.OK)] + [SwaggerOperation(OperationId = "getFileUrlVsphereVirtualMachine")] + public async Task GetFileUrl([FromRoute] Guid id, [FromBody] GetVmFileUrl.Command command) + { + command.Id = id; + return Ok(await _mediator.Send(command)); + } + /// /// Get isos available to be mounted to a vsphere virtual machine ///