mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) make the support account configurable, and allow listing public sites
Summary: This makes two small tweaks based on a user's questions about sharing sites publicly for a self-managed installation: * The support user `support@getgrist.com` is made configurable with `GRIST_SUPPORT_EMAIL`. This came up because only the support user can share material with the special "everyone" user. This restriction was added to avoid spam. * Regardless of public sharing settings, for our SaaS we had decided not to list public sites to anonymous users. That is somewhat a question of taste, so a `GRIST_LIST_PUBLIC_SITES` flag is added to override this choice. Public sharing isn't in a well polished state, and this diff doesn't advance that, in fact it adds a new wrinkle :-/ Test Plan: existing tests pass; manual testing Reviewers: jarek Reviewed By: jarek Subscribers: jarek Differential Revision: https://phab.getgrist.com/D3663
This commit is contained in:
@@ -16,7 +16,7 @@ import {IWidgetRepository} from 'app/server/lib/WidgetRepository';
|
||||
import {Request} from 'express';
|
||||
|
||||
import {User} from './entity/User';
|
||||
import {HomeDBManager} from './lib/HomeDBManager';
|
||||
import {HomeDBManager, QueryResult, Scope} from './lib/HomeDBManager';
|
||||
|
||||
// Special public organization that contains examples and templates.
|
||||
export const TEMPLATES_ORG_DOMAIN = process.env.GRIST_ID_PREFIX ?
|
||||
@@ -330,8 +330,9 @@ export class ApiServer {
|
||||
// Get user access information regarding an org
|
||||
this._app.get('/api/orgs/:oid/access', expressWrap(async (req, res) => {
|
||||
const org = getOrgKey(req);
|
||||
const scope = addPermit(getScope(req), this._dbManager.getSupportUserId(), {org});
|
||||
const query = await this._dbManager.getOrgAccess(scope, org);
|
||||
const query = await this._withSupportUserAllowedToView(
|
||||
org, req, (scope) => this._dbManager.getOrgAccess(scope, org)
|
||||
);
|
||||
return sendReply(req, res, query);
|
||||
}));
|
||||
|
||||
@@ -454,9 +455,9 @@ export class ApiServer {
|
||||
this._app.get('/api/session/access/active', expressWrap(async (req, res) => {
|
||||
const fullUser = await this._getFullUser(req);
|
||||
const domain = getOrgFromRequest(req);
|
||||
// Allow the support user enough access to every org to see the billing pages.
|
||||
const scope = domain ? addPermit(getScope(req), this._dbManager.getSupportUserId(), {org: domain}) : null;
|
||||
const org = scope ? (await this._dbManager.getOrg(scope, domain)) : null;
|
||||
const org = domain ? (await this._withSupportUserAllowedToView(
|
||||
domain, req, (scope) => this._dbManager.getOrg(scope, domain)
|
||||
)) : null;
|
||||
const orgError = (org && org.errMessage) ? {error: org.errMessage, status: org.status} : undefined;
|
||||
return sendOkReply(req, res, {
|
||||
user: {...fullUser, helpScoutSignature: helpScoutSign(fullUser.email)},
|
||||
@@ -527,6 +528,30 @@ export class ApiServer {
|
||||
const allowGoogleLogin = user.options?.allowGoogleLogin ?? true;
|
||||
return {...fullUser, loginMethod, allowGoogleLogin};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Run a query, and, if it is denied and the user is the support
|
||||
* user, rerun the query with permission to view the current
|
||||
* org. This is a bit inefficient, but only affects the support
|
||||
* user. We wait to add the special permission only if needed, since
|
||||
* it will in fact override any other access the support user has
|
||||
* been granted, which could reduce their apparent access if that is
|
||||
* part of what is returned by the query.
|
||||
*/
|
||||
private async _withSupportUserAllowedToView<T>(
|
||||
org: string|number, req: express.Request,
|
||||
op: (scope: Scope) => Promise<QueryResult<T>>
|
||||
): Promise<QueryResult<T>> {
|
||||
const scope = getScope(req);
|
||||
const userId = getUserId(req);
|
||||
const result = await op(scope);
|
||||
if (result.status === 200 || userId !== this._dbManager.getSupportUserId()) {
|
||||
return result;
|
||||
}
|
||||
const extendedScope = addPermit(scope, this._dbManager.getSupportUserId(), {org});
|
||||
return await op(extendedScope);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
Organization as OrgInfo,
|
||||
PermissionData,
|
||||
PermissionDelta,
|
||||
SUPPORT_EMAIL,
|
||||
UserAccessData,
|
||||
UserOptions,
|
||||
WorkspaceProperties
|
||||
@@ -50,6 +49,7 @@ import {
|
||||
now,
|
||||
readJson
|
||||
} from 'app/gen-server/sqlUtils';
|
||||
import {appSettings} from 'app/server/lib/AppSettings';
|
||||
import {getOrCreateConnection} from 'app/server/lib/dbUtils';
|
||||
import {makeId} from 'app/server/lib/idUtils';
|
||||
import log from 'app/server/lib/log';
|
||||
@@ -90,12 +90,25 @@ export type NotifierEvent = typeof NotifierEvents.type;
|
||||
// Nominal email address of a user who can view anything (for thumbnails).
|
||||
export const PREVIEWER_EMAIL = 'thumbnail@getgrist.com';
|
||||
|
||||
// A special user allowed to add/remove the EVERYONE_EMAIL to/from a resource.
|
||||
export const SUPPORT_EMAIL = appSettings.section('access').flag('supportEmail').requireString({
|
||||
envVar: 'GRIST_SUPPORT_EMAIL',
|
||||
defaultValue: 'support@getgrist.com',
|
||||
});
|
||||
|
||||
// A list of emails we don't expect to see logins for.
|
||||
const NON_LOGIN_EMAILS = [PREVIEWER_EMAIL, EVERYONE_EMAIL, ANONYMOUS_USER_EMAIL];
|
||||
|
||||
// Name of a special workspace with examples in it.
|
||||
export const EXAMPLE_WORKSPACE_NAME = 'Examples & Templates';
|
||||
|
||||
// Flag controlling whether sites that are publicly accessible should be listed
|
||||
// to the anonymous user. Defaults to not listing such sites.
|
||||
const listPublicSites = appSettings.section('access').flag('listPublicSites').readBool({
|
||||
envVar: 'GRIST_LIST_PUBLIC_SITES',
|
||||
defaultValue: false,
|
||||
});
|
||||
|
||||
// A TTL in milliseconds for caching the result of looking up access level for a doc,
|
||||
// which is a burden under heavy traffic.
|
||||
const DOC_AUTH_CACHE_TTL = 5000;
|
||||
@@ -474,6 +487,9 @@ export class HomeDBManager extends EventEmitter {
|
||||
if (this.getAnonymousUserId() === user.id) {
|
||||
result.anonymous = true;
|
||||
}
|
||||
if (this.getSupportUserId() === user.id) {
|
||||
result.isSupport = true;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1100,7 +1116,7 @@ export class HomeDBManager extends EventEmitter {
|
||||
queryBuilder = this._withAccess(queryBuilder, users, 'orgs');
|
||||
// Add a direct, efficient filter to remove irrelevant personal orgs from consideration.
|
||||
queryBuilder = this._filterByOrgGroups(queryBuilder, users, domain, options);
|
||||
if (this._isAnonymousUser(users)) {
|
||||
if (this._isAnonymousUser(users) && !listPublicSites) {
|
||||
// The anonymous user is a special case. It may have access to potentially
|
||||
// many orgs, but listing them all would be kind of a misfeature. but reporting
|
||||
// nothing would complicate the client. We compromise, and report at most
|
||||
@@ -2303,6 +2319,7 @@ export class HomeDBManager extends EventEmitter {
|
||||
roles.getStrongestRole(wsMap[u.id] || null, inheritFromOrg)
|
||||
),
|
||||
isMember: orgAccess !== 'guests' && orgAccess !== null,
|
||||
isSupport: u.id === this.getSupportUserId() ? true : undefined,
|
||||
};
|
||||
});
|
||||
let maxInheritedRole = this._getMaxInheritedRole(doc);
|
||||
|
||||
Reference in New Issue
Block a user