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