diff --git a/app/client/components/CodeEditorPanel.css b/app/client/components/CodeEditorPanel.css index 1237e5a3..7233bb6f 100644 --- a/app/client/components/CodeEditorPanel.css +++ b/app/client/components/CodeEditorPanel.css @@ -17,3 +17,7 @@ .g-code-viewer.hljs { background-color: inherit; } + +.g-code-panel-denied { + text-align: center; +} diff --git a/app/client/components/CodeEditorPanel.js b/app/client/components/CodeEditorPanel.js index 5162e7bf..d6b80301 100644 --- a/app/client/components/CodeEditorPanel.js +++ b/app/client/components/CodeEditorPanel.js @@ -14,6 +14,7 @@ hljs.registerLanguage('python', require('highlight.js/lib/languages/python')); function CodeEditorPanel(gristDoc) { this._gristDoc = gristDoc; this._schema = ko.observable(''); + this._denied = ko.observable(false); this.listenTo(this._gristDoc, 'schemaUpdateAction', this.onSchemaAction); this.onSchemaAction(); // Fetch the schema to initialize @@ -28,6 +29,10 @@ CodeEditorPanel.prototype.buildDom = function() { // interfere with text selection even for un-focusable elements. return dom('div.g-code-panel.clipboard', {tabIndex: "-1"}, + kd.maybe(this._denied, () => dom('div.g-code-panel-denied', + dom('h2', kd.text('Access denied')), + dom('div', kd.text('Code View is available only when you have full document access.')), + )), kd.scope(this._schema, function(schema) { // The reason to scope and rebuild instead of using `kd.text(schema)` is because // hljs.highlightBlock(elem) replaces `elem` with a whole new dom tree. @@ -45,13 +50,22 @@ CodeEditorPanel.prototype.buildDom = function() { ); }; -CodeEditorPanel.prototype.onSchemaAction = function(actions) { - return this._gristDoc.docComm.fetchTableSchema() - .then(schema => { +CodeEditorPanel.prototype.onSchemaAction = async function(actions) { + try { + const schema = await this._gristDoc.docComm.fetchTableSchema(); if (!this.isDisposed()) { this._schema(schema); + this._denied(false); } - }); + } catch (err) { + if (!String(err).match(/Cannot view code/)) { + throw err; + } + if (!this.isDisposed()) { + this._schema(''); + this._denied(true); + } + } }; module.exports = CodeEditorPanel; diff --git a/app/server/lib/ActiveDoc.ts b/app/server/lib/ActiveDoc.ts index 6a0d3540..22145a86 100644 --- a/app/server/lib/ActiveDoc.ts +++ b/app/server/lib/ActiveDoc.ts @@ -658,6 +658,12 @@ export class ActiveDoc extends EventEmitter { */ public async fetchTableSchema(docSession: DocSession): Promise { this.logInfo(docSession, "fetchTableSchema(%s)", docSession); + // Permit code view if user can read everything, or can download/copy (perhaps + // via an exceptional permission for sample documents) + if (!(await this._granularAccess.canReadEverything(docSession) || + await this.canDownload(docSession))) { + throw new ApiError('Cannot view code, it may contain private material', 403); + } await this.waitForInitialization(); return this._pyCall('fetch_table_schema'); }