(core) distinguish open public documents from listing them

Summary:
getOrgWorkspaces and getWorkspaces had an unintended feature where
if a user had access to a workspace, they could list all publically
shared documents within that workspace.  This diff stops considering
resources shared with everyone@ when listing orgs or workspaces.
Resources shared with anon@ remain listed - this is how the example
workspace operates.

Test Plan: added test

Reviewers: dsagal

Reviewed By: dsagal

Differential Revision: https://phab.getgrist.com/D2671
pull/3/head
Paul Fitzpatrick 4 years ago
parent 32f3d03c3d
commit c1c17bf54e

@ -118,6 +118,10 @@ export interface Scope {
specialPermit?: Permit; // When set, extra rights are granted on a specific resource.
}
// Flag for whether we are listing resources or opening them. This makes a difference
// for public resources, which we allow users to open but not necessarily list.
type AccessStyle = 'list' | 'open';
// A Scope for documents, with mandatory urlId.
export interface DocScope extends Scope {
urlId: string;
@ -778,7 +782,7 @@ export class HomeDBManager extends EventEmitter {
queryBuilder = this._addIsSupportWorkspace(userId, queryBuilder, 'orgs', 'workspaces');
// Add access information and query limits
// TODO: allow generic org limit once sample/support workspace is done differently
queryBuilder = this._applyLimit(queryBuilder, {...scope, org: undefined}, ['orgs', 'workspaces', 'docs']);
queryBuilder = this._applyLimit(queryBuilder, {...scope, org: undefined}, ['orgs', 'workspaces', 'docs'], 'list');
const result = await this._verifyAclPermissions(queryBuilder, { scope });
// Return the workspaces, not the org(s).
@ -815,7 +819,7 @@ export class HomeDBManager extends EventEmitter {
queryBuilder = this._addIsSupportWorkspace(userId, queryBuilder, 'orgs', 'workspaces');
// Add access information and query limits
// TODO: allow generic org limit once sample/support workspace is done differently
queryBuilder = this._applyLimit(queryBuilder, {...scope, org: undefined}, ['workspaces', 'docs']);
queryBuilder = this._applyLimit(queryBuilder, {...scope, org: undefined}, ['workspaces', 'docs'], 'list');
const result = await this._verifyAclPermissions(queryBuilder, { scope });
// Return a single workspace.
if (result.status === 200) {
@ -2121,7 +2125,7 @@ export class HomeDBManager extends EventEmitter {
const docQuery = this._doc(scope, {
manager
})
.addSelect(this._markIsPermitted('orgs', scope.userId, permissions), 'is_permitted');
.addSelect(this._markIsPermitted('orgs', scope.userId, 'open', permissions), 'is_permitted');
const docQueryResult = await verifyIsPermitted(docQuery);
if (docQueryResult.status !== 200) {
// If the query for the doc failed, return the failure result.
@ -2353,7 +2357,7 @@ export class HomeDBManager extends EventEmitter {
}
// Compute whether we have access to the doc
query = query.addSelect(
this._markIsPermitted('orgs', effectiveUserId, threshold),
this._markIsPermitted('orgs', effectiveUserId, 'open', threshold),
'is_permitted'
);
}
@ -2803,7 +2807,7 @@ export class HomeDBManager extends EventEmitter {
// TODO includeSupport should really be false, and the support for it should be removed.
// (For this, example doc URLs should be under docs.getgrist.com rather than team domains.)
// Add access information and query limits
query = this._applyLimit(query, {...scope, includeSupport: true}, ['docs', 'workspaces', 'orgs']);
query = this._applyLimit(query, {...scope, includeSupport: true}, ['docs', 'workspaces', 'orgs'], 'open');
if (options.markPermissions) {
let effectiveUserId = userId;
let threshold = options.markPermissions;
@ -2814,7 +2818,7 @@ export class HomeDBManager extends EventEmitter {
}
// Compute whether we have access to the doc
query = query.addSelect(
this._markIsPermitted('docs', effectiveUserId, threshold),
this._markIsPermitted('docs', effectiveUserId, 'open', threshold),
'is_permitted'
);
}
@ -2865,7 +2869,7 @@ export class HomeDBManager extends EventEmitter {
}
// Compute whether we have access to the doc
query = query.addSelect(
this._markIsPermitted('workspaces', effectiveUserId, threshold),
this._markIsPermitted('workspaces', effectiveUserId, 'open', threshold),
'is_permitted'
);
}
@ -2921,9 +2925,10 @@ export class HomeDBManager extends EventEmitter {
}
private _withAccess(qb: SelectQueryBuilder<any>, users: AvailableUsers,
table: 'orgs'|'workspaces'|'docs') {
table: 'orgs'|'workspaces'|'docs',
accessStyle: AccessStyle = 'open') {
return qb
.addSelect(this._markIsPermitted(table, users, null), `${table}_permissions`);
.addSelect(this._markIsPermitted(table, users, accessStyle, null), `${table}_permissions`);
}
/**
@ -3299,6 +3304,7 @@ export class HomeDBManager extends EventEmitter {
private _markIsPermitted(
resType: 'orgs'|'workspaces'|'docs',
users: AvailableUsers,
accessStyle: AccessStyle,
permissions: Permissions|null = Permissions.VIEW
): (qb: SelectQueryBuilder<any>) => SelectQueryBuilder<any> {
const idColumn = resType.slice(0, -1) + "_id";
@ -3316,14 +3322,22 @@ export class HomeDBManager extends EventEmitter {
// optimized if we eliminate one of those users. The guN
// aliases are joining in _getUsersAcls, and refer to the
// group_users table at different levels of nesting.
// When listing, everyone@ shares do not contribute to access permissions,
// only to the public flag. So resources available to the user only because
// they are publically available will not be listed. Shares with anon@,
// on the other hand, *are* listed.
const everyoneContribution = accessStyle === 'list' ? Permissions.PUBLIC :
`${Permissions.PUBLIC} | acl_rules.permissions`;
q = q.select(
bitOr(this._dbType, `(acl_rules.permissions | (case when ` +
`${everyoneId} IN (gu0.user_id, gu1.user_id, gu2.user_id, gu3.user_id) OR ` +
bitOr(this._dbType, `(case when ` +
`${everyoneId} IN (gu0.user_id, gu1.user_id, gu2.user_id, gu3.user_id) ` +
`then ${everyoneContribution} else (case when ` +
`${anonId} IN (gu0.user_id, gu1.user_id, gu2.user_id, gu3.user_id) ` +
`then ${Permissions.PUBLIC} else 0 end))`, 8), 'permissions');
`then ${Permissions.PUBLIC} | acl_rules.permissions else acl_rules.permissions end) end)`, 8), 'permissions');
}
q = q.from('acl_rules', 'acl_rules');
q = this._getUsersAcls(q, users);
q = this._getUsersAcls(q, users, accessStyle);
q = q.andWhere(`acl_rules.${idColumn} = ${resType}.id`);
if (permissions !== null) {
q = q.andWhere(`(acl_rules.permissions & ${permissions}) = ${permissions}`).limit(1);
@ -3357,7 +3371,8 @@ export class HomeDBManager extends EventEmitter {
// sufficient for our current ACL setup. A third is added as a low-cost preparation
// for implementing something like teams in the future. It has no measurable effect on
// speed.
private _getUsersAcls(qb: SelectQueryBuilder<any>, users: AvailableUsers) {
private _getUsersAcls(qb: SelectQueryBuilder<any>, users: AvailableUsers,
accessStyle: AccessStyle) {
// Every acl_rule is associated with a single group. A user may
// be a direct member of that group, via the group_users table.
// Or they may be a member of a group that is a member of that
@ -3383,6 +3398,15 @@ export class HomeDBManager extends EventEmitter {
cond = cond.orWhere(`gu1.user_id = ${everyoneId}`);
cond = cond.orWhere(`gu2.user_id = ${everyoneId}`);
cond = cond.orWhere(`gu3.user_id = ${everyoneId}`);
if (accessStyle === 'list') {
// Support also the special anonymous user. Currently, by convention, sharing a
// resource with anonymous should make it listable.
const anonId = this._specialUserIds[ANONYMOUS_USER_EMAIL];
cond = cond.orWhere(`gu0.user_id = ${anonId}`);
cond = cond.orWhere(`gu1.user_id = ${anonId}`);
cond = cond.orWhere(`gu2.user_id = ${anonId}`);
cond = cond.orWhere(`gu3.user_id = ${anonId}`);
}
// Add an exception for the previewer user, if present.
const previewerId = this._specialUserIds[PREVIEWER_EMAIL];
if (users === previewerId) {
@ -3433,7 +3457,8 @@ export class HomeDBManager extends EventEmitter {
// if request is from a branded webpage; results should be limited to a
// specific user or set of users.
private _applyLimit<T>(qb: SelectQueryBuilder<T>, limit: Scope,
resources: Array<'docs'|'workspaces'|'orgs'>): SelectQueryBuilder<T> {
resources: Array<'docs'|'workspaces'|'orgs'>,
accessStyle: AccessStyle): SelectQueryBuilder<T> {
if (limit.org) {
// Filtering on merged org is a special case, see urlIdQuery
const mergedOrg = this.isMergedOrg(limit.org || null);
@ -3443,7 +3468,7 @@ export class HomeDBManager extends EventEmitter {
}
if (limit.users || limit.userId) {
for (const res of resources) {
qb = this._withAccess(qb, limit.users || limit.userId, res);
qb = this._withAccess(qb, limit.users || limit.userId, res, accessStyle);
}
}
if (resources.includes('docs') && resources.includes('workspaces') && !limit.showAll) {

Loading…
Cancel
Save