(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

@@ -80,5 +80,6 @@ export interface DocListAPI {
/**
* Opens a document, loads it, subscribes to its userAction events, and returns its metadata.
*/
openDoc(userDocName: string, openMode?: OpenDocMode): Promise<OpenLocalDocResult>;
openDoc(userDocName: string, openMode?: OpenDocMode,
linkParameters?: Record<string, string>): Promise<OpenLocalDocResult>;
}

View File

@@ -26,7 +26,7 @@ export interface InfoView {
}
// Represents user info, which may include properties which are themselves RowRecords.
export type UserInfo = Record<string, CellValue|InfoView>;
export type UserInfo = Record<string, CellValue|InfoView|Record<string, string>>;
/**
* Input into the AclMatchFunc. Compiled formulas evaluate AclMatchInput to produce a boolean.

View File

@@ -3,7 +3,6 @@ import {OpenDocMode} from 'app/common/DocListAPI';
import {encodeQueryParams, isAffirmative} from 'app/common/gutil';
import {localhostRegex} from 'app/common/LoginState';
import {Document} from 'app/common/UserAPI';
import identity = require('lodash/identity');
import pickBy = require('lodash/pickBy');
import {StringUnion} from './StringUnion';
@@ -67,6 +66,8 @@ export interface IGristUrlState {
style?: InterfaceStyle;
compare?: string;
aclUI?: boolean;
linkParameters?: Record<string, string>; // Parameters to pass as 'user.Link' in granular ACLs.
// Encoded in URL as query params with extra '_' suffix.
};
hash?: HashLink; // if present, this specifies an individual row within a section of a page.
}
@@ -173,10 +174,13 @@ export function encodeUrl(gristConfig: Partial<GristLoadConfig>,
parts.push(`welcome/${state.welcome}`);
}
const queryParams = pickBy(state.params, identity) as {[key: string]: string};
const queryParams = pickBy(state.params, (v, k) => k !== 'linkParameters') as {[key: string]: string};
if (state.newui !== undefined) {
queryParams.newui = state.newui ? '1' : '0';
}
for (const [k, v] of Object.entries(state.params?.linkParameters || {})) {
queryParams[`${k}_`] = v;
}
const hashParts: string[] = [];
if (state.hash && state.hash.rowId) {
const hash = state.hash;
@@ -264,6 +268,12 @@ export function decodeUrl(gristConfig: Partial<GristLoadConfig>, location: Locat
if (sp.has('aclUI')) {
state.params!.aclUI = isAffirmative(sp.get('aclUI'));
}
for (const [k, v] of sp.entries()) {
if (k.endsWith('_')) {
if (!state.params!.linkParameters) { state.params!.linkParameters = {}; }
state.params!.linkParameters[k.slice(0, k.length - 1)] = v;
}
}
if (location.hash) {
const hash = location.hash;
const hashParts = hash.split('.');