Skip to content

Commit a85178f

Browse files
committed
MB-71275 - better permissions handling in the document editor
Merge remote-tracking branch 'couchbase/trinity' into HEAD Change-Id: Ieb4241d3555a0dd04efc33dd15cea149599f2e9a
2 parents 3a9bb9b + dcb5f79 commit a85178f

9 files changed

Lines changed: 116 additions & 42 deletions

File tree

query-ui/angular-components/documents/qw.documents.component.js

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ class QwDocumentsComponent extends MnLifeCycleHooksToStream {
9797
}
9898

9999
docViewer() {
100-
return this.rbac.init && this.rbac.cluster.collection['.:.:.'].data.docs.read;
100+
return this.rbac.init && (this.rbac.cluster.collection['.:.:.'].data.docs.read || this.rbac.cluster.collection['.:.:.'].data.docs.upsert);
101101
}
102102

103103
constructor(
@@ -169,6 +169,7 @@ class QwDocumentsComponent extends MnLifeCycleHooksToStream {
169169
dec.how_to_query = how_to_query;
170170
dec.can_use_n1ql = can_use_n1ql;
171171
dec.has_indexes = has_indexes;
172+
dec.get_n1ql_placeholder = get_n1ql_placeholder;
172173

173174
// whenever the collection menu is changed, remove any 'where' clause and offset
174175
dec.collectionMenuCallback = function(event) {
@@ -184,8 +185,12 @@ class QwDocumentsComponent extends MnLifeCycleHooksToStream {
184185
dec.searchForm.get('where_clause').setValue('');
185186
dec.searchForm.get('offset').setValue(0);
186187

187-
if (event.bucket && event.scope && event.collection)
188-
retrieveDocs_inner();
188+
if (event.bucket && event.scope && event.collection) {
189+
// get permissions for new collection
190+
qwMetadataService.checkCollectionPerms(dec.options.selected_bucket, dec.options.selected_scope, dec.options.selected_collection).then(function () {
191+
retrieveDocs_inner();
192+
});
193+
}
189194
}
190195
};
191196

@@ -197,22 +202,25 @@ class QwDocumentsComponent extends MnLifeCycleHooksToStream {
197202
//
198203
// what are we allowed to access?
199204
//
205+
200206
dec.upsertAllowed = function() {
201-
if (!this.rbac.init || !this.options.selected_collection || !this.rbac.cluster.collection) {
207+
if (!this.rbac.init || !this.options.selected_bucket || !this.options.selected_scope || !this.options.selected_collection)
202208
return false;
203-
}
204209

205-
// return the cached value
206-
return this.rbac.cluster.collection[`${this.options.selected_bucket}:${this.options.selected_scope}:${this.options.selected_collection}`]?.data.docs.upsert;
210+
const fullName = `${dec.options.selected_bucket}:${dec.options.selected_scope}:${dec.options.selected_collection}`;
211+
212+
return (this.rbac.cluster.collection[fullName]?.data?.docs?.upsert ||
213+
this.rbac.cluster.bucket[this.options.selected_bucket].data.docs.upsert);
207214
};
208215

209216
dec.deleteAllowed = function() {
210-
if (!this.rbac.init || !this.options.selected_collection || !this.rbac.cluster.collection) {
217+
if (!this.rbac.init || !this.options.selected_bucket || !this.options.selected_scope || !this.options.selected_collection)
211218
return false;
212-
}
213219

214-
// return the cached value
215-
return this.rbac.cluster.collection[`${this.options.selected_bucket}:${this.options.selected_scope}:${this.options.selected_collection}`]?.data.docs.delete;
220+
const fullName = `${dec.options.selected_bucket}:${dec.options.selected_scope}:${dec.options.selected_collection}`;
221+
222+
return (this.rbac.cluster.collection[fullName]?.data?.docs?.delete ||
223+
this.rbac.cluster.bucket[this.options.selected_bucket].data.docs.delete);
216224
};
217225

218226
//
@@ -292,10 +300,20 @@ class QwDocumentsComponent extends MnLifeCycleHooksToStream {
292300
return (false);
293301
}
294302

303+
// corner case - can't query if we can't read documents
304+
if (dec.rbac.cluster.collection[`${dec.options.selected_bucket}:${dec.options.selected_scope}:${dec.options.selected_collection}`]?.data?.docs?.read !== true) {
305+
dec.options.current_result = "No permission to read documents.";
306+
return (false);
307+
}
308+
295309
// always use KV for single doc lookups by ID
296310
if (dec.options.show_id && dec.options.doc_id)
297311
return KV;
298312

313+
// N1QL is not available if the user lacks n1ql.select permissions on the current collection
314+
if (dec.rbac.cluster.collection[`${dec.options.selected_bucket}:${dec.options.selected_scope}:${dec.options.selected_collection}`]?.n1ql.select.execute !== true)
315+
return KV;
316+
299317
// key range lookup or limit/offset with no WHERE clause
300318
// - use N1QL if primary index, otherwise KV (though fail if ephemeral)
301319
if ((!dec.options.show_id && (dec.options.doc_id_start || dec.options.doc_id_end)) || dec.options.where_clause.length == 0) {
@@ -333,7 +351,18 @@ class QwDocumentsComponent extends MnLifeCycleHooksToStream {
333351
//
334352

335353
function can_use_n1ql() {
336-
return (has_prim() || has_sec());
354+
// N1QL is not available if the user lacks n1ql.select permissions on the current collection
355+
return (dec.rbac.cluster.collection[`${dec.options.selected_bucket}:${dec.options.selected_scope}:${dec.options.selected_collection}`]?.n1ql.select.execute === true
356+
&& (has_prim() || has_sec()));
357+
}
358+
359+
function get_n1ql_placeholder() {
360+
if (dec.rbac.cluster.collection[`${dec.options.selected_bucket}:${dec.options.selected_scope}:${dec.options.selected_collection}`]?.n1ql.select.execute !== true)
361+
return 'no query permissions';
362+
else if (!has_indexes())
363+
return 'no indexes available...';
364+
else
365+
return 'optional...';
337366
}
338367

339368
//
@@ -764,6 +793,7 @@ class QwDocumentsComponent extends MnLifeCycleHooksToStream {
764793
case false: // error status
765794
showErrorDialog("Document Error", dec.options.current_result, true);
766795
dec.options.current_query = dec.options.selected_bucket;
796+
refreshResults();
767797
break;
768798
}
769799
}
@@ -1261,7 +1291,10 @@ class QwDocumentsComponent extends MnLifeCycleHooksToStream {
12611291
metadataUpdate(meta);
12621292
// MB-51579 - when collection unspecified, don't try to retrieve documents
12631293
if (dec.options.selected_bucket && dec.options.selected_scope && dec.options.selected_collection && dec.docViewer())
1264-
retrieveDocs();
1294+
// get permissions for new collection
1295+
qwMetadataService.checkCollectionPerms(dec.options.selected_bucket, dec.options.selected_scope, dec.options.selected_collection).then(function () {
1296+
retrieveDocs_inner();
1297+
});
12651298
});
12661299
});
12671300
}

query-ui/angular-components/documents/qw.documents.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
-->
1010

1111
<div *ngIf="!docViewer()">
12-
Insufficient permissions to view documents. User must have at least Data Reader on one or more
12+
Insufficient permissions to view documents. User must have at least Data Reader or Writer on one or more
1313
collections, and also the ability to view scopes and collections in that bucket.
1414
</div>
1515

@@ -151,7 +151,7 @@ <h5 class="inline">Document ID&nbsp;</h5>
151151
(change)="dec.where_changed()"
152152
[attr.disabled]="(!dec.can_use_n1ql() || (dec.options.show_id && dec.options.doc_id) || (!dec.options.show_id && (dec.options.doc_id_start || dec.options.doc_id_end))) || null"
153153
class="width-12"
154-
placeholder="{{dec.has_indexes() ? 'optional...' : 'no indexes available...'}}">
154+
placeholder="{{dec.get_n1ql_placeholder()}}">
155155
</div>
156156

157157
<div style="display:none;" class="resp-show-sml margin-top-half width-12"></div>

query-ui/angular-components/documents/qw.documents.module.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ let documentsStates = [
5050
name: 'app.admin.docs.editor',
5151
data: {
5252
title: "Documents", // appears in breadcrumbs in title bar
53-
permissions: "cluster.collection['.:.:.'].data.docs.read",
53+
permissions: "cluster.collection['.:.:.'].data.docs.read || cluster.collection['.:.:.'].data.docs.upsert",
5454
},
5555
params: { // can parameters be sent via the URL?
5656
bucket: {

query-ui/angular-directives/qw.collection.menu.component.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,13 @@ class QwCollectionMenu extends MnLifeCycleHooksToStream {
251251
this.scopes[selectedBucket].indexOf(selectedScope) < 0))
252252
this.keyspaceForm.get("scopeName").setValue(this.scopes[selectedBucket][0]);
253253

254-
// make sure we have collections to work with
254+
// if no scopes, blank out scope selection
255+
else if (this.scopes[selectedBucket].length === 0) {
256+
this.keyspaceForm.get("scopeName").setValue('');
257+
this.keyspaceForm.get("collectionName").setValue('');
258+
}
259+
260+
// make sure we have collections to work with
255261
if (!selectedScope || !this.collections[selectedBucket] ||
256262
!this.collections[selectedBucket][selectedScope])
257263
return;

query-ui/angular-directives/qw.collection.menu.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,6 @@
3636
*ngIf="errors && compat.atLeast70"
3737
placement="auto"
3838
container="body"
39-
class="fa-warning icon orange-3 cursor-pointer"
39+
class="fa-warning icon orange-3 cursor-pointer margin-right-half"
4040
[ngbTooltip]="errors">
4141
</span>

query-ui/angular-directives/qw.json.table.editor.directive.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,7 @@ function createHTMLFromJson(json) {
344344

345345
// if json not array, it must be error message string
346346
else {
347-
resultHTML = wrapperStart + json + wrapperEnd;
347+
resultHTML = `${wrapperStart} <div class="error text-small"> ${json} </div> ${wrapperEnd}`;
348348
}
349349

350350
return (resultHTML);

query-ui/angular-services/qw.collections.service.js

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -170,23 +170,27 @@ function getQwCollectionsService(
170170

171171

172172
function refreshScopesAndCollectionsForBucket(bucket, proxy) {
173-
var meta = getMeta(proxy);
174-
const canUseEndpoints = qms.rbac && qms.rbac.cluster.collection['.:.:.'].collections.read;
175-
let promise = canUseEndpoints ?
176-
refreshScopesAndCollectionsForBucketUsingApi(bucket, proxy) :
177-
refreshScopesAndCollectionsForBucketUsingQuery(bucket, proxy);
178-
179-
// when we get the list of scopes and collections, check the permissions
180-
return promise.then((meta) =>
181-
qms.getAllCollectionPermissions(bucket,meta.collections[bucket])
182-
.then(() => {
183-
// remove any collections that we don't have permission to read
184-
Object.keys(meta.collections[bucket]).forEach(scope => {
185-
meta.collections[bucket][scope] = meta.collections[bucket][scope].filter(collection =>
186-
qms.rbac.cluster.collection[`${bucket}:${scope}:${collection}`]?.data.docs.read);
187-
});
188-
return meta;
189-
}));
173+
// if we are fetching data from a remote cluster, we don't have any permission info, must use API and hope
174+
// for the best
175+
if (proxy || !qms.rbac) {
176+
return refreshScopesAndCollectionsForBucketUsingApi(bucket, proxy);
177+
}
178+
179+
// otherwise, which API do we have permission to use, if any?
180+
const canUseEndpoints = qms.rbac?.cluster.collection[`${bucket}:.:.`].collections.read;
181+
const canUseQuery = qms.rbac?.cluster.n1ql.meta.read;
182+
if (canUseEndpoints)
183+
return refreshScopesAndCollectionsForBucketUsingApi(bucket, proxy);
184+
else if (canUseQuery)
185+
return refreshScopesAndCollectionsForBucketUsingQuery(bucket, proxy);
186+
187+
// no permissions - return an error
188+
const meta = getMeta(proxy);
189+
meta.errors.length = 0;
190+
meta.errors.push(`No permission to read scopes and collections for ${bucket}. User needs either 'cluster.collection[${bucket}:.:.].collections!read' or 'cluster.n1ql.meta!read'`);
191+
meta.scopes[bucket] = [];
192+
meta.collections[bucket] = {};
193+
return Promise.resolve(meta);
190194
}
191195

192196
function refreshScopesAndCollectionsForBucketUsingQuery(bucket, proxy) {

query-ui/angular-services/qw.metadata.service.js

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,32 @@ class QwMetadataService {
7272
error => {This.compat = {err: error}});
7373
}
7474

75+
// find out what permissions we have on a selected collection
76+
checkCollectionPerms(bucket, scope, collection) {
77+
const fullName = `${bucket}:${scope}:${collection}`;
78+
const perms = [
79+
`cluster.collection[${fullName}].data.docs!read`,
80+
`cluster.collection[${fullName}].data.docs!upsert`,
81+
`cluster.collection[${fullName}].data.docs!delete`,
82+
`cluster.collection[${fullName}].n1ql.select!execute`,
83+
];
84+
85+
// do we need to get the permissions?
86+
if (this.rbac.cluster.collection[fullName]?.data?.docs?.read === undefined ||
87+
this.rbac.cluster.collection[fullName]?.data?.docs?.upsert === undefined ||
88+
this.rbac.cluster.collection[fullName]?.data?.docs?.delete === undefined ||
89+
this.rbac.cluster.collection[fullName]?.n1ql?.select?.execute === undefined) {
90+
return this.qwHttp.post('/pools/default/checkPermissions',perms.join(','))
91+
.then(result => {
92+
this.decodePermissions(result.data,true)
93+
},
94+
err => {this.rbac = err});
95+
}
96+
else
97+
return Promise.resolve();
98+
}
99+
100+
75101
decodePoolsDefault(poolDefault) {
76102
let This = this;
77103
// figure out compat version
@@ -122,6 +148,9 @@ class QwMetadataService {
122148

123149
...this.bucketList.map(bucketName => 'cluster.collection[' + bucketName + ':.:.].n1ql.udf!manage'),
124150
...this.bucketList.map(bucketName => 'cluster.collection[' + bucketName + ':.:.].n1ql.udf_external!manage'),
151+
152+
// permissions needed for reading scopes & collections
153+
...this.bucketList.map(bucketName => 'cluster.collection[' + bucketName + ':.:.].collections!read'),
125154
];
126155

127156
let promise = this.qwHttp.post('/pools/default/checkPermissions',perms.join(','))
@@ -152,7 +181,7 @@ class QwMetadataService {
152181
}
153182

154183
// decode permissions from the array of 'name':<bool> to a tree
155-
decodePermissions(permissions) {
184+
decodePermissions(permissions, skipIndexes) {
156185
//console.log("Got permissions: " + JSON.stringify(permissions,null,2));
157186

158187
this.rbac.init = true;
@@ -191,10 +220,12 @@ class QwMetadataService {
191220

192221
// get index info, if possible. If query nodes exist, use a query,
193222
// which works even if /indexStatus API is forbidden
194-
if (this.queryNodes.length > 0)
195-
return this.getIndexesN1QL();
196-
else
197-
return this.getIndexesREST();
223+
if (!skipIndexes) {
224+
if (this.queryNodes.length > 0)
225+
return this.getIndexesN1QL();
226+
else
227+
return this.getIndexesREST();
228+
}
198229
}
199230

200231
// do we have enough permissions to run queries?

query-ui/ui-current/main.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ angular
4242
state: 'app.admin.docs.editor',
4343
includedByState: 'app.admin.docs',
4444
plugIn: 'workbenchTab',
45-
ngShow: "rbac.cluster.collection['.:.:.'].data.docs.read",
45+
ngShow: "rbac.cluster.collection['.:.:.'].data.docs.read || rbac.cluster.collection['.:.:.'].data.docs.upsert",
4646
index: 0
4747
});
4848

0 commit comments

Comments
 (0)