Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Task: use kibali for permissions #1864

Merged
merged 27 commits into from
Jan 9, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
4ca803b
add kibali as a dependency
thewahome Nov 29, 2023
ad7163e
use Kibali to import and partially search for a resource
thewahome Nov 29, 2023
12381d5
change document streaming source
thewahome Nov 30, 2023
3d48929
Remove template matcher algorithms
thewahome Nov 30, 2023
f40a65a
confirm that the TryGetValue succeeded to avoid null reference errors
thewahome Dec 4, 2023
dbb4471
filter early for least privileged permissions
thewahome Dec 5, 2023
5876ef5
extract processes into individual functions
thewahome Dec 5, 2023
8834af3
use linq queries instead of foreach statements
thewahome Dec 5, 2023
4d106b3
stop overwriting the concurrent dictionary
thewahome Dec 5, 2023
dbeb725
use Kibali's permission document inside the all permissions function
thewahome Dec 5, 2023
73ef82b
make functions static
thewahome Dec 5, 2023
b712ac4
remove unused usings
thewahome Dec 5, 2023
8c74df6
move declaration closer to implementation
thewahome Dec 5, 2023
bd7db54
fetch kibali compatible permissions; Allow forward slash in blob name
thewahome Dec 7, 2023
245ad4c
add updated test document
thewahome Dec 7, 2023
39830c2
ensure code is inline with tests [attempt]
thewahome Dec 7, 2023
3d75059
get least privileged permissions for source
thewahome Dec 7, 2023
85ce52e
clean url before finding the resource
thewahome Dec 7, 2023
0214ec5
return early when no scopes are found
thewahome Dec 7, 2023
7f02b44
remove raw permissions URL
thewahome Dec 7, 2023
87c1bea
Refactor FetchLeastPrivilege method to be static
thewahome Dec 7, 2023
5971b7b
Make GetPermissionsFromResource method static
thewahome Dec 7, 2023
04a8bd4
change comparer function
thewahome Jan 8, 2024
42cddb6
use parallel foreach
thewahome Jan 8, 2024
b5aa42b
delete dead commented code
thewahome Jan 8, 2024
bac6575
Merge branch 'task/use-kibali-for-permissions' of https://github.com/…
thewahome Jan 8, 2024
1a0e798
make explicit check for error message
thewahome Jan 8, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions PermissionsService/PermissionsService.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.0" />
<PackageReference Include="Microsoft.Graph.Kibali" Version="0.15.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>

Expand Down
187 changes: 76 additions & 111 deletions PermissionsService/Services/PermissionsStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using FileService.Common;
using FileService.Interfaces;
using Kibali;
using Microsoft.ApplicationInsights;
using Microsoft.ApplicationInsights.DataContracts;
using Microsoft.Extensions.Caching.Memory;
Expand All @@ -35,7 +37,7 @@ public class PermissionsStore : IPermissionsStore
private readonly Dictionary<string, string> _permissionsTracePropertiesWithSanitizeIgnore =
new() { { UtilityConstants.TelemetryPropertyKey_Permissions, nameof(PermissionsStore) },
{ UtilityConstants.TelemetryPropertyKey_SanitizeIgnore, nameof(PermissionsStore) } };
private readonly Dictionary<ScopeType, string> permissionDescriptionGroups =
private readonly Dictionary<ScopeType, string> permissionDescriptionGroups =
new() { { ScopeType.DelegatedWork, Delegated },
{ ScopeType.DelegatedPersonal, Delegated },
{ ScopeType.Application, Application } };
Expand Down Expand Up @@ -63,7 +65,7 @@ public UriTemplateMatcher UriTemplateMatcher
public Dictionary<int, Dictionary<string, Dictionary<ScopeType, SchemePermissions>>> PathPermissions
{
get; set;
} = new ();
} = new();
}

public PermissionsStore(IConfiguration configuration, IHttpClientUtility httpClientUtility,
Expand Down Expand Up @@ -95,6 +97,35 @@ public PermissionsStore(IConfiguration configuration, IHttpClientUtility httpCli
return permissionsData;
});

private Task<PermissionsDocument> LoadDocument =>
_cache.GetOrCreateAsync("Permissionsdocument", async entry =>
{
_telemetryClient?.TrackTrace($"Fetching permissions from file source '{_permissionsBlobName}'",
SeverityLevel.Information,
_permissionsTracePropertiesWithSanitizeIgnore);
PermissionsDocument permissionsDocument;

try
{
var permissions = @"https://raw.githubusercontent.com/microsoftgraph/microsoft-graph-devx-content/dev/permissions/new/permissions.json";
thewahome marked this conversation as resolved.
Show resolved Hide resolved
using HttpClient client = new HttpClient();
thewahome marked this conversation as resolved.
Show resolved Hide resolved
var response = await client.GetAsync(permissions);
response.EnsureSuccessStatusCode();
var fileStream = await response.Content.ReadAsStreamAsync();
permissionsDocument = PermissionsDocument.Load(fileStream);

entry.AbsoluteExpirationRelativeToNow = permissionsDocument is not null ? TimeSpan.FromHours(_defaultRefreshTimeInHours) : TimeSpan.FromMilliseconds(1);
}
catch (Exception exception)
{

_telemetryClient?.TrackException(exception);
permissionsDocument = null;
}
return permissionsDocument;

});

/// <summary>
/// Populates the template table with the request urls and the scopes table with the permission scopes.
/// </summary>
Expand All @@ -110,14 +141,15 @@ private async Task<PermissionsDataInfo> LoadPermissionsDataAsync()

// Get file contents from source
string relativePermissionPath = FileServiceHelper.GetLocalizedFilePathSource(_permissionsContainerName, _permissionsBlobName);

string permissionsJson = await _fileUtility.ReadFromFile(relativePermissionPath);
var fetchedPermissions = JsonConvert.DeserializeObject<Dictionary<string, Dictionary<string, Dictionary<ScopeType, SchemePermissions>>>>(permissionsJson);

_telemetryClient?.TrackTrace("Finished fetching permissions from file",
SeverityLevel.Information,
_permissionsTraceProperties);


int count = 0;
var urlTemplateMatcher = new UriTemplateMatcher();
var pathPermissions = new Dictionary<int, Dictionary<string, Dictionary<ScopeType, SchemePermissions>>>();
Expand All @@ -137,7 +169,7 @@ private async Task<PermissionsDataInfo> LoadPermissionsDataAsync()
urlTemplateMatcher.Add(count.ToString(), requestUrl);

// Add the permission scopes for path
pathPermissions.Add(count, fetchedPermissions[key]);
pathPermissions.Add(count, fetchedPermissions[key]);
}

permissionsData = new PermissionsDataInfo()
Expand All @@ -157,7 +189,7 @@ private async Task<PermissionsDataInfo> LoadPermissionsDataAsync()
}

return permissionsData;
}
}

/// <summary>
/// Gets or creates the localized permissions descriptions from the cache.
Expand Down Expand Up @@ -316,10 +348,13 @@ public async Task<PermissionResult> GetScopesAsync(List<RequestInfo> requests =
string org = null,
string branchName = null)
{
var permissionsData = await PermissionsData;
if (permissionsData?.PathPermissions == null)
var permissionsDocument = await LoadDocument;
if (permissionsDocument == null)
throw new InvalidOperationException("Failed to fetch permissions");

var authZChecker = new AuthZChecker();
authZChecker.Load(permissionsDocument);

var scopes = new List<ScopeInformation>();
var errors = new List<PermissionError>();

Expand All @@ -340,10 +375,26 @@ await Parallel.ForEachAsync(uniqueRequests, async (request, _) =>
{
try
{
var scopesForUrl = await GetScopesForRequestUrlAsync(request.RequestUrl, request.HttpMethod, scopeType);
if (scopesForUrl == null || !scopesForUrl.Any())
throw new InvalidOperationException($"Permissions information for '{request.HttpMethod} {request.RequestUrl}' was not found.");
var resource = authZChecker.FindResource(request.RequestUrl);

if (resource == null)
throw new InvalidOperationException($"Permissions information for '{request.HttpMethod} {request.RequestUrl}' was not found.");
var leastPrivilege = resource.FetchLeastPrivilege(request.HttpMethod);
leastPrivilege.TryGetValue(request.HttpMethod, out var methodPermissions);
methodPermissions.TryGetValue(scopeType.ToString(), out var leastPrivilegedPermissions);
thewahome marked this conversation as resolved.
Show resolved Hide resolved
IEnumerable<ScopeInformation> scopesForUrl = new List<ScopeInformation>
{
new ScopeInformation
{
Description = "",
DisplayName = "",
IsAdmin = false,
IsHidden = false,
IsLeastPrivilege = true,
ScopeName = leastPrivilegedPermissions.First(),
ScopeType = scopeType
}
};
scopesByRequestUrl.TryAdd($"{request.HttpMethod} {request.RequestUrl}", scopesForUrl);
}
catch (Exception exception)
Expand Down Expand Up @@ -385,11 +436,11 @@ await Parallel.ForEachAsync(uniqueRequests, async (request, _) =>

// Get consent display name and description
var scopesInfo = GetAdditionalScopesInformation(
scopesInformationDictionary,
scopesInformationDictionary,
scopes.DistinctBy(static x => $"{x.ScopeName}{x.ScopeType}", StringComparer.OrdinalIgnoreCase).ToList(),
scopeType,
scopeType,
getAllScopes);

// exclude hidden permissions unless stated otherwise
scopesInfo = scopesInfo.Where(x => includeHidden || !x.IsHidden).ToList();

Expand All @@ -401,9 +452,9 @@ await Parallel.ForEachAsync(uniqueRequests, async (request, _) =>
});
}

_telemetryClient?.TrackTrace(requests == null || !requests.Any() ?
"Return all permissions" : $"Return permissions for '{string.Join(", ", requests.Select(x => x.RequestUrl))}'",
SeverityLevel.Information,
_telemetryClient?.TrackTrace(requests == null || !requests.Any() ?
"Return all permissions" : $"Return permissions for '{string.Join(", ", requests.Select(x => x.RequestUrl))}'",
SeverityLevel.Information,
_permissionsTraceProperties);

return new PermissionResult()
Expand All @@ -430,92 +481,6 @@ private async Task<IEnumerable<ScopeInformation>> GetAllScopesAsync(ScopeType? s
return scopes;
}

private async Task<IEnumerable<ScopeInformation>> GetScopesForRequestUrlAsync(string requestUrl,
string method,
ScopeType? scopeType = null)
{
if (string.IsNullOrEmpty(requestUrl))
throw new InvalidOperationException("The request URL cannot be null or empty.");

if (string.IsNullOrEmpty(method))
throw new InvalidOperationException("The HTTP method value cannot be null or empty.");

var permissionsData = await PermissionsData;

requestUrl = CleanRequestUrl(requestUrl);

var resultMatch = permissionsData.UriTemplateMatcher.Match(new Uri(requestUrl, UriKind.RelativeOrAbsolute));

if (resultMatch is null)
{
_telemetryClient?.TrackTrace($"Url '{requestUrl}' not found", SeverityLevel.Error, _permissionsTraceProperties);
return Enumerable.Empty<ScopeInformation>();
}

if (!int.TryParse(resultMatch.Key, out int key))
throw new InvalidOperationException($"Failed to parse '{resultMatch.Key}' to int.");

if (!permissionsData.PathPermissions.TryGetValue(key, out var pathPermissions))
{
_telemetryClient?.TrackTrace($"Permissions information for {requestUrl} were not found.", SeverityLevel.Error, _permissionsTraceProperties);
return Enumerable.Empty<ScopeInformation>();
}

if (pathPermissions == null)
{
_telemetryClient?.TrackTrace($"Key '{permissionsData.PathPermissions[key]}' in the {nameof(permissionsData.PathPermissions)} has a null value.",
SeverityLevel.Error,
_permissionsTraceProperties);

return Enumerable.Empty<ScopeInformation>();
}

var scopes = pathPermissions
.Where(x => x.Key.Equals(method, StringComparison.OrdinalIgnoreCase))
.SelectMany(static x => x.Value)
.Where(x => scopeType == null || x.Key == scopeType)
.SelectMany(x => x.Value.AllPermissions,
(x, permission) => new ScopeInformation
{
ScopeType = x.Key,
ScopeName = permission,
IsLeastPrivilege = x.Value.LeastPrivilegePermissions.Contains(permission)
})
.DistinctBy(static x => $"{x.ScopeName}{x.ScopeType}", StringComparer.OrdinalIgnoreCase);

return scopes;
}

/// <summary>
/// Cleans up the request url by applying string formatting operations
/// on the target value in line with the expected standardized output value.
/// </summary>
/// <remarks>The expected standardized output value is the request url value
/// format as captured in the permissions doc. This is to ensure efficacy
/// of the uri template matching.</remarks>
/// <param name="requestUrl">The target request url string value.</param>
/// <returns>The target request url formatted to the expected standardized
/// output value.</returns>
private static string CleanRequestUrl(string requestUrl)
{
if (string.IsNullOrEmpty(requestUrl))
{
return requestUrl;
}

requestUrl = requestUrl.BaseUriPath() // remove any query params
.UriTemplatePathFormat(true)
.RemoveParentheses();

/* Remove ${value} segments from paths,
* ex: /me/photo/$value --> $value or /applications/{application-id}/owners/$ref --> $ref
* Because these segments are not accounted for in the permissions doc.
* ${value} segments will always appear as the last segment in a path.
*/
return Regex.Replace(requestUrl, @"(\$.*)", string.Empty, RegexOptions.None, TimeSpan.FromSeconds(5))
.TrimEnd('/')
.ToLowerInvariant();
}

/// <summary>
/// Retrieves the scopes information for a given list of scopes.
Expand Down Expand Up @@ -550,15 +515,15 @@ private List<ScopeInformation> GetAdditionalScopesInformation(IDictionary<string
if (scopesInformationDictionary[schemeKey].TryGetValue(scope.ScopeName, out var scopeInfo))
{
return new ScopeInformation()
{
ScopeName = scopeInfo.ScopeName,
DisplayName = scopeInfo.DisplayName,
IsAdmin = scopeInfo.IsAdmin,
IsHidden = scopeInfo.IsHidden,
Description = scopeInfo.Description,
ScopeType = scope.ScopeType,
IsLeastPrivilege = scope.IsLeastPrivilege
};
{
ScopeName = scopeInfo.ScopeName,
DisplayName = scopeInfo.DisplayName,
IsAdmin = scopeInfo.IsAdmin,
IsHidden = scopeInfo.IsHidden,
Description = scopeInfo.Description,
ScopeType = scope.ScopeType,
IsLeastPrivilege = scope.IsLeastPrivilege
};
}
return scope;
}).ToList();
Expand Down
Loading