2021-07-22 22:21:09 +00:00
|
|
|
import {auth} from '@googleapis/oauth2';
|
|
|
|
import {ApiError} from 'app/common/ApiError';
|
|
|
|
import {parseSubdomain} from 'app/common/gristUrls';
|
|
|
|
import {expressWrap} from 'app/server/lib/expressWrap';
|
2022-07-04 14:14:55 +00:00
|
|
|
import log from 'app/server/lib/log';
|
(core) support python3 in grist-core, and running engine via docker and/or gvisor
Summary:
* Moves essential plugins to grist-core, so that basic imports (e.g. csv) work.
* Adds support for a `GRIST_SANDBOX_FLAVOR` flag that can systematically override how the data engine is run.
- `GRIST_SANDBOX_FLAVOR=pynbox` is "classic" nacl-based sandbox.
- `GRIST_SANDBOX_FLAVOR=docker` runs engines in individual docker containers. It requires an image specified in `sandbox/docker` (alternative images can be named with `GRIST_SANDBOX` flag - need to contain python and engine requirements). It is a simple reference implementation for sandboxing.
- `GRIST_SANDBOX_FLAVOR=unsandboxed` runs whatever local version of python is specified by a `GRIST_SANDBOX` flag directly, with no sandboxing. Engine requirements must be installed, so an absolute path to a python executable in a virtualenv is easiest to manage.
- `GRIST_SANDBOX_FLAVOR=gvisor` runs the data engine via gvisor's runsc. Experimental, with implementation not included in grist-core. Since gvisor runs on Linux only, this flavor supports wrapping the sandboxes in a single shared docker container.
* Tweaks some recent express query parameter code to work in grist-core, which has a slightly different version of express (smoke test doesn't catch this since in Jenkins core is built within a workspace that has node_modules, and wires get crossed - in a dev environment the problem on master can be seen by doing `buildtools/build_core.sh /tmp/any_path_outside_grist`).
The new sandbox options do not have tests yet, nor does this they change the behavior of grist servers today. They are there to clean up and consolidate a collection of patches I've been using that were getting cumbersome, and make it easier to run experiments.
I haven't looked closely at imports beyond core.
Test Plan: tested manually against regular grist and grist-core, including imports
Reviewers: alexmojaki, dsagal
Reviewed By: alexmojaki
Differential Revision: https://phab.getgrist.com/D2942
2021-07-27 23:43:21 +00:00
|
|
|
import {getOriginUrl, optStringParam, stringParam} from 'app/server/lib/requestUtils';
|
2021-07-21 08:46:03 +00:00
|
|
|
import * as express from 'express';
|
2021-07-22 22:21:09 +00:00
|
|
|
import {URL} from 'url';
|
2021-07-21 08:46:03 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Google Auth Endpoint for performing server side authentication. More information can be found
|
|
|
|
* at https://developers.google.com/identity/protocols/oauth2/web-server.
|
|
|
|
*
|
|
|
|
* Environmental variables used:
|
|
|
|
* - GOOGLE_CLIENT_ID : key obtained from a Google Project, not secret can be shared publicly,
|
|
|
|
* the same client id is used in Google Drive Plugin
|
|
|
|
* - GOOGLE_CLIENT_SECRET: secret key for Google Project, can't be shared - it is used to (d)encrypt
|
|
|
|
* data that we obtain from Google Auth Service (all done in the api)
|
2021-09-30 08:19:22 +00:00
|
|
|
* - GOOGLE_DRIVE_SCOPE: scope requested for the Google drive integration (defaults to drive.file which allows
|
|
|
|
* to create files and get files via Google Drive Picker)
|
2021-07-21 08:46:03 +00:00
|
|
|
*
|
|
|
|
* High level description:
|
|
|
|
*
|
|
|
|
* Each API endpoint that wants to talk to Google Api needs an access_token that identifies our application
|
|
|
|
* and permits us to access some of the user data or work with the API on the user's behalf (examples for that are
|
|
|
|
* Google Drive plugin and Google Export endpoint, [Send to Google Drive] feature on the UI).)
|
|
|
|
* To obtain this token on the server-side, the application needs to redirect the user to a
|
|
|
|
* Google Consent Screen - where the user can log into his account and give consent for the permissions
|
|
|
|
* we need. Permissions are defined by SCOPES - that exactly describes what we are allowed to do.
|
|
|
|
* More on scopes can be read on https://developers.google.com/identity/protocols/oauth2/scopes.
|
|
|
|
* When we are redirecting the user to a Google Consent Screen, we are also sending a static URL for an endpoint
|
|
|
|
* where Google will redirect the user after he gives us permissions or declines our request. For that, we are exposing
|
|
|
|
* static URL http://docs.getgrist.com/auth/google (on prod) or http://localhost:8080/auth/google (dev).
|
|
|
|
*
|
|
|
|
* NOTE: Actually, we are exposing only auth/google endpoint that can be accessed in various ways, including any
|
|
|
|
* subdomain, but Google will always redirect to the configured endpoint (example: http://docs.getgrist.com/auth/google)
|
|
|
|
*
|
|
|
|
* This endpoint will render a simple page (see /static/message.html) that will immediately post
|
|
|
|
* a message to the parent window with a response from Google in the form of { code? : string, error?: string }.
|
|
|
|
* Code returned from Google will be an encrypted access_token that the client-side code should use to invoke
|
|
|
|
* the server-side API endpoint that wishes to call one of the Google API endpoints. A server-side endpoint can use
|
|
|
|
* "googleAuthTokenMiddleware" middleware to convert code to access_token (by making a separate call to Google
|
|
|
|
* for exchanging code for an access_token).
|
|
|
|
* This access_token could be stored in the user's session for further use, but since we are making only a single call
|
|
|
|
* very rarely, and access_token will expire eventually; it is better to acquire access_token every time.
|
|
|
|
* More on storing access_token offline can be read on:
|
|
|
|
* https://developers.google.com/identity/protocols/oauth2/web-server#obtainingaccesstokens
|
|
|
|
*
|
|
|
|
* How to use:
|
|
|
|
*
|
|
|
|
* To call server-side endpoint that expects access_token, first decorate it with "googleAuthTokenMiddleware"
|
|
|
|
* middleware, then perform "server-side" authentication with Google on the client-side to acquire an encrypted token.
|
|
|
|
* Client code should open up a popup window with an URL to Grist's Google Auth endpoint (/auth/google) and wait
|
|
|
|
* for a message from a popup window containing an encrypted token.
|
|
|
|
* Having encrypted token ("code"), a client can call the server-side endpoint by adding to the query string the code
|
|
|
|
* acquired from the popup window. Server endpoint (decorated by "googleAuthTokenMiddleware") will get access_token
|
|
|
|
* in a query string.
|
|
|
|
*/
|
|
|
|
|
|
|
|
// Path for the auth endpoint.
|
|
|
|
const authHandlerPath = "/auth/google";
|
|
|
|
|
|
|
|
// Redirect host after the Google Auth login form is completed. This reuses the same domain name
|
|
|
|
// as for Cognito login.
|
|
|
|
const AUTH_SUBDOMAIN = process.env.GRIST_ID_PREFIX ? `docs-${process.env.GRIST_ID_PREFIX}` : 'docs';
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return a full url for Google Auth handler. Examples are:
|
|
|
|
*
|
|
|
|
* http://localhost:8080/auth/google in dev
|
|
|
|
* https://docs-s.getgrist.com in staging
|
|
|
|
* https://docs.getgrist.com in prod
|
|
|
|
*/
|
|
|
|
function getFullAuthEndpointUrl(): string {
|
|
|
|
const homeUrl = process.env.APP_HOME_URL;
|
|
|
|
// if homeUrl is localhost - (in dev environment) - use the development url
|
|
|
|
if (homeUrl && new URL(homeUrl).hostname === "localhost") {
|
|
|
|
return `${homeUrl}${authHandlerPath}`;
|
|
|
|
}
|
|
|
|
const homeBaseDomain = homeUrl && parseSubdomain(new URL(homeUrl).host).base;
|
|
|
|
const baseDomain = homeBaseDomain || '.getgrist.com';
|
|
|
|
return `https://${AUTH_SUBDOMAIN}${baseDomain}${authHandlerPath}`;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Middleware for obtaining access_token from Google Auth Service.
|
|
|
|
* It expects code query parameter (provided by frontend code) add adds to access_token to query parameters.
|
|
|
|
*/
|
|
|
|
export async function googleAuthTokenMiddleware(
|
|
|
|
req: express.Request,
|
|
|
|
res: express.Response,
|
|
|
|
next: express.NextFunction) {
|
|
|
|
// If access token is in place, proceed
|
(core) support python3 in grist-core, and running engine via docker and/or gvisor
Summary:
* Moves essential plugins to grist-core, so that basic imports (e.g. csv) work.
* Adds support for a `GRIST_SANDBOX_FLAVOR` flag that can systematically override how the data engine is run.
- `GRIST_SANDBOX_FLAVOR=pynbox` is "classic" nacl-based sandbox.
- `GRIST_SANDBOX_FLAVOR=docker` runs engines in individual docker containers. It requires an image specified in `sandbox/docker` (alternative images can be named with `GRIST_SANDBOX` flag - need to contain python and engine requirements). It is a simple reference implementation for sandboxing.
- `GRIST_SANDBOX_FLAVOR=unsandboxed` runs whatever local version of python is specified by a `GRIST_SANDBOX` flag directly, with no sandboxing. Engine requirements must be installed, so an absolute path to a python executable in a virtualenv is easiest to manage.
- `GRIST_SANDBOX_FLAVOR=gvisor` runs the data engine via gvisor's runsc. Experimental, with implementation not included in grist-core. Since gvisor runs on Linux only, this flavor supports wrapping the sandboxes in a single shared docker container.
* Tweaks some recent express query parameter code to work in grist-core, which has a slightly different version of express (smoke test doesn't catch this since in Jenkins core is built within a workspace that has node_modules, and wires get crossed - in a dev environment the problem on master can be seen by doing `buildtools/build_core.sh /tmp/any_path_outside_grist`).
The new sandbox options do not have tests yet, nor does this they change the behavior of grist servers today. They are there to clean up and consolidate a collection of patches I've been using that were getting cumbersome, and make it easier to run experiments.
I haven't looked closely at imports beyond core.
Test Plan: tested manually against regular grist and grist-core, including imports
Reviewers: alexmojaki, dsagal
Reviewed By: alexmojaki
Differential Revision: https://phab.getgrist.com/D2942
2021-07-27 23:43:21 +00:00
|
|
|
if (!optStringParam(req.query.code)) {
|
2021-07-21 08:46:03 +00:00
|
|
|
throw new ApiError("Google Auth endpoint requires a code parameter in the query string", 400);
|
|
|
|
} else {
|
|
|
|
try {
|
2021-09-30 08:19:22 +00:00
|
|
|
const oAuth2Client = getGoogleAuth();
|
2021-07-21 08:46:03 +00:00
|
|
|
// Decrypt code that was send back from Google Auth service. Uses GOOGLE_CLIENT_SECRET key.
|
2021-11-29 20:12:45 +00:00
|
|
|
const tokenResponse = await oAuth2Client.getToken(stringParam(req.query.code, 'code'));
|
2021-07-21 08:46:03 +00:00
|
|
|
// Get the access token (access token will be present in a default request configuration).
|
|
|
|
const access_token = tokenResponse.tokens.access_token!;
|
|
|
|
req.query.access_token = access_token;
|
|
|
|
next();
|
|
|
|
} catch (err) {
|
|
|
|
log.error("GoogleAuth - Error", err);
|
|
|
|
throw err;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Adds a static Google Auth endpoint. This will be used by Google Auth Service to redirect back, after the user
|
|
|
|
* finishes a signing and a consent flow. Google will pass 2 arguments in a query string:
|
|
|
|
* - code: encrypted access token when user gave permissions
|
|
|
|
* - error: error code when user declined our request.
|
|
|
|
*/
|
|
|
|
export function addGoogleAuthEndpoint(
|
|
|
|
expressApp: express.Application,
|
|
|
|
messagePage: (req: express.Request, res: express.Response, message: any) => any
|
|
|
|
) {
|
|
|
|
if (!process.env.GOOGLE_CLIENT_SECRET) {
|
(core) support python3 in grist-core, and running engine via docker and/or gvisor
Summary:
* Moves essential plugins to grist-core, so that basic imports (e.g. csv) work.
* Adds support for a `GRIST_SANDBOX_FLAVOR` flag that can systematically override how the data engine is run.
- `GRIST_SANDBOX_FLAVOR=pynbox` is "classic" nacl-based sandbox.
- `GRIST_SANDBOX_FLAVOR=docker` runs engines in individual docker containers. It requires an image specified in `sandbox/docker` (alternative images can be named with `GRIST_SANDBOX` flag - need to contain python and engine requirements). It is a simple reference implementation for sandboxing.
- `GRIST_SANDBOX_FLAVOR=unsandboxed` runs whatever local version of python is specified by a `GRIST_SANDBOX` flag directly, with no sandboxing. Engine requirements must be installed, so an absolute path to a python executable in a virtualenv is easiest to manage.
- `GRIST_SANDBOX_FLAVOR=gvisor` runs the data engine via gvisor's runsc. Experimental, with implementation not included in grist-core. Since gvisor runs on Linux only, this flavor supports wrapping the sandboxes in a single shared docker container.
* Tweaks some recent express query parameter code to work in grist-core, which has a slightly different version of express (smoke test doesn't catch this since in Jenkins core is built within a workspace that has node_modules, and wires get crossed - in a dev environment the problem on master can be seen by doing `buildtools/build_core.sh /tmp/any_path_outside_grist`).
The new sandbox options do not have tests yet, nor does this they change the behavior of grist servers today. They are there to clean up and consolidate a collection of patches I've been using that were getting cumbersome, and make it easier to run experiments.
I haven't looked closely at imports beyond core.
Test Plan: tested manually against regular grist and grist-core, including imports
Reviewers: alexmojaki, dsagal
Reviewed By: alexmojaki
Differential Revision: https://phab.getgrist.com/D2942
2021-07-27 23:43:21 +00:00
|
|
|
log.warn("Failed to create GoogleAuth endpoint: GOOGLE_CLIENT_SECRET is not defined");
|
2021-07-21 08:46:03 +00:00
|
|
|
expressApp.get(authHandlerPath, expressWrap(async (req: express.Request, res: express.Response) => {
|
|
|
|
throw new Error("Send to Google Drive is not configured.");
|
|
|
|
}));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
log.info(`GoogleAuth - auth handler at ${getFullAuthEndpointUrl()}`);
|
|
|
|
|
|
|
|
expressApp.get(authHandlerPath, expressWrap(async (req: express.Request, res: express.Response) => {
|
2021-07-21 16:38:57 +00:00
|
|
|
|
2021-07-21 08:46:03 +00:00
|
|
|
// Test if the code is in a query string. Google sends it back after user has given a concent for
|
|
|
|
// our request. It is encrypted (with CLIENT_SECRET) and signed with redirect url.
|
2021-07-21 16:38:57 +00:00
|
|
|
// In state query parameter we will receive an url that was send as part of the request to Google.
|
|
|
|
|
(core) support python3 in grist-core, and running engine via docker and/or gvisor
Summary:
* Moves essential plugins to grist-core, so that basic imports (e.g. csv) work.
* Adds support for a `GRIST_SANDBOX_FLAVOR` flag that can systematically override how the data engine is run.
- `GRIST_SANDBOX_FLAVOR=pynbox` is "classic" nacl-based sandbox.
- `GRIST_SANDBOX_FLAVOR=docker` runs engines in individual docker containers. It requires an image specified in `sandbox/docker` (alternative images can be named with `GRIST_SANDBOX` flag - need to contain python and engine requirements). It is a simple reference implementation for sandboxing.
- `GRIST_SANDBOX_FLAVOR=unsandboxed` runs whatever local version of python is specified by a `GRIST_SANDBOX` flag directly, with no sandboxing. Engine requirements must be installed, so an absolute path to a python executable in a virtualenv is easiest to manage.
- `GRIST_SANDBOX_FLAVOR=gvisor` runs the data engine via gvisor's runsc. Experimental, with implementation not included in grist-core. Since gvisor runs on Linux only, this flavor supports wrapping the sandboxes in a single shared docker container.
* Tweaks some recent express query parameter code to work in grist-core, which has a slightly different version of express (smoke test doesn't catch this since in Jenkins core is built within a workspace that has node_modules, and wires get crossed - in a dev environment the problem on master can be seen by doing `buildtools/build_core.sh /tmp/any_path_outside_grist`).
The new sandbox options do not have tests yet, nor does this they change the behavior of grist servers today. They are there to clean up and consolidate a collection of patches I've been using that were getting cumbersome, and make it easier to run experiments.
I haven't looked closely at imports beyond core.
Test Plan: tested manually against regular grist and grist-core, including imports
Reviewers: alexmojaki, dsagal
Reviewed By: alexmojaki
Differential Revision: https://phab.getgrist.com/D2942
2021-07-27 23:43:21 +00:00
|
|
|
if (optStringParam(req.query.code)) {
|
2021-07-21 08:46:03 +00:00
|
|
|
log.debug("GoogleAuth - response from Google with valid code");
|
2021-11-29 20:12:45 +00:00
|
|
|
messagePage(req, res, { code: stringParam(req.query.code, 'code'),
|
|
|
|
origin: stringParam(req.query.state, 'state') });
|
(core) support python3 in grist-core, and running engine via docker and/or gvisor
Summary:
* Moves essential plugins to grist-core, so that basic imports (e.g. csv) work.
* Adds support for a `GRIST_SANDBOX_FLAVOR` flag that can systematically override how the data engine is run.
- `GRIST_SANDBOX_FLAVOR=pynbox` is "classic" nacl-based sandbox.
- `GRIST_SANDBOX_FLAVOR=docker` runs engines in individual docker containers. It requires an image specified in `sandbox/docker` (alternative images can be named with `GRIST_SANDBOX` flag - need to contain python and engine requirements). It is a simple reference implementation for sandboxing.
- `GRIST_SANDBOX_FLAVOR=unsandboxed` runs whatever local version of python is specified by a `GRIST_SANDBOX` flag directly, with no sandboxing. Engine requirements must be installed, so an absolute path to a python executable in a virtualenv is easiest to manage.
- `GRIST_SANDBOX_FLAVOR=gvisor` runs the data engine via gvisor's runsc. Experimental, with implementation not included in grist-core. Since gvisor runs on Linux only, this flavor supports wrapping the sandboxes in a single shared docker container.
* Tweaks some recent express query parameter code to work in grist-core, which has a slightly different version of express (smoke test doesn't catch this since in Jenkins core is built within a workspace that has node_modules, and wires get crossed - in a dev environment the problem on master can be seen by doing `buildtools/build_core.sh /tmp/any_path_outside_grist`).
The new sandbox options do not have tests yet, nor does this they change the behavior of grist servers today. They are there to clean up and consolidate a collection of patches I've been using that were getting cumbersome, and make it easier to run experiments.
I haven't looked closely at imports beyond core.
Test Plan: tested manually against regular grist and grist-core, including imports
Reviewers: alexmojaki, dsagal
Reviewed By: alexmojaki
Differential Revision: https://phab.getgrist.com/D2942
2021-07-27 23:43:21 +00:00
|
|
|
} else if (optStringParam(req.query.error)) {
|
2021-11-29 20:12:45 +00:00
|
|
|
log.debug("GoogleAuth - response from Google with error code", stringParam(req.query.error, 'error'));
|
|
|
|
if (stringParam(req.query.error, 'error') === "access_denied") {
|
|
|
|
messagePage(req, res, { error: stringParam(req.query.error, 'error'),
|
|
|
|
origin: stringParam(req.query.state, 'state') });
|
2021-07-21 08:46:03 +00:00
|
|
|
} else {
|
|
|
|
// This should not happen, either code or error is a mandatory query parameter.
|
|
|
|
throw new ApiError("Error authenticating with Google", 500);
|
|
|
|
}
|
|
|
|
} else {
|
2021-09-30 08:19:22 +00:00
|
|
|
const oAuth2Client = getGoogleAuth();
|
2021-11-29 20:12:45 +00:00
|
|
|
const scope = stringParam(req.query.scope, 'scope');
|
2021-07-21 16:38:57 +00:00
|
|
|
// Create url for origin parameter for a popup window.
|
2021-07-22 22:21:09 +00:00
|
|
|
const origin = getOriginUrl(req);
|
2021-07-21 08:46:03 +00:00
|
|
|
const authUrl = oAuth2Client.generateAuthUrl({
|
|
|
|
scope,
|
2021-07-21 16:38:57 +00:00
|
|
|
prompt: 'select_account',
|
|
|
|
state: origin
|
2021-07-21 08:46:03 +00:00
|
|
|
});
|
|
|
|
log.debug(`GoogleAuth - redirecting to Google consent screen`, {
|
|
|
|
authUrl,
|
2021-07-21 16:38:57 +00:00
|
|
|
scope,
|
|
|
|
state: origin
|
2021-07-21 08:46:03 +00:00
|
|
|
});
|
|
|
|
res.redirect(authUrl);
|
|
|
|
}
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Builds the OAuth2 Google client.
|
|
|
|
*/
|
2021-09-30 08:19:22 +00:00
|
|
|
export function getGoogleAuth() {
|
2021-07-21 08:46:03 +00:00
|
|
|
const CLIENT_ID = process.env.GOOGLE_CLIENT_ID;
|
|
|
|
const CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET;
|
|
|
|
const oAuth2Client = new auth.OAuth2(CLIENT_ID, CLIENT_SECRET, getFullAuthEndpointUrl());
|
|
|
|
return oAuth2Client;
|
|
|
|
}
|