mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) support granular read access for attachments
Summary: When a user requests to read the contents of an attachment, only allow the request if there exists a cell in an attachment column that contains the attachment and which they have read access to. This does not cover: * Granular write access for attachments. In particular, a user who can write to any attachment column should be considered to have full read access to all attachment columns, currently. * Access control of attachment metadata such as name and format. The implementation uses a sql query that requires a scan, and some notes on how this could be optimized in future. The web client was updated to specify the cell to check for access, and performance seemed fine in casual testing on a doc with 1000s of attachments. I'm not sure how performance would hold up as the set of access rules grows as well. Test Plan: added tests Reviewers: alexmojaki Reviewed By: alexmojaki Differential Revision: https://phab.getgrist.com/D3490
This commit is contained in:
@@ -16,6 +16,7 @@ import {IModalControl, modal} from 'app/client/ui2018/modals';
|
||||
import {renderFileType} from 'app/client/widgets/AttachmentsWidget';
|
||||
import {NewBaseEditor, Options} from 'app/client/widgets/NewBaseEditor';
|
||||
import {CellValue} from 'app/common/DocActions';
|
||||
import {SingleCell} from 'app/common/TableData';
|
||||
import {clamp, encodeQueryParams} from 'app/common/gutil';
|
||||
import {UploadResult} from 'app/common/uploads';
|
||||
import * as mimeTypes from 'mime-types';
|
||||
@@ -60,6 +61,11 @@ export class AttachmentsEditor extends NewBaseEditor {
|
||||
|
||||
const docData: DocData = options.gristDoc.docData;
|
||||
const cellValue: CellValue = options.cellValue;
|
||||
const cell: SingleCell = {
|
||||
rowId: options.rowId,
|
||||
colId: options.field.colId(),
|
||||
tableId: options.field.column().table().tableId(),
|
||||
};
|
||||
|
||||
// editValue is abused slightly to indicate a 1-based index of the attachment.
|
||||
const initRowIndex: number|undefined = (options.editValue && parseInt(options.editValue, 0) - 1) || 0;
|
||||
@@ -79,8 +85,8 @@ export class AttachmentsEditor extends NewBaseEditor {
|
||||
fileType,
|
||||
filename,
|
||||
hasPreview: Boolean(this._attachmentsTable.getValue(val, 'imageHeight')),
|
||||
url: computed((use) => this._getUrl(fileIdent, use(filename))),
|
||||
inlineUrl: computed((use) => this._getUrl(fileIdent, use(filename), true))
|
||||
url: computed((use) => this._getUrl(cell, val, use(filename))),
|
||||
inlineUrl: computed((use) => this._getUrl(cell, val, use(filename), true))
|
||||
};
|
||||
});
|
||||
this._index = makeLiveIndex(this, this._attachments, initRowIndex);
|
||||
@@ -190,11 +196,12 @@ export class AttachmentsEditor extends NewBaseEditor {
|
||||
att.filename.set(this._attachmentsTable.getValue(att.rowId, 'fileName')!);
|
||||
}
|
||||
|
||||
private _getUrl(fileIdent: string, filename: string, inline?: boolean): string {
|
||||
private _getUrl(cell: SingleCell, attId: number, filename: string, inline?: boolean): string {
|
||||
return this._docComm.docUrl('attachment') + '?' + encodeQueryParams({
|
||||
...this._docComm.getUrlParams(),
|
||||
ident: fileIdent,
|
||||
name: filename,
|
||||
...cell,
|
||||
attId,
|
||||
...(inline ? {inline: 1} : {})
|
||||
});
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {encodeQueryParams} from 'app/common/gutil';
|
||||
import {MetaTableData} from 'app/client/models/TableData';
|
||||
import {UploadResult} from 'app/common/uploads';
|
||||
import {extname} from 'path';
|
||||
import { SingleCell } from 'app/common/TableData';
|
||||
|
||||
const testId: TestId = makeTestId('test-pw-');
|
||||
|
||||
@@ -90,6 +91,8 @@ export class AttachmentsWidget extends NewAbstractWidget {
|
||||
const values = Computed.create(null, fromKo(cellValue), (use, _cellValue) =>
|
||||
Array.isArray(_cellValue) ? _cellValue.slice(1) : []);
|
||||
|
||||
const colId = this.field.colId();
|
||||
const tableId = this.field.column().table().tableId();
|
||||
return attachmentWidget(
|
||||
dom.autoDispose(values),
|
||||
|
||||
@@ -98,9 +101,12 @@ export class AttachmentsWidget extends NewAbstractWidget {
|
||||
dom.cls('attachment_hover_icon', (use) => use(values).length > 0),
|
||||
dom.on('click', () => this._selectAndSave(cellValue))
|
||||
),
|
||||
dom.forEach(values, (value: number) =>
|
||||
isNaN(value) ? null : this._buildAttachment(value, values)
|
||||
),
|
||||
dom.maybe(_row.id, rowId => {
|
||||
return dom.forEach(values, (value: number) =>
|
||||
isNaN(value) ? null : this._buildAttachment(value, values, {
|
||||
rowId, colId, tableId,
|
||||
}));
|
||||
}),
|
||||
dom.on('drop', ev => this._uploadAndSave(cellValue, ev.dataTransfer!.files))
|
||||
);
|
||||
}
|
||||
@@ -121,7 +127,7 @@ export class AttachmentsWidget extends NewAbstractWidget {
|
||||
);
|
||||
}
|
||||
|
||||
protected _buildAttachment(value: number, allValues: Computed<number[]>): Element {
|
||||
protected _buildAttachment(value: number, allValues: Computed<number[]>, cell: SingleCell): Element {
|
||||
const filename = this._attachmentsTable.getValue(value, 'fileName')!;
|
||||
const fileIdent = this._attachmentsTable.getValue(value, 'fileIdent')!;
|
||||
const height = this._attachmentsTable.getValue(value, 'imageHeight')!;
|
||||
@@ -134,7 +140,7 @@ export class AttachmentsWidget extends NewAbstractWidget {
|
||||
dom.style('width', (use) => `${parseInt(use(this._height), 10) * ratio}px`),
|
||||
// TODO: Update to legitimately determine whether a file preview exists.
|
||||
hasPreview ? dom('img', {style: 'height: 100%; min-width: 100%; vertical-align: top;'},
|
||||
dom.attr('src', this._getUrl(value))
|
||||
dom.attr('src', this._getUrl(value, cell))
|
||||
) : renderFileType(filename, fileIdent, this._height),
|
||||
// Open editor as if with input, using it to tell it which of the attachments to show. We
|
||||
// pass in a 1-based index. Hitting a key opens the cell, and this approach allows an
|
||||
@@ -145,18 +151,14 @@ export class AttachmentsWidget extends NewAbstractWidget {
|
||||
}
|
||||
|
||||
// Returns the attachment download url.
|
||||
private _getUrl(rowId: number): string {
|
||||
const ident = this._attachmentsTable.getValue(rowId, 'fileIdent');
|
||||
if (!ident) {
|
||||
return '';
|
||||
} else {
|
||||
const docComm = this._getDocComm();
|
||||
return docComm.docUrl('attachment') + '?' + encodeQueryParams({
|
||||
...docComm.getUrlParams(),
|
||||
ident,
|
||||
name: this._attachmentsTable.getValue(rowId, 'fileName')
|
||||
});
|
||||
}
|
||||
private _getUrl(attId: number, cell: SingleCell): string {
|
||||
const docComm = this._getDocComm();
|
||||
return docComm.docUrl('attachment') + '?' + encodeQueryParams({
|
||||
...docComm.getUrlParams(),
|
||||
...cell,
|
||||
attId,
|
||||
name: this._attachmentsTable.getValue(attId, 'fileName')
|
||||
});
|
||||
}
|
||||
|
||||
private async _selectAndSave(value: SavingObservable<number[]>): Promise<void> {
|
||||
|
||||
@@ -204,6 +204,7 @@ export class FieldEditor extends Disposable {
|
||||
gristDoc: this._gristDoc,
|
||||
field: this._field,
|
||||
cellValue,
|
||||
rowId: this._editRow.id(),
|
||||
formulaError: error,
|
||||
editValue,
|
||||
cursorPos,
|
||||
|
||||
@@ -292,6 +292,7 @@ export function openFormulaEditor(options: {
|
||||
const editor = FormulaEditor.create(holder, {
|
||||
gristDoc,
|
||||
field,
|
||||
rowId: editRow ? editRow.id() : 0,
|
||||
cellValue: column.formula(),
|
||||
formulaError: editRow ? getFormulaError(gristDoc, editRow, column) : undefined,
|
||||
editValue: options.editValue,
|
||||
|
||||
@@ -17,6 +17,7 @@ export interface Options {
|
||||
gristDoc: GristDoc;
|
||||
field: ViewFieldRec;
|
||||
cellValue: CellValue;
|
||||
rowId: number;
|
||||
formulaError?: Observable<CellValue>;
|
||||
editValue?: string;
|
||||
cursorPos: number;
|
||||
|
||||
Reference in New Issue
Block a user