(core) implement authorization via query parameter

Summary:
This adds any parameters in a document url whose key ends in '_'
into a `user.Link` object available in access control formulas
and in setting up characteristic tables.

This allows, for example, sending links to a document that contain
a hard-to-guess token, and having that link grant access to a
controlled part of the document (invoices for a specific customer
for example).

A `user.Origin` field is also added, set during rest api calls,
but is only tested manually at this point.  It could be elaborated
for embedding use-cases.

Test Plan: added test

Reviewers: dsagal

Reviewed By: dsagal

Differential Revision: https://phab.getgrist.com/D2680
This commit is contained in:
Paul Fitzpatrick
2020-12-09 08:57:35 -05:00
parent 131fbbdb92
commit 8f023a6446
9 changed files with 46 additions and 12 deletions

View File

@@ -363,6 +363,9 @@ export interface Authorizer {
// get the id of the document.
getDocId(): string;
// get any link parameters in place when accessing the resource.
getLinkParameters(): Record<string, string>;
// Fetch the doc metadata from HomeDBManager.
getDoc(): Promise<Document>;
@@ -385,6 +388,7 @@ export class DocAuthorizer implements Authorizer {
private _dbManager: HomeDBManager,
private _key: DocAuthKey,
public readonly openMode: OpenDocMode,
public readonly linkParameters: Record<string, string>,
private _docAuth?: DocAuthResult,
private _profile?: UserProfile
) {
@@ -403,6 +407,10 @@ export class DocAuthorizer implements Authorizer {
return this._key.urlId;
}
public getLinkParameters(): Record<string, string> {
return this.linkParameters;
}
public async getDoc(): Promise<Document> {
return this._dbManager.getDoc(this._key);
}
@@ -424,6 +432,7 @@ export class DummyAuthorizer implements Authorizer {
public getUserId() { return null; }
public getUser() { return null; }
public getDocId() { return this.docId; }
public getLinkParameters() { return {}; }
public async getDoc(): Promise<Document> { throw new Error("Not supported in standalone"); }
public async assertAccess() { /* noop */ }
public getCachedAuth(): DocAuthResult {
@@ -481,11 +490,14 @@ export function getTransitiveHeaders(req: Request): {[key: string]: string} {
const PermitHeader = req.get('Permit');
const Organization = (req as RequestWithOrg).org;
const XRequestedWith = req.get('X-Requested-With');
const Origin = req.get('Origin'); // Pass along the original Origin since it may
// play a role in granular access control.
return {
...(Authorization ? { Authorization } : undefined),
...(Cookie ? { Cookie } : undefined),
...(Organization ? { Organization } : undefined),
...(PermitHeader ? { Permit: PermitHeader } : undefined),
...(XRequestedWith ? { 'X-Requested-With': XRequestedWith } : undefined),
...(Origin ? { Origin } : undefined),
};
}

View File

@@ -244,7 +244,8 @@ export class DocManager extends EventEmitter {
* `doc` - the object with metadata tables.
*/
public async openDoc(client: Client, docId: string,
mode: OpenDocMode = 'default'): Promise<OpenLocalDocResult> {
mode: OpenDocMode = 'default',
linkParameters: Record<string, string> = {}): Promise<OpenLocalDocResult> {
let auth: Authorizer;
const dbManager = this._homeDbManager;
if (!isSingleUserMode()) {
@@ -266,7 +267,7 @@ export class DocManager extends EventEmitter {
// than a docId.
throw new Error(`openDoc expected docId ${docAuth.docId} not urlId ${docId}`);
}
auth = new DocAuthorizer(dbManager, key, mode, docAuth, client.getProfile() || undefined);
auth = new DocAuthorizer(dbManager, key, mode, linkParameters, docAuth, client.getProfile() || undefined);
} else {
log.debug(`DocManager.openDoc not using authorization for ${docId} because GRIST_SINGLE_USER`);
auth = new DummyAuthorizer('owners', docId);

View File

@@ -694,7 +694,7 @@ export class GranularAccess {
if (value && typeof value === 'object' && !Array.isArray(value)) {
value = value.get('id');
}
return JSON.stringify(value).toLowerCase();
return JSON.stringify(value)?.toLowerCase() || '';
}
/**
@@ -755,6 +755,12 @@ export class GranularAccess {
user.UserID = fullUser?.id || null;
user.Email = fullUser?.email || null;
user.Name = fullUser?.name || null;
// If viewed from a websocket, collect any link parameters included.
// TODO: could also get this from rest api access, just via a different route.
user.Link = docSession.authorizer?.getLinkParameters() || {};
// Include origin info if accessed via the rest api.
// TODO: could also get this for websocket access, just via a different route.
user.Origin = docSession.req?.get('origin') || null;
for (const clause of this._ruleCollection.getUserAttributeRules().values()) {
if (clause.name in user) {