mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(core) fix bug where sharing doc with everyone@ as viewer made it unlisted for site viewers
Summary: Shares of the same role (e.g. viewer) at different levels could interact for a resource (e.g. a doc) shared with everyone@, potentially blocking the listing of that resource. This diff removes the interaction. The permission of a user on a resource is calculated by finding all acl rules that link that resource to a group to which the user belongs, or to a group that has a subgroup to which the user belongs, etc, and then bitwise-or-ing the permissions on the acl rules. A later wrinkle was to allow public sharing via special users. A still later wrinkle was to avoid listing resources if they were only shared with the special everyone@ user, while allowing access to them if user has their full link. That wrinkle had a bug, where if e.g. a doc were shared with everyone@ as a viewer, and the org the doc was in was shared with someone@ as a viewer, and the doc inherited the org permissions via a workspace, then that doc would end up not being listed. The fix is straightforward enough, but needs different code for postgres and sqlite, and is a bit verbose because we unwrap subgroups to a few levels rather than doing recursion (which looks cleaner but was slower in benchmarks). Test Plan: added test that fails without this fix Reviewers: georgegevoian Reviewed By: georgegevoian Differential Revision: https://phab.getgrist.com/D3095
This commit is contained in:
parent
e58df5df5b
commit
35e18cc0ad
@ -28,7 +28,8 @@ import {Workspace} from "app/gen-server/entity/Workspace";
|
||||
import {Permissions} from 'app/gen-server/lib/Permissions';
|
||||
import {scrubUserFromOrg} from "app/gen-server/lib/scrubUserFromOrg";
|
||||
import {applyPatch} from 'app/gen-server/lib/TypeORMPatches';
|
||||
import {bitOr, getRawAndEntities, now, readJson} from 'app/gen-server/sqlUtils';
|
||||
import {bitOr, getRawAndEntities, hasAtLeastOneOfTheseIds, hasOnlyTheseIdsOrNull,
|
||||
now, readJson} from 'app/gen-server/sqlUtils';
|
||||
import {makeId} from 'app/server/lib/idUtils';
|
||||
import * as log from 'app/server/lib/log';
|
||||
import {Permit} from 'app/server/lib/Permit';
|
||||
@ -3562,15 +3563,37 @@ export class HomeDBManager extends EventEmitter {
|
||||
// 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`;
|
||||
|
||||
// At this point, we have user ids available for a group associated with the acl
|
||||
// rule, or a subgroup of that group, of a subgroup of that group, or a subgroup
|
||||
// of that group (this is enough nesting to support docs in workspaces in orgs,
|
||||
// with one level of nesting held for future use).
|
||||
const userIdCols = ['gu0.user_id', 'gu1.user_id', 'gu2.user_id', 'gu3.user_id'];
|
||||
|
||||
// If any of the user ids is public (everyone@, anon@), we set the PUBLIC flag.
|
||||
// This is only advisory, for display in the client - it plays no role in access
|
||||
// control.
|
||||
const publicFlagSql = `case when ` +
|
||||
hasAtLeastOneOfTheseIds(this._dbType, [everyoneId, anonId], userIdCols) +
|
||||
` then ${Permissions.PUBLIC} else 0 end`;
|
||||
|
||||
// The contribution made by the acl rule to overall user permission is contained
|
||||
// in acl_rules.permissions. BUT if we are listing resources, we discount the
|
||||
// permission contribution if it is only made with everyone@, and not anon@
|
||||
// or any of the ids associated with the user. The resource may end up being
|
||||
// accessible but unlisted for this user.
|
||||
const contributionSql = accessStyle !== 'list' ? 'acl_rules.permissions' :
|
||||
`case when ` +
|
||||
hasOnlyTheseIdsOrNull(this._dbType, [everyoneId], userIdCols) +
|
||||
` then 0 else acl_rules.permissions end`;
|
||||
|
||||
// Finally, if all users are null, the resource is being viewed by the special
|
||||
// previewer user.
|
||||
const previewerSql = `case when coalesce(${userIdCols.join(',')}) is null` +
|
||||
` then acl_rules.permissions else 0 end`;
|
||||
|
||||
q = q.select(
|
||||
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} | acl_rules.permissions else acl_rules.permissions end) end)`, 8
|
||||
),
|
||||
bitOr(this._dbType, `(${publicFlagSql} | ${contributionSql} | ${previewerSql})`, 8),
|
||||
'permissions'
|
||||
);
|
||||
}
|
||||
|
@ -42,6 +42,39 @@ export function bitOr(dbType: DatabaseType, column: string, bits: number): strin
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks if a set of columns contains only the given ids (or null).
|
||||
* Uses array containment operator on postgres (with array_remove to deal with nulls),
|
||||
* and a clunkier syntax for sqlite.
|
||||
*/
|
||||
export function hasOnlyTheseIdsOrNull(dbType: DatabaseType, ids: number[], columns: string[]): string {
|
||||
switch (dbType) {
|
||||
case 'postgres':
|
||||
return `array[${ids.join(',')}] @> array_remove(array[${columns.join(',')}],null)`;
|
||||
case 'sqlite':
|
||||
return columns.map(col => `coalesce(${col} in (${ids.join(',')}), true)`).join(' AND ');
|
||||
default:
|
||||
throw new Error(`hasOnlyTheseIdsOrNull not implemented for ${dbType}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if at least one of a set of ids is present in a set of columns.
|
||||
* There must be at least one id and one column.
|
||||
* Uses the intersection operator on postgres, and a clunkier syntax for sqlite.
|
||||
*/
|
||||
export function hasAtLeastOneOfTheseIds(dbType: DatabaseType, ids: number[], columns: string[]): string {
|
||||
switch (dbType) {
|
||||
case 'postgres':
|
||||
return `array[${ids.join(',')}] && array[${columns.join(',')}]`;
|
||||
case 'sqlite':
|
||||
return ids.map(id => `${id} in (${columns.join(',')})`).join(' OR ');
|
||||
default:
|
||||
throw new Error(`hasAtLeastOneOfTheseIds not implemented for ${dbType}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a json value returned by the database into a javascript
|
||||
* object. For postgres, the value is already unpacked, but for sqlite
|
||||
|
Loading…
Reference in New Issue
Block a user