mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
bd6a54e901
Summary: For methods other than `GET`, `HEAD`, and `OPTIONS`, allow cookie-based authentication only if a certain custom header is present. Specifically, we check that `X-Requested-With` is set to `XMLHttpRequest`. This is somewhat arbitrary, but allows us to use https://expressjs.com/en/api.html#req.xhr. A request send from a browser that sets a custom header will prompt a preflight check, giving us a chance to check if the origin is trusted. This diff deals with getting the header in place. There will be more work to do after this: * Make sure that all important endpoints are checking origin. Skimming code, /api endpoint check origin, and some but not all others. * Add tests spot-testing origin checks. * Check on cases that authenticate differently. - Check the websocket endpoint - it can be connected to from an arbitrary site; there is per-doc access control but probably better to lock it down more. - There may be old endpoints that authenticate based on knowledge of a client id rather than cookies. Test Plan: added a test Reviewers: dsagal Reviewed By: dsagal Differential Revision: https://phab.getgrist.com/D2631
151 lines
5.9 KiB
TypeScript
151 lines
5.9 KiB
TypeScript
import {get as getBrowserGlobals} from 'app/client/lib/browserGlobals';
|
|
import * as log from 'app/client/lib/log';
|
|
import {INotifyOptions, Notifier} from 'app/client/models/NotifyModel';
|
|
import {ApiErrorDetails} from 'app/common/ApiError';
|
|
import {fetchFromHome, pageHasHome} from 'app/common/urlUtils';
|
|
import isError = require('lodash/isError');
|
|
import pick = require('lodash/pick');
|
|
|
|
const G = getBrowserGlobals('document', 'window');
|
|
|
|
let _notifier: Notifier;
|
|
|
|
export class UserError extends Error {
|
|
public name: string = "UserError";
|
|
public key?: string;
|
|
constructor(message: string, options: {key?: string} = {}) {
|
|
super(message);
|
|
this.key = options.key;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This error causes Notifer to show the message with an upgrade link.
|
|
*/
|
|
export class NeedUpgradeError extends Error {
|
|
public name: string = 'NeedUpgradeError';
|
|
constructor(message: string = 'This feature is not available in your plan') {
|
|
super(message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the global Notifier instance used by subsequent reportError calls.
|
|
*/
|
|
export function setErrorNotifier(notifier: Notifier) {
|
|
_notifier = notifier;
|
|
}
|
|
|
|
// Returns application errors collected by NotifyModel. Used in tests.
|
|
export function getAppErrors(): string[] {
|
|
return _notifier.getFullAppErrors().map((e) => e.error.message);
|
|
}
|
|
|
|
/**
|
|
* Report an error to the user using the global Notifier instance. If the argument is a UserError
|
|
* or an error with a status in the 400 range, it indicates a user error. Otherwise, it's an
|
|
* application error, which the user can report to us as a bug.
|
|
*/
|
|
export function reportError(err: Error|string): void {
|
|
log.error(`ERROR:`, err);
|
|
_logError(err);
|
|
if (_notifier && !_notifier.isDisposed()) {
|
|
if (!isError(err)) {
|
|
err = new Error(String(err));
|
|
}
|
|
|
|
const details: ApiErrorDetails|undefined = (err as any).details;
|
|
const code: unknown = (err as any).code;
|
|
const status: unknown = (err as any).status;
|
|
const message = (details && details.userError) || err.message;
|
|
if (details && details.limit) {
|
|
// This is a notification about reaching a plan limit. Key prevents showing multiple
|
|
// notifications for the same type of limit.
|
|
const options: Partial<INotifyOptions> = {
|
|
title: "Reached plan limit",
|
|
key: `limit:${details.limit.quantity || message}`,
|
|
actions: ['upgrade'],
|
|
};
|
|
if (details.tips && details.tips.some(tip => tip.action === 'add-members')) {
|
|
// When adding members would fix a problem, give more specific advice.
|
|
options.title = "Add users as team members first";
|
|
options.actions = [];
|
|
}
|
|
_notifier.createUserError(message, options);
|
|
} else if (err.name === 'UserError' || (typeof status === 'number' && status >= 400 && status < 500)) {
|
|
// This is explicitly a user error, or one in the "Client Error" range, so treat it as user
|
|
// error rather than a bug. Using message as the key causes same-message notifications to
|
|
// replace previous ones rather than accumulate.
|
|
const options: Partial<INotifyOptions> = {key: (err as UserError).key || message};
|
|
if (details && details.tips && details.tips.some(tip => tip.action === 'ask-for-help')) {
|
|
options.actions = ['ask-for-help'];
|
|
}
|
|
_notifier.createUserError(message, options);
|
|
} else if (err.name === 'NeedUpgradeError') {
|
|
_notifier.createUserError(err.message, {actions: ['upgrade'], key: 'NEED_UPGRADE'});
|
|
} else if (code === 'AUTH_NO_EDIT') {
|
|
_notifier.createUserError(message, {key: code});
|
|
} else {
|
|
// If we don't recognize it, consider it an application error (bug) that the user should be
|
|
// able to report.
|
|
_notifier.createAppError(err);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set up error handlers, to report uncaught errors and rejections. These are logged to the
|
|
* console and displayed as notifications, when the notifications UI is set up.
|
|
*
|
|
* koUtil, if passed, will enable reporting errors from the evaluation of knockout computeds. It
|
|
* is passed-in as an argument to avoid creating a dependency when knockout isn't used otherwise.
|
|
*/
|
|
export function setUpErrorHandling(doReportError = reportError, koUtil?: any) {
|
|
if (koUtil) {
|
|
koUtil.setComputedErrorHandler((err: any) => doReportError(err));
|
|
}
|
|
|
|
// Report also uncaught JS errors and unhandled Promise rejections.
|
|
G.window.onerror = ((ev: any, url: any, lineNo: any, colNo: any, err: any) =>
|
|
doReportError(err || ev));
|
|
|
|
G.window.addEventListener('unhandledrejection', (ev: any) => {
|
|
const reason = ev.reason || (ev.detail && ev.detail.reason);
|
|
doReportError(reason || ev);
|
|
});
|
|
|
|
// Expose globally a function to report a notification. This is for compatibility with old UI;
|
|
// in new UI, it renders messages as user errors. New code should use `reportError()` instead.
|
|
G.window.gristNotify = (message: string) => doReportError(new UserError(message));
|
|
|
|
// Expose the function used in tests to get a list of errors in the notifier.
|
|
G.window.getAppErrors = getAppErrors;
|
|
}
|
|
|
|
/**
|
|
* Send information about a problem to the backend. This is crude; there is some
|
|
* over-logging (regular errors such as access rights or account limits) and
|
|
* under-logging (javascript errors during startup might never get reported).
|
|
*/
|
|
function _logError(error: Error|string) {
|
|
if (!pageHasHome()) { return; }
|
|
fetchFromHome('/api/log', {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
// Errors don't stringify, so pick out properties explicitly for errors.
|
|
event: (error instanceof Error) ? pick(error, Object.getOwnPropertyNames(error)) : error,
|
|
page: G.window.location.href,
|
|
browser: pick(G.window.navigator, ['language', 'platform', 'userAgent'])
|
|
}),
|
|
credentials: 'include',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-Requested-With': 'XMLHttpRequest',
|
|
}
|
|
}).catch(e => {
|
|
// There ... isn't much we can do about this.
|
|
// tslint:disable-next-line:no-console
|
|
console.warn('Failed to log event', event);
|
|
});
|
|
}
|