Skip to content
4 changes: 3 additions & 1 deletion src/app/services/actions/permissions-action-creator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,14 @@ export function fetchScopes(query?: IQuery): Function {
}
throw (response);
} catch (error) {
const errorMessage = error instanceof Response ?
`ApiError: ${error.status}` : `${error}`;
telemetry.trackException(
new Error(errorTypes.NETWORK_ERROR),
SeverityLevel.Error,
{
ComponentName: componentNames.FETCH_PERMISSIONS_ACTION,
Message: `${error}`
Message: errorMessage
});
return dispatch(fetchScopesError(error));
}
Expand Down
42 changes: 25 additions & 17 deletions src/app/services/actions/query-action-creator-util.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { AuthenticationHandlerOptions, ResponseType } from '@microsoft/microsoft-graph-client';
import { MSALAuthenticationProviderOptions } from
'@microsoft/microsoft-graph-client/lib/src/MSALAuthenticationProviderOptions';
import {
AuthenticationHandlerOptions,
ResponseType,
} from '@microsoft/microsoft-graph-client';
import { MSALAuthenticationProviderOptions } from '@microsoft/microsoft-graph-client/lib/src/MSALAuthenticationProviderOptions';
import { IAction } from '../../../types/action';
import { ContentType } from '../../../types/enums';
import { IQuery } from '../../../types/query-runner';
import { IRequestOptions } from '../../../types/request';
import { GraphClient } from '../graph-client';
import { authProvider } from '../graph-client/msal-agent';
import { DEFAULT_USER_SCOPES } from '../graph-constants';
import { DEFAULT_USER_SCOPES, GRAPH_API_SANDBOX_URL } from '../graph-constants';
import { QUERY_GRAPH_SUCCESS } from '../redux-constants';
import { queryRunningStatus } from './query-loading-action-creators';

Expand All @@ -19,22 +21,21 @@ export function queryResponse(response: object): IAction {
}

export async function anonymousRequest(dispatch: Function, query: IQuery) {

const authToken = '{token:https://graph.microsoft.com/}';
const escapedUrl = encodeURIComponent(query.sampleUrl);
const graphUrl = `https://proxy.apisandbox.msdn.microsoft.com/svc?url=${escapedUrl}`;
const graphUrl = `${GRAPH_API_SANDBOX_URL}/svc?url=${escapedUrl}`;
const sampleHeaders: any = {};
if (query.sampleHeaders && query.sampleHeaders.length > 0) {
query.sampleHeaders.forEach(header => {
query.sampleHeaders.forEach((header) => {
sampleHeaders[header.name] = header.value;
});
}

const headers = {
'Authorization': `Bearer ${authToken}`,
Authorization: `Bearer ${authToken}`,
'Content-Type': 'application/json',
'SdkVersion': 'GraphExplorer/4.0',
...sampleHeaders
SdkVersion: 'GraphExplorer/4.0',
...sampleHeaders,
};

const options: IRequestOptions = { method: query.selectedVerb, headers };
Expand All @@ -44,16 +45,20 @@ export async function anonymousRequest(dispatch: Function, query: IQuery) {
return fetch(graphUrl, options);
}

export function authenticatedRequest(dispatch: Function, query: IQuery,
scopes: string[] = DEFAULT_USER_SCOPES.split(' ')) {
export function authenticatedRequest(
dispatch: Function,
query: IQuery,
scopes: string[] = DEFAULT_USER_SCOPES.split(' ')
) {
return makeRequest(query.selectedVerb, scopes)(dispatch, query);
}

export function isImageResponse(contentType: string | undefined) {
if (!contentType) { return false; }
if (!contentType) {
return false;
}
return (
contentType === 'application/octet-stream' ||
contentType.includes('image/')
contentType === 'application/octet-stream' || contentType.includes('image/')
);
}

Expand Down Expand Up @@ -98,13 +103,16 @@ const makeRequest = (httpVerb: string, scopes: string[]): Function => {
sampleHeaders.SdkVersion = 'GraphExplorer/4.0';

if (query.sampleHeaders && query.sampleHeaders.length > 0) {
query.sampleHeaders.forEach(header => {
query.sampleHeaders.forEach((header) => {
sampleHeaders[header.name] = header.value;
});
}

const msalAuthOptions = new MSALAuthenticationProviderOptions(scopes);
const middlewareOptions = new AuthenticationHandlerOptions(authProvider, msalAuthOptions);
const middlewareOptions = new AuthenticationHandlerOptions(
authProvider,
msalAuthOptions
);
const client = GraphClient.getInstance()
.api(query.sampleUrl)
.middlewareOptions([middlewareOptions])
Expand Down
4 changes: 3 additions & 1 deletion src/app/services/actions/samples-action-creators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,14 @@ export function fetchSamples(): Function {
const res = await response.json();
return dispatch(fetchSamplesSuccess(res.sampleQueries));
} catch (error) {
const errorMessage = error instanceof Response ?
`ApiError: ${error.status}` : `${error}`;
telemetry.trackException(
new Error(errorTypes.NETWORK_ERROR),
SeverityLevel.Error,
{
ComponentName: componentNames.FETCH_SAMPLES_ACTION,
Message: `${error}`
Message: errorMessage
});
return dispatch(fetchSamplesError({ error }));
}
Expand Down
20 changes: 12 additions & 8 deletions src/app/services/actions/snippet-action-creator.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { SeverityLevel } from '@microsoft/applicationinsights-web';
import { componentNames, errorTypes, telemetry } from '../../../telemetry';
import { IAction } from '../../../types/action';
import { IRequestOptions } from '../../../types/request';
import { sanitizeQueryUrl } from '../../utils/query-url-sanitization';
import { parseSampleUrl } from '../../utils/sample-url-generation';
import { GET_SNIPPET_ERROR, GET_SNIPPET_PENDING, GET_SNIPPET_SUCCESS } from '../redux-constants';
Expand Down Expand Up @@ -41,31 +42,34 @@ export function getSnippet(language: string): Function {

dispatch(getSnippetPending());

const method = 'POST';
const headers = {
'Content-Type': 'application/http'
};
// tslint:disable-next-line: max-line-length
const body = `${sampleQuery.selectedVerb} /${queryVersion}/${requestUrl + search} HTTP/1.1\r\nHost: graph.microsoft.com\r\nContent-Type: application/json\r\n\r\n${JSON.stringify(sampleQuery.sampleBody)}`;
const options: IRequestOptions = { method, headers, body };
const obj: any = {};
const response = await fetch(snippetsUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/http'
},
body
});

const response = await fetch(snippetsUrl, options);
if (response.ok) {
const result = await response.text();
obj[language] = result;
return dispatch(getSnippetSuccess(obj));
}
throw (response);
} catch (error) {
const errorMessage = error instanceof Response ?
`ApiError: ${error.status}` : `${error}`;
const sanitizedUrl = sanitizeQueryUrl(sampleQuery.sampleUrl);
telemetry.trackException(
new Error(errorTypes.NETWORK_ERROR),
SeverityLevel.Error,
{
ComponentName: componentNames.GET_SNIPPET_ACTION,
QuerySignature: `${sampleQuery.selectedVerb} ${sanitizedUrl}`,
Message: `${error}`
Language: language,
Message: errorMessage
}
);
return dispatch(getSnippetError(error));
Expand Down
1 change: 1 addition & 0 deletions src/app/services/graph-constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export const ADMIN_AUTH_URL = 'https://signIn.microsoftonline.com/common/adminco
export const TOKEN_URL = 'https://signIn.microsoftonline.com/common/oauth2/v2.0/token';
export const DEFAULT_USER_SCOPES = 'openid profile User.Read';
export const DEVX_API_URL = 'https://graphexplorerapi.azurewebsites.net';
export const GRAPH_API_SANDBOX_URL = 'https://proxy.apisandbox.msdn.microsoft.com';
26 changes: 19 additions & 7 deletions src/app/utils/open-api-parser.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
import {
IOpenApiParseContent, IParameters, IParameterValue,
IParsedOpenApiResponse, IPathValue, IQueryParameter
IOpenApiParseContent,
IParameters,
IParameterValue,
IParsedOpenApiResponse,
IPathValue,
IQueryParameter,
} from '../../types/open-api';

export function parseOpenApiResponse(params: IOpenApiParseContent): IParsedOpenApiResponse {
const { response: { paths }, url } = params;
export function parseOpenApiResponse(
params: IOpenApiParseContent
): IParsedOpenApiResponse {
const {
response: { paths },
url,
} = params;

try {
const parameters: IParameters[] = [];
Expand All @@ -16,7 +25,7 @@ export function parseOpenApiResponse(params: IOpenApiParseContent): IParsedOpenA
parameters.push({
verb,
values: getVerbParameterValues(pathValues[`${verb}`]),
links: getLinkValues(pathValues[`${verb}`])
links: getLinkValues(pathValues[`${verb}`]),
});
});

Expand All @@ -35,7 +44,10 @@ function getVerbParameterValues(values: IPathValue): IParameterValue[] {
if (parameter.name && parameter.in === 'query') {
parameterValues.push({
name: parameter.name,
items: (parameter.schema && parameter.schema.items) ? parameter.schema.items.enum : []
items:
parameter.schema && parameter.schema.items
? parameter.schema.items.enum
: [],
});
}
});
Expand All @@ -52,4 +64,4 @@ function getLinkValues(values: IPathValue): string[] {
}
}
return [];
}
}
73 changes: 55 additions & 18 deletions src/app/utils/query-url-sanitization.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
/* eslint-disable no-useless-escape */
import { GRAPH_URL } from '../services/graph-constants';
import { isAllAlpha, sanitizeQueryParameter } from './query-parameter-sanitization';
import {
isAllAlpha,
sanitizeQueryParameter,
} from './query-parameter-sanitization';
import { parseSampleUrl } from './sample-url-generation';

// Matches strings with deprecation identifier
Expand Down Expand Up @@ -33,25 +36,48 @@ export function isFunctionCall(segment: string): boolean {
}

/**
* @param url - query url to be sanitized e.g. https://graph.microsoft.com/v1.0/users/{user-id}
* Sanitize Graph API Sandbox URL used when a user is not signed in
* @param url - URL to be sanitized
*/
export function sanitizeGraphAPISandboxUrl(url: string): string {
const urlObject = new URL(url);
const queryParams = urlObject.searchParams;
// This query parameter holds Graph query URL
const queryUrl = queryParams.get('url');
if (queryUrl) {
queryParams.set('url', sanitizeQueryUrl(queryUrl));
}
return urlObject.toString();
}

/**
* @param url - query URL to be sanitized e.g. https://graph.microsoft.com/v1.0/users/{user-id}
*/
export function sanitizeQueryUrl(url: string): string {
url = decodeURIComponent(url);

const { search, queryVersion, requestUrl } = parseSampleUrl(url);
const queryString: string = search ? `?${sanitizeQueryParameters(search)}` : '';
const queryString: string = search
? `?${sanitizeQueryParameters(search)}`
: '';

// Sanitize item path specified in query url
let resourceUrl = requestUrl;
if (resourceUrl) {
resourceUrl = requestUrl.replace(ITEM_PATH_REGEX, (match: string): string => {
return `${match.substring(0, match.indexOf(':'))}:<value>`;
});
resourceUrl = requestUrl.replace(
ITEM_PATH_REGEX,
(match: string): string => {
return `${match.substring(0, match.indexOf(':'))}:<value>`;
}
);

// Split requestUrl into segments that can be sanitized individually
const urlSegments = resourceUrl.split('/');
urlSegments.forEach((segment, index) => {
const sanitizedSegment = sanitizePathSegment(urlSegments[index - 1], segment);
const sanitizedSegment = sanitizePathSegment(
urlSegments[index - 1],
segment
);
resourceUrl = resourceUrl.replace(segment, sanitizedSegment);
});
}
Expand All @@ -69,23 +95,34 @@ export function sanitizeQueryUrl(url: string): string {
function sanitizePathSegment(previousSegment: string, segment: string): string {
const segmentsToIgnore = ['$value', '$count', '$ref'];

if (isAllAlpha(segment) || isDeprecation(segment) || SANITIZED_ITEM_PATH_REGEX.test(segment)
|| segmentsToIgnore.includes(segment.toLowerCase()) || ENTITY_NAME_REGEX.test(segment)) {
if (
isAllAlpha(segment) ||
isDeprecation(segment) ||
SANITIZED_ITEM_PATH_REGEX.test(segment) ||
segmentsToIgnore.includes(segment.toLowerCase()) ||
ENTITY_NAME_REGEX.test(segment)
) {
return segment;
}

// Check if segment is in this form: users('<some-id>|<UPN>') and tranform to users(<value>)
if (isFunctionCall(segment)) {
const openingBracketIndex = segment.indexOf('(');
const textWithinBrackets = segment.substr(openingBracketIndex + 1, segment.length - 2);
const sanitizedText = textWithinBrackets.split(',').map(text => {
if (text.includes('=')) {
let key = text.split('=')[0];
key = !isAllAlpha(key) ? '<key>' : key;
return `${key}=<value>`;
}
return '<value>';
}).join(',');
const textWithinBrackets = segment.substr(
openingBracketIndex + 1,
segment.length - 2
);
const sanitizedText = textWithinBrackets
.split(',')
.map((text) => {
if (text.includes('=')) {
let key = text.split('=')[0];
key = !isAllAlpha(key) ? '<key>' : key;
return `${key}=<value>`;
}
return '<value>';
})
.join(',');
return `${segment.substring(0, openingBracketIndex)}(${sanitizedText})`;
}

Expand Down
4 changes: 2 additions & 2 deletions src/app/views/query-response/snippets/snippets-helper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ function Snippet(props: ISnippetProps) {
iconProps={copyIcon}
onClick={async () => {
genericCopy(snippet);
trackCopyEvent(sampleQuery, language);
trackSnippetCopyEvent(sampleQuery, language);
}}
/>
<Monaco
Expand All @@ -90,7 +90,7 @@ function Snippet(props: ISnippetProps) {
);
}

function trackCopyEvent(query: IQuery, language: string) {
function trackSnippetCopyEvent(query: IQuery, language: string) {
const sanitizedUrl = sanitizeQueryUrl(query.sampleUrl);
telemetry.trackEvent(eventTypes.BUTTON_CLICK_EVENT,
{
Expand Down
4 changes: 2 additions & 2 deletions src/app/views/query-runner/request/auth/Auth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export function Auth(props: any) {

const handleCopy = async () => {
await genericCopy(accessToken!);
trackCopyEvent();
trackTokenCopyEvent();
};

useEffect(() => {
Expand Down Expand Up @@ -70,7 +70,7 @@ export function Auth(props: any) {
</div>);
}

function trackCopyEvent() {
function trackTokenCopyEvent() {
telemetry.trackEvent(
eventTypes.BUTTON_CLICK_EVENT,
{
Expand Down
3 changes: 2 additions & 1 deletion src/telemetry/ITelemetry.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { SeverityLevel } from '@microsoft/applicationinsights-web';
import { ComponentType } from 'react';
import { IQuery } from '../types/query-runner';
import { IRequestOptions } from '../types/request';

export default interface ITelemetry {
initialize(): void;
trackEvent(name: string, properties: {}): void;
trackReactComponent(Component: ComponentType, componentName?: string): ComponentType;
trackTabClickEvent(tabKey: string, sampleQuery: IQuery): void;
trackTabClickEvent(tabKey: string, sampleQuery?: IQuery): void;
trackLinkClickEvent(url: string, componentName: string): void;
trackException(error: Error, severityLevel: SeverityLevel, properties: {}): void;
}
Loading