Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Show information about CORS policy if query fails due to CORS restrictions #1318

Merged
merged 3 commits into from
Jan 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 3 additions & 7 deletions src/app/services/actions/query-action-creator-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import {
GraphRequest,
ResponseType
} from '@microsoft/microsoft-graph-client';
import { MSALAuthenticationProviderOptions } from
'@microsoft/microsoft-graph-client/lib/src/MSALAuthenticationProviderOptions';
import {
MSALAuthenticationProviderOptions
} from '@microsoft/microsoft-graph-client/lib/src/MSALAuthenticationProviderOptions';

import { IAction } from '../../../types/action';
import { ContentType } from '../../../types/enums';
Expand Down Expand Up @@ -226,8 +227,3 @@ export function parseResponse(
}
return response;
}

export function queryResultsInCorsError(sampleQuery: IQuery) {
const requestUrl = new URL(sampleQuery.sampleUrl);
return requestUrl.pathname.match(/\/content(\/)*$/i) != null;
}
101 changes: 19 additions & 82 deletions src/app/services/actions/query-action-creators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,19 @@ import { ContentType } from '../../../types/enums';
import { IHistoryItem } from '../../../types/history';
import { IQuery } from '../../../types/query-runner';
import { IStatus } from '../../../types/status';
import { sanitizeQueryUrl } from '../../utils/query-url-sanitization';
import { parseSampleUrl } from '../../utils/sample-url-generation';
import { setStatusMessage } from '../../utils/status-message';
import { writeHistoryData } from '../../views/sidebar/history/history-utils';
import { CORS_ERROR_QUERIES } from '../graph-constants';
import {
anonymousRequest,
authenticatedRequest,
generateResponseDownloadUrl,
isFileResponse,
isImageResponse,
parseResponse,
queryResponse,
queryResultsInCorsError
queryResponse
} from './query-action-creator-util';
import { setQueryResponseStatus } from './query-status-action-creator';
import { addHistoryItem } from './request-history-action-creators';
Expand Down Expand Up @@ -96,7 +98,10 @@ export function runQuery(query: IQuery): Function {

if (response) {
status.status = response.status;
status.statusText = response.statusText === '' ? setStatusMessage(response.status) : response.statusText;
status.statusText =
response.statusText === ''
? setStatusMessage(response.status)
: response.statusText;
}

if (response && response.ok) {
Expand Down Expand Up @@ -124,92 +129,24 @@ export function runQuery(query: IQuery): Function {
})
);
} else {
if (
response.status === 0 &&
tokenPresent &&
queryResultsInCorsError(query)
) {
fetchContentDownloadUrl(query, dispatch);
} else {
dispatch(
queryResponse({
body: result,
headers: respHeaders
})
);
return dispatch(setQueryResponseStatus(status));
const { requestUrl } = parseSampleUrl(sanitizeQueryUrl(query.sampleUrl));
// check if this is one of the queries that result in a CORS error
if (response.status === 0 && CORS_ERROR_QUERIES.has(requestUrl)) {
result = {
throwsCorsError: true,
workload: CORS_ERROR_QUERIES.get(requestUrl)
};
}
}
}
}

async function fetchContentDownloadUrl(
sampleQuery: IQuery,
dispatch: Function
) {
const requestUrl = new URL(sampleQuery.sampleUrl);
const isOriginalFormat = !requestUrl.searchParams.has('format');

// drop any search params from query URL
requestUrl.search = '';

// remove /content from path
requestUrl.pathname = requestUrl.pathname.replace(/\/content(\/)*$/i, '');

// set new sampleUrl for fetching download URL
const query: IQuery = { ...sampleQuery };
query.sampleUrl = requestUrl.toString();

const status: IStatus = {
messageType: MessageBarType.error,
ok: false,
status: 400,
statusText: ''
};

authenticatedRequest(dispatch, query)
.then(async (response: Response) => {
if (response) {
status.status = response.status;
status.statusText = response.statusText;
status.ok = response.ok;

if (response.ok) {
status.messageType = MessageBarType.success;

const result = await parseResponse(response);
const downloadUrl = result['@microsoft.graph.downloadUrl'];

dispatch(
queryResponse({
body: {
contentDownloadUrl: downloadUrl,
isOriginalFormat,
isWorkaround: true
},
headers: null
})
);
}
} else {
dispatch(
queryResponse({
body: null,
headers: null
})
);
}
return dispatch(setQueryResponseStatus(status));
})
.catch(async (error: any) => {
dispatch(
queryResponse({
body: error,
headers: null
body: result,
headers: respHeaders
})
);
return dispatch(setQueryResponseStatus(status));
});
}
}
}

async function createHistory(
Expand Down
24 changes: 21 additions & 3 deletions src/app/services/graph-constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,29 @@ export enum ACCOUNT_TYPE {
export enum PERMS_SCOPE {
WORK = 'DelegatedWork',
APPLICATION = 'Application',
PERSONAL = 'DelegatedPersonal',
PERSONAL = 'DelegatedPersonal'
}
export enum WORKLOAD {
ONEDRIVE = 'OneDrive',
O365REPORTING = 'O365Reporting'
}
export const ADAPTIVE_CARD_URL =
'https://templates.adaptivecards.io/graph.microsoft.com';
export const GRAPH_TOOOLKIT_EXAMPLE_URL = 'https://mgt.dev/?path=/story';
export const MOZILLA_CORS_DOCUMENTATION_LINK =
'https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS';
export const ONE_DRIVE_CONTENT_DOWNLOAD_DOCUMENTATION_LINK =
'https://docs.microsoft.com/en-us/onedrive/developer/rest-api/concepts/working-with-cors' +
'#downloading-onedrive-files-in-javascript-apps';
'https://docs.microsoft.com/en-us/graph/api/driveitem-get-content?view=graph-rest-1.0&tabs=http';
export const CORS_ERROR_QUERIES = new Map([
['groups/{groups-id}/drive/items/{items-id}/content', WORKLOAD.ONEDRIVE],
['sites/{sites-id}/drive/items/{items-id}/content', WORKLOAD.ONEDRIVE],
['users/{users-id}/drive/items/{items-id}/content', WORKLOAD.ONEDRIVE],
['drives/{drives-id}/items/{items-id}/content', WORKLOAD.ONEDRIVE],
['shares/{shares-id}/driveItem/content', WORKLOAD.ONEDRIVE],
['me/drive/items/{items-id}/content', WORKLOAD.ONEDRIVE],
['me/drive/root:<value>/content', WORKLOAD.ONEDRIVE],
['reports/getYammerGroupsActivityDetail(period=<value>)', WORKLOAD.O365REPORTING],
['reports/getTeamsDeviceUsageUserCounts(period=<value>)', WORKLOAD.O365REPORTING],
['reports/getSharePointSiteUsageDetail(period=<value>)',WORKLOAD.O365REPORTING],
['reports/getOneDriveUsageFileCounts(period=<value>)', WORKLOAD.O365REPORTING]
]);
51 changes: 28 additions & 23 deletions src/app/views/app-sections/ResponseMessages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ import { IGraphResponse } from '../../../types/query-response';
import { IQuery } from '../../../types/query-runner';
import { runQuery } from '../../services/actions/query-action-creators';
import { setSampleQuery } from '../../services/actions/query-input-action-creators';
import { ONE_DRIVE_CONTENT_DOWNLOAD_DOCUMENTATION_LINK } from '../../services/graph-constants';
import {
MOZILLA_CORS_DOCUMENTATION_LINK,
ONE_DRIVE_CONTENT_DOWNLOAD_DOCUMENTATION_LINK,
WORKLOAD
} from '../../services/graph-constants';

interface ODataLink {
link: string;
Expand All @@ -15,14 +19,14 @@ interface ODataLink {

export function responseMessages(graphResponse: IGraphResponse, sampleQuery: IQuery, dispatch: Function) {

function getOdataLinkFromResponseBody(body: any): ODataLink | null {
function getOdataLinkFromResponseBody(responseBody: any): ODataLink | null {
const odataLinks = ['nextLink', 'deltaLink'];
let data = null;
if (body) {
if (responseBody) {
odataLinks.forEach(link => {
if (body[`@odata.${link}`]) {
if (responseBody[`@odata.${link}`]) {
data = {
link: body[`@odata.${link}`],
link: responseBody[`@odata.${link}`],
name: link
};
}
Expand Down Expand Up @@ -53,32 +57,33 @@ export function responseMessages(graphResponse: IGraphResponse, sampleQuery: IQu
);
}

// Display link to downlod file response
// Display link to download file response
if (body?.contentDownloadUrl) {
return (
<div>
<MessageBar messageBarType={MessageBarType.info}>
<MessageBar messageBarType={MessageBarType.warning}>
<FormattedMessage id={'This response contains unviewable content'} />
<Link href={body?.contentDownloadUrl} download>
<FormattedMessage id={'Click to download file'} />
</Link>&nbsp;
</MessageBar>
{body?.isWorkaround &&
<MessageBar messageBarType={MessageBarType.warning}>
<FormattedMessage id={'Response is result of workaround'} />
{!body?.isOriginalFormat &&
<span>
&nbsp;
<FormattedMessage id={'File response is available in original format only'} />
</span>
}
&nbsp;
<FormattedMessage id={'For more information'} />
<Link href={ONE_DRIVE_CONTENT_DOWNLOAD_DOCUMENTATION_LINK} target='_blank'>
<FormattedMessage id={'documentation'} />.
</Link>
</MessageBar>
}
</div>
);
}

// Show CORS compliance message
if (body?.throwsCorsError) {
const documentationLink = body?.workload === WORKLOAD.ONEDRIVE
? ONE_DRIVE_CONTENT_DOWNLOAD_DOCUMENTATION_LINK
: MOZILLA_CORS_DOCUMENTATION_LINK;
return (
<div>
<MessageBar messageBarType={MessageBarType.warning}>
<FormattedMessage id={'Response content not available due to CORS policy'} />
<Link href={documentationLink}>
<FormattedMessage id={'here'} />
</Link>.
</MessageBar>
</div>
);
}
Expand Down
14 changes: 2 additions & 12 deletions src/app/views/query-response/headers/ResponseHeaders.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@

import { IconButton, MessageBar, MessageBarType } from '@fluentui/react';
import { IconButton } from '@fluentui/react';
import React from 'react';
import { FormattedMessage } from 'react-intl';
import { useSelector } from 'react-redux';
import { RESPONSE_HEADERS_COPY_BUTTON } from '../../../../telemetry/component-names';
import { IRootState } from '../../../../types/root';
Expand All @@ -14,19 +13,10 @@ import { convertVhToPx, getResponseHeight } from '../../common/dimensions-adjust
const ResponseHeaders = () => {
const { dimensions: { response }, graphResponse, responseAreaExpanded, sampleQuery } =
useSelector((state: IRootState) => state);
const { body, headers } = graphResponse;
const { headers } = graphResponse;

const height = convertVhToPx(getResponseHeight(response.height, responseAreaExpanded), 100);

const responseIsDownloadUrl = body?.contentDownloadUrl;
if (!headers && responseIsDownloadUrl) {
return (
<MessageBar messageBarType={MessageBarType.warning}>
<FormattedMessage id={'Missing response headers for query workaround'} />
</MessageBar>
)
}

if (headers) {
return (
<div>
Expand Down
5 changes: 3 additions & 2 deletions src/app/views/query-response/response/Response.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,15 @@ const Response = () => {
const height = convertVhToPx(getResponseHeight(response.height, responseAreaExpanded), 100);

const contentDownloadUrl = body?.contentDownloadUrl;
const throwsCorsError = body?.throwsCorsError;
const contentType = getContentType(headers);
return (
<div style={{ display: 'block' }}>
{responseMessages(graphResponse, sampleQuery, dispatch)}
{headers &&
{!contentDownloadUrl && !throwsCorsError && headers &&
<ResponseDisplay
contentType={contentType}
body={!contentDownloadUrl && body}
body={body}
height={height}
/>}
</div>
Expand Down
11 changes: 4 additions & 7 deletions src/messages/GE.json
Original file line number Diff line number Diff line change
Expand Up @@ -381,11 +381,6 @@
"access_denied_consent": "Your consent to this permission has been blocked by your tenant admin. Ask your admin to grant you access and then try again.",
"This response contains unviewable content": "This response contains content that may not be viewable on this editor.",
"Click to download file": "Click to download file.",
"File response is available in original format only": "Due to the limitations of the workaround, the file is only available for download in its original format.",
"Response is result of workaround": "An alternative query has been used to fetch the download URL since the redirected URL from the original request is not CORS-compliant.",
"For more information": "For more information, see",
"documentation": "documentation",
"Missing response headers for query workaround": "Some headers, typically returned for this request, may be missing due to the limitations of the workaround.",
"Learn more": "Learn more",
"loading resources": "loading resources",
"Isolate": "Isolate",
Expand Down Expand Up @@ -432,5 +427,7 @@
"Download postman collection": "Download postman collection",
"You can export the entire list as a Postman Collection. If there are items in the list you would not want, select them to remove": "You can export the entire list as a Postman Collection. If there are items in the list you would not want, select them to remove",
"Copied": "Copied",
"Invalid whitespace in URL": "Invalid whitespace in URL"
}
"Invalid whitespace in URL": "Invalid whitespace in URL",
"Response content not available due to CORS policy": "The response content is not available in Graph Explorer due to CORS policy. You can execute this request in an API client, like Postman. Read more about CORS and understand how it works",
"here": "here"
}
7 changes: 1 addition & 6 deletions src/messages/GE_de-DE.json
Original file line number Diff line number Diff line change
Expand Up @@ -381,11 +381,6 @@
"access_denied_consent": "Ihre Zustimmung zu dieser Berechtigung wurde von Ihrem Mandantenadministrator blockiert. Bitten Sie Ihren Administrator, Ihnen Zugriff zu gewähren, und versuchen Sie es dann erneut.",
"This response contains unviewable content": "Diese Antwort enthält Inhalte, die in diesem Editor möglicherweise nicht angezeigt werden können.",
"Click to download file": "Zum Herunterladen der Datei klicken.",
"File response is available in original format only": "Aufgrund der Einschränkungen der Problemumgehung steht die Datei nur im ursprünglichen Format zum Download zur Verfügung.",
"Response is result of workaround": "Zum Abrufen der Download-URL wurde eine alternative Abfrage verwendet, da die umgeleitete URL aus der ursprünglichen Anforderung nicht CORS-kompatibel ist.",
"For more information": "Weitere Informationen finden Sie unter",
"documentation": "Dokumentation",
"Missing response headers for query workaround": "Einige Header, die normalerweise für diese Anforderung zurückgegeben werden, fehlen möglicherweise aufgrund der Einschränkungen der Problemumgehung.",
"Learn more": "Weitere Informationen",
"loading resources": "Ressourcen werden geladen",
"Isolate": "Isolieren",
Expand Down Expand Up @@ -431,4 +426,4 @@
"Preview collection": "Vorschau der Sammlung",
"Download postman collection": "Postman-Sammlung herunterladen",
"You can export the entire list as a Postman Collection. If there are items in the list you would not want, select them to remove": "Sie können die gesamte Liste als Postman-Sammlung exportieren. Wenn die Liste Elemente enthält, die Sie nicht benötigen, wählen Sie diese aus, um sie zu entfernen."
}
}
7 changes: 1 addition & 6 deletions src/messages/GE_es-ES.json
Original file line number Diff line number Diff line change
Expand Up @@ -381,11 +381,6 @@
"access_denied_consent": "El administrador del espacio empresarial ha bloqueado su consentimiento para este permiso. Pida al administrador que le conceda acceso e inténtelo de nuevo.",
"This response contains unviewable content": "Esta respuesta contiene contenido que puede que no se pueda ver en este editor.",
"Click to download file": "Haga clic para descargar el archivo.",
"File response is available in original format only": "Debido a las limitaciones de la solución alternativa, el archivo solo está disponible para su descarga en su formato original.",
"Response is result of workaround": "Se ha usado una consulta alternativa para capturar la dirección URL de descarga, ya que la dirección URL redirigida de la solicitud original no es compatible con CORS.",
"For more information": "Para más información, consulte",
"documentation": "documentación",
"Missing response headers for query workaround": "Es posible que falten algunos encabezados, que normalmente se devuelven para esta solicitud, debido a las limitaciones de la solución alternativa.",
"Learn more": "Más información",
"loading resources": "cargando recursos",
"Isolate": "Aislar",
Expand Down Expand Up @@ -431,4 +426,4 @@
"Preview collection": "Vista previa de la colección",
"Download postman collection": "Descargar colección Postman",
"You can export the entire list as a Postman Collection. If there are items in the list you would not want, select them to remove": "Se puede exportar toda la lista como una colección de Postman. Si hay elementos en la lista que no desea, selecciónelos para quitarlos."
}
}
Loading