mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(core) Allow the support user to access everyone's billing pages
Summary: Give specialPermit to the support user for page loads and API requests needed to serve billing pages. Test Plan: Added new test cases Reviewers: paulfitz Reviewed By: paulfitz Differential Revision: https://phab.getgrist.com/D2554
This commit is contained in:
parent
4452a816ff
commit
671dc24214
@ -10,7 +10,7 @@ import {getSessionUser, linkOrgWithEmail} from 'app/server/lib/BrowserSession';
|
|||||||
import {expressWrap} from 'app/server/lib/expressWrap';
|
import {expressWrap} from 'app/server/lib/expressWrap';
|
||||||
import {RequestWithOrg} from 'app/server/lib/extractOrg';
|
import {RequestWithOrg} from 'app/server/lib/extractOrg';
|
||||||
import * as log from 'app/server/lib/log';
|
import * as log from 'app/server/lib/log';
|
||||||
import {getDocScope, getScope, integerParam, isParameterOn, sendOkReply,
|
import {addPermit, getDocScope, getScope, integerParam, isParameterOn, sendOkReply,
|
||||||
sendReply, stringParam} from 'app/server/lib/requestUtils';
|
sendReply, stringParam} from 'app/server/lib/requestUtils';
|
||||||
import {Request} from 'express';
|
import {Request} from 'express';
|
||||||
|
|
||||||
@ -289,7 +289,8 @@ export class ApiServer {
|
|||||||
// Get user access information regarding an org
|
// Get user access information regarding an org
|
||||||
this._app.get('/api/orgs/:oid/access', expressWrap(async (req, res) => {
|
this._app.get('/api/orgs/:oid/access', expressWrap(async (req, res) => {
|
||||||
const org = getOrgKey(req);
|
const org = getOrgKey(req);
|
||||||
const query = await this._dbManager.getOrgAccess(getScope(req), org);
|
const scope = addPermit(getScope(req), this._dbManager.getSupportUserId(), {org});
|
||||||
|
const query = await this._dbManager.getOrgAccess(scope, org);
|
||||||
return sendReply(req, res, query);
|
return sendReply(req, res, query);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -375,7 +376,9 @@ export class ApiServer {
|
|||||||
this._app.get('/api/session/access/active', expressWrap(async (req, res) => {
|
this._app.get('/api/session/access/active', expressWrap(async (req, res) => {
|
||||||
const fullUser = await this._getFullUser(req);
|
const fullUser = await this._getFullUser(req);
|
||||||
const domain = getOrgFromRequest(req);
|
const domain = getOrgFromRequest(req);
|
||||||
const org = domain ? (await this._dbManager.getOrg(getScope(req), domain || null)) : null;
|
// 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 orgError = (org && org.errMessage) ? {error: org.errMessage, status: org.status} : undefined;
|
const orgError = (org && org.errMessage) ? {error: org.errMessage, status: org.status} : undefined;
|
||||||
return sendOkReply(req, res, {
|
return sendOkReply(req, res, {
|
||||||
user: {...fullUser, helpScoutSignature: helpScoutSign(fullUser.email)},
|
user: {...fullUser, helpScoutSignature: helpScoutSign(fullUser.email)},
|
||||||
|
@ -635,7 +635,11 @@ export class HomeDBManager extends EventEmitter {
|
|||||||
needRealOrg: true
|
needRealOrg: true
|
||||||
});
|
});
|
||||||
qb = this._addBillingAccount(qb, scope.userId);
|
qb = this._addBillingAccount(qb, scope.userId);
|
||||||
qb = this._withAccess(qb, scope.userId, 'orgs');
|
let effectiveUserId = scope.userId;
|
||||||
|
if (scope.specialPermit && scope.specialPermit.org === orgKey) {
|
||||||
|
effectiveUserId = this.getPreviewerUserId();
|
||||||
|
}
|
||||||
|
qb = this._withAccess(qb, effectiveUserId, 'orgs');
|
||||||
qb = qb.leftJoinAndSelect('orgs.owner', 'owner');
|
qb = qb.leftJoinAndSelect('orgs.owner', 'owner');
|
||||||
const result = await this._verifyAclPermissions(qb);
|
const result = await this._verifyAclPermissions(qb);
|
||||||
if (result.status === 200) {
|
if (result.status === 200) {
|
||||||
@ -661,11 +665,13 @@ export class HomeDBManager extends EventEmitter {
|
|||||||
* To include `managers` and `orgs` fields listing all billing account managers
|
* To include `managers` and `orgs` fields listing all billing account managers
|
||||||
* and organizations linked to the account, set `includeOrgsAndManagers`.
|
* and organizations linked to the account, set `includeOrgsAndManagers`.
|
||||||
*/
|
*/
|
||||||
public async getBillingAccount(userId: number, orgKey: string|number,
|
public async getBillingAccount(scope: Scope, orgKey: string|number,
|
||||||
includeOrgsAndManagers: boolean,
|
includeOrgsAndManagers: boolean,
|
||||||
transaction?: EntityManager): Promise<BillingAccount> {
|
transaction?: EntityManager): Promise<BillingAccount> {
|
||||||
const org = this.unwrapQueryResult(await this.getOrg({userId}, orgKey, transaction));
|
const org = this.unwrapQueryResult(await this.getOrg(scope, orgKey, transaction));
|
||||||
if (!org.billingAccount.isManager && userId !== this.getPreviewerUserId()) {
|
if (!org.billingAccount.isManager && scope.userId !== this.getPreviewerUserId() &&
|
||||||
|
// The special permit (used for the support user) allows access to the billing account.
|
||||||
|
scope.specialPermit?.org !== orgKey) {
|
||||||
throw new ApiError('User does not have access to billing account', 401);
|
throw new ApiError('User does not have access to billing account', 401);
|
||||||
}
|
}
|
||||||
if (!includeOrgsAndManagers) { return org.billingAccount; }
|
if (!includeOrgsAndManagers) { return org.billingAccount; }
|
||||||
@ -1544,7 +1550,7 @@ export class HomeDBManager extends EventEmitter {
|
|||||||
callback: (billingAccount: BillingAccount, transaction: EntityManager) => void|Promise<void>
|
callback: (billingAccount: BillingAccount, transaction: EntityManager) => void|Promise<void>
|
||||||
): Promise<QueryResult<void>> {
|
): Promise<QueryResult<void>> {
|
||||||
return await this._connection.transaction(async transaction => {
|
return await this._connection.transaction(async transaction => {
|
||||||
const billingAccount = await this.getBillingAccount(userId, orgKey, false, transaction);
|
const billingAccount = await this.getBillingAccount({userId}, orgKey, false, transaction);
|
||||||
const billingAccountCopy = Object.assign({}, billingAccount);
|
const billingAccountCopy = Object.assign({}, billingAccount);
|
||||||
await callback(billingAccountCopy, transaction);
|
await callback(billingAccountCopy, transaction);
|
||||||
// Pick out properties that are allowed to be changed, to prevent accidental updating
|
// Pick out properties that are allowed to be changed, to prevent accidental updating
|
||||||
@ -1575,7 +1581,7 @@ export class HomeDBManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return await this._connection.transaction(async transaction => {
|
return await this._connection.transaction(async transaction => {
|
||||||
const billingAccount = await this.getBillingAccount(userId, orgKey, true, transaction);
|
const billingAccount = await this.getBillingAccount({userId}, orgKey, true, transaction);
|
||||||
// At this point, we'll have thrown an error if userId is not a billing account manager.
|
// At this point, we'll have thrown an error if userId is not a billing account manager.
|
||||||
// Now check if the billing account has mutable managers (individual account does not).
|
// Now check if the billing account has mutable managers (individual account does not).
|
||||||
if (billingAccount.individual) {
|
if (billingAccount.individual) {
|
||||||
@ -1796,7 +1802,8 @@ export class HomeDBManager extends EventEmitter {
|
|||||||
public async getOrgAccess(scope: Scope, orgKey: string|number): Promise<QueryResult<PermissionData>> {
|
public async getOrgAccess(scope: Scope, orgKey: string|number): Promise<QueryResult<PermissionData>> {
|
||||||
const orgQuery = this.org(scope, orgKey, {
|
const orgQuery = this.org(scope, orgKey, {
|
||||||
markPermissions: Permissions.VIEW,
|
markPermissions: Permissions.VIEW,
|
||||||
needRealOrg: true
|
needRealOrg: true,
|
||||||
|
allowSpecialPermit: true
|
||||||
})
|
})
|
||||||
// Join the org's ACL rules (with 1st level groups/users listed).
|
// Join the org's ACL rules (with 1st level groups/users listed).
|
||||||
.leftJoinAndSelect('orgs.aclRules', 'acl_rules')
|
.leftJoinAndSelect('orgs.aclRules', 'acl_rules')
|
||||||
@ -2242,26 +2249,34 @@ export class HomeDBManager extends EventEmitter {
|
|||||||
*/
|
*/
|
||||||
public org(scope: Scope, org: string|number|null,
|
public org(scope: Scope, org: string|number|null,
|
||||||
options: QueryOptions = {}): SelectQueryBuilder<Organization> {
|
options: QueryOptions = {}): SelectQueryBuilder<Organization> {
|
||||||
return this._org(scope.userId, scope.includeSupport || false, org, options);
|
return this._org(scope, scope.includeSupport || false, org, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _org(userId: number|null, includeSupport: boolean, org: string|number|null,
|
private _org(scope: Scope|null, includeSupport: boolean, org: string|number|null,
|
||||||
options: QueryOptions = {}): SelectQueryBuilder<Organization> {
|
options: QueryOptions = {}): SelectQueryBuilder<Organization> {
|
||||||
let query = this._orgs(options.manager);
|
let query = this._orgs(options.manager);
|
||||||
// merged pseudo-org must become personal org.
|
// merged pseudo-org must become personal org.
|
||||||
if (org === null || (options.needRealOrg && this.isMergedOrg(org))) {
|
if (org === null || (options.needRealOrg && this.isMergedOrg(org))) {
|
||||||
if (!userId) { throw new Error('_org: requires userId'); }
|
if (!scope || !scope.userId) { throw new Error('_org: requires userId'); }
|
||||||
query = query.where('orgs.owner_id = :userId', {userId});
|
query = query.where('orgs.owner_id = :userId', {userId: scope.userId});
|
||||||
} else {
|
} else {
|
||||||
query = this._whereOrg(query, org, includeSupport);
|
query = this._whereOrg(query, org, includeSupport);
|
||||||
}
|
}
|
||||||
if (options.markPermissions) {
|
if (options.markPermissions) {
|
||||||
if (!userId) {
|
if (!scope || !scope.userId) {
|
||||||
throw new Error(`_orgQuery error: userId must be set to mark permissions`);
|
throw new Error(`_orgQuery error: userId must be set to mark permissions`);
|
||||||
}
|
}
|
||||||
|
let effectiveUserId = scope.userId;
|
||||||
|
let threshold = options.markPermissions;
|
||||||
|
// TODO If the specialPermit is used across the network, requests could refer to orgs in
|
||||||
|
// different ways (number vs string), causing this comparison to fail.
|
||||||
|
if (options.allowSpecialPermit && scope.specialPermit && scope.specialPermit.org === org) {
|
||||||
|
effectiveUserId = this.getPreviewerUserId();
|
||||||
|
threshold = Permissions.VIEW;
|
||||||
|
}
|
||||||
// Compute whether we have access to the doc
|
// Compute whether we have access to the doc
|
||||||
query = query.addSelect(
|
query = query.addSelect(
|
||||||
this._markIsPermitted('orgs', userId, options.markPermissions),
|
this._markIsPermitted('orgs', effectiveUserId, threshold),
|
||||||
'is_permitted'
|
'is_permitted'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -37,7 +37,7 @@ import {getLoginMiddleware} from 'app/server/lib/logins';
|
|||||||
import {getAppPathTo, getAppRoot, getUnpackedAppRoot} from 'app/server/lib/places';
|
import {getAppPathTo, getAppRoot, getUnpackedAppRoot} from 'app/server/lib/places';
|
||||||
import {addPluginEndpoints, limitToPlugins} from 'app/server/lib/PluginEndpoint';
|
import {addPluginEndpoints, limitToPlugins} from 'app/server/lib/PluginEndpoint';
|
||||||
import {PluginManager} from 'app/server/lib/PluginManager';
|
import {PluginManager} from 'app/server/lib/PluginManager';
|
||||||
import {adaptServerUrl, optStringParam, RequestWithGristInfo, stringParam, TEST_HTTPS_OFFSET,
|
import {adaptServerUrl, addPermit, getScope, optStringParam, RequestWithGristInfo, stringParam, TEST_HTTPS_OFFSET,
|
||||||
trustOrigin} from 'app/server/lib/requestUtils';
|
trustOrigin} from 'app/server/lib/requestUtils';
|
||||||
import {ISendAppPageOptions, makeSendAppPage} from 'app/server/lib/sendAppPage';
|
import {ISendAppPageOptions, makeSendAppPage} from 'app/server/lib/sendAppPage';
|
||||||
import * as ServerMetrics from 'app/server/lib/ServerMetrics';
|
import * as ServerMetrics from 'app/server/lib/ServerMetrics';
|
||||||
@ -897,7 +897,9 @@ export class FlexServer implements GristServer {
|
|||||||
if (!orgDomain) {
|
if (!orgDomain) {
|
||||||
return this._sendAppPage(req, resp, {path: 'error.html', status: 404, config: {errPage: 'not-found'}});
|
return this._sendAppPage(req, resp, {path: 'error.html', status: 404, config: {errPage: 'not-found'}});
|
||||||
}
|
}
|
||||||
const query = await this.dbManager.getOrg({userId: mreq.userId!}, orgDomain);
|
// Allow the support user access to billing pages.
|
||||||
|
const scope = addPermit(getScope(mreq), this.dbManager.getSupportUserId(), {org: orgDomain});
|
||||||
|
const query = await this.dbManager.getOrg(scope, orgDomain);
|
||||||
const org = this.dbManager.unwrapQueryResult(query);
|
const org = this.dbManager.unwrapQueryResult(query);
|
||||||
// This page isn't availabe for personal site.
|
// This page isn't availabe for personal site.
|
||||||
if (org.owner) {
|
if (org.owner) {
|
||||||
|
@ -4,6 +4,7 @@ import {DocScope, QueryResult, Scope} from 'app/gen-server/lib/HomeDBManager';
|
|||||||
import {getUserId, RequestWithLogin} from 'app/server/lib/Authorizer';
|
import {getUserId, RequestWithLogin} from 'app/server/lib/Authorizer';
|
||||||
import {RequestWithOrg} from 'app/server/lib/extractOrg';
|
import {RequestWithOrg} from 'app/server/lib/extractOrg';
|
||||||
import * as log from 'app/server/lib/log';
|
import * as log from 'app/server/lib/log';
|
||||||
|
import {Permit} from 'app/server/lib/Permit';
|
||||||
import {Request, Response} from 'express';
|
import {Request, Response} from 'express';
|
||||||
import {URL} from 'url';
|
import {URL} from 'url';
|
||||||
|
|
||||||
@ -132,6 +133,13 @@ export function getScope(req: Request): Scope {
|
|||||||
return {urlId, userId, org, includeSupport, showRemoved, specialPermit};
|
return {urlId, userId, org, includeSupport, showRemoved, specialPermit};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If scope is for the given userId, return a new Scope with the special permit added.
|
||||||
|
*/
|
||||||
|
export function addPermit(scope: Scope, userId: number, specialPermit: Permit): Scope {
|
||||||
|
return {...scope, ...(scope.userId === userId ? {specialPermit} : {})};
|
||||||
|
}
|
||||||
|
|
||||||
// Return a JSON response reflecting the output of a query.
|
// Return a JSON response reflecting the output of a query.
|
||||||
// Filter out keys we don't want crossing the api.
|
// Filter out keys we don't want crossing the api.
|
||||||
// Set req to null to not log any information about request.
|
// Set req to null to not log any information about request.
|
||||||
|
Loading…
Reference in New Issue
Block a user