diff --git a/src/api/Data/AuthorizationRequest.cs b/src/api/Data/AuthorizationRequest.cs new file mode 100644 index 0000000..761c26f --- /dev/null +++ b/src/api/Data/AuthorizationRequest.cs @@ -0,0 +1,17 @@ +namespace Microsoft.UsEduCsu.Saas; + +// TODO: Convert from camelCase to ProperCase during serialization +public class AuthorizationRequest +{ + public string identity { get; set; } + public string role { get; set; } + + public string Identity + { + get => identity; + } + public string Role + { + get => role; + } +} \ No newline at end of file diff --git a/src/api/Data/ContainerDetail.cs b/src/api/Data/ContainerDetail.cs index 0698a29..9255eae 100644 --- a/src/api/Data/ContainerDetail.cs +++ b/src/api/Data/ContainerDetail.cs @@ -12,4 +12,5 @@ public class ContainerDetail public string StorageExplorerDirectLink { get; set; } public IDictionary Metadata { get; internal set; } public IList Access { get; set; } + public bool CanModifyRbac { get; set; } } diff --git a/src/api/Data/Subscription.cs b/src/api/Data/Subscription.cs new file mode 100644 index 0000000..9ed939c --- /dev/null +++ b/src/api/Data/Subscription.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.UsEduCsu.Saas.Data +{ + internal class Subscription + { + public string Id { get; set; } + public string Name { get; set; } + public string TenantId { get; set; } + } +} diff --git a/src/api/Endpoints/FileSystems.cs b/src/api/Endpoints/FileSystems.cs index 04727f0..da89a13 100644 --- a/src/api/Endpoints/FileSystems.cs +++ b/src/api/Endpoints/FileSystems.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System; +using System.Text.Json; using Azure.Identity; using Azure.Storage.Files.DataLake; using Microsoft.AspNetCore.Http; @@ -48,4 +50,129 @@ public static IActionResult GetContainer( ? new OkObjectResult(detail) : new NotFoundObjectResult("Container or storage account doesn't exist"); } + + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [FunctionName("AuthorizationDelete")] + public static IActionResult AuthorizationDelete( + [HttpTrigger(AuthorizationLevel.Anonymous, "DELETE", Route = "FileSystems/{account}/{container}/authorization/{rbacId}")] + HttpRequest req, ILogger log, string account, string container, string rbacId) + { + // Verify Parameter + if (Services.Extensions.AnyNullOrEmpty(account, container, rbacId)) + { + return new BadRequestResult(); + } + + if (req == null) + log.LogError("err"); + + // Validate Authorized Principal + ClaimsPrincipalResult cpr = new(UserOperations.GetClaimsPrincipal(req)); + + if (!cpr.IsValid) + { + log.LogWarning("No valid ClaimsPrincipal found in the request: '{0}'", cpr.Message); + return new UnauthorizedResult(); + } + + var principalId = UserOperations.GetUserPrincipalId(cpr.ClaimsPrincipal); + + // Get Role Operations Setup + var roleOps = new RoleOperations(log); + + // Verify user can manage RBAC + ResourceGraphOperations rgo = new(log, new DefaultAzureCredential()); + string accountResourceId = rgo.GetAccountResourceId(account); + var canModifyRbac = roleOps.CanModifyRbac(accountResourceId, container, principalId); + + if (!canModifyRbac) + return new UnauthorizedResult(); + + // Submit Role Authorization Request + var roleAssignment = roleOps.DeleteRoleAssignment(accountResourceId, container, rbacId); + if (roleAssignment is null) + return new NotFoundResult(); + + // Http No-Content 204 + return new NoContentResult(); + } + + + [ProducesResponseType(typeof(StorageRbacEntry), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [FunctionName("AuthorizationCreate")] + public static IActionResult AuthorizationCreate( + [HttpTrigger(AuthorizationLevel.Anonymous, "POST", Route = "FileSystems/{account}/{container}/authorization")] + HttpRequest req, ILogger log, string account, string container) + { + // Validate Parameters + if (Services.Extensions.AnyNullOrEmpty(account, container)) + { + return new BadRequestResult(); + } + + // Validate Authorized Principal + ClaimsPrincipalResult cpr = new(UserOperations.GetClaimsPrincipal(req)); + if (!cpr.IsValid) + { + log.LogWarning("No valid ClaimsPrincipal found in the request: '{0}'", cpr.Message); + return new UnauthorizedResult(); + } + var ownerPrincipalId = UserOperations.GetUserPrincipalId(cpr.ClaimsPrincipal); + + // Get Role Operations Setup + var roleOps = new RoleOperations(log); + + // Verify user can manage RBAC + ResourceGraphOperations rgo = new(log, new DefaultAzureCredential()); + string accountResourceId = rgo.GetAccountResourceId(account); + var canModifyRbac = roleOps.CanModifyRbac(accountResourceId, container, ownerPrincipalId); + + if (!canModifyRbac) + return new UnauthorizedResult(); + + // Request body is supposed to contain the user's identity claim + var rbac = (req.Body.Length > 0) + ? JsonSerializer.Deserialize(req.Body) + : new AuthorizationRequest(); + if (Services.Extensions.AnyNullOrEmpty(rbac.Identity, rbac.Role)) + return new BadRequestResult(); + + // Convert Identity into principal ID + var mgo = new MicrosoftGraphOperations(log, new DefaultAzureCredential()); + var targetPrincipalId = mgo.GetObjectId(rbac.Identity); + if (Services.Extensions.AnyNullOrEmpty(targetPrincipalId)) + return new BadRequestResult(); + + // Convert Shortened Rolls to Full Names + if (!rbac.Role.StartsWith("Storage Blob Data", StringComparison.OrdinalIgnoreCase)) + rbac.role = $"Storage Blob Data {rbac.Role}"; + + // Submit Role Authorization Request + var roleAssignment = roleOps.AssignRole(accountResourceId, container, rbac.Role, targetPrincipalId); + if (roleAssignment == null) + return new ConflictObjectResult("The role assignment already exists."); + + // Get Principal Name + var principalName = mgo.GetDisplayName(targetPrincipalId); + + // Convert to Storage Entry + var storageRbacEntry = new StorageRbacEntry() + { + PrincipalId = roleAssignment.PrincipalId, + PrincipalName = principalName, + RoleName = roleAssignment.RoleName.Replace("Storage Blob Data ", string.Empty), + // Preserve only the GUID part of the role assignment ID + RoleAssignmentId = roleAssignment.RoleAssignmentId[(roleAssignment.RoleAssignmentId.LastIndexOf('/') + 1)..], + IsInherited = false, + Order = 0 + }; + + return new OkObjectResult(storageRbacEntry); + } } diff --git a/src/api/Endpoints/StorageAccounts.cs b/src/api/Endpoints/StorageAccounts.cs index 8f1108b..a873482 100644 --- a/src/api/Endpoints/StorageAccounts.cs +++ b/src/api/Endpoints/StorageAccounts.cs @@ -16,7 +16,6 @@ using System.Linq; using System.Threading.Tasks; using System.Web; -using static Microsoft.UsEduCsu.Saas.FileSystems; namespace Microsoft.UsEduCsu.Saas; @@ -97,7 +96,7 @@ private static List PopulateContainerDetail(string account, str Parallel.ForEach(filesystems, (fs) => { var cd = GetContainerDetail(roleOperations, graphOps, account, accountResourceId, - fs.Name, fs.Properties); + fs.Name, fs.Properties, principalId); containerDetails.Add(cd); }); } @@ -114,7 +113,7 @@ private static List PopulateContainerDetail(string account, str } private static ContainerDetail GetContainerDetail(RoleOperations roleOps, MicrosoftGraphOperations graphOps, - string account, string accountResourceId, string container, FileSystemProperties properties) + string account, string accountResourceId, string container, FileSystemProperties properties, string principalId) { // TODO: Move to FileSystemOperations @@ -136,9 +135,15 @@ private static ContainerDetail GetContainerDetail(RoleOperations roleOps, Micros // Determine Access Roles // TODO: Optimization opportunity: Retrieve the role assignments for the account once, and then only the assignments at the container scope - var roles = roleOps.GetStorageDataPlaneRoleAssignments(accountResourceId, container); - var rbacEntries = roles - .Where(r => validTypes.Contains(r.PrincipalType)) // Only display User and Groups (no Service Principals) + var roleAssignments = roleOps.GetStorageDataPlaneRoleAssignments(accountResourceId, container) + // TODO: Should this be true every time we get assignments? + .Where(r => validTypes.Contains(r.PrincipalType)); // Only display User and Groups (no Service Principals); + + // Determine if the user can modify RBAC on this container + var CanModifyRbac = roleAssignments.Any(ra => roleOps.CanModifyRbac(ra, principalId)); + + // Cast RoleAssignment to StorageRbacEntry because that's what the client expects + var rbacEntries = roleAssignments .Select(r => new StorageRbacEntry() { RoleName = r.RoleName.Replace("Storage Blob Data ", string.Empty), @@ -148,8 +153,7 @@ private static ContainerDetail GetContainerDetail(RoleOperations roleOps, Micros IsInherited = r.IsInherited, RoleAssignmentId = r.RoleAssignmentId }) - .OrderBy(r => r.Order).ThenBy(r => r.PrincipalName - ).ToList(); + .OrderBy(r => r.Order).ThenBy(r => r.PrincipalName).ToList(); // Package in ContainerDetail var uri = Configuration.GetStorageUri(account, container).ToString(); @@ -159,7 +163,8 @@ private static ContainerDetail GetContainerDetail(RoleOperations roleOps, Micros Metadata = metadata, Access = rbacEntries, StorageExplorerDirectLink = $"storageexplorer://?v=2&tenantId={Configuration.TenantId}&type=fileSystem&container={container}&serviceEndpoint={HttpUtility.UrlEncode(uri)}", - Uri = uri + Uri = uri, + CanModifyRbac = CanModifyRbac }; return cd; diff --git a/src/api/Services/Cache.cs b/src/api/Services/Cache.cs index ebbf0ef..7ff64e2 100644 --- a/src/api/Services/Cache.cs +++ b/src/api/Services/Cache.cs @@ -9,6 +9,7 @@ using Microsoft.UsEduCsu.Saas.Data; using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Text.Json; @@ -150,6 +151,9 @@ private T Items(string itemName, string key, Func updateMethod, DateTimeOf // TODO: Handle RedisTimeoutException (retry) byte[] byteArray = _cache.Get(nameKey); +#if DEBUG + Debug.WriteLine("Debug skipping cache: {0} {1}", nameKey, byteArray?.Length); +#else // The cache will return value if found // TODO: Consider ignoring cached value if 2 bytes only (empty JSON object) if (byteArray != null) @@ -159,6 +163,7 @@ private T Items(string itemName, string key, Func updateMethod, DateTimeOf _logger.LogDebug($"{nameKey} (bytes: {byteArray.Length}) pulled from cache."); return obj; } +#endif // Get User by invoking Function T value = updateMethod.Invoke(); @@ -204,5 +209,5 @@ private void SetCacheValue(string nameKey, T value, DateTimeOffset? expiratio _logger.LogDebug($"{nameKey} (bytes: {data.Length}) written to cache."); } - #endregion +#endregion } diff --git a/src/api/Services/MicrosoftGraphOperations.cs b/src/api/Services/MicrosoftGraphOperations.cs index ccc910b..a3615cc 100644 --- a/src/api/Services/MicrosoftGraphOperations.cs +++ b/src/api/Services/MicrosoftGraphOperations.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using Azure.Core; +using Microsoft.Azure.Management.Authorization.Models; using Microsoft.Extensions.Logging; using Microsoft.Graph; using System; @@ -41,6 +42,49 @@ internal string GetDisplayName(string userIdentifier) return dirObj.AdditionalData["displayName"]?.ToString(); } + internal string GetObjectId(string principalName) + { + // If this looks like a UPN + if (principalName.Contains('@')) + { + try + { + // Retrieve a user by userPrincipalName + var myTask = graphClient + .Users[principalName] + .Request() + .GetAsync(graphClientCancellationToken); + + var directoryObject = myTask.Result; + return directoryObject?.Id; + } + catch (Exception ex) + { + log.LogInformation(ex, "User '{0}' not found.", principalName); + } + } + else + { + try + { + // Retrieve a group by name + var myTask = graphClient + .Groups[principalName] + .Request() + .GetAsync(graphClientCancellationToken); + + var directoryObject = myTask.Result; + return directoryObject?.Id; + } + catch (Exception ex) + { + log.LogInformation(ex, "Group '{0}' not found.", principalName); + } + } + + return null; + } + private DirectoryObject GetDirectoryObject(string principalId) { DirectoryObject GetDirectoryObjectMethod() diff --git a/src/api/Services/ResourceGraphOperations.cs b/src/api/Services/ResourceGraphOperations.cs index 6182571..885b284 100644 --- a/src/api/Services/ResourceGraphOperations.cs +++ b/src/api/Services/ResourceGraphOperations.cs @@ -8,8 +8,10 @@ using Microsoft.Rest; using Newtonsoft.Json.Linq; using System; +using System.Collections.Generic; using System.Text.RegularExpressions; using System.Threading; +using Microsoft.UsEduCsu.Saas.Data; namespace Microsoft.UsEduCsu.Saas.Services; @@ -97,6 +99,74 @@ public string GetAccountResourceTagValue(string storageAccountName, string tagNa return (string)tags?[tagName]; } + public List GetSubscriptions() + { + string queryText = $@"resourcecontainers + | where type == 'microsoft.resources/subscriptions' + | project subscriptionId, name, tenantId"; + QueryResponse queryResponse; + + try + { + var subs = new List(); + using (var resourceGraphClient = new ResourceGraphClient(_tokenCredentials)) + { + var query = new QueryRequest(queryText); + queryResponse = resourceGraphClient.Resources(query); + } + + if (queryResponse.Count == 0) + return null; + + // Transform results into Subscription objects + dynamic data = queryResponse; + foreach (var d in data.Data) + { + subs.Add(new Subscription() { Id = d.subscriptionId, Name = d.name, TenantId = d.tenantId }); + } + return subs; + } + catch (Exception ex) + { + log.LogError($"Exception in GetGraphStorageAccountQueryResponse: {ex} ", ex); + return null; + } + } + + public List GetStorageAccounts() + { + string queryText = $@"resources + | where type == 'microsoft.storage/storageaccounts' + | project name"; + QueryResponse queryResponse; + + try + { + var subs = new List(); + using (var resourceGraphClient = new ResourceGraphClient(_tokenCredentials)) + { + var query = new QueryRequest(queryText); + queryResponse = resourceGraphClient.Resources(query); + } + + if (queryResponse.Count == 0) + return null; + + // Transform results into Subscription objects + dynamic data = queryResponse; + foreach (var d in data.Data) + { + subs.Add(new StorageAccount() { StorageAccountName = d.name }); + } + return subs; + } + catch (Exception ex) + { + log.LogError($"Exception in GetGraphStorageAccountQueryResponse: {ex} ", ex); + return null; + } + } + /// /// Queries the Azure Resource Graph for the specified storage account /// diff --git a/src/api/Services/RoleOperations.cs b/src/api/Services/RoleOperations.cs index 791cac0..9a9125c 100644 --- a/src/api/Services/RoleOperations.cs +++ b/src/api/Services/RoleOperations.cs @@ -5,6 +5,7 @@ using Azure.Identity; using Microsoft.Azure.Management.Authorization; using Microsoft.Azure.Management.Authorization.Models; +using Microsoft.Azure.Management.ResourceGraph; using Microsoft.Extensions.Logging; using Microsoft.Rest.Azure.OData; using Microsoft.UsEduCsu.Saas.Data; @@ -21,12 +22,12 @@ internal sealed class RoleOperations : IDisposable { // https://blogs.aaddevsup.xyz/2020/05/using-azure-management-libraries-for-net-to-manage-azure-ad-users-groups-and-rbac-role-assignments/ - private readonly ILogger log; - private Rest.TokenCredentials _tokenCredentials; - private AccessToken _accessToken; - private AuthorizationManagementClient _amClient; - private readonly CacheHelper _cache; - private bool disposedValue; + private readonly ILogger log; + private Rest.TokenCredentials _tokenCredentials; + private AccessToken _accessToken; + private AuthorizationManagementClient _amClient; + private readonly CacheHelper _cache; + private bool disposedValue; private AuthorizationManagementClient AuthMgtClient { @@ -38,47 +39,90 @@ private AuthorizationManagementClient AuthMgtClient } // Caches the list of storage plane data role definitions - private static ConcurrentDictionary> roleDefinitions = - new(); + private static ConcurrentDictionary> roleDefinitions = new(); // Lock objects for thread-safety private readonly object tokenCredentialsLock = new(); - public RoleOperations(ILogger log) + public RoleOperations(ILogger log) + { + this.log = log; + _cache = CacheHelper.GetRedisCacheHelper(log); + } + + #region Internal Methods + + internal RoleAssignment AssignRole(string accountResourceId, string container, string role, string principalId) + { + try { - this.log = log; - _cache = CacheHelper.GetRedisCacheHelper(log); + // Create container resource ID + string containerScope = $"{accountResourceId}/blobServices/default/containers/{container}"; + + var roleAssignment = AddRoleAssignment(containerScope, role, principalId); + + return roleAssignment; } + catch (Exception ex) + { + log.LogError(ex, ex.Message); + return null; + } + } + + internal RoleAssignment DeleteRoleAssignment(string accountResourceId, string container, string rbacId) + { + try + { + // Get the container resource ID + var containerResourceId = $"{accountResourceId}/blobServices/default/containers/{container}"; - #region Public and Internal Methods + // Find the role assignment to be deleted + var roleAssignments = GetRoleAssignments(containerResourceId); + var authRoleAssignment = roleAssignments.FirstOrDefault(ra => ra.Id.EndsWith($"/{rbacId}", StringComparison.OrdinalIgnoreCase)); + if (authRoleAssignment is null) + return null; - //public Result AssignRoles(string account, string container, string ownerId) - //{ - // var result = new Result(); + // Try to delete + authRoleAssignment = AuthMgtClient.RoleAssignments.DeleteById(authRoleAssignment.Id); - // try - // { - // ResourceGraphOperations rgo = new(log, TokenCredentials); - // // Get Storage Account Resource ID - // var accountResourceId = rgo.GetAccountResourceId(account); + // Convert to Internal Role Assignment + var roleAssignment = new RoleAssignment(authRoleAssignment, null, false); - // // Create Role Assignments - // string containerScope = $"{accountResourceId}/blobServices/default/containers/{container}"; + return roleAssignment; + } + catch (Exception ex) + { + log.LogError(ex, ex.Message); + return null; + } + } - // // Allow user to manage ACL for container - // AddRoleAssignment(containerScope, "Storage Blob Data Owner", ownerId); + internal bool CanModifyRbac(string accountResourceId, string container, string principalId) + { + // Determine Access Roles + // TODO: This might not work if role assignment is via group membership + var roleAssignments = GetStorageDataPlaneRoleAssignments(accountResourceId, container); + + var canModifyRbac = roleAssignments + .Any(ra => CanModifyRbac(ra, principalId)); - // result.Success = true; - // } - // catch (Exception ex) - // { - // // TODO: Consider customizing error message - // log.LogError(ex, ex.Message); - // result.Message = ex.Message; - // } + return canModifyRbac; + } - // return result; - //} + /// + /// Determines whether the specified principal ID can modify RBAC assignments + /// based on the specified role assignment. + /// + /// The role assignment to verify against. + /// The principal to verify. + /// + /// This method is meant to be called from a lambda processing a enumerable of role assignments. + internal bool CanModifyRbac(RoleAssignment ra, string principalId) + { + return ra.RoleName.Equals("Storage Blob Data Owner", StringComparison.OrdinalIgnoreCase) + && ra.PrincipalId.Equals(principalId, StringComparison.OrdinalIgnoreCase); + } /// /// Retrieves a complete list of storage accounts and containers in those storage accounts @@ -102,108 +146,84 @@ internal IList GetAccessibleContainersForPrincipal( List results = new(); - // Create RGO object to get Azure tag information - var rgo = new ResourceGraphOperations(log, TokenCredentials); + // Create RGO object to get Azure tag information + var rgo = new ResourceGraphOperations(log, TokenCredentials); - // Get the existing storage account properties from the cache. If there is no cached entry, create a new one. - var storageAccountProperties = _cache.GetStorageAccountProperties(); + // Get the existing storage account properties from the cache. If there is no cached entry, create a new one. + var storageAccountProperties = _cache.GetStorageAccountProperties(); - // Process the role assignments into storage account and container names - foreach (var sdpr in roleAssignments) + // Process the role assignments into storage account and container names + foreach (var sdpr in roleAssignments) + { + // Determine if this is a storage account or container assignment + // (No support currently for higher-level assignments, it would require a list of storage accounts.) + Match m = re.Match(sdpr.Scope); + if (!m.Success) + // TODO: Issue #142: Enable processing all storage accounts in that scope + continue; // No Match, move to next one + + // There will always be a storage account name if there was a Regex match + string storageAccountName = m.Groups["accountName"].Value; + + // Find an existing entry for this storage account in the result set + StorageAccountAndContainers fsr = results + .SingleOrDefault(x => x.Account.StorageAccountName.Equals(storageAccountName, StringComparison.OrdinalIgnoreCase)); + + // If this is the first time we've encountered this storage account, Set the storage account name property and add to result set + if (fsr == null) { - // Determine if this is a storage account or container assignment - // (No support currently for higher-level assignments, it would require a list of storage accounts.) - Match m = re.Match(sdpr.Scope); - if (!m.Success) - continue; // No Match, move to next one - - // There will always be a storage account name if there was a Regex match - string storageAccountName = m.Groups["accountName"].Value; - - // Find an existing entry for this storage account in the result set - StorageAccountAndContainers fsr = results - .SingleOrDefault(x => x.Account.StorageAccountName.Equals(storageAccountName, StringComparison.OrdinalIgnoreCase)); + // Check for cached storage account properties for the storage account name + var val = storageAccountProperties?.Value.FirstOrDefault(x => x.StorageAccountName == storageAccountName); - // If this is the first time we've encountered this storage account, Set the storage account name property and add to result set - if (fsr == null) + // Check for the friendly name in the cache. If it doesn't exist, get it from Azure and add it to the cache. + var fname = val?.FriendlyName; + if (val is null) { - // Check for cached storage account properties for the storage account name - var val = storageAccountProperties?.Value.FirstOrDefault(x => x.StorageAccountName == storageAccountName); - - // Check for the friendly name in the cache. If it doesn't exist, get it from Azure and add it to the cache. - var fname = val?.FriendlyName; - if (val is null) + // Get the friendly name from Azure + // If there is no friendly name tag specified in settings configuration, this will end up displaying the storage account name instead using the property definition + fname = rgo.GetAccountResourceTagValue(storageAccountName, Configuration.StorageAccountFriendlyTagNameKey); + // Save back into cache + storageAccountProperties.Value.Add(new StorageAccount { - // Get the friendly name from Azure - // If there is no friendly name tag specified in settings configuration, this will end up displaying the storage account name instead using the property definition - fname = rgo.GetAccountResourceTagValue(storageAccountName, Configuration.StorageAccountFriendlyTagNameKey); - // Save back into cache - storageAccountProperties.Value.Add(new StorageAccount - { - StorageAccountName = storageAccountName, - FriendlyName = fname - }); - } - - fsr = new StorageAccountAndContainers(); - fsr.Account.StorageAccountName = storageAccountName; - fsr.Account.FriendlyName = fname; - results.Add(fsr); + StorageAccountName = storageAccountName, + FriendlyName = fname + }); } - // If there are potentially containers in this storage account that aren't listed yet - if (!fsr.AllContainers) + fsr = new StorageAccountAndContainers(); + fsr.Account.StorageAccountName = storageAccountName; + fsr.Account.FriendlyName = fname; + results.Add(fsr); + } + + // If there are potentially containers in this storage account that aren't listed yet + if (!fsr.AllContainers) + { + var containerGroup = m.Groups["containerName"]; + // If the container Regex group was successfully parsed + // but the container hasn't been added to the list yet + if (containerGroup.Success && + !fsr.Containers.Contains(containerGroup.Value)) { - var containerGroup = m.Groups["containerName"]; - // If the container Regex group was successfully parsed - // but the container hasn't been added to the list yet - if (containerGroup.Success && - !fsr.Containers.Contains(containerGroup.Value)) - { - fsr.Containers.Add(containerGroup.Value); // Assume access is only to this container - } - // If this is not a container-level assignment - else if (!containerGroup.Success) - { - // The role assignment applies to the entire storage account (at least) - var adls = new FileSystemOperations(log, appCred, fsr.Account.StorageAccountName); - var containers = adls.GetContainers(); // Access is to entire storage account; retrieve all containers - fsr.Containers = containers.Select(fs => fs.Name).ToList(); // Replace any previously included containers - fsr.AllContainers = true; // There can't be any more containers in this storage account - } + fsr.Containers.Add(containerGroup.Value); // Assume access is only to this container + } + // If this is not a container-level assignment + else if (!containerGroup.Success) + { + // The role assignment applies to the entire storage account (at least) + var adls = new FileSystemOperations(log, appCred, fsr.Account.StorageAccountName); + var containers = adls.GetContainers(); // Access is to entire storage account; retrieve all containers + fsr.Containers = containers.Select(fs => fs.Name).ToList(); // Replace any previously included containers + fsr.AllContainers = true; // There can't be any more containers in this storage account } } - - // Update the account properties cache - if (storageAccountProperties is not null) - _cache.SetStorageAccountProperties(storageAccountProperties); - - return results; } - private IList GetAllStorageDataPlaneRoleAssignments(string principalId) - { - var subscriptions = Configuration.GetSubscriptions(); - // Use a thread-safe unordered collection - var roleAssignments = new ConcurrentBag(); - - Parallel.ForEach(subscriptions, subscription => - { - // GetStorageDataPlaneRoles will not return null - var scope = $"/subscriptions/{subscription}/"; - var assignments = GetStorageDataPlaneRolesByScope(scope, principalId); // TODO: Getting them out in order of storage account to make processing more efficient? - log.LogTrace("RoleOperations.GetStoragePlaneDataRoles({0}, {1}) returned {2} role assignments.", - subscription, principalId, assignments.Count); - - foreach (var ra in assignments) - { - roleAssignments.Add(ra); - } - }); + // Update the account properties cache + if (storageAccountProperties is not null) + _cache.SetStorageAccountProperties(storageAccountProperties); - // ToList as an extension method is not known to be thread-safe - // ToArray is a method defined in the ConcurrentBag class and is thread-safe - return roleAssignments.ToArray().ToList(); + return results; } /// @@ -213,7 +233,7 @@ private IList GetAllStorageDataPlaneRoleAssignments(string princ /// The Azure resource ID of the storage account. /// A container name in the storage account. /// The list of role assignments. - public IList GetStorageDataPlaneRoleAssignments(string accountResourceId, string container) + internal IList GetStorageDataPlaneRoleAssignments(string accountResourceId, string container) { // /subscriptions/[subscription id]/resourceGroups/[resource group name]/providers/Microsoft.Storage/storageAccounts/[storage account]/blobServices/default/containers/[container name] var scope = (container is null) @@ -223,39 +243,6 @@ public IList GetStorageDataPlaneRoleAssignments(string accountRe return GetStorageDataPlaneRolesByScope(scope); } - /// - /// Return a list of containers where the specified AAD principal has data plane access. - /// - /// The storage account for which to retrieve container access. - /// The AAD principal for which to retrieve role assignments. - /// An List. - //public IList GetContainerRoleAssignments(string account, string principalId) - //{ - // ResourceGraphOperations rgo = new(log, TokenCredentials); - // var accountResourceId = rgo.GetAccountResourceId(account); - - // IList ScopedRoleDefinitions = GetRoleDefinitions(accountResourceId); - - // // Retrieve the applicable role assignments scoped to containers for the specified AAD principal - // var roleDefinitionIds = ScopedRoleDefinitions.Select(rd => rd.Id); // Create an IList of the role definition IDs - - // // Project Role Assignments into Container Roles - // var roleAssignments = GetRoleAssignments(account, principalId) - // ?.Where(ra => ra.Scope.Contains("/blobServices/default/containers/") - // && roleDefinitionIds.Contains(ra.RoleDefinitionId)) - // // Transform matching role assignments into the method's return value - // .Select(ra => new ContainerRoleAssignment() - // { - // RoleName = ScopedRoleDefinitions.Single(rd => rd.Id.Equals(ra.RoleDefinitionId, StringComparison.Ordinal)).RoleName, - // Container = ra.Scope.Split('/').Last(), - // PrincipalId = ra.PrincipalId, - // Id = ra.Id - // }) - // .ToList(); - - // return roleAssignments; - //} - #endregion #region Private Methods @@ -320,32 +307,44 @@ private IList GetRoleDefinitions(string resourceId) return new List(); } - //private void AddRoleAssignment(string scope, string roleName, string principalId) - //{ - // var ScopedRoleDefinitions = GetRoleDefinitions(scope); - - // // Get the specific role definition by name - // var roleDefinition = ScopedRoleDefinitions - // .FirstOrDefault(x => x.RoleName == roleName); - - // if (roleDefinition is not null) - // { - // // Get Current Role Assignments - // var roleAssignments = GetRoleAssignments(scope, principalId); - - // // Filter down to the specific role definition - // var roleAssignment = roleAssignments?.FirstOrDefault(ra => ra.PrincipalId == principalId - // && ra.RoleDefinitionId == roleDefinition.Id); - - // // Create New Role Assignment - // if (roleAssignment is null) - // { - // var racp = new RoleAssignmentCreateParameters(roleDefinition.Id, principalId); - // var roleAssignmentId = Guid.NewGuid().ToString(); - // roleAssignment = AuthMgtClient.RoleAssignments.Create(scope, roleAssignmentId, racp); - // } - // } - //} + private RoleAssignment AddRoleAssignment(string scope, string roleName, string principalId) + { + var ScopedRoleDefinitions = GetRoleDefinitions(scope); + + // Get the specific role definition by name + var roleDefinition = ScopedRoleDefinitions + .FirstOrDefault(x => x.RoleName == roleName); + + if (roleDefinition is not null) + { + // Get Current Role Assignments + var roleAssignments = GetRoleAssignments(scope, principalId); + + // Filter down to the specific role definition + var authRoleAssignment = roleAssignments?.FirstOrDefault(ra => ra.PrincipalId == principalId + && ra.RoleDefinitionId == roleDefinition.Id); + + // Create New Role Assignment + if (authRoleAssignment is null) + { + var racp = new RoleAssignmentCreateParameters(roleDefinition.Id, principalId); + var roleAssignmentId = Guid.NewGuid().ToString(); + authRoleAssignment = AuthMgtClient.RoleAssignments.Create(scope, roleAssignmentId, racp); + + // Convert to Internal Role Assignment + var roleAssignment = new RoleAssignment(authRoleAssignment, roleDefinition, false); + + return roleAssignment; + } + else + { + // This role assignment already exists (but could be inherited) + // Should create a return status to indicate that + } + } + + return null; + } /// /// Retrieves storage data plane role assignments for the specified scope and optional principal. @@ -353,7 +352,7 @@ private IList GetRoleDefinitions(string resourceId) /// The Azure resource ID for which to retrieve role assignments. /// (optional) The AAD object ID of the principal to retrieve assignments for. /// The list of storage data plane role assignments. - internal IList GetStorageDataPlaneRolesByScope(string scope, string principalId = null) + private IList GetStorageDataPlaneRolesByScope(string scope, string principalId = null) { var ScopedRoleDefinitions = GetRoleDefinitions(scope); @@ -364,17 +363,8 @@ internal IList GetStorageDataPlaneRolesByScope(string scope, str { // Join Role Assignments and Role Definitions var storageDataPlaneRoles = assignments.Join(ScopedRoleDefinitions, ra => ra.RoleDefinitionId, rd => rd.Id, - (ra, rd) => new RoleAssignment() - { - RoleName = rd.RoleName, - Scope = ra.Scope, - PrincipalType = ra.PrincipalType, - PrincipalId = ra.PrincipalId, - IsInherited = !ra.Scope.Equals(scope, StringComparison.OrdinalIgnoreCase), - // Return only the GUID part of the assignment ID (not the full resource ID, - // because it would leak subscription IDs, etc. to the client) - RoleAssignmentId = ra.Id[(ra.Id.LastIndexOf('/') + 1)..] - }).ToList(); + (ra, rd) => new RoleAssignment(ra, rd, !ra.Scope.Equals(scope, StringComparison.OrdinalIgnoreCase))) + .ToList(); return storageDataPlaneRoles; } @@ -384,9 +374,6 @@ internal IList GetStorageDataPlaneRolesByScope(string scope, str private IList GetRoleAssignments(string scope, string principalId = null) { - // Make sure role definitions for the subscription have been retrieved - //_ = GetRoleDefinitions(scope); - /* Query the scope's role assignments. * This will only return role assignments where the provided token has Microsoft.Authorization/roleAssignments/read authorization. * For example, by granting the app registration User Access Administrator on storage accounts @@ -418,12 +405,36 @@ internal IList GetStorageDataPlaneRolesByScope(string scope, str } } - #endregion + private IList GetAllStorageDataPlaneRoleAssignments(string principalId) + { + //var subscriptions = Configuration.GetSubscriptions(); + + var rgo = new ResourceGraphOperations(log, new DefaultAzureCredential()); + var subscriptions = rgo.GetSubscriptions().Select(sub => sub.Id); + + // Use a thread-safe unordered collection + var roleAssignments = new ConcurrentBag(); + + Parallel.ForEach(subscriptions, subscription => + { + // GetStorageDataPlaneRoles will not return null + var scope = $"/subscriptions/{subscription}/"; + var assignments = GetStorageDataPlaneRolesByScope(scope, principalId); // TODO: Getting them out in order of storage account to make processing more efficient? + log.LogTrace("RoleOperations.GetStoragePlaneDataRoles({0}, {1}) returned {2} role assignments.", + subscription, principalId, assignments.Count); - //public class ContainerRoleAssignment : RoleAssignment - //{ - // public string Container { get; set; } - //} + foreach (var ra in assignments) + { + roleAssignments.Add(ra); + } + }); + + // ToList as an extension method is not known to be thread-safe + // ToArray is a method defined in the ConcurrentBag class and is thread-safe + return roleAssignments.ToArray().ToList(); + } + + #endregion internal class RoleAssignment { @@ -431,9 +442,25 @@ internal class RoleAssignment public string Scope { get; set; } public string PrincipalId { get; set; } public string PrincipalType { get; set; } - public string Id { get; set; } public bool IsInherited { get; set; } public string RoleAssignmentId { get; set; } + + /// + /// Creates a new Microsoft.UsEduCsu.Saas.RoleOperations.RoleAssignment object + /// from the specified Azure.Management.Authorization.Models.RoleAssignment object. + /// + /// + internal RoleAssignment(Azure.Management.Authorization.Models.RoleAssignment authRoleAssignment, + Azure.Management.Authorization.Models.RoleDefinition roleDefinition, + bool isInherited) + { + RoleAssignmentId = authRoleAssignment.Id[(authRoleAssignment.Id.LastIndexOf('/') + 1)..]; + RoleName = roleDefinition?.RoleName; + PrincipalId = authRoleAssignment.PrincipalId; + PrincipalType = authRoleAssignment.PrincipalType; + Scope = authRoleAssignment.Scope; + IsInherited = isInherited; + } } #region Disposable Pattern diff --git a/src/front/src/components/EditDetails/EditDetails.css b/src/front/src/components/EditDetails/EditDetails.css new file mode 100644 index 0000000..1a6c098 --- /dev/null +++ b/src/front/src/components/EditDetails/EditDetails.css @@ -0,0 +1,18 @@ +.directoryDetails .title { + font-size: larger; + font-weight: 600; +} + +.directoryDetails .detail { + margin-top: 5px; + margin-bottom: 5px; +} + +.directoryDetails .label { + display: inline-block; + width: 100px; +} + +.directoryDetails .detail .label { + font-size: 0.9em; +} diff --git a/src/front/src/components/EditDetails/EditDetails.js b/src/front/src/components/EditDetails/EditDetails.js new file mode 100644 index 0000000..13ee1bc --- /dev/null +++ b/src/front/src/components/EditDetails/EditDetails.js @@ -0,0 +1,226 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import * as React from 'react'; +import PropTypes from 'prop-types' +import Grid from '@mui/material/Grid' +import './EditDetails.css' +import { useAutocomplete } from '@mui/base/AutocompleteUnstyled'; +import { createRoleAssignment, deleteRoleAssignment } from '../../services/StorageManager.service' +import CloseIcon from '@mui/icons-material/Close'; +import { red } from '@mui/material/colors'; +import { styled, useTheme } from '@mui/material/styles'; +import TextField from '@mui/material/TextField'; +import MenuItem from '@mui/material/MenuItem'; +import Select from '@mui/material/Select'; +import Chip from '@mui/material/Chip'; +import AssignmentIndIcon from '@mui/icons-material/AssignmentInd'; +import Button from '@mui/material/Button'; + +const ITEM_HEIGHT = 48; +const ITEM_PADDING_TOP = 8; +const MenuProps = { + PaperProps: { + style: { + maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP, + width: 250, + }, + }, +}; + +const roles = ["Owner", "Contributor", "Reader"] + +function getStyles(role, roleName, theme) { + return { + fontWeight: + roleName.indexOf(role) === -1 + ? theme.typography.fontWeightRegular + : theme.typography.fontWeightMedium, + }; +} + +const Root = styled('div')( + ({ theme }) => ` + color: ${theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.65)' : 'rgba(0,0,0,.85)' + }; + font-size: 14px; +`, +); + +const InputWrapper = styled('div')( + ({ theme }) => ` + border: 1px solid ${theme.palette.mode === 'dark' ? '#434343' : '#d9d9d9'}; + background-color: ${theme.palette.mode === 'dark' ? '#141414' : '#fff'}; + border-radius: 4px; + padding: 1px; + display: flex; + flex-wrap: wrap; + + &:hover { + border-color: ${theme.palette.mode === 'dark' ? '#177ddc' : '#40a9ff'}; + } + + &.focused { + border-color: ${theme.palette.mode === 'dark' ? '#177ddc' : '#40a9ff'}; + box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2); + } + + & input { + background-color: ${theme.palette.mode === 'dark' ? '#141414' : '#fff'}; + color: ${theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.65)' : 'rgba(0,0,0,.85)' + }; + height: 30px; + box-sizing: border-box; + padding: 4px 6px; + width: 0; + min-width: 30px; + flex-grow: 1; + border: 0; + margin: 0; + outline: 0; + } +`, +); + +function Tag(props) { + const { label, onDelete, ...other } = props; + return ( +
+ {label} + +
+ ); +} + +Tag.propTypes = { + principalName: PropTypes.string.isRequired, + roleName: PropTypes.func.isRequired, +}; + + +const EditDetails = ({ data, storageAccount, strings }) => { + const [roleName, setRoleName] = React.useState('') + const [principalName, setPrincipalName] = React.useState('') + const [newData] = React.useState(data) + const [assignmentData, setAssignmentData] = React.useState(data.access.filter(element => !element.isInherited)) + const [selectedStorageAccount] = React.useState(storageAccount) + const theme = useTheme() + + const { + getRootProps, + getInputProps, + value, + focused, + setAnchorEl, + } = useAutocomplete({ + id: 'User-Accesses', + value: [...assignmentData], + multiple: true, + options: [], + getOptionLabel: (option) => option + }); + + function handleRoleChange(event) { + const { + target: { value }, + } = event; + setRoleName(value); + } + + function handlePrincipalChange(event) { + setPrincipalName(event.target.value) + } + + function onCustomDelete(event) { + const deleteStuff = async() => { + const index = event.index + const deleteAssignment = assignmentData[index] + await deleteRoleAssignment(selectedStorageAccount, newData.name, deleteAssignment.roleAssignmentId) + console.log("I just deleted " + deleteAssignment.roleAssignmentId) + setAssignmentData(assignmentData.filter(item => assignmentData.indexOf(item) !== index)) + newData.access = newData.access.filter( item => item.roleAssignmentId !== deleteAssignment.roleAssignmentId) + } + deleteStuff(); + } + + function handleAdd() { + const addStuff = async() => { + let roleAssignmentResponse = await createRoleAssignment(selectedStorageAccount, newData.name, + { "roleName": roleName, "principalName": principalName }) + if (roleAssignmentResponse.isSuccess) { + setAssignmentData(assignmentData.concat(roleAssignmentResponse.roleAssignment)) + newData.access = newData.access.concat(roleAssignmentResponse.roleAssignment) + setPrincipalName('') + setRoleName('') + } + } + // TODO: Set Message in dialog + addStuff(); + } + + let chipCount = 0; + + return ( + + +
{()} {strings.editorTitle}
+
+ + + + + + + +
+ + {value.map((option, index) => ( + + {option.roleName + " : " + option.principalName} + onCustomDelete({index})} /> +
} + /> + ))} + + + +
+
+
+ ) +} + +EditDetails.propTypes = { + data: PropTypes.object, + storageAccount: PropTypes.string, + strings: PropTypes.shape({ + roleName: PropTypes.string, + principalName: PropTypes.string + }) +} + +EditDetails.defaultProps = { + data: {}, + storageAccount: 'Storage Account', + strings: { + roleName: 'Contributor', + principalName: 'John Snow', + editorTitle: 'Role Assignment' + } +} + +export default EditDetails \ No newline at end of file diff --git a/src/front/src/components/EditDetails/index.js b/src/front/src/components/EditDetails/index.js new file mode 100644 index 0000000..5b55ca0 --- /dev/null +++ b/src/front/src/components/EditDetails/index.js @@ -0,0 +1,5 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import EditDetails from './EditDetails' +export default EditDetails diff --git a/src/front/src/components/FileSystemsPage/FileSystemsPage.js b/src/front/src/components/FileSystemsPage/FileSystemsPage.js index c6ff99f..bdfb072 100644 --- a/src/front/src/components/FileSystemsPage/FileSystemsPage.js +++ b/src/front/src/components/FileSystemsPage/FileSystemsPage.js @@ -28,6 +28,7 @@ import DialogTitle from '@mui/material/DialogTitle' import DirectoryDetails from '../DirectoryDetails' import ConnectDetails from '../ConnectDetails/ConnnectDetails' import PageLoader from '../PageLoader/PageLoader' +import EditDetails from '../EditDetails' /** @@ -39,6 +40,7 @@ const FileSystemsPage = ({ strings }) => { const { isAuthenticated } = useAuthentication() // Setup state hooks + const [edit, setEdit] = useState({ show: false, data: {}, storageAccount: '' }) const [details, setDetails] = useState({ show: false, data: {} }) const [selectedStorageAccount, setSelectedStorageAccount] = useState('') const [storageAccounts, setStorageAccounts] = useState([]) @@ -124,8 +126,12 @@ const FileSystemsPage = ({ strings }) => { setSelectedStorageAccount(id) }, []) - const onEdit = async () => { - // test + const onEdit = (rowData, selectedStorageAccount) => { + setEdit({ show: true, data: rowData, storageAccount: selectedStorageAccount }) + } + + const handleCancelEdit = () => { + setEdit({ show: false, data: {} }) } const onDetails = (rowData) => { @@ -209,7 +215,7 @@ const FileSystemsPage = ({ strings }) => { {row.metadata.FundCode} - {onEdit && onEdit(row)} className='action' />} + {onEdit && row.canModifyRbac && onEdit(row, selectedStorageAccount)} className='action' />} {onDetails && onDetails(row)} className='action' /> @@ -231,6 +237,19 @@ const FileSystemsPage = ({ strings }) => { + {edit.show && + + + {strings.editDetailsTitle} + + + + + + + + + } {details.show && diff --git a/src/front/src/components/LandingPage/LandingPage.js b/src/front/src/components/LandingPage/LandingPage.js index f1c841f..72fe880 100644 --- a/src/front/src/components/LandingPage/LandingPage.js +++ b/src/front/src/components/LandingPage/LandingPage.js @@ -1,75 +1,67 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import React, { useEffect, useState } from 'react' -import PropTypes from 'prop-types' -import Link from "@mui/material/Link" -import LogInButton from "../LogInButton" -import './LandingPage.css' -import { getServerStatus } from '../../services/StorageManager.service' - -const LandingPage = ({ strings }) => { - - const [serverStatus, setServerStatus] = useState(''); - - useEffect(() => { - getServerStatus() - .then(u => { - setServerStatus(u.message); - }) - - return function cleanup() { - //mounted = false - } - }); - - return ( -
-
-
-
- To have access to the storage as a service platform, you need to use your corporative credentials. - If you have it handy, please click on the "Log in" button below. If you don't, please click on - the "How to gain access" link below. -
-
- -
-
- How to gain access -
-
-
-
-
- What you can do here? -
-
    -
  1. Ask for a new space to store your data files in a highly scalable way in the cloud.
  2. -
  3. Manage who has access to the space you are creating.
  4. -
  5. Have a view about capacity of your space, cost and more.
  6. -
  7. Move data between different layers as the data changes in terms of priority.
  8. -
  9. Decommission the storage when it is no longer needed.
  10. -
-
-
-
- {serverStatus &&

Server Status: {serverStatus}
} -
-
- ); -} - -LandingPage.propTypes = { - strings: PropTypes.shape({ - logIn: PropTypes.string - }) -} - -LandingPage.defaultProps = { - strings: { - logIn: 'Log In' - } -} - -export default LandingPage +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import React, { useEffect, useState } from 'react' +import PropTypes from 'prop-types' +import LogInButton from "../LogInButton" +import './LandingPage.css' +import { getServerStatus } from '../../services/StorageManager.service' + +const LandingPage = ({ strings }) => { + + const [serverStatus, setServerStatus] = useState(''); + + useEffect(() => { + getServerStatus() + .then(u => { + setServerStatus(u.message); + }) + + return function cleanup() { + //mounted = false + } + }); + + return ( +
+
+
+
+ To have access to the storage as a service platform, you need to use your work or school account. +
+
+ +
+
+
+
+
+ What you can do here? +
+
    +
  1. Find which Azure Storage blob containers you have access to use.
  2. +
  3. Manage who has access to the space you are creating.
  4. +
  5. Easily connect to the storage accounts and containers.
  6. +
+
+
+
+ {serverStatus &&

Server Status: {serverStatus}
} +
+
+ ); +} + +LandingPage.propTypes = { + strings: PropTypes.shape({ + logIn: PropTypes.string + }) +} + +LandingPage.defaultProps = { + strings: { + logIn: 'Log In' + } +} + +export default LandingPage diff --git a/src/front/src/config/urls.js b/src/front/src/config/urls.js index 64fe096..996e6e6 100644 --- a/src/front/src/config/urls.js +++ b/src/front/src/config/urls.js @@ -22,6 +22,14 @@ const URLS = { createFolder: { method: 'POST', endpoint: '/api/TopLevelFolders/{account}/{filesystem}' + }, + createRoleAssignment: { + method: 'POST', + endpoint: '/api/FileSystems/{storageaccount}/{container}/authorization' + }, + deleteRoleAssignment: { + method: 'DELETE', + endpoint: '/api/FileSystems/{storageaccount}/{container}/authorization/{guid}' } } diff --git a/src/front/src/services/StorageManager.service.js b/src/front/src/services/StorageManager.service.js index e45c532..bcb1e63 100644 --- a/src/front/src/services/StorageManager.service.js +++ b/src/front/src/services/StorageManager.service.js @@ -25,7 +25,6 @@ import HttpException from './HttpException' }) } - /** * Returns the list of storage accounts and their file systems (containers) */ @@ -33,7 +32,7 @@ import HttpException from './HttpException' const { endpoint, method } = URLS.storageAccounts const options = getOptions(method) - return fetch(endpoint, options) + return await fetch(endpoint, options) .then(response => { if (response.status === 200) { var json = response.json(); @@ -49,7 +48,6 @@ import HttpException from './HttpException' }) } - /** * Returns the list of file systems (containers) for a storage account */ @@ -57,7 +55,7 @@ export const getFileSystems = async (storageAccount) => { const endpoint = URLS.fileSystems.endpoint.replace('{account}', storageAccount) const options = getOptions(URLS.fileSystems.method) - return fetch(endpoint, options) + return await fetch(endpoint, options) .then(response => { if (response.status === 200) { return response.json() @@ -72,29 +70,6 @@ export const getFileSystems = async (storageAccount) => { }) } -/** - * Returns the list of directories - */ -export const getDirectories = async (storageAccount, fileSystem) => { - const endpoint = URLS.listDirectories.endpoint.replace('{account}', storageAccount).replace('{filesystem}', fileSystem) - const options = getOptions(URLS.listDirectories.method) - - return fetch(endpoint, options) - .then(response => { - if (response.status === 200) { - return response.json() - } else { - throw new HttpException(response.status, response.statusText) - } - }) - .catch(error => { - console.log(`Call to API (${endpoint}) failed with the following details:`) - console.log(error) - throw error - }) -} - - /** * Create the options object to pass to the API call */ @@ -106,39 +81,69 @@ const getOptions = (method) => { return options } +/** + * Delete a user role from the storage account container + */ +export const deleteRoleAssignment = async (storageaccount, container, guid) => { + const endpoint = URLS.deleteRoleAssignment.endpoint.replace('{storageaccount}', storageaccount).replace('{container}', container).replace('{guid}', guid) + const options = getOptions(URLS.deleteRoleAssignment.method) + + try { + var response = await fetch(endpoint, options); + let deleteResponse = { + IsSucces: false, + Message: "" + }; + + // If the result code is success (should always be HTTP 201) + if (response.ok) { + deleteResponse.IsSuccess = true + deleteResponse.Message = "Deleted." + } else { + deleteResponse.Message = "Failed to delete." + } + + return deleteResponse + } + catch (error) { + console.error(error); + } +} /** - * Create a new folder in the storage account container + * Create a role assigment for the storage account container */ -export const createFolder = async (storageAccount, fileSystem, owner, content) => { - const endpoint = URLS.createFolder.endpoint.replace('{account}', storageAccount).replace('{filesystem}', fileSystem) - const options = getOptions(URLS.createFolder.method) - let userAccessList = content.userAccess ? content.userAccess.replace(" ", "").replace(";", ",").split(",") : '' +export const createRoleAssignment = async (storageaccount, container, userObject) => { + const endpoint = URLS.createRoleAssignment.endpoint.replace('{storageaccount}', storageaccount).replace('{container}', container) + const options = getOptions(URLS.createRoleAssignment.method) options.body = JSON.stringify({ - Folder: content.name, - FundCode: content.fundCode, - FolderOwner: owner, - UserAccessList: userAccessList + identity: userObject.principalName, + role: userObject.roleName }) try { var response = await fetch(endpoint, options); - let folderResponse = { - Folder: "", - Message: "" + let roleAssignmentResponse = { + roleAssignment: {}, + Message: "", + isSuccess: false }; - let body = await response.json(); - // If the result code is success (should always be HTTP 201) - if (response.status >= 200 && response.status <= 299) - folderResponse.Folder = body.folderDetail - - // Regardless of success, there can always be a message - folderResponse.Message = body.message ? body.message : body.Message - - return folderResponse + if (response.ok) + { + let body = await response.json(); + roleAssignmentResponse.roleAssignment = body + roleAssignmentResponse.isSuccess = true + roleAssignmentResponse.Message = "Role assignment successful" + } + else + { + roleAssignmentResponse.Message = response.statusText; + } + + return roleAssignmentResponse } catch (error) { console.error(error);