gristlabs_grist-core/app/common/GranularAccessClause.ts
Paul Fitzpatrick 3d3fe92bd0 (core) support access control on columns
Summary: Adds a granular access clause for columns. Permissions can be specified for a set of columns within a table. Permissions accumulate over clauses, in a way that is intended as a placeholder pending final design.

Test Plan: Added tests. Tested manually that updates to private columns are not sent to people who don't have access to them. There are a lot of extra tests needed and TODOs to be paid down after this experimental phase.

Reviewers: dsagal

Reviewed By: dsagal

Differential Revision: https://phab.getgrist.com/D2651
2020-11-03 19:08:44 -05:00

131 lines
3.9 KiB
TypeScript

import { safeJsonParse } from 'app/common/gutil';
import { CellValue } from 'app/plugin/GristData';
/**
* All possible access clauses. In future the clauses will become more generalized.
* The consequences of clauses are currently combined in a naive and ad-hoc way,
* this will need systematizing.
*/
export type GranularAccessClause =
GranularAccessDocClause |
GranularAccessTableClause |
GranularAccessRowClause |
GranularAccessColumnClause |
GranularAccessCharacteristicsClause;
/**
* A clause that forbids anyone but owners from modifying the document structure.
*/
export interface GranularAccessDocClause {
kind: 'doc';
match: MatchSpec;
}
/**
* A clause to control access to a specific table.
*/
export interface GranularAccessTableClause {
kind: 'table';
tableId: string;
match: MatchSpec;
}
/**
* A clause to control access to rows within a specific table.
* If "scope" is provided, this rule is simply ignored if the scope does not match
* the user.
*/
export interface GranularAccessRowClause {
kind: 'row';
tableId: string;
match: MatchSpec;
scope?: MatchSpec;
}
/**
* A clause to control access to columns within a specific table.
*/
export interface GranularAccessColumnClause {
kind: 'column';
tableId: string;
colIds: string[];
match: MatchSpec;
onMatch?: AccessPermissionDelta; // permissions to apply if match succeeds
onFail?: AccessPermissionDelta; // permissions to apply if match fails
}
/**
* A clause to make more information about the user/request available for access
* control decisions.
* - charId specifies a property of the user (e.g. Access/Email/UserID/Name, or a
* property added by another clause) to use as a key.
* - We look for a matching record in the specified table, comparing the specified
* column with the charId property. Outcome is currently unspecified if there are
* multiple matches.
* - Compare using lower case for now (because of Email). Could generalize in future.
* - All fields from a matching record are added to the variables available for MatchSpecs.
*/
export interface GranularAccessCharacteristicsClause {
kind: 'character';
tableId: string;
charId: string; // characteristic to look up
lookupColId: string; // column in which to look it up
}
/**
* A sketch of permissions, intended as a placeholder.
*/
export type AccessPermission = 'read' | 'update' | 'create' | 'delete';
export type AccessPermissions = 'all' | AccessPermission[];
export interface AccessPermissionDelta {
allow?: AccessPermissions; // permit the named operations
allowOnly?: AccessPermissions; // permit the named operations, and forbid others
forbid?: AccessPermissions; // forbid the named operations
}
// Type for expressing matches.
export type MatchSpec = ConstMatchSpec | TruthyMatchSpec | PairMatchSpec | NotMatchSpec;
// Invert a match.
export interface NotMatchSpec {
kind: 'not';
match: MatchSpec;
}
// Compare property of user with a constant.
export interface ConstMatchSpec {
kind: 'const';
charId: string;
value: CellValue;
}
// Check if a table column is truthy.
export interface TruthyMatchSpec {
kind: 'truthy';
colId: string;
}
// Check if a property of user matches a table column.
export interface PairMatchSpec {
kind: 'pair';
charId: string;
colId: string;
}
// Convert a clause to a string. Trivial, but fluid currently.
export function serializeClause(clause: GranularAccessClause) {
return '~acl ' + JSON.stringify(clause);
}
export function decodeClause(code: string): GranularAccessClause|null {
// TODO: be strict about format. But it isn't super-clear what to do with
// a document if access control gets corrupted. Maybe go into an emergency
// mode where only owners have access, and they have unrestricted access?
// Also, format should be plain JSON once no longer stored in a random
// reused column.
if (code.startsWith('~acl ')) {
return safeJsonParse(code.slice(5), null);
}
return null;
}