mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) move home server into core
Summary: This moves enough server material into core to run a home server. The data engine is not yet incorporated (though in manual testing it works when ported). Test Plan: existing tests pass Reviewers: dsagal Reviewed By: dsagal Differential Revision: https://phab.getgrist.com/D2552
This commit is contained in:
209
app/gen-server/lib/scrubUserFromOrg.ts
Normal file
209
app/gen-server/lib/scrubUserFromOrg.ts
Normal file
@@ -0,0 +1,209 @@
|
||||
import {EntityManager} from "typeorm";
|
||||
import * as roles from 'app/common/roles';
|
||||
import {Document} from "app/gen-server/entity/Document";
|
||||
import {Group} from "app/gen-server/entity/Group";
|
||||
import {Organization} from "app/gen-server/entity/Organization";
|
||||
import {Workspace} from "app/gen-server/entity/Workspace";
|
||||
import pick = require('lodash/pick');
|
||||
|
||||
/**
|
||||
*
|
||||
* Remove the given user from the given org and every resource inside the org.
|
||||
* If the user being removed is an owner of any resources in the org, the caller replaces
|
||||
* them as the owner. This is to prevent complete loss of access to any resource.
|
||||
*
|
||||
* This method transforms ownership without regard to permissions. We all talked this
|
||||
* over and decided this is what we wanted, but there's no denying it is funky and could
|
||||
* be surprising.
|
||||
* TODO: revisit user scrubbing when we can.
|
||||
*
|
||||
*/
|
||||
export async function scrubUserFromOrg(
|
||||
orgId: number,
|
||||
removeUserId: number,
|
||||
callerUserId: number,
|
||||
manager: EntityManager
|
||||
): Promise<void> {
|
||||
await addMissingGuestMemberships(callerUserId, orgId, manager);
|
||||
|
||||
// This will be a list of all mentions of removeUser and callerUser in any resource
|
||||
// within the org.
|
||||
const mentions: Mention[] = [];
|
||||
|
||||
// Base query for all group_users related to these two users and this org.
|
||||
const q = manager.createQueryBuilder()
|
||||
.select('group_users.group_id, group_users.user_id')
|
||||
.from('group_users', 'group_users')
|
||||
.leftJoin(Group, 'groups', 'group_users.group_id = groups.id')
|
||||
.addSelect('groups.name as name')
|
||||
.leftJoin('groups.aclRule', 'acl_rules')
|
||||
.where('(group_users.user_id = :removeUserId or group_users.user_id = :callerUserId)',
|
||||
{removeUserId, callerUserId})
|
||||
.andWhere('orgs.id = :orgId', {orgId});
|
||||
|
||||
// Pick out group_users related specifically to the org resource, in 'mentions' format
|
||||
// (including resource id, a tag for the kind of resource, the group name, the user
|
||||
// id, and the group id).
|
||||
const orgs = q.clone()
|
||||
.addSelect(`'org' as kind, orgs.id`)
|
||||
.innerJoin(Organization, 'orgs', 'orgs.id = acl_rules.org_id');
|
||||
mentions.push(...await orgs.getRawMany());
|
||||
|
||||
// Pick out mentions related to any workspace within the org.
|
||||
const wss = q.clone()
|
||||
.innerJoin(Workspace, 'workspaces', 'workspaces.id = acl_rules.workspace_id')
|
||||
.addSelect(`'ws' as kind, workspaces.id`)
|
||||
.innerJoin('workspaces.org', 'orgs');
|
||||
mentions.push(...await wss.getRawMany());
|
||||
|
||||
// Pick out mentions related to any doc within the org.
|
||||
const docs = q.clone()
|
||||
.innerJoin(Document, 'docs', 'docs.id = acl_rules.doc_id')
|
||||
.addSelect(`'doc' as kind, docs.id`)
|
||||
.innerJoin('docs.workspace', 'workspaces')
|
||||
.innerJoin('workspaces.org', 'orgs');
|
||||
mentions.push(...await docs.getRawMany());
|
||||
|
||||
// Prepare to add and delete group_users.
|
||||
const toDelete: Mention[] = [];
|
||||
const toAdd: Mention[] = [];
|
||||
|
||||
// Now index the mentions by whether they are for the removeUser or the callerUser,
|
||||
// and the resource they apply to.
|
||||
const removeUserMentions = new Map<MentionKey, Mention>();
|
||||
const callerUserMentions = new Map<MentionKey, Mention>();
|
||||
for (const mention of mentions) {
|
||||
const isGuest = mention.name === roles.GUEST;
|
||||
if (mention.user_id === removeUserId) {
|
||||
// We can safely remove any guest roles for the removeUser without any
|
||||
// further inspection.
|
||||
if (isGuest) { toDelete.push(mention); continue; }
|
||||
removeUserMentions.set(getMentionKey(mention), mention);
|
||||
} else {
|
||||
if (isGuest) { continue; }
|
||||
callerUserMentions.set(getMentionKey(mention), mention);
|
||||
}
|
||||
}
|
||||
// Now iterate across the mentions of removeUser, and see what we need to do
|
||||
// for each of them.
|
||||
for (const [key, removeUserMention] of removeUserMentions) {
|
||||
toDelete.push(removeUserMention);
|
||||
if (removeUserMention.name !== roles.OWNER) {
|
||||
// Nothing fancy needed for cases where the removeUser is not the owner.
|
||||
// Just discard those.
|
||||
continue;
|
||||
}
|
||||
// The removeUser was a direct owner on this resource, but the callerUser was
|
||||
// not. We set the callerUser as a direct owner on this resource, to preserve
|
||||
// access to it.
|
||||
// TODO: the callerUser might inherit sufficient access, in which case this
|
||||
// step is unnecessary and could be skipped. I believe it does no harm though.
|
||||
const callerUserMention = callerUserMentions.get(key);
|
||||
if (callerUserMention && callerUserMention.name === roles.OWNER) { continue; }
|
||||
if (callerUserMention) { toDelete.push(callerUserMention); }
|
||||
toAdd.push({...removeUserMention, user_id: callerUserId});
|
||||
}
|
||||
if (toDelete.length > 0) {
|
||||
await manager.createQueryBuilder()
|
||||
.delete()
|
||||
.from('group_users')
|
||||
.whereInIds(toDelete.map(m => pick(m, ['user_id', 'group_id'])))
|
||||
.execute();
|
||||
}
|
||||
if (toAdd.length > 0) {
|
||||
await manager.createQueryBuilder()
|
||||
.insert()
|
||||
.into('group_users')
|
||||
.values(toAdd.map(m => pick(m, ['user_id', 'group_id'])))
|
||||
.execute();
|
||||
}
|
||||
|
||||
// TODO: At this point, we've removed removeUserId from every mention in group_users.
|
||||
// The user may still be mentioned in billing_account_managers. If the billing_account
|
||||
// is linked to just this single organization, perhaps it would make sense to remove
|
||||
// the user there, if the callerUser is themselves a billing account manager?
|
||||
|
||||
await addMissingGuestMemberships(callerUserId, orgId, manager);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds specified user to any guest groups for the resources of an org where the
|
||||
* user needs to be and is not already.
|
||||
*/
|
||||
export async function addMissingGuestMemberships(userId: number, orgId: number,
|
||||
manager: EntityManager) {
|
||||
// For workspaces:
|
||||
// User should be in guest group if mentioned in a doc within that workspace.
|
||||
let groupUsers = await manager.createQueryBuilder()
|
||||
.select('workspace_groups.id as group_id, cast(:userId as int) as user_id')
|
||||
.setParameter('userId', userId)
|
||||
.from(Workspace, 'workspaces')
|
||||
.where('workspaces.org_id = :orgId', {orgId})
|
||||
.innerJoin('workspaces.docs', 'docs')
|
||||
.innerJoin('docs.aclRules', 'doc_acl_rules')
|
||||
.innerJoin('doc_acl_rules.group', 'doc_groups')
|
||||
.innerJoin('doc_groups.memberUsers', 'doc_group_users')
|
||||
.andWhere('doc_group_users.id = :userId', {userId})
|
||||
.leftJoin('workspaces.aclRules', 'workspace_acl_rules')
|
||||
.leftJoin('workspace_acl_rules.group', 'workspace_groups')
|
||||
.leftJoin('group_users', 'workspace_group_users',
|
||||
'workspace_group_users.group_id = workspace_groups.id and ' +
|
||||
'workspace_group_users.user_id = :userId')
|
||||
.andWhere('workspace_groups.name = :guestName', {guestName: roles.GUEST})
|
||||
.groupBy('workspaces.id, workspace_groups.id, workspace_group_users.user_id')
|
||||
.having('workspace_group_users.user_id is null')
|
||||
.getRawMany();
|
||||
if (groupUsers.length > 0) {
|
||||
await manager.createQueryBuilder()
|
||||
.insert()
|
||||
.into('group_users')
|
||||
.values(groupUsers)
|
||||
.execute();
|
||||
}
|
||||
|
||||
// For org:
|
||||
// User should be in guest group if mentioned in a workspace within that org.
|
||||
groupUsers = await manager.createQueryBuilder()
|
||||
.select('org_groups.id as group_id, cast(:userId as int) as user_id')
|
||||
.setParameter('userId', userId)
|
||||
.from(Organization, 'orgs')
|
||||
.where('orgs.id = :orgId', {orgId})
|
||||
.innerJoin('orgs.workspaces', 'workspaces')
|
||||
.innerJoin('workspaces.aclRules', 'workspaces_acl_rules')
|
||||
.innerJoin('workspaces_acl_rules.group', 'workspace_groups')
|
||||
.innerJoin('workspace_groups.memberUsers', 'workspace_group_users')
|
||||
.andWhere('workspace_group_users.id = :userId', {userId})
|
||||
.leftJoin('orgs.aclRules', 'org_acl_rules')
|
||||
.leftJoin('org_acl_rules.group', 'org_groups')
|
||||
.leftJoin('group_users', 'org_group_users',
|
||||
'org_group_users.group_id = org_groups.id and ' +
|
||||
'org_group_users.user_id = :userId')
|
||||
.andWhere('org_groups.name = :guestName', {guestName: roles.GUEST})
|
||||
.groupBy('org_groups.id, org_group_users.user_id')
|
||||
.having('org_group_users.user_id is null')
|
||||
.getRawMany();
|
||||
if (groupUsers.length > 0) {
|
||||
await manager.createQueryBuilder()
|
||||
.insert()
|
||||
.into('group_users')
|
||||
.values(groupUsers)
|
||||
.execute();
|
||||
}
|
||||
|
||||
// For doc:
|
||||
// Guest groups are not used.
|
||||
}
|
||||
|
||||
interface Mention {
|
||||
id: string|number; // id of resource
|
||||
kind: 'org'|'ws'|'doc'; // type of resource
|
||||
user_id: number; // id of user in group
|
||||
group_id: number; // id of group
|
||||
name: string; // name of group
|
||||
}
|
||||
|
||||
type MentionKey = string;
|
||||
|
||||
function getMentionKey(mention: Mention): MentionKey {
|
||||
return `${mention.kind} ${mention.id}`;
|
||||
}
|
||||
Reference in New Issue
Block a user