gristlabs_grist-core/app/client/aclui/ACLUsers.ts
Dmitry S 1a5bacc807 (core) Disallow the combination of Public Edit access and granular ACLs.
Summary:
- When Public Edit access is enabled, Access Rules page shows a warning and
  prevents saving rules.
- When any ACL rules are present, attempts to set Public access to Editor role
  get downgraded to Viewer role, with a warning notification.
- No checks are made on the server side, so the combination may be achieved via
  the API (but we may block it in the future).

Test Plan: Added a test case.

Reviewers: paulfitz

Reviewed By: paulfitz

Differential Revision: https://phab.getgrist.com/D2767
2021-03-26 09:59:41 -04:00

114 lines
3.8 KiB
TypeScript

import {copyToClipboard} from 'app/client/lib/copyToClipboard';
import {DocPageModel} from 'app/client/models/DocPageModel';
import {urlState} from 'app/client/models/gristUrlState';
import {createUserImage} from 'app/client/ui/UserImage';
import * as um from 'app/client/ui/UserManager';
import {basicButton, basicButtonLink} from 'app/client/ui2018/buttons';
import {colors, testId} from 'app/client/ui2018/cssVars';
import {icon} from 'app/client/ui2018/icons';
import {menuCssClass} from 'app/client/ui2018/menus';
import {userOverrideParams} from 'app/common/gristUrls';
import {FullUser} from 'app/common/LoginSessionAPI';
import * as roles from 'app/common/roles';
import {ANONYMOUS_USER_EMAIL, EVERYONE_EMAIL} from 'app/common/UserAPI';
import {getRealAccess, PermissionData, UserAccessData} from 'app/common/UserAPI';
import {Disposable, dom, Observable, styled} from 'grainjs';
import {cssMenu, cssMenuWrap, defaultMenuOptions, IOpenController, setPopupToCreateDom} from 'popweasel';
const roleNames: {[role: string]: string} = {
[roles.OWNER]: 'Owner',
[roles.EDITOR]: 'Editor',
[roles.VIEWER]: 'Viewer',
};
function buildUserRow(user: UserAccessData, currentUser: FullUser|null, ctl: IOpenController) {
const isCurrentUser = Boolean(currentUser && user.id === currentUser.id);
return cssUserItem(
um.cssMemberImage(
createUserImage(user, 'large')
),
um.cssMemberText(
um.cssMemberPrimary(user.name || dom('span', user.email),
cssRole('(', roleNames[user.access!] || user.access, ')', testId('acl-user-access')),
),
user.name ? um.cssMemberSecondary(user.email) : null
),
basicButton(cssUserButton.cls(''), icon('Copy'), 'Copy Email',
testId('acl-user-copy'),
dom.on('click', async (ev, elem) => { await copyToClipboard(user.email); ctl.close(); }),
),
basicButtonLink(cssUserButton.cls(''), cssUserButton.cls('-disabled', isCurrentUser),
testId('acl-user-view-as'),
icon('FieldLink'), 'View As',
urlState().setHref(userOverrideParams(user.email, {docPage: undefined})),
),
testId('acl-user-item'),
);
}
function isSpecialEmail(email: string) {
return email === ANONYMOUS_USER_EMAIL || email === EVERYONE_EMAIL;
}
export class ACLUsersPopup extends Disposable {
public readonly isInitialized = Observable.create(this, false);
private _usersInDoc: UserAccessData[] = [];
private _currentUser: FullUser|null = null;
public init(pageModel: DocPageModel, permissionData: PermissionData|null) {
this._currentUser = pageModel.userOverride.get()?.user || pageModel.appModel.currentValidUser;
if (permissionData) {
this._usersInDoc = permissionData.users.map(user => ({
...user,
access: getRealAccess(user, permissionData),
}))
.filter(user => user.access && !isSpecialEmail(user.email));
this.isInitialized.set(true);
}
}
public attachPopup(elem: Element) {
setPopupToCreateDom(elem, (ctl) => cssMenuWrap(cssMenu(
dom.cls(menuCssClass),
cssUsers.cls(''),
dom.forEach(this._usersInDoc, user => buildUserRow(user, this._currentUser, ctl)),
(el) => { setTimeout(() => el.focus(), 0); },
dom.onKeyDown({Escape: () => ctl.close()}),
)),
{...defaultMenuOptions, placement: 'bottom-end'}
);
}
}
const cssUsers = styled('div', `
max-width: unset;
`);
const cssUserItem = styled(um.cssMemberListItem, `
width: auto;
padding: 8px 16px;
align-items: center;
&:hover {
background-color: ${colors.lightGrey};
}
`);
const cssRole = styled('span', `
margin: 0 8px;
font-weight: normal;
`);
const cssUserButton = styled('div', `
margin: 0 8px;
border: none;
display: inline-flex;
white-space: nowrap;
gap: 4px;
&:hover {
background-color: ${colors.darkGrey};
}
&-disabled {
visibility: hidden;
}
`);