diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..dd84ea782 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..bbcbbe7d6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..901fbe66f --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,20 @@ + +## Overview + +Brief description of what this PR does, and why it is needed. + +### Demo + +Optional. Screenshots, `curl` examples, etc. + +### Notes + +Optional. Ancillary topics, caveats, alternative strategies that didn't work out, anything else. + +## Testing Instructions + +* How to test this PR +* Prefer bulleted description +* Start after checking out this branch +* Include any setup required, such as bundling scripts, restarting services, etc. +* Include test case, and expected output \ No newline at end of file diff --git a/GraphExplorerAppModeService/Interfaces/IGraphService.cs b/GraphExplorerAppModeService/Interfaces/IGraphService.cs new file mode 100644 index 000000000..872078367 --- /dev/null +++ b/GraphExplorerAppModeService/Interfaces/IGraphService.cs @@ -0,0 +1,22 @@ +using Microsoft.Graph; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace GraphExplorerAppModeService.Interfaces +{ + /// + /// This class is for functions that call Microsoft Graph to check if the client owns the Teams resource they are trying to call against. + /// + public interface IGraphService + { + /// + /// ErrorMessage will contain the error message from the Graph Client call. + /// + string ErrorMessage { get; set; } + + Task VerifyOwnership(GraphServiceClient graphClient, string query, string clientId); + } +} diff --git a/GraphExplorerAppModeService/Interfaces/IGraphServiceClient.cs b/GraphExplorerAppModeService/Interfaces/IGraphServiceClient.cs deleted file mode 100644 index 21d8c5757..000000000 --- a/GraphExplorerAppModeService/Interfaces/IGraphServiceClient.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace GraphExplorerAppModeService.Interfaces -{ - interface IGraphServiceClient - { - } -} diff --git a/GraphExplorerAppModeService/Services/GraphAppAuthProvider.cs b/GraphExplorerAppModeService/Services/GraphAppAuthProvider.cs index 877ebea9c..952cd8589 100644 --- a/GraphExplorerAppModeService/Services/GraphAppAuthProvider.cs +++ b/GraphExplorerAppModeService/Services/GraphAppAuthProvider.cs @@ -1,14 +1,6 @@ using GraphExplorerAppModeService.Interfaces; -using Microsoft.Extensions.Configuration; using Microsoft.Graph; -using Microsoft.Identity.Web; -using System; -using System.Collections.Generic; -using System.Linq; using System.Net.Http.Headers; -using System.Text; -using System.Threading.Tasks; - namespace GraphExplorerAppModeService.Services { diff --git a/GraphExplorerAppModeService/Services/GraphClientService.cs b/GraphExplorerAppModeService/Services/GraphClientService.cs deleted file mode 100644 index eb90cd15b..000000000 --- a/GraphExplorerAppModeService/Services/GraphClientService.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using GraphExplorerAppModeService.Interfaces; -using Microsoft.Extensions.Configuration; - -namespace GraphExplorerAppModeService.Services -{ - class GraphClientService : IGraphServiceClient - { - public GraphClientService(IConfiguration configuration) - { - - } - } -} diff --git a/GraphExplorerAppModeService/Services/GraphService.cs b/GraphExplorerAppModeService/Services/GraphService.cs new file mode 100644 index 000000000..ebc1bac1b --- /dev/null +++ b/GraphExplorerAppModeService/Services/GraphService.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using GraphExplorerAppModeService.Interfaces; +using Microsoft.Extensions.Configuration; +using Microsoft.Graph; +using Newtonsoft.Json; + +namespace GraphExplorerAppModeService.Services +{ + public class GraphService : IGraphService + { + private string errorMessage; + + // Docs defined in IGraphService + public string ErrorMessage { get; set; } + + public async Task VerifyOwnership(GraphServiceClient graphClient, string query, string clientId) + { + string[] queryList = query.Split("/"); + + for (int i=0; i < queryList.Length; i++) + { + if (queryList[i] == "teams" && i + 1 < queryList.Length) + { + return await VerifyTeamsOwnership(graphClient, queryList[i + 1], clientId); + } else if (queryList[i] == "chats" && i + 1 < queryList.Length) + { + return await VerifyChatOwnership(graphClient, queryList[i + 1], clientId); + } + } + + return false; + } + + private async Task VerifyTeamsOwnership(GraphServiceClient graphClient, string teamId, string clientId) + { + try + { + var owners = await graphClient.Groups[teamId].Owners.Request().GetAsync(); + + foreach (var owner in owners) + { + if (clientId == owner.Id) return true; + } + } + catch (ServiceException e) + { + ErrorMessage = e.Message; + } + + return false; + } + + private async Task VerifyChatOwnership(GraphServiceClient graphClient, string chatId, string clientId) + { + try + { + var members = await graphClient.Chats[chatId].Members.Request().GetAsync(); + + foreach (AadUserConversationMember member in members) + { + if (clientId == member.UserId && member.Roles.Contains("owner")) return true; + } + } + catch (ServiceException e) + { + ErrorMessage = e.Message; + } + + return false; + } + } +} diff --git a/GraphExplorerPermissionsService/Services/PermissionsStore.cs b/GraphExplorerPermissionsService/Services/PermissionsStore.cs index a1bfe8730..6cbe2a867 100644 --- a/GraphExplorerPermissionsService/Services/PermissionsStore.cs +++ b/GraphExplorerPermissionsService/Services/PermissionsStore.cs @@ -267,33 +267,22 @@ private IDictionary> CreateScopesI SeverityLevel.Information, _permissionsTraceProperties); - ScopesInformationList scopesInformationList = JsonConvert.DeserializeObject(scopesInfoJson); - - var _delegatedScopesInfoTable = new Dictionary(); - var _applicationScopesInfoTable = new Dictionary(); - - foreach (ScopeInformation delegatedScopeInfo in scopesInformationList.DelegatedScopesList) - { - _delegatedScopesInfoTable.Add(delegatedScopeInfo.ScopeName, delegatedScopeInfo); - } - - foreach (ScopeInformation applicationScopeInfo in scopesInformationList.ApplicationScopesList) - { - _applicationScopesInfoTable.Add(applicationScopeInfo.ScopeName, applicationScopeInfo); - } + var scopesInformationList = JsonConvert.DeserializeObject(scopesInfoJson); + var delegatedScopesInfoTable = scopesInformationList.DelegatedScopesList.ToDictionary(x => x.ScopeName); + var applicationScopesInfoTable = scopesInformationList.ApplicationScopesList.ToDictionary(x => x.ScopeName); _permissionsTraceProperties.Add(UtilityConstants.TelemetryPropertyKey_SanitizeIgnore, nameof(PermissionsStore)); _telemetryClient?.TrackTrace("Finished creating the scopes information tables. " + - $"Delegated scopes count: {_delegatedScopesInfoTable.Count}. " + - $"Application scopes count: {_applicationScopesInfoTable.Count}", + $"Delegated scopes count: {delegatedScopesInfoTable.Count}. " + + $"Application scopes count: {applicationScopesInfoTable.Count}", SeverityLevel.Information, _permissionsTraceProperties); _permissionsTraceProperties.Remove(UtilityConstants.TelemetryPropertyKey_SanitizeIgnore); return new Dictionary> { - { Delegated, _delegatedScopesInfoTable }, - { Application, _applicationScopesInfoTable } + { Delegated, delegatedScopesInfoTable }, + { Application, applicationScopesInfoTable } }; } @@ -341,40 +330,26 @@ public async Task> GetScopesAsync(string scopeType = "Del scopesInformationDictionary = await GetOrCreatePermissionsDescriptionsAsync(locale); } + var scopesInfo = new List(); + if (string.IsNullOrEmpty(requestUrl)) // fetch all permissions { _telemetryClient?.TrackTrace("Fetching all permissions", SeverityLevel.Information, _permissionsTraceProperties); - List scopesListInfo = new List(); + var scopes = _scopesListTable.Values.OfType() + .SelectMany(x => x).OfType() + .SelectMany(x => x.Value).OfType() + .Where(x => x.Name.Equals(scopeType)) + .SelectMany(x => x.Value) + .Values().Distinct().ToList(); - if (scopeType.Contains(Delegated)) - { - if (scopesInformationDictionary.ContainsKey(Delegated)) - { - foreach (var scopesInfo in scopesInformationDictionary[Delegated]) - { - scopesListInfo.Add(scopesInfo.Value); - } - } - } - else // Application scopes - { - if (scopesInformationDictionary.ContainsKey(Application)) - { - foreach (var scopesInfo in scopesInformationDictionary[Application]) - { - scopesListInfo.Add(scopesInfo.Value); - } - } - } + scopesInfo = GetScopesInformation(scopesInformationDictionary, scopes, scopeType); _telemetryClient?.TrackTrace("Return all permissions", SeverityLevel.Information, _permissionsTraceProperties); - - return scopesListInfo; } else // fetch permissions for a given request url and method { @@ -388,72 +363,52 @@ public async Task> GetScopesAsync(string scopeType = "Del // Check if requestUrl is contained in our Url Template table TemplateMatch resultMatch = _urlTemplateMatcher.Match(new Uri(requestUrl, UriKind.RelativeOrAbsolute)); - if (resultMatch == null) + if (resultMatch is null) { _telemetryClient?.TrackTrace($"Url '{requestUrl}' not found", - SeverityLevel.Error, - _permissionsTraceProperties); + SeverityLevel.Error, + _permissionsTraceProperties); return null; } - JArray resultValue = new JArray(); - resultValue = (JArray)_scopesListTable[int.Parse(resultMatch.Key)]; - - var scopes = resultValue.FirstOrDefault(x => x.Value("HttpVerb") == method)? - .SelectToken(scopeType)? - .Select(s => (string)s) - .ToArray(); + if (int.TryParse(resultMatch.Key, out int key) is false) + { + throw new InvalidOperationException($"Failed to parse '{resultMatch.Key}' to int."); + } - if (scopes == null) + if (_scopesListTable[key] is not JToken resultValue) { - _telemetryClient?.TrackTrace($"No '{scopeType}' permissions found for the url '{requestUrl}' and method '{method}'", + _telemetryClient?.TrackTrace($"Key '{_scopesListTable[key]}' in the {nameof(_scopesListTable)} has a null value.", SeverityLevel.Error, _permissionsTraceProperties); return null; } - List scopesList = new List(); + var scopes = resultValue.OfType() + .Where(x => method.Equals(x.Name, StringComparison.OrdinalIgnoreCase)) + .SelectMany(x => x.Value).OfType() + .FirstOrDefault(x => scopeType.Equals(x.Name, StringComparison.OrdinalIgnoreCase)) + ?.Value?.ToObject>().Distinct().ToList(); - foreach (string scopeName in scopes) + if (scopes is null) { - ScopeInformation scopeInfo = null; + _telemetryClient?.TrackTrace($"No '{scopeType}' permissions found for the url '{requestUrl}' and method '{method}'", + SeverityLevel.Error, + _permissionsTraceProperties); - if (scopeType.Contains(Delegated)) - { - if (scopesInformationDictionary[Delegated].ContainsKey(scopeName)) - { - scopeInfo = scopesInformationDictionary[Delegated][scopeName]; - } - } - else // Application scopes - { - if (scopesInformationDictionary[Application].ContainsKey(scopeName)) - { - scopeInfo = scopesInformationDictionary[Application][scopeName]; - } - } - if (scopeInfo == null) - { - scopesList.Add(new ScopeInformation - { - ScopeName = scopeName - }); - } - else - { - scopesList.Add(scopeInfo); - } + return null; } + scopesInfo = GetScopesInformation(scopesInformationDictionary, scopes, scopeType); + _telemetryClient?.TrackTrace($"Return '{scopeType}' permissions for url '{requestUrl}' and method '{method}'", SeverityLevel.Information, _permissionsTraceProperties); - - return scopesList; - } + + return scopesInfo; } /// @@ -520,6 +475,40 @@ private static string CleanRequestUrl(string requestUrl) .ToLowerInvariant(); } + /// + /// Retrieves the scopes information for a given list of scopes. + /// + /// The source of the scopes information. + /// The target list of scopes. + /// The type of scope from which to retrieve the scopes information for. + /// A list of . + private static List GetScopesInformation(IDictionary> scopesInformationDictionary, + List scopes, + string scopeType) + { + if (scopesInformationDictionary is null) + { + throw new ArgumentNullException(nameof(scopesInformationDictionary)); + } + + if (scopes is null) + { + throw new ArgumentNullException(nameof(scopes)); + } + + if (string.IsNullOrEmpty(scopeType)) + { + throw new ArgumentException($"'{nameof(scopeType)}' cannot be null or empty.", nameof(scopeType)); + } + + var key = scopeType.Contains(Delegated) ? Delegated : Application; + return scopes.Select(scope => + { + scopesInformationDictionary[key].TryGetValue(scope, out var scopeInfo); + return scopeInfo ?? new() { ScopeName = scope }; + }).ToList(); + } + /// public UriTemplateMatcher GetUriTemplateMatcher() { @@ -527,4 +516,4 @@ public UriTemplateMatcher GetUriTemplateMatcher() return _urlTemplateMatcher; } } -} \ No newline at end of file +} diff --git a/GraphExplorerSamplesService/Models/SampleQueriesList.cs b/GraphExplorerSamplesService/Models/SampleQueriesList.cs index b5a668373..18c707868 100644 --- a/GraphExplorerSamplesService/Models/SampleQueriesList.cs +++ b/GraphExplorerSamplesService/Models/SampleQueriesList.cs @@ -11,10 +11,12 @@ public class SampleQueriesList /// A list of objects /// public List SampleQueries { get; set; } + public List TeamsAppSampleQueries { get; set; } public SampleQueriesList() { SampleQueries = new List(); + TeamsAppSampleQueries = new List(); } } } diff --git a/GraphExplorerSamplesService/Services/SamplesService.cs b/GraphExplorerSamplesService/Services/SamplesService.cs index 959aef8ef..d8965b96a 100644 --- a/GraphExplorerSamplesService/Services/SamplesService.cs +++ b/GraphExplorerSamplesService/Services/SamplesService.cs @@ -211,13 +211,20 @@ private static SampleQueriesList OrderSamplesQueries(SampleQueriesList sampleQue .Where(s => s.Category != "Getting Started") // skipped, as it should always be the top-most sample query in the list .ToList(); + List teamsAppSortedSampleQueries = sampleQueriesList.TeamsAppSampleQueries + .OrderBy(s => s.Category) + .Where(s => s.Category != "Getting Started") // skipped, as it should always be the top-most sample query in the list + .ToList(); + SampleQueriesList sortedSampleQueriesList = new SampleQueriesList(); // Add back 'Getting Started' to the top of the list sortedSampleQueriesList.SampleQueries.AddRange(sampleQueriesList.SampleQueries.FindAll(s => s.Category == "Getting Started")); + sortedSampleQueriesList.TeamsAppSampleQueries.AddRange(sampleQueriesList.TeamsAppSampleQueries.FindAll(s => s.Category == "Getting Started")); // Add the rest of the sample queries sortedSampleQueriesList.SampleQueries.AddRange(sortedSampleQueries); + sortedSampleQueriesList.TeamsAppSampleQueries.AddRange(teamsAppSortedSampleQueries); return sortedSampleQueriesList; } diff --git a/GraphWebApi/Controllers/GraphExplorerAppModeController.cs b/GraphWebApi/Controllers/GraphExplorerAppModeController.cs index 4d311b3b9..e818def75 100644 --- a/GraphWebApi/Controllers/GraphExplorerAppModeController.cs +++ b/GraphWebApi/Controllers/GraphExplorerAppModeController.cs @@ -1,6 +1,5 @@ using System; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using System.Collections.Generic; using System.Linq; @@ -10,54 +9,49 @@ using System.Net.Http.Headers; using Microsoft.Graph; using System.Net; -using Microsoft.Identity.Client; using System.Threading; using Microsoft.Extensions.Primitives; using GraphExplorerAppModeService.Services; using GraphExplorerAppModeService.Interfaces; using Microsoft.Extensions.Configuration; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using Microsoft.IdentityModel.Tokens; +using Microsoft.IdentityModel.Logging; +using Newtonsoft.Json; namespace GraphWebApi.Controllers { [ApiController] public class GraphExplorerAppModeController : ControllerBase { - private readonly IGraphAppAuthProvider _graphClient; + private readonly IGraphAppAuthProvider _graphAuthClient; private readonly ITokenAcquisition _tokenAcquisition; + private readonly IGraphService _graphService; - private readonly IConfiguration _config; - - public GraphExplorerAppModeController(IConfiguration configuration, ITokenAcquisition tokenAcquisition, IGraphAppAuthProvider graphServiceClient) + public GraphExplorerAppModeController(ITokenAcquisition tokenAcquisition, IGraphAppAuthProvider graphServiceClient, IGraphService graphService) { - this._graphClient = graphServiceClient; + this._graphAuthClient = graphServiceClient; this._tokenAcquisition = tokenAcquisition; - this._config = configuration; + this._graphService = graphService; } + [Route("api/[controller]/{*all}")] [Route("graphproxy/{*all}")] [HttpGet] [AuthorizeForScopes(Scopes = new[] { "https://graph.microsoft.com/.default" })] - public async Task GetAsync(string all) + public async Task GetAsync(string all, [FromHeader] string Authorization) { - return await this.ProcessRequestAsync("GET", all, null, _config.GetSection("AzureAd").GetSection("TenantId").Value).ConfigureAwait(false); + return await this.ProcessRequestAsync("GET", all, null, Authorization).ConfigureAwait(false); } - private async Task GetTokenAsync(string tenantId) - { - // Acquire the access token. - string scopes = "https://graph.microsoft.com/.default"; - - return await _tokenAcquisition.GetAccessTokenForAppAsync(scopes, tenantId, null); - } - - [Route("api/[controller]/{*all}")] [Route("graphproxy/{*all}")] [HttpPost] [AuthorizeForScopes(Scopes = new[] { "https://graph.microsoft.com/.default" })] - public async Task PostAsync(string all, [FromBody] object body) + public async Task PostAsync(string all, [FromBody] object body, [FromHeader] string Authorization) { - return await ProcessRequestAsync("POST", all, body, _config.GetSection("AzureAd").GetSection("TenantId").Value).ConfigureAwait(false); + return await ProcessRequestAsync("POST", all, body, Authorization).ConfigureAwait(false); } [Route("api/[controller]/{*all}")] @@ -65,71 +59,79 @@ public async Task PostAsync(string all, [FromBody] object body) [HttpDelete] public async Task DeleteAsync(string all, [FromHeader] string Authorization) { - return await ProcessRequestAsync("DELETE", all, null, _config.GetSection("AzureAd").GetSection("TenantId").Value).ConfigureAwait(false); + return await ProcessRequestAsync("DELETE", all, null, Authorization).ConfigureAwait(false); } [Route("api/[controller]/{*all}")] [Route("graphproxy/{*all}")] [HttpPut] [AuthorizeForScopes(Scopes = new[] { "https://graph.microsoft.com/.default" })] - public async Task PutAsync(string all, [FromBody] object body) + public async Task PutAsync(string all, [FromBody] object body, [FromHeader] string Authorization) { - return await ProcessRequestAsync("PUT", all, body, _config.GetSection("AzureAd").GetSection("TenantId").Value).ConfigureAwait(false); + return await ProcessRequestAsync("PUT", all, body, Authorization).ConfigureAwait(false); } - private string GetBaseUrlWithoutVersion(GraphServiceClient graphClient) + [Route("api/[controller]/{*all}")] + [Route("graphproxy/{*all}")] + [HttpPatch] + [AuthorizeForScopes(Scopes = new[] { "https://graph.microsoft.com/.default" })] + public async Task PatchAsync(string all, [FromBody] object body, [FromHeader] string Authorization) { - var baseUrl = graphClient.BaseUrl; - var index = baseUrl.LastIndexOf('/'); - return baseUrl.Substring(0, index); + return await ProcessRequestAsync("PATCH", all, body, Authorization).ConfigureAwait(false); } - - public class HttpResponseMessageResult : IActionResult + private async Task ProcessRequestAsync(string method, string all, object content, string Authorization) { - private readonly HttpResponseMessage _responseMessage; - - public HttpResponseMessageResult(HttpResponseMessage responseMessage) - { - _responseMessage = responseMessage; // could add throw if null - } - - public async Task ExecuteResultAsync(ActionContext context) + // decode JWT Auth token + string userToken = Authorization.Split(" ")[1]; + + // Retrieve tenantId and clientId from token + IEnumerable jwtTokenClaims = new JwtSecurityToken(userToken).Claims; + string tenantId = jwtTokenClaims.First(claim => claim.Type == "tid").Value; + string clientId = jwtTokenClaims.First(claim => claim.Type == "oid").Value; + + string errorContentType = "application/json"; + try { - context.HttpContext.Response.StatusCode = (int)_responseMessage.StatusCode; + // Authentication provider using a generated application context token + GraphServiceClient graphServiceClient = _graphAuthClient.GetAuthenticatedGraphClient(GetTokenAsync(tenantId).Result.ToString()); - foreach (var header in _responseMessage.Headers) - { - context.HttpContext.Response.Headers.TryAdd(header.Key, new StringValues(header.Value.ToArray())); - } + // Processing the graph request + GraphResponse processedGraphRequest = await ProcessGraphRequest(method, all, content, graphServiceClient); + + // Authentication provider using user's delegated token + GraphServiceClient userGraphServiceClient = _graphAuthClient.GetAuthenticatedGraphClient(userToken); - context.HttpContext.Response.ContentType = _responseMessage.Content.Headers.ContentType.ToString(); + // Check if user owns the resource in question + bool userOwnership = await _graphService.VerifyOwnership(userGraphServiceClient, all, clientId); - using (var stream = await _responseMessage.Content.ReadAsStreamAsync()) + if (userOwnership) { + return new HttpResponseMessageResult(ReturnHttpResponseMessage(HttpStatusCode.OK, processedGraphRequest.contentType, new ByteArrayContent(processedGraphRequest.contentByteArray))); + } else { - await stream.CopyToAsync(context.HttpContext.Response.Body); - await context.HttpContext.Response.Body.FlushAsync(); + Error error = new Error(); + error.Code = "Forbidden"; + error.Message = "The logged in user is not the owner of the resource."; + return new HttpResponseMessageResult(ReturnHttpResponseMessage(HttpStatusCode.Forbidden, errorContentType, new StringContent(JsonConvert.SerializeObject(error)))); } } + catch (ServiceException ex) + { + return new HttpResponseMessageResult(ReturnHttpResponseMessage(ex.StatusCode, errorContentType, new StringContent(JsonConvert.SerializeObject(ex.Error)))); + } + } - [Route("api/[controller]/{*all}")] - [Route("graphproxy/{*all}")] - [HttpPatch] - [AuthorizeForScopes(Scopes = new[] { "https://graph.microsoft.com/.default" })] - public async Task PatchAsync(string all, [FromBody] object body) + struct GraphResponse { - return await ProcessRequestAsync("PATCH", all, body, _config.GetSection("AzureAd").GetSection("TenantId").Value).ConfigureAwait(false); + public string contentType; + public byte[] contentByteArray; } - private async Task ProcessRequestAsync(string method, string all, object content, string tenantId) + private async Task ProcessGraphRequest(string method, string all, object content, GraphServiceClient graphClient) { - GraphServiceClient _graphServiceClient = _graphClient.GetAuthenticatedGraphClient(GetTokenAsync(tenantId).Result.ToString()); + var url = $"{GetBaseUrlWithoutVersion(graphClient)}/{all}{HttpContext.Request.QueryString.ToUriComponent()}"; - var qs = HttpContext.Request.QueryString; - - var url = $"{GetBaseUrlWithoutVersion(_graphServiceClient)}/{all}{qs.ToUriComponent()}"; - - var request = new BaseRequest(url, _graphServiceClient, null) + var request = new BaseRequest(url, graphClient, null) { Method = method, ContentType = HttpContext.Request.ContentType, @@ -145,25 +147,31 @@ private async Task ProcessRequestAsync(string method, string all, } var contentType = "application/json"; - try + + using (var response = await request.SendRequestAsync(content?.ToString(), CancellationToken.None, HttpCompletionOption.ResponseContentRead).ConfigureAwait(false)) { - using (var response = await request.SendRequestAsync(content?.ToString(), CancellationToken.None, HttpCompletionOption.ResponseContentRead).ConfigureAwait(false)) - { - response.Content.Headers.TryGetValues("content-type", out var contentTypes); + response.Content.Headers.TryGetValues("content-type", out var contentTypes); - contentType = contentTypes?.FirstOrDefault() ?? contentType; + contentType = contentTypes?.FirstOrDefault() ?? contentType; - var byteArrayContent = await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false); - Console.WriteLine(byteArrayContent); - return new HttpResponseMessageResult(ReturnHttpResponseMessage(HttpStatusCode.OK, contentType, new ByteArrayContent(byteArrayContent))); - } - } - catch (ServiceException ex) - { - return new HttpResponseMessageResult(ReturnHttpResponseMessage(ex.StatusCode, contentType, new StringContent(ex.Error.ToString()))); + var byteArrayContent = await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false); + + return new GraphResponse + { + contentByteArray = byteArrayContent, + contentType = contentType + }; } } + // Acquire the application context access token. + private async Task GetTokenAsync(string tenantId) + { + string scopes = "https://graph.microsoft.com/.default"; + + return await _tokenAcquisition.GetAccessTokenForAppAsync(scopes, tenantId, null); + } + private static HttpResponseMessage ReturnHttpResponseMessage(HttpStatusCode httpStatusCode, string contentType, HttpContent httpContent) { var httpResponseMessage = new HttpResponseMessage(httpStatusCode) @@ -182,5 +190,40 @@ private static HttpResponseMessage ReturnHttpResponseMessage(HttpStatusCode http return httpResponseMessage; } + + private string GetBaseUrlWithoutVersion(GraphServiceClient graphClient) + { + var baseUrl = graphClient.BaseUrl; + var index = baseUrl.LastIndexOf('/'); + return baseUrl.Substring(0, index); + } + + public class HttpResponseMessageResult : IActionResult + { + private readonly HttpResponseMessage _responseMessage; + + public HttpResponseMessageResult(HttpResponseMessage responseMessage) + { + _responseMessage = responseMessage; // could add throw if null + } + + public async Task ExecuteResultAsync(ActionContext context) + { + context.HttpContext.Response.StatusCode = (int)_responseMessage.StatusCode; + + foreach (var header in _responseMessage.Headers) + { + context.HttpContext.Response.Headers.TryAdd(header.Key, new StringValues(header.Value.ToArray())); + } + + context.HttpContext.Response.ContentType = _responseMessage.Content.Headers.ContentType.ToString(); + + using (var stream = await _responseMessage.Content.ReadAsStreamAsync()) + { + await stream.CopyToAsync(context.HttpContext.Response.Body); + await context.HttpContext.Response.Body.FlushAsync(); + } + } + } } } diff --git a/GraphWebApi/Controllers/GraphExplorerPermissionsController.cs b/GraphWebApi/Controllers/GraphExplorerPermissionsController.cs index 2a4c11101..016e6d26d 100644 --- a/GraphWebApi/Controllers/GraphExplorerPermissionsController.cs +++ b/GraphWebApi/Controllers/GraphExplorerPermissionsController.cs @@ -70,7 +70,7 @@ public async Task GetPermissionScopes([FromQuery]string scopeType } _permissionsTraceProperties.Add(UtilityConstants.TelemetryPropertyKey_SanitizeIgnore, nameof(GraphExplorerPermissionsController)); - _telemetryClient?.TrackTrace($"Fetched {result.Count} permissions", + _telemetryClient?.TrackTrace($"Fetched {result?.Count ?? 0} permissions", SeverityLevel.Information, _permissionsTraceProperties); diff --git a/GraphWebApi/Controllers/GraphExplorerSamplesController.cs b/GraphWebApi/Controllers/GraphExplorerSamplesController.cs index 13fd56566..65f562e51 100644 --- a/GraphWebApi/Controllers/GraphExplorerSamplesController.cs +++ b/GraphWebApi/Controllers/GraphExplorerSamplesController.cs @@ -72,7 +72,7 @@ public async Task GetSampleQueriesListAsync(string search, string } _samplesTraceProperties.Add(UtilityConstants.TelemetryPropertyKey_SanitizeIgnore, nameof(GraphExplorerSamplesController)); - _telemetryClient?.TrackTrace($"{filteredSampleQueries.Count} sample queries found from search value '{search}'", + _telemetryClient?.TrackTrace($"{filteredSampleQueries?.Count ?? 0} sample queries found from search value '{search}'", SeverityLevel.Information, _samplesTraceProperties); @@ -337,7 +337,7 @@ public async Task DeleteSampleQueryAsync(string id) /// A list of Sample Queries. private async Task FetchSampleQueriesListAsync(string org, string branchName) { - string locale = RequestHelper.GetPreferredLocaleLanguage(Request); + string locale = RequestHelper.GetPreferredLocaleLanguage(Request) ?? Constants.DefaultLocale; _telemetryClient?.TrackTrace($"Request to fetch samples for locale '{locale}'", SeverityLevel.Information, _samplesTraceProperties); @@ -353,7 +353,7 @@ private async Task FetchSampleQueriesListAsync(string org, st } _samplesTraceProperties.Add(UtilityConstants.TelemetryPropertyKey_SanitizeIgnore, nameof(GraphExplorerSamplesController)); - _telemetryClient?.TrackTrace($"Fetched {sampleQueriesList?.SampleQueries.Count} samples", + _telemetryClient?.TrackTrace($"Fetched {sampleQueriesList?.SampleQueries?.Count ?? 0} samples", SeverityLevel.Information, _samplesTraceProperties); diff --git a/GraphWebApi/GraphWebApi.csproj b/GraphWebApi/GraphWebApi.csproj index 20b3fd145..6ee0b7eaa 100644 --- a/GraphWebApi/GraphWebApi.csproj +++ b/GraphWebApi/GraphWebApi.csproj @@ -46,8 +46,8 @@ - - + + diff --git a/GraphWebApi/Startup.cs b/GraphWebApi/Startup.cs index 342d1aab5..9ac807ecd 100644 --- a/GraphWebApi/Startup.cs +++ b/GraphWebApi/Startup.cs @@ -107,6 +107,7 @@ public void ConfigureServices(IServiceCollection services) services.AddMemoryCache(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/GraphWebApi/appsettings.json b/GraphWebApi/appsettings.json index dae7c854d..f2684cd36 100644 --- a/GraphWebApi/appsettings.json +++ b/GraphWebApi/appsettings.json @@ -18,8 +18,8 @@ }, "AllowedHosts": "*", "GraphMetadata": { - "V1.0": "https://raw.githubusercontent.com/microsoftgraph/msgraph-metadata/master/clean_v10_metadata/cleanMetadataWithDescriptionsAndAnnotationsv1.0.xml", - "Beta": "https://raw.githubusercontent.com/microsoftgraph/msgraph-metadata/master/clean_beta_metadata/cleanMetadataWithDescriptionsAndAnnotationsbeta.xml" + "V1.0": "https://raw.githubusercontent.com/microsoftgraph/msgraph-metadata/master/clean_v10_metadata/cleanMetadataWithDescriptionsv1.0.xml", + "Beta": "https://raw.githubusercontent.com/microsoftgraph/msgraph-metadata/master/clean_beta_metadata/cleanMetadataWithDescriptionsbeta.xml" }, "BlobStorage": { "AzureConnectionString": "ENTER_AZURE_STORAGE_CONNECTION_STRING", diff --git a/OpenAPIService.Test/OpenAPIDocumentCreatorMock.cs b/OpenAPIService.Test/OpenAPIDocumentCreatorMock.cs index b30da4d04..1bb5a52bb 100644 --- a/OpenAPIService.Test/OpenAPIDocumentCreatorMock.cs +++ b/OpenAPIService.Test/OpenAPIDocumentCreatorMock.cs @@ -352,6 +352,21 @@ private OpenApiDocument CreateOpenApiDocument() OperationId = "users.GetMessages", Summary = "Get messages from users", Description = "The messages in a mailbox or folder. Read-only. Nullable.", + Parameters = new List + { + new OpenApiParameter() + { + Name = "$select", + In = ParameterLocation.Query, + Required = true, + Description = "Select properties to be returned", + Schema = new OpenApiSchema() + { + Type = "array" + } + // missing explode parameter + } + }, Responses = new OpenApiResponses() { { @@ -563,22 +578,20 @@ private OpenApiDocument CreateOpenApiDocument() Summary = "Invoke action keepAlive", Parameters = new List { + new OpenApiParameter() { - new OpenApiParameter() + Name = "call-id", + In = ParameterLocation.Path, + Description = "key: id of call", + Required = true, + Schema = new OpenApiSchema() + { + Type = "string" + }, + Extensions = new Dictionary { - Name = "call-id", - In = ParameterLocation.Path, - Description = "key: id of call", - Required = true, - Schema = new OpenApiSchema() - { - Type = "string" - }, - Extensions = new Dictionary { - { - "x-ms-docs-key-type", new OpenApiString("call") - } + "x-ms-docs-key-type", new OpenApiString("call") } } } @@ -611,52 +624,46 @@ private OpenApiDocument CreateOpenApiDocument() { Tags = new List { + new OpenApiTag() { - new OpenApiTag() - { - Name = "groups.Functions" - } + Name = "groups.Functions" } }, OperationId = "groups.group.events.event.calendar.events.delta", Summary = "Invoke function delta", Parameters = new List { + new OpenApiParameter() { - new OpenApiParameter() + Name = "group-id", + In = ParameterLocation.Path, + Description = "key: id of group", + Required = true, + Schema = new OpenApiSchema() + { + Type = "string" + }, + Extensions = new Dictionary { - Name = "group-id", - In = ParameterLocation.Path, - Description = "key: id of group", - Required = true, - Schema = new OpenApiSchema() - { - Type = "string" - }, - Extensions = new Dictionary { - { - "x-ms-docs-key-type", new OpenApiString("group") - } + "x-ms-docs-key-type", new OpenApiString("group") } } }, + new OpenApiParameter() { - new OpenApiParameter() + Name = "event-id", + In = ParameterLocation.Path, + Description = "key: id of event", + Required = true, + Schema = new OpenApiSchema() + { + Type = "string" + }, + Extensions = new Dictionary { - Name = "event-id", - In = ParameterLocation.Path, - Description = "key: id of event", - Required = true, - Schema = new OpenApiSchema() - { - Type = "string" - }, - Extensions = new Dictionary { - { - "x-ms-docs-key-type", new OpenApiString("event") - } + "x-ms-docs-key-type", new OpenApiString("event") } } } diff --git a/OpenAPIService.Test/OpenAPIServiceShould.cs b/OpenAPIService.Test/OpenAPIServiceShould.cs index 9172b6f0e..6d22204a5 100644 --- a/OpenAPIService.Test/OpenAPIServiceShould.cs +++ b/OpenAPIService.Test/OpenAPIServiceShould.cs @@ -378,5 +378,33 @@ public void ResolveActionFunctionOperationIdsForPowerShellStyle(string url, Oper // Assert Assert.Equal(expectedOperationId, operationId); } + + [Theory] + [InlineData(OpenApiStyle.GEAutocomplete)] + [InlineData(OpenApiStyle.Plain)] + [InlineData(OpenApiStyle.PowerPlatform)] + [InlineData(OpenApiStyle.PowerShell)] + public void SetExplodePropertyToFalseInParametersWithStyleEqualsFormForAllStyles(OpenApiStyle style) + { + // Arrange + OpenApiDocument source = _graphBetaSource; + + // Act + var predicate = _openApiService.CreatePredicate(operationIds: null, + tags: null, + url: "/users/{user-id}/messages/{message-id}", + source: source, + graphVersion: GraphVersion); + + var subsetOpenApiDocument = _openApiService.CreateFilteredDocument(source, Title, GraphVersion, predicate); + subsetOpenApiDocument = _openApiService.ApplyStyle(style, subsetOpenApiDocument); + + var parameter = subsetOpenApiDocument.Paths + .FirstOrDefault().Value + .Operations.FirstOrDefault().Value + .Parameters.First(s => s.Name.Equals("$select")); + // Assert + Assert.False(parameter.Explode); + } } } diff --git a/OpenAPIService/OperationSearch.cs b/OpenAPIService/OperationSearch.cs index f1159419c..0af0a7d5d 100644 --- a/OpenAPIService/OperationSearch.cs +++ b/OpenAPIService/OperationSearch.cs @@ -1,23 +1,31 @@ -using Microsoft.OpenApi.Models; +// ------------------------------------------------------------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. +// ------------------------------------------------------------------------------------------------------------------------------------------------------ + +using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Services; using System; using System.Collections.Generic; +using System.Linq; namespace OpenAPIService { public class OperationSearch : OpenApiVisitorBase { private readonly Func _predicate; + private readonly List _searchResults = new(); - private List _searchResults = new List(); - - public IList SearchResults { get { return _searchResults; } } + public IList SearchResults => _searchResults; public OperationSearch(Func predicate) { - this._predicate = predicate; + _predicate = predicate ?? throw new ArgumentNullException(nameof(predicate)); } + /// + /// Visits . + /// + /// The target . public override void Visit(OpenApiOperation operation) { if (_predicate(operation)) @@ -30,16 +38,32 @@ public override void Visit(OpenApiOperation operation) } } - private CurrentKeys CopyCurrentKeys(CurrentKeys currentKeys) + /// + /// Visits list of . + /// + /// The target list of . + public override void Visit(IList parameters) + { + /* The Parameter.Explode property should be true + * if Parameter.Style == Form; but OData query params + * as used in Microsoft Graph implement explode: false + * ex: $select=id,displayName,givenName + */ + foreach (var parameter in parameters.Where(x => x.Style == ParameterStyle.Form)) + { + parameter.Explode = false; + } + + base.Visit(parameters); + } + + private static CurrentKeys CopyCurrentKeys(CurrentKeys currentKeys) { - var keys = new CurrentKeys + return new CurrentKeys { Path = currentKeys.Path, Operation = currentKeys.Operation }; - - return keys; } } - } diff --git a/OpenAPIService/PowershellFormatter.cs b/OpenAPIService/PowershellFormatter.cs index a13337cb0..3903abff7 100644 --- a/OpenAPIService/PowershellFormatter.cs +++ b/OpenAPIService/PowershellFormatter.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------------------------------------------------------------------------------------ +// ------------------------------------------------------------------------------------------------------------------------------------------------------ // Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. // ------------------------------------------------------------------------------------------------------------------------------------------------------ diff --git a/PermissionsService.Test/PermissionsStoreShould.cs b/PermissionsService.Test/PermissionsStoreShould.cs index 63824f5f0..b6d49f583 100644 --- a/PermissionsService.Test/PermissionsStoreShould.cs +++ b/PermissionsService.Test/PermissionsStoreShould.cs @@ -46,14 +46,17 @@ public void GetRequiredPermissionScopesGivenAnExistingRequestUrl() }); } - [Fact] - public void GetAllPermissionScopesGivenNoRequestUrl() + [Theory] + [InlineData("DelegatedWork", 20)] + [InlineData("Application", 14)] + public void GetAllPermissionScopesGivenNoRequestUrl(string scopeType, int expectedCount) { // Act - List result = _permissionsStore.GetScopesAsync().GetAwaiter().GetResult(); + List result = _permissionsStore.GetScopesAsync(scopeType: scopeType).GetAwaiter().GetResult(); // Assert Assert.NotEmpty(result); + Assert.Equal(expectedCount, result.Count); } [Fact] diff --git a/PermissionsService.Test/TestFiles/permissions-test-file-v1.0_ver1.json b/PermissionsService.Test/TestFiles/permissions-test-file-v1.0_ver1.json index d8c2da905..e185b55e4 100644 --- a/PermissionsService.Test/TestFiles/permissions-test-file-v1.0_ver1.json +++ b/PermissionsService.Test/TestFiles/permissions-test-file-v1.0_ver1.json @@ -1,8 +1,7 @@ { "ApiPermissions": { - "/security/alerts/{id}": [ - { - "HttpVerb": "GET", + "/security/alerts/{id}": { + "GET": { "DelegatedWork": [ "SecurityEvents.Read.All", "SecurityEvents.ReadWrite.All" @@ -13,8 +12,7 @@ "SecurityEvents.ReadWrite.All" ] }, - { - "HttpVerb": "PATCH", + "PATCH": { "DelegatedWork": [ "SecurityEvents.ReadWrite.All" ], @@ -23,10 +21,10 @@ "SecurityEvents.ReadWrite.All" ] } - ], - "/security/alerts": [ - { - "HttpVerb": "GET", + }, + "/security/alerts": { + + "GET": { "DelegatedWork": [ "SecurityEvents.Read.All", "SecurityEvents.ReadWrite.All" @@ -37,10 +35,10 @@ "SecurityEvents.ReadWrite.All" ] } - ], - "/me/calendars/{id}": [ - { - "HttpVerb": "DELETE", + }, + "/me/calendars/{id}": { + + "DELETE": { "DelegatedWork": [ "Calendars.ReadWrite" ], @@ -51,8 +49,8 @@ "Calendars.ReadWrite" ] }, - { - "HttpVerb": "GET", + + "GET": { "DelegatedWork": [ "Calendars.Read" ], @@ -63,8 +61,8 @@ "Calendars.Read" ] }, - { - "HttpVerb": "PATCH", + + "PATCH": { "DelegatedWork": [ "Calendars.ReadWrite" ], @@ -75,10 +73,12 @@ "Calendars.ReadWrite" ] } - ], - "/users/{id}/calendars/{id}": [ - { - "HttpVerb": "DELETE", + + }, + "/users/{id}/calendars/{id}": { + + + "DELETE": { "DelegatedWork": [ "Calendars.ReadWrite" ], @@ -88,10 +88,13 @@ "Application": [ "Calendars.ReadWrite" ] + }, - { - "HttpVerb": "GET", + + "GET": { "DelegatedWork": [ + "Calendars.Read", + "Calendars.Read", "Calendars.Read" ], "DelegatedPersonal": [ @@ -101,8 +104,8 @@ "Calendars.Read" ] }, - { - "HttpVerb": "PATCH", + + "PATCH": { "DelegatedWork": [ "Calendars.ReadWrite" ], @@ -113,10 +116,10 @@ "Calendars.ReadWrite" ] } - ], - "/me/calendars/{id}": [ - { - "HttpVerb": "DELETE", + }, + "/me/calendars/{id}": { + + "DELETE": { "DelegatedWork": [ "Calendars.ReadWrite" ], @@ -127,8 +130,8 @@ "Calendars.ReadWrite" ] }, - { - "HttpVerb": "GET", + + "GET": { "DelegatedWork": [ "Calendars.Read" ], @@ -139,8 +142,7 @@ "Calendars.Read" ] }, - { - "HttpVerb": "PATCH", + "PATCH": { "DelegatedWork": [ "Calendars.ReadWrite" ], @@ -151,10 +153,9 @@ "Calendars.ReadWrite" ] } - ], - "/me/calendargroup/calendars/{id}": [ - { - "HttpVerb": "DELETE", + }, + "/me/calendargroup/calendars/{id}": { + "DELETE": { "DelegatedWork": [ "Calendars.ReadWrite" ], @@ -165,8 +166,7 @@ "Calendars.ReadWrite" ] }, - { - "HttpVerb": "GET", + "GET": { "DelegatedWork": [ "Calendars.Read" ], @@ -177,8 +177,7 @@ "Calendars.Read" ] }, - { - "HttpVerb": "PATCH", + "PATCH": { "DelegatedWork": [ "Calendars.ReadWrite" ], @@ -189,10 +188,9 @@ "Calendars.ReadWrite" ] } - ], - "/users/{id}/calendargroup/calendars": [ - { - "HttpVerb": "GET", + }, + "/users/{id}/calendargroup/calendars": { + "GET": { "DelegatedWork": [ "Calendars.Read" ], @@ -202,9 +200,9 @@ "Application": [ "Calendars.Read" ] + }, - { - "HttpVerb": "POST", + "POST": { "DelegatedWork": [ "Calendars.ReadWrite" ], @@ -215,10 +213,9 @@ "Calendars.ReadWrite" ] } - ], - "/me/calendargroup/calendars/{id}": [ - { - "HttpVerb": "DELETE", + }, + "/me/calendargroup/calendars/{id}": { + "DELETE": { "DelegatedWork": [ "Calendars.ReadWrite" ], @@ -229,8 +226,8 @@ "Calendars.ReadWrite" ] }, - { - "HttpVerb": "GET", + + "GET": { "DelegatedWork": [ "Calendars.Read" ], @@ -241,8 +238,7 @@ "Calendars.Read" ] }, - { - "HttpVerb": "PATCH", + "PATCH": { "DelegatedWork": [ "Calendars.ReadWrite" ], @@ -253,38 +249,35 @@ "Calendars.ReadWrite" ] } - ], - "/workbook/worksheets/{id}/charts/{id}": [ - { - "HttpVerb": "GET", + }, + "/workbook/worksheets/{id}/charts/{id}": { + "GET": { "DelegatedWork": [ "Files.ReadWrite" ], "DelegatedPersonal": [], "Application": [] }, - { - "HttpVerb": "PATCH", + "PATCH": { "DelegatedWork": [ "Files.ReadWrite" ], "DelegatedPersonal": [], "Application": [] } - ], - "/workbook/worksheets/{id}/charts/{id}/image(width=640)": [ - { - "HttpVerb": "GET", + + }, + "/workbook/worksheets/{id}/charts/{id}/image(width=640)": { + "GET": { "DelegatedWork": [ "Files.ReadWrite" ], "DelegatedPersonal": [], "Application": [] } - ], - "/me/photo": [ - { - "HttpVerb": "GET", + }, + "/me/photo": { + "GET": { "DelegatedWork": [ "User.ReadBasic.All", "User.Read", @@ -299,8 +292,7 @@ "Not supported." ] }, - { - "HttpVerb": "PATCH", + "PATCH": { "DelegatedWork": [ "User.ReadWrite", "User.ReadWrite.All" @@ -312,8 +304,7 @@ "Not supported." ] }, - { - "HttpVerb": "PUT", + "PUT": { "DelegatedWork": [ "User.ReadWrite", "User.ReadWrite.All" @@ -325,6 +316,6 @@ "Not supported." ] } - ] + } } } \ No newline at end of file diff --git a/PermissionsService.Test/TestFiles/permissions-test-file-v1.0_ver2.json b/PermissionsService.Test/TestFiles/permissions-test-file-v1.0_ver2.json index 14b9db9eb..39e96989a 100644 --- a/PermissionsService.Test/TestFiles/permissions-test-file-v1.0_ver2.json +++ b/PermissionsService.Test/TestFiles/permissions-test-file-v1.0_ver2.json @@ -1,8 +1,7 @@ { "ApiPermissions": { - "/accessreviews('{id}')/reviewers": [ - { - "HttpVerb": "POST", + "/accessreviews('{id}')/reviewers": { + "POST": { "DelegatedWork": [ "AccessReview.ReadWrite.All" ], @@ -11,8 +10,8 @@ "AccessReview.ReadWrite.Membership" ] }, - { - "HttpVerb": "GET", + + "GET": { "DelegatedWork": [ "AccessReview.Read.All", "AccessReview.ReadWrite.All" @@ -23,10 +22,9 @@ "AccessReview.ReadWrite.Membership" ] } - ], - "/accessreviews('')/applydecisions()": [ - { - "HttpVerb": "POST", + }, + "/accessreviews('')/applydecisions()": { + "POST": { "DelegatedWork": [ "AccessReview.ReadWrite.All" ], @@ -35,10 +33,11 @@ "AccessReview.ReadWrite.Membership" ] } - ], - "/administrativeunits/{id}": [ - { - "HttpVerb": "DELETE", + + }, + "/administrativeunits/{id}": { + + "DELETE": { "DelegatedWork": [ "AdministrativeUnit.ReadWrite.All", "Directory.AccessAsUser.All" @@ -48,8 +47,7 @@ "AdministrativeUnit.ReadWrite.All" ] }, - { - "HttpVerb": "GET", + "GET": { "DelegatedWork": [ "AdministrativeUnit.Read.All", "Directory.Read.All", @@ -65,8 +63,7 @@ "Directory.ReadWrite.All" ] }, - { - "HttpVerb": "PATCH", + "PATCH": { "DelegatedWork": [ "AdministrativeUnit.ReadWrite.All", "Directory.AccessAsUser.All" @@ -76,10 +73,9 @@ "AdministrativeUnit.ReadWrite.All" ] } - ], - "/administrativeunits/{id}/members/{id}": [ - { - "HttpVerb": "GET", + }, + "/administrativeunits/{id}/members/{id}": { + "GET": { "DelegatedWork": [ "AdministrativeUnit.Read.All", "Directory.Read.All", @@ -95,10 +91,9 @@ "Directory.ReadWrite.All" ] } - ], - "/security/alerts/{id}": [ - { - "HttpVerb": "GET", + }, + "/security/alerts/{id}": { + "GET": { "DelegatedWork": [ "SecurityEvents.Read.All", "SecurityEvents.ReadWrite.All" @@ -109,10 +104,9 @@ "SecurityEvents.ReadWrite.All" ] } - ], - "/anonymousipriskevents/{id}": [ - { - "HttpVerb": "GET", + }, + "/anonymousipriskevents/{id}": { + "GET": { "DelegatedWork": [ "IdentityRiskEvent.Read.All" ], @@ -121,10 +115,9 @@ "IdentityRiskEvent.Read.All" ] } - ], - "/lorem/ipsum/{id}": [ - { - "HttpVerb": "GET", + }, + "/lorem/ipsum/{id}": { + "GET": { "DelegatedWork": [ "LoremIpsum.Read.All", "LoremIpsum.ReadWrite.All" @@ -135,6 +128,6 @@ "LoremIpsum.ReadWrite.All" ] } - ] + } } } \ No newline at end of file diff --git a/TelemetryService.Test/TelemetrySanitizerService.Test.csproj b/TelemetryService.Test/TelemetrySanitizerService.Test.csproj index 227459787..a9fdd1899 100644 --- a/TelemetryService.Test/TelemetrySanitizerService.Test.csproj +++ b/TelemetryService.Test/TelemetrySanitizerService.Test.csproj @@ -20,7 +20,7 @@ - + diff --git a/TelemetryService/TelemetrySanitizerService.csproj b/TelemetryService/TelemetrySanitizerService.csproj index 5ecc6d0d1..6cc6d8d9d 100644 --- a/TelemetryService/TelemetrySanitizerService.csproj +++ b/TelemetryService/TelemetrySanitizerService.csproj @@ -6,7 +6,7 @@ - + diff --git a/pipelines/interns-build-and-publish.yml b/pipelines/interns-build-and-publish.yml new file mode 100644 index 000000000..a735397cf --- /dev/null +++ b/pipelines/interns-build-and-publish.yml @@ -0,0 +1,71 @@ +# NOTE: This file is used by the Garage Interns for our Azure DevOps build and release pipelines. + +# ASP.NET Core (.NET Framework) +# Build and test ASP.NET Core projects targeting the full .NET Framework. +# Add steps that publish symbols, save build artifacts, and more: +# https://docs.microsoft.com/azure/devops/pipelines/languages/dotnet-core + +trigger: + branches: + include: + - master + - dev + - feature/* + - task/* + - fix/* + - interns/* + +pr: + branches: + include: + - master + - dev + - feature/* + - task/* + - fix/* + - interns/* + +pool: + vmImage: 'windows-latest' + +variables: + solution: '**/*.sln' + buildPlatform: 'Any CPU' + buildConfiguration: 'Release' + +steps: +- checkout: self + clean: true + fetchDepth: 1 + +- task: NuGetToolInstaller@1 + +- task: NuGetCommand@2 + inputs: + restoreSolution: '$(solution)' + +- task: VSBuild@1 + inputs: + solution: '$(solution)' + msbuildArgs: '/p:WebPublishMethod=Package /p:PackageAsSingleFile=true /p:SkipInvalidConfigurations=true /p:DesktopBuildPackageLocation="$(build.artifactStagingDirectory)\WebApp.zip" /p:DeployIisAppPath="Default Web Site"' + platform: '$(buildPlatform)' + configuration: '$(buildConfiguration)' + +- task: VSTest@2 + inputs: + platform: '$(buildPlatform)' + configuration: '$(buildConfiguration)' + testAssemblyVer2: | + **/net5.0/**.Test.dll + !**/*TestAdapter.dll + !**/obj/** + diagnosticsEnabled: true + +- task: VSBuild@1 + inputs: + solution: '$(solution)' + msbuildArgs: '/p:DeployOnBuild=true /p:WebPublishMethod=Package /p:PackageAsSingleFile=true /p:SkipInvalidConfigurations=true /p:DesktopBuildPackageLocation="$(build.artifactStagingDirectory)\WebApp.zip" /p:DeployIisAppPath="Default Web Site"' + platform: '$(buildPlatform)' + configuration: '$(buildConfiguration)' + +- task: PublishBuildArtifacts@1 \ No newline at end of file