(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

@@ -345,7 +345,7 @@ export class ApiServer {
// Get user's profile
this._app.get('/api/profile/user', expressWrap(async (req, res) => {
const fullUser = await this._getFullUser(req);
return sendOkReply(req, res, fullUser);
return sendOkReply(req, res, fullUser, {allowedFields: new Set(['allowGoogleLogin'])});
}));
// POST /api/profile/user/name
@@ -361,6 +361,24 @@ export class ApiServer {
res.sendStatus(200);
}));
// POST /api/profile/allowGoogleLogin
// Update user's preference for allowing Google login.
this._app.post('/api/profile/allowGoogleLogin', expressWrap(async (req, res) => {
const userId = getAuthorizedUserId(req);
const fullUser = await this._getFullUser(req);
if (fullUser.loginMethod !== 'Email + Password') {
throw new ApiError('Only users signed in via email can enable/disable Google login', 401);
}
const allowGoogleLogin: boolean | undefined = req.body.allowGoogleLogin;
if (allowGoogleLogin === undefined) {
throw new ApiError('Missing body param: allowGoogleLogin', 400);
}
await this._dbManager.updateUserOptions(userId, {allowGoogleLogin});
res.sendStatus(200);
}));
// GET /api/profile/apikey
// Get user's apiKey
this._app.get('/api/profile/apikey', expressWrap(async (req, res) => {
@@ -471,11 +489,15 @@ export class ApiServer {
private async _getFullUser(req: Request): Promise<FullUser> {
const mreq = req as RequestWithLogin;
const userId = getUserId(mreq);
const fullUser = await this._dbManager.getFullUser(userId);
const user = await this._dbManager.getUser(userId);
if (!user) { throw new ApiError("unable to find user", 400); }
const fullUser = this._dbManager.makeFullUser(user);
const domain = getOrgFromRequest(mreq);
const sessionUser = getSessionUser(mreq.session, domain || '', fullUser.email);
const loginMethod = sessionUser && sessionUser.profile ? sessionUser.profile.loginMethod : undefined;
return {...fullUser, loginMethod};
const allowGoogleLogin = user.options?.allowGoogleLogin ?? true;
return {...fullUser, loginMethod, allowGoogleLogin};
}
}

View File

@@ -1,3 +1,5 @@
import {UserOptions} from 'app/common/UserAPI';
import {nativeValues} from 'app/gen-server/lib/values';
import {BaseEntity, Column, Entity, JoinTable, ManyToMany, OneToMany, OneToOne,
PrimaryGeneratedColumn} from "typeorm";
@@ -42,6 +44,9 @@ export class User extends BaseEntity {
@Column({name: 'is_first_time_user', default: false})
public isFirstTimeUser: boolean;
@Column({name: 'options', type: nativeValues.jsonEntityType, nullable: true})
public options: UserOptions | null;
/**
* Get user's email. Returns undefined if logins has not been joined, or no login
* is available

View File

@@ -11,6 +11,7 @@ import * as roles from 'app/common/roles';
import {ANONYMOUS_USER_EMAIL, DocumentProperties, EVERYONE_EMAIL, getRealAccess,
ManagerDelta, NEW_DOCUMENT_CODE, OrganizationProperties,
Organization as OrgInfo, PermissionData, PermissionDelta, SUPPORT_EMAIL, UserAccessData,
UserOptions,
WorkspaceProperties} from "app/common/UserAPI";
import {AclRule, AclRuleDoc, AclRuleOrg, AclRuleWs} from "app/gen-server/entity/AclRule";
import {Alias} from "app/gen-server/entity/Alias";
@@ -432,6 +433,15 @@ export class HomeDBManager extends EventEmitter {
await user.save();
}
public async updateUserOptions(userId: number, props: Partial<UserOptions>) {
const user = await User.findOne(userId);
if (!user) { throw new ApiError("unable to find user", 400); }
const newOptions = {...(user.options ?? {}), ...props};
user.options = newOptions;
await user.save();
}
// Fetch user from login, creating the user if previously unseen, allowing one retry
// for an email key conflict failure. This is in case our transaction conflicts with a peer
// doing the same thing. This is quite likely if the first page visited by a previously

View File

@@ -0,0 +1,17 @@
import {nativeValues} from "app/gen-server/lib/values";
import {MigrationInterface, QueryRunner, TableColumn} from "typeorm";
export class UserOptions1644363380225 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<any> {
await queryRunner.addColumn("users", new TableColumn({
name: "options",
type: nativeValues.jsonType,
isNullable: true,
}));
}
public async down(queryRunner: QueryRunner): Promise<any> {
await queryRunner.dropColumn("users", "options");
}
}