2021-01-22 14:01:20 +00:00
|
|
|
import {DocPageModel} from 'app/client/models/DocPageModel';
|
|
|
|
import {urlState} from 'app/client/models/gristUrlState';
|
|
|
|
import {createUserImage} from 'app/client/ui/UserImage';
|
2021-10-01 14:24:23 +00:00
|
|
|
import {cssMemberImage, cssMemberListItem, cssMemberPrimary,
|
|
|
|
cssMemberSecondary, cssMemberText} from 'app/client/ui/UserItem';
|
2023-01-03 10:52:25 +00:00
|
|
|
import {testId, theme, vars} from 'app/client/ui2018/cssVars';
|
2021-10-08 14:56:33 +00:00
|
|
|
import {PermissionDataWithExtraUsers} from 'app/common/ActiveDocAPI';
|
2023-01-09 16:26:09 +00:00
|
|
|
import {menu, menuCssClass, menuItemLink} from 'app/client/ui2018/menus';
|
2023-01-19 10:29:52 +00:00
|
|
|
import {IGristUrlState, userOverrideParams} from 'app/common/gristUrls';
|
2021-01-22 14:01:20 +00:00
|
|
|
import {FullUser} from 'app/common/LoginSessionAPI';
|
2021-03-25 23:15:34 +00:00
|
|
|
import {ANONYMOUS_USER_EMAIL, EVERYONE_EMAIL} from 'app/common/UserAPI';
|
2021-10-08 14:56:33 +00:00
|
|
|
import {getRealAccess, UserAccessData} from 'app/common/UserAPI';
|
2021-01-22 14:01:20 +00:00
|
|
|
import {Disposable, dom, Observable, styled} from 'grainjs';
|
2023-01-09 16:26:09 +00:00
|
|
|
import {cssMenu, cssMenuWrap, defaultMenuOptions, IMenuOptions, IPopupOptions, setPopupToCreateDom} from 'popweasel';
|
2023-01-03 10:52:25 +00:00
|
|
|
import {getUserRoleText} from 'app/common/UserAPI';
|
|
|
|
import {makeT} from 'app/client/lib/localization';
|
2023-01-09 16:26:09 +00:00
|
|
|
import {waitGrainObs} from 'app/common/gutil';
|
|
|
|
import noop from 'lodash/noop';
|
2021-01-22 14:01:20 +00:00
|
|
|
|
2023-01-09 16:26:09 +00:00
|
|
|
const t = makeT("ViewAsDropdown");
|
2021-01-22 14:01:20 +00:00
|
|
|
|
|
|
|
function isSpecialEmail(email: string) {
|
|
|
|
return email === ANONYMOUS_USER_EMAIL || email === EVERYONE_EMAIL;
|
|
|
|
}
|
|
|
|
|
|
|
|
export class ACLUsersPopup extends Disposable {
|
|
|
|
public readonly isInitialized = Observable.create(this, false);
|
2023-01-09 16:26:09 +00:00
|
|
|
public readonly allUsers = Observable.create<UserAccessData[]>(this, []);
|
2021-10-08 14:56:33 +00:00
|
|
|
private _shareUsers: UserAccessData[] = []; // Users doc is shared with.
|
|
|
|
private _attributeTableUsers: UserAccessData[] = []; // Users mentioned in attribute tables.
|
|
|
|
private _exampleUsers: UserAccessData[] = []; // Example users.
|
2021-01-22 14:01:20 +00:00
|
|
|
private _currentUser: FullUser|null = null;
|
|
|
|
|
2023-01-09 16:26:09 +00:00
|
|
|
constructor(public pageModel: DocPageModel,
|
|
|
|
public fetch: () => Promise<PermissionDataWithExtraUsers|null> = () => this._fetchData()) {
|
|
|
|
super();
|
|
|
|
}
|
|
|
|
|
|
|
|
public async load() {
|
|
|
|
const permissionData = await this.fetch();
|
|
|
|
if (this.isDisposed()) { return; }
|
|
|
|
this.init(permissionData);
|
|
|
|
}
|
|
|
|
|
|
|
|
public getUsers() {
|
|
|
|
const users = [...this._shareUsers, ...this._attributeTableUsers];
|
|
|
|
if (this._showExampleUsers()) { users.push(...this._exampleUsers); }
|
|
|
|
return users;
|
|
|
|
}
|
|
|
|
|
|
|
|
public init(permissionData: PermissionDataWithExtraUsers|null) {
|
|
|
|
const pageModel = this.pageModel;
|
2021-01-22 14:01:20 +00:00
|
|
|
this._currentUser = pageModel.userOverride.get()?.user || pageModel.appModel.currentValidUser;
|
2023-01-09 16:26:09 +00:00
|
|
|
|
2021-03-25 23:15:34 +00:00
|
|
|
if (permissionData) {
|
2021-10-08 14:56:33 +00:00
|
|
|
this._shareUsers = permissionData.users.map(user => ({
|
2021-01-22 14:01:20 +00:00
|
|
|
...user,
|
|
|
|
access: getRealAccess(user, permissionData),
|
|
|
|
}))
|
2023-01-03 10:52:25 +00:00
|
|
|
.filter(user => user.access && !isSpecialEmail(user.email))
|
|
|
|
.filter(user => this._currentUser?.id !== user.id);
|
2021-10-08 14:56:33 +00:00
|
|
|
this._attributeTableUsers = permissionData.attributeTableUsers;
|
|
|
|
this._exampleUsers = permissionData.exampleUsers;
|
2023-01-09 16:26:09 +00:00
|
|
|
this.allUsers.set(this.getUsers());
|
2021-01-22 14:01:20 +00:00
|
|
|
this.isInitialized.set(true);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-19 10:29:52 +00:00
|
|
|
// Optionnally have document page reverts to the default page upon activation of the view as mode
|
|
|
|
// by setting `options.resetDocPage` to true.
|
|
|
|
public attachPopup(elem: Element, options: IPopupOptions & {resetDocPage?: boolean}) {
|
2021-10-08 14:56:33 +00:00
|
|
|
setPopupToCreateDom(elem, (ctl) => {
|
2023-01-03 10:52:25 +00:00
|
|
|
const buildRow =
|
2023-01-19 10:29:52 +00:00
|
|
|
(user: UserAccessData) => this._buildUserRow(user, options);
|
2023-01-03 10:52:25 +00:00
|
|
|
const buildExampleUserRow =
|
2023-01-19 10:29:52 +00:00
|
|
|
(user: UserAccessData) => this._buildUserRow(user, {isExampleUser: true, ...options});
|
2021-10-08 14:56:33 +00:00
|
|
|
return cssMenuWrap(cssMenu(
|
2021-01-22 14:01:20 +00:00
|
|
|
dom.cls(menuCssClass),
|
|
|
|
cssUsers.cls(''),
|
2022-12-09 14:09:36 +00:00
|
|
|
cssHeader(t('View As'), dom.show(this._shareUsers.length > 0)),
|
2021-10-08 14:56:33 +00:00
|
|
|
dom.forEach(this._shareUsers, buildRow),
|
2022-12-09 14:09:36 +00:00
|
|
|
(this._attributeTableUsers.length > 0) ? cssHeader(t("Users from table")) : null,
|
2023-01-03 10:52:25 +00:00
|
|
|
dom.forEach(this._attributeTableUsers, buildExampleUserRow),
|
2021-10-08 14:56:33 +00:00
|
|
|
// Include example users only if there are not many "real" users.
|
|
|
|
// It might be better to have an expandable section with these users, collapsed
|
|
|
|
// by default, but that's beyond my UI ken.
|
2023-01-09 16:26:09 +00:00
|
|
|
this._showExampleUsers() ? [
|
2022-12-09 14:09:36 +00:00
|
|
|
(this._exampleUsers.length > 0) ? cssHeader(t("Example Users")) : null,
|
2023-01-03 10:52:25 +00:00
|
|
|
dom.forEach(this._exampleUsers, buildExampleUserRow)
|
2021-10-08 14:56:33 +00:00
|
|
|
] : null,
|
2021-01-22 14:01:20 +00:00
|
|
|
(el) => { setTimeout(() => el.focus(), 0); },
|
|
|
|
dom.onKeyDown({Escape: () => ctl.close()}),
|
2021-10-08 14:56:33 +00:00
|
|
|
));
|
2023-01-03 10:52:25 +00:00
|
|
|
}, {...defaultMenuOptions, ...options});
|
2021-01-22 14:01:20 +00:00
|
|
|
}
|
(core) go ahead and create fork if non-owner wants to do "View As" on prefork
Summary:
Backstory: to make examples easier to play with, we:
* Add a special FullCopies permission to let anyone fork/copy them regardless of other access rules
* Open the examples in "prefork" mode by default
That means a random person can open an example and already feel like an owner of it. Getting to this point requires some gymnastics on the back end. As soon as the person makes any change to the document they become truly the owner (of their fork), and life is simple for the back end.
But, if that person does "View As" to look at the preforked document, that is a step too far for the back end - a user, with a special somewhat complicated exception allowing them to act as an owner for some purposes, now wants to pretend to be another user. The logic for this on the back end was doable, but looked hard to review and be confident of, with now three identities with subtle nuances in their interrelationship.
So with this diff, if a non-owner attempts to "View As" another user on a prefork, the client will just fork the document first. This is in principle not necessary, but is much simpler from a security perspective.
Test Plan: extended test
Reviewers: georgegevoian
Reviewed By: georgegevoian
Differential Revision: https://phab.getgrist.com/D3179
2021-12-09 21:29:03 +00:00
|
|
|
|
2023-01-19 10:29:52 +00:00
|
|
|
// See 'attachPopup' for more info on the 'resetDocPage' option.
|
2023-01-09 16:26:09 +00:00
|
|
|
public menu(options: IMenuOptions) {
|
|
|
|
return menu(() => {
|
|
|
|
this.load().catch(noop);
|
|
|
|
return [
|
|
|
|
cssMenuHeader('view as'),
|
|
|
|
dom.forEach(this.allUsers, user => menuItemLink(
|
|
|
|
`${user.name || user.email} (${getUserRoleText(user)})`,
|
|
|
|
testId('acl-user-access'),
|
|
|
|
this._viewAs(user),
|
|
|
|
)),
|
|
|
|
];
|
|
|
|
}, options);
|
|
|
|
}
|
|
|
|
|
|
|
|
private async _fetchData() {
|
|
|
|
const doc = this.pageModel.currentDoc.get();
|
|
|
|
const gristDoc = await waitGrainObs(this.pageModel.gristDoc);
|
|
|
|
return doc && gristDoc.docComm.getUsersForViewAs();
|
|
|
|
}
|
|
|
|
|
|
|
|
private _showExampleUsers() {
|
|
|
|
return this._shareUsers.length + this._attributeTableUsers.length < 5;
|
|
|
|
}
|
|
|
|
|
2023-01-19 10:29:52 +00:00
|
|
|
private _buildUserRow(user: UserAccessData, opt: {isExampleUser?: boolean, resetDocPage?: boolean} = {}) {
|
2023-01-03 10:52:25 +00:00
|
|
|
return dom('a',
|
|
|
|
{class: cssMemberListItem.className + ' ' + cssUserItem.className},
|
(core) go ahead and create fork if non-owner wants to do "View As" on prefork
Summary:
Backstory: to make examples easier to play with, we:
* Add a special FullCopies permission to let anyone fork/copy them regardless of other access rules
* Open the examples in "prefork" mode by default
That means a random person can open an example and already feel like an owner of it. Getting to this point requires some gymnastics on the back end. As soon as the person makes any change to the document they become truly the owner (of their fork), and life is simple for the back end.
But, if that person does "View As" to look at the preforked document, that is a step too far for the back end - a user, with a special somewhat complicated exception allowing them to act as an owner for some purposes, now wants to pretend to be another user. The logic for this on the back end was doable, but looked hard to review and be confident of, with now three identities with subtle nuances in their interrelationship.
So with this diff, if a non-owner attempts to "View As" another user on a prefork, the client will just fork the document first. This is in principle not necessary, but is much simpler from a security perspective.
Test Plan: extended test
Reviewers: georgegevoian
Reviewed By: georgegevoian
Differential Revision: https://phab.getgrist.com/D3179
2021-12-09 21:29:03 +00:00
|
|
|
cssMemberImage(
|
2023-01-03 10:52:25 +00:00
|
|
|
createUserImage(opt.isExampleUser ? 'exampleUser' : user, 'large')
|
(core) go ahead and create fork if non-owner wants to do "View As" on prefork
Summary:
Backstory: to make examples easier to play with, we:
* Add a special FullCopies permission to let anyone fork/copy them regardless of other access rules
* Open the examples in "prefork" mode by default
That means a random person can open an example and already feel like an owner of it. Getting to this point requires some gymnastics on the back end. As soon as the person makes any change to the document they become truly the owner (of their fork), and life is simple for the back end.
But, if that person does "View As" to look at the preforked document, that is a step too far for the back end - a user, with a special somewhat complicated exception allowing them to act as an owner for some purposes, now wants to pretend to be another user. The logic for this on the back end was doable, but looked hard to review and be confident of, with now three identities with subtle nuances in their interrelationship.
So with this diff, if a non-owner attempts to "View As" another user on a prefork, the client will just fork the document first. This is in principle not necessary, but is much simpler from a security perspective.
Test Plan: extended test
Reviewers: georgegevoian
Reviewed By: georgegevoian
Differential Revision: https://phab.getgrist.com/D3179
2021-12-09 21:29:03 +00:00
|
|
|
),
|
|
|
|
cssMemberText(
|
|
|
|
cssMemberPrimary(user.name || dom('span', user.email),
|
2023-01-03 10:52:25 +00:00
|
|
|
cssRole('(', getUserRoleText(user), ')', testId('acl-user-access')),
|
(core) go ahead and create fork if non-owner wants to do "View As" on prefork
Summary:
Backstory: to make examples easier to play with, we:
* Add a special FullCopies permission to let anyone fork/copy them regardless of other access rules
* Open the examples in "prefork" mode by default
That means a random person can open an example and already feel like an owner of it. Getting to this point requires some gymnastics on the back end. As soon as the person makes any change to the document they become truly the owner (of their fork), and life is simple for the back end.
But, if that person does "View As" to look at the preforked document, that is a step too far for the back end - a user, with a special somewhat complicated exception allowing them to act as an owner for some purposes, now wants to pretend to be another user. The logic for this on the back end was doable, but looked hard to review and be confident of, with now three identities with subtle nuances in their interrelationship.
So with this diff, if a non-owner attempts to "View As" another user on a prefork, the client will just fork the document first. This is in principle not necessary, but is much simpler from a security perspective.
Test Plan: extended test
Reviewers: georgegevoian
Reviewed By: georgegevoian
Differential Revision: https://phab.getgrist.com/D3179
2021-12-09 21:29:03 +00:00
|
|
|
),
|
|
|
|
user.name ? cssMemberSecondary(user.email) : null
|
|
|
|
),
|
2023-01-19 10:29:52 +00:00
|
|
|
this._viewAs(user, opt.resetDocPage),
|
(core) go ahead and create fork if non-owner wants to do "View As" on prefork
Summary:
Backstory: to make examples easier to play with, we:
* Add a special FullCopies permission to let anyone fork/copy them regardless of other access rules
* Open the examples in "prefork" mode by default
That means a random person can open an example and already feel like an owner of it. Getting to this point requires some gymnastics on the back end. As soon as the person makes any change to the document they become truly the owner (of their fork), and life is simple for the back end.
But, if that person does "View As" to look at the preforked document, that is a step too far for the back end - a user, with a special somewhat complicated exception allowing them to act as an owner for some purposes, now wants to pretend to be another user. The logic for this on the back end was doable, but looked hard to review and be confident of, with now three identities with subtle nuances in their interrelationship.
So with this diff, if a non-owner attempts to "View As" another user on a prefork, the client will just fork the document first. This is in principle not necessary, but is much simpler from a security perspective.
Test Plan: extended test
Reviewers: georgegevoian
Reviewed By: georgegevoian
Differential Revision: https://phab.getgrist.com/D3179
2021-12-09 21:29:03 +00:00
|
|
|
testId('acl-user-item'),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-01-19 10:29:52 +00:00
|
|
|
private _viewAs(user: UserAccessData, resetDocPage: boolean = false) {
|
|
|
|
const extraState: IGristUrlState = {};
|
|
|
|
if (resetDocPage) { extraState.docPage = undefined; }
|
2023-01-09 16:26:09 +00:00
|
|
|
if (this.pageModel?.isPrefork.get() &&
|
|
|
|
this.pageModel?.currentDoc.get()?.access !== 'owners') {
|
(core) go ahead and create fork if non-owner wants to do "View As" on prefork
Summary:
Backstory: to make examples easier to play with, we:
* Add a special FullCopies permission to let anyone fork/copy them regardless of other access rules
* Open the examples in "prefork" mode by default
That means a random person can open an example and already feel like an owner of it. Getting to this point requires some gymnastics on the back end. As soon as the person makes any change to the document they become truly the owner (of their fork), and life is simple for the back end.
But, if that person does "View As" to look at the preforked document, that is a step too far for the back end - a user, with a special somewhat complicated exception allowing them to act as an owner for some purposes, now wants to pretend to be another user. The logic for this on the back end was doable, but looked hard to review and be confident of, with now three identities with subtle nuances in their interrelationship.
So with this diff, if a non-owner attempts to "View As" another user on a prefork, the client will just fork the document first. This is in principle not necessary, but is much simpler from a security perspective.
Test Plan: extended test
Reviewers: georgegevoian
Reviewed By: georgegevoian
Differential Revision: https://phab.getgrist.com/D3179
2021-12-09 21:29:03 +00:00
|
|
|
// "View As" is restricted to document owners on the back-end. Non-owners can be
|
|
|
|
// permitted to pretend to be owners of a pre-forked document, but if they want
|
|
|
|
// to do "View As", that would be layering pretence over pretense. Better to just
|
|
|
|
// go ahead and create the fork, so the user becomes a genuine owner, so the
|
|
|
|
// back-end doesn't have to become too metaphysical (and maybe hard to review).
|
|
|
|
return dom.on('click', async () => {
|
2023-01-09 16:26:09 +00:00
|
|
|
const forkResult = await this.pageModel?.gristDoc.get()?.docComm.fork();
|
(core) go ahead and create fork if non-owner wants to do "View As" on prefork
Summary:
Backstory: to make examples easier to play with, we:
* Add a special FullCopies permission to let anyone fork/copy them regardless of other access rules
* Open the examples in "prefork" mode by default
That means a random person can open an example and already feel like an owner of it. Getting to this point requires some gymnastics on the back end. As soon as the person makes any change to the document they become truly the owner (of their fork), and life is simple for the back end.
But, if that person does "View As" to look at the preforked document, that is a step too far for the back end - a user, with a special somewhat complicated exception allowing them to act as an owner for some purposes, now wants to pretend to be another user. The logic for this on the back end was doable, but looked hard to review and be confident of, with now three identities with subtle nuances in their interrelationship.
So with this diff, if a non-owner attempts to "View As" another user on a prefork, the client will just fork the document first. This is in principle not necessary, but is much simpler from a security perspective.
Test Plan: extended test
Reviewers: georgegevoian
Reviewed By: georgegevoian
Differential Revision: https://phab.getgrist.com/D3179
2021-12-09 21:29:03 +00:00
|
|
|
if (!forkResult) { throw new Error('Failed to create fork'); }
|
|
|
|
window.location.assign(urlState().makeUrl(userOverrideParams(user.email,
|
2023-01-19 10:29:52 +00:00
|
|
|
{...extraState, doc: forkResult.urlId})));
|
(core) go ahead and create fork if non-owner wants to do "View As" on prefork
Summary:
Backstory: to make examples easier to play with, we:
* Add a special FullCopies permission to let anyone fork/copy them regardless of other access rules
* Open the examples in "prefork" mode by default
That means a random person can open an example and already feel like an owner of it. Getting to this point requires some gymnastics on the back end. As soon as the person makes any change to the document they become truly the owner (of their fork), and life is simple for the back end.
But, if that person does "View As" to look at the preforked document, that is a step too far for the back end - a user, with a special somewhat complicated exception allowing them to act as an owner for some purposes, now wants to pretend to be another user. The logic for this on the back end was doable, but looked hard to review and be confident of, with now three identities with subtle nuances in their interrelationship.
So with this diff, if a non-owner attempts to "View As" another user on a prefork, the client will just fork the document first. This is in principle not necessary, but is much simpler from a security perspective.
Test Plan: extended test
Reviewers: georgegevoian
Reviewed By: georgegevoian
Differential Revision: https://phab.getgrist.com/D3179
2021-12-09 21:29:03 +00:00
|
|
|
});
|
|
|
|
} else {
|
|
|
|
// When forking isn't needed, we return a direct link to be maximally transparent
|
|
|
|
// about where button will go.
|
2023-01-19 10:29:52 +00:00
|
|
|
return urlState().setHref(userOverrideParams(user.email, extraState));
|
(core) go ahead and create fork if non-owner wants to do "View As" on prefork
Summary:
Backstory: to make examples easier to play with, we:
* Add a special FullCopies permission to let anyone fork/copy them regardless of other access rules
* Open the examples in "prefork" mode by default
That means a random person can open an example and already feel like an owner of it. Getting to this point requires some gymnastics on the back end. As soon as the person makes any change to the document they become truly the owner (of their fork), and life is simple for the back end.
But, if that person does "View As" to look at the preforked document, that is a step too far for the back end - a user, with a special somewhat complicated exception allowing them to act as an owner for some purposes, now wants to pretend to be another user. The logic for this on the back end was doable, but looked hard to review and be confident of, with now three identities with subtle nuances in their interrelationship.
So with this diff, if a non-owner attempts to "View As" another user on a prefork, the client will just fork the document first. This is in principle not necessary, but is much simpler from a security perspective.
Test Plan: extended test
Reviewers: georgegevoian
Reviewed By: georgegevoian
Differential Revision: https://phab.getgrist.com/D3179
2021-12-09 21:29:03 +00:00
|
|
|
}
|
|
|
|
}
|
2021-01-22 14:01:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const cssUsers = styled('div', `
|
|
|
|
max-width: unset;
|
|
|
|
`);
|
|
|
|
|
2021-10-01 14:24:23 +00:00
|
|
|
const cssUserItem = styled(cssMemberListItem, `
|
2021-01-22 14:01:20 +00:00
|
|
|
width: auto;
|
|
|
|
padding: 8px 16px;
|
|
|
|
align-items: center;
|
|
|
|
&:hover {
|
2022-09-06 01:51:57 +00:00
|
|
|
background-color: ${theme.lightHover};
|
2021-01-22 14:01:20 +00:00
|
|
|
}
|
2023-01-03 10:52:25 +00:00
|
|
|
&, &:hover, &:focus {
|
|
|
|
text-decoration: none;
|
|
|
|
}
|
2021-01-22 14:01:20 +00:00
|
|
|
`);
|
|
|
|
|
|
|
|
const cssRole = styled('span', `
|
|
|
|
margin: 0 8px;
|
|
|
|
font-weight: normal;
|
|
|
|
`);
|
|
|
|
|
2023-01-03 10:52:25 +00:00
|
|
|
const cssHeader = styled('div', `
|
|
|
|
margin: 11px 24px 14px 24px;
|
|
|
|
font-weight: 700;
|
|
|
|
text-transform: uppercase;
|
|
|
|
font-size: ${vars.xsmallFontSize};
|
|
|
|
color: ${theme.darkText};
|
2021-01-22 14:01:20 +00:00
|
|
|
`);
|
2023-01-09 16:26:09 +00:00
|
|
|
|
|
|
|
const cssMenuHeader = styled('div', `
|
|
|
|
margin: 8px 24px;
|
|
|
|
margin-bottom: 4px;
|
|
|
|
font-weight: 700;
|
|
|
|
text-transform: uppercase;
|
|
|
|
font-size: ${vars.xsmallFontSize};
|
|
|
|
color: ${theme.darkText};
|
|
|
|
`);
|