(core) Add account page option to allow Google login

Summary:
Enabled by default, the new checkbox is only visible to
users logged in with email/password, and controls whether it is possible
to log in to the same account via a Google account
(with matching email). When disabled, CognitoClient will refuse logins
from Google if a Grist account with the same email exists.

Test Plan:
Server and browser tests for setting flag. Manual tests to verify
Cognito doesn't allow signing in with Google when flag is disabled.

Reviewers: paulfitz

Reviewed By: paulfitz

Differential Revision: https://phab.getgrist.com/D3257
This commit is contained in:
George Gevoian
2022-02-14 13:26:21 -08:00
parent 99f3422217
commit e264094412
8 changed files with 115 additions and 10 deletions

View File

@@ -20,7 +20,7 @@ export const TEST_HTTPS_OFFSET = process.env.GRIST_TEST_HTTPS_OFFSET ?
// Database fields that we permit in entities but don't want to cross the api.
const INTERNAL_FIELDS = new Set(['apiKey', 'billingAccountId', 'firstLoginAt', 'filteredOut', 'ownerId',
'stripeCustomerId', 'stripeSubscriptionId', 'stripePlanId',
'stripeProductId', 'userId', 'isFirstTimeUser']);
'stripeProductId', 'userId', 'isFirstTimeUser', 'allowGoogleLogin']);
/**
* Adapt a home-server or doc-worker URL to match the hostname in the request URL. For custom
@@ -159,11 +159,20 @@ export function addPermit(scope: Scope, userId: number, specialPermit: Permit):
return {...scope, ...(scope.userId === userId ? {specialPermit} : {})};
}
export interface SendReplyOptions {
allowedFields?: Set<string>;
}
// Return a JSON response reflecting the output of a query.
// Filter out keys we don't want crossing the api.
// Set req to null to not log any information about request.
export async function sendReply<T>(req: Request|null, res: Response, result: QueryResult<T>) {
const data = pruneAPIResult(result.data || null);
export async function sendReply<T>(
req: Request|null,
res: Response,
result: QueryResult<T>,
options: SendReplyOptions = {},
) {
const data = pruneAPIResult(result.data || null, options.allowedFields);
if (shouldLogApiDetails && req) {
const mreq = req as RequestWithLogin;
log.rawDebug('api call', {
@@ -183,11 +192,16 @@ export async function sendReply<T>(req: Request|null, res: Response, result: Que
}
}
export async function sendOkReply<T>(req: Request|null, res: Response, result?: T) {
return sendReply(req, res, {status: 200, data: result});
export async function sendOkReply<T>(
req: Request|null,
res: Response,
result?: T,
options: SendReplyOptions = {}
) {
return sendReply(req, res, {status: 200, data: result}, options);
}
export function pruneAPIResult<T>(data: T): T {
export function pruneAPIResult<T>(data: T, allowedFields?: Set<string>): T {
// TODO: This can be optimized by pruning data recursively without serializing in between. But
// it's fairly fast even with serializing (on the order of 15usec/kb).
const output = JSON.stringify(data,
@@ -197,6 +211,8 @@ export function pruneAPIResult<T>(data: T): T {
if (key === 'removedAt' && value === null) { return undefined; }
// Don't bother sending option fields if there are no options set.
if (key === 'options' && value === null) { return undefined; }
// Don't prune anything that is explicitly allowed.
if (allowedFields?.has(key)) { return value; }
return INTERNAL_FIELDS.has(key) ? undefined : value;
});
return JSON.parse(output);