(core) Add methods for quarantining documents

Summary:
Adds a new CLI command, doc, with a subcommand that quarantines
an active document. Adds a group query param to a housekeeping
endpoint for updating the document group prior to checking if a doc
needs to be reassigned. Both methods require support user credentials.

Test Plan: Server tests. (Additional testing will be done manually on staging.)

Reviewers: paulfitz

Reviewed By: paulfitz

Differential Revision: https://phab.getgrist.com/D3570
This commit is contained in:
George Gevoian 2022-08-09 08:50:18 -07:00
parent ee109e9186
commit fbba6b8f52
4 changed files with 34 additions and 11 deletions

View File

@ -137,6 +137,10 @@ class DummyDocWorkerMap implements IDocWorkerMap {
return null;
}
public async updateDocGroup(docId: string, docGroup: string): Promise<void> {
// nothing to do
}
public getRedisClient() {
return null;
}
@ -517,6 +521,10 @@ export class DocWorkerMap implements IDocWorkerMap {
return this._client.getAsync(`doc-${docId}-group`);
}
public async updateDocGroup(docId: string, docGroup: string): Promise<void> {
await this._client.setAsync(`doc-${docId}-group`, docGroup);
}
public getRedisClient(): RedisClient {
return this._client;
}

View File

@ -9,7 +9,7 @@ import { GristServer } from 'app/server/lib/GristServer';
import { IElectionStore } from 'app/server/lib/IElectionStore';
import log from 'app/server/lib/log';
import { IPermitStore } from 'app/server/lib/Permit';
import { stringParam } from 'app/server/lib/requestUtils';
import { optStringParam, stringParam } from 'app/server/lib/requestUtils';
import * as express from 'express';
import fetch from 'node-fetch';
import * as Fetch from 'node-fetch';
@ -130,7 +130,7 @@ export class Housekeeper {
// Remove unlisted snapshots that are not recorded in inventory.
// Once all such snapshots have been removed, there should be no
// further need for this endpoint.
app.post('/api/housekeeping/docs/:docId/snapshots/clean', this._withSupport(async (docId, headers) => {
app.post('/api/housekeeping/docs/:docId/snapshots/clean', this._withSupport(async (_req, docId, headers) => {
const url = await this._server.getHomeUrlByDocId(docId, `/api/docs/${docId}/snapshots/remove`);
return fetch(url, {
method: 'POST',
@ -143,7 +143,7 @@ export class Housekeeper {
// use, for allowing support to help users looking to purge some
// information that leaked into document history that they'd
// prefer not be there, until there's an alternative.
app.post('/api/housekeeping/docs/:docId/states/remove', this._withSupport(async (docId, headers) => {
app.post('/api/housekeeping/docs/:docId/states/remove', this._withSupport(async (_req, docId, headers) => {
const url = await this._server.getHomeUrlByDocId(docId, `/api/docs/${docId}/states/remove`);
return fetch(url, {
method: 'POST',
@ -154,7 +154,7 @@ export class Housekeeper {
// Force a document to reload. Can be useful during administrative
// actions.
app.post('/api/housekeeping/docs/:docId/force-reload', this._withSupport(async (docId, headers) => {
app.post('/api/housekeeping/docs/:docId/force-reload', this._withSupport(async (_req, docId, headers) => {
const url = await this._server.getHomeUrlByDocId(docId, `/api/docs/${docId}/force-reload`);
return fetch(url, {
method: 'POST',
@ -164,13 +164,19 @@ export class Housekeeper {
// Move a document to its assigned worker. Can be useful during administrative
// actions.
app.post('/api/housekeeping/docs/:docId/assign', this._withSupport(async (docId, headers) => {
const url = await this._server.getHomeUrlByDocId(docId, `/api/docs/${docId}/assign`);
return fetch(url, {
//
// Optionally accepts a `group` query param for updating the document's group prior
// to moving. This is useful for controlling which worker group the document is assigned
// a worker from.
app.post('/api/housekeeping/docs/:docId/assign', this._withSupport(async (req, docId, headers) => {
const url = new URL(await this._server.getHomeUrlByDocId(docId, `/api/docs/${docId}/assign`));
const group = optStringParam(req.query.group);
if (group) { url.searchParams.set('group', group); }
return fetch(url.toString(), {
method: 'POST',
headers,
});
}));
}, 'assign-doc'));
}
/**
@ -221,7 +227,8 @@ export class Housekeeper {
// Call a document endpoint with a permit, cleaning up after the call.
// Checks that the user is the support user.
private _withSupport(
callback: (docId: string, headers: Record<string, string>) => Promise<Fetch.Response>
callback: (req: express.Request, docId: string, headers: Record<string, string>) => Promise<Fetch.Response>,
permitAction?: string,
): express.RequestHandler {
return expressWrap(async (req, res) => {
const userId = getAuthorizedUserId(req);
@ -229,9 +236,9 @@ export class Housekeeper {
throw new ApiError('access denied', 403);
}
const docId = stringParam(req.params.docId, 'docId');
const permitKey = await this._permitStore.setPermit({docId});
const permitKey = await this._permitStore.setPermit({docId, action: permitAction});
try {
const result = await callback(docId, {
const result = await callback(req, docId, {
Permit: permitKey,
'Content-Type': 'application/json',
});

View File

@ -599,8 +599,15 @@ export class DocWorkerApi {
// and frees it for reassignment if not. Has no effect if document is in the
// expected group. Does not require specific rights. Returns true if the document
// is freed up for reassignment, otherwise false.
//
// Optionally accepts a `group` query param for updating the document's group prior
// to (possible) reassignment. (Note: Requires special permit.)
this._app.post('/api/docs/:docId/assign', canEdit, throttled(async (req, res) => {
const docId = getDocId(req);
const group = optStringParam(req.query.group);
if (group && req.specialPermit?.action === 'assign-doc') {
await this._docWorkerMap.updateDocGroup(docId, group);
}
const status = await this._docWorkerMap.getDocWorker(docId);
if (!status) { res.json(false); return; }
const workerGroup = await this._docWorkerMap.getWorkerGroup(status.docWorker.id);

View File

@ -67,6 +67,7 @@ export interface IDocWorkerMap extends IPermitStores, IElectionStore, IChecksumS
getWorkerGroup(workerId: string): Promise<string|null>;
getDocGroup(docId: string): Promise<string|null>;
updateDocGroup(docId: string, docGroup: string): Promise<void>;
getRedisClient(): RedisClient|null;
}