gristlabs_grist-core/app/server/lib/Authorizer.ts

505 lines
20 KiB
TypeScript
Raw Normal View History

import {ApiError} from 'app/common/ApiError';
import {OpenDocMode} from 'app/common/DocListAPI';
import {ErrorWithCode} from 'app/common/ErrorWithCode';
(core) support adding user characteristic tables for granular ACLs Summary: This is a prototype for expanding the conditions that can be used in granular ACLs. When processing ACLs, the following variables (called "characteristics") are now available in conditions: * UserID * Email * Name * Access (owners, editors, viewers) The set of variables can be expanded by adding a "characteristic" clause. This is a clause which specifies: * A tableId * The name of an existing characteristic * A colId The effect of the clause is to expand the available characteristics with all the columns in the table, with values taken from the record where there is a match between the specified characteristic and the specified column. Existing clauses are generalized somewhat to demonstrate and test the use these variables. That isn't the main point of this diff though, and I propose to leave generalizing+systematizing those clauses for a future diff. Issues I'm not dealing with here: * How clauses combine. (The scope on GranularAccessRowClause is a hack to save me worrying about that yet). * The full set of matching methods we'll allow. * Refreshing row access in clients when the tables mentioned in characteristic tables change. * Full CRUD permission control. * Default rules (part of combination). * Reporting errors in access rules. That said, with this diff it is possible to e.g. assign a City to editors by their email address or name, and have only rows for those Cities be visible in their client. Ability to modify those rows, and remain updates about them, remains under incomplete control. Test Plan: added tests Reviewers: dsagal Reviewed By: dsagal Differential Revision: https://phab.getgrist.com/D2642
2020-10-19 14:25:21 +00:00
import {FullUser, UserProfile} from 'app/common/LoginSessionAPI';
import {canEdit, canView, getWeakestRole, Role} from 'app/common/roles';
import {Document} from 'app/gen-server/entity/Document';
import {User} from 'app/gen-server/entity/User';
import {DocAuthKey, DocAuthResult, HomeDBManager} from 'app/gen-server/lib/HomeDBManager';
import {getSessionProfiles, getSessionUser, linkOrgWithEmail, SessionObj,
SessionUserObj} from 'app/server/lib/BrowserSession';
import {RequestWithOrg} from 'app/server/lib/extractOrg';
import {COOKIE_MAX_AGE, getAllowedOrgForSessionID} from 'app/server/lib/gristSessions';
import * as log from 'app/server/lib/log';
import {IPermitStore, Permit} from 'app/server/lib/Permit';
import {allowHost} from 'app/server/lib/requestUtils';
import {NextFunction, Request, RequestHandler, Response} from 'express';
export interface RequestWithLogin extends Request {
sessionID: string;
session: SessionObj;
org?: string;
isCustomHost?: boolean; // when set, the request's domain is a recognized custom host linked
// with the specified org.
users?: UserProfile[];
userId?: number;
user?: User;
userIsAuthorized?: boolean; // If userId is for "anonymous", this will be false.
docAuth?: DocAuthResult; // For doc requests, the docId and the user's access level.
specialPermit?: Permit;
}
/**
* Extract the user id from a request, assuming we've added it via appropriate middleware.
* Throws ApiError with code 401 (unauthorized) if the user id is missing.
*/
export function getUserId(req: Request): number {
const userId = (req as RequestWithLogin).userId;
if (!userId) {
throw new ApiError("user not known", 401);
}
return userId;
}
/**
* Extract the user object from a request, assuming we've added it via appropriate middleware.
* Throws ApiError with code 401 (unauthorized) if the user is missing.
*/
export function getUser(req: Request): User {
const user = (req as RequestWithLogin).user;
if (!user) {
throw new ApiError("user not known", 401);
}
return user;
}
/**
* Extract the user profiles from a request, assuming we've added them via appropriate middleware.
* Throws ApiError with code 401 (unauthorized) if the profiles are missing.
*/
export function getUserProfiles(req: Request): UserProfile[] {
const users = (req as RequestWithLogin).users;
if (!users) {
throw new ApiError("user profile not found", 401);
}
return users;
}
// Extract the user id from a request, requiring it to be authorized (not an anonymous session).
export function getAuthorizedUserId(req: Request) {
const userId = getUserId(req);
if (isAnonymousUser(req)) {
throw new ApiError("user not authorized", 401);
}
return userId;
}
export function isAnonymousUser(req: Request) {
return !(req as RequestWithLogin).userIsAuthorized;
}
// True if Grist is configured for a single user without specific authorization
// (classic standalone/electron mode).
export function isSingleUserMode(): boolean {
return process.env.GRIST_SINGLE_USER === '1';
}
/**
* Returns the express request object with user information added, if it can be
* found based on passed in headers or the session. Specifically, sets:
* - req.userId: the id of the user in the database users table
* - req.userIsAuthorized: set if user has presented credentials that were accepted
* (the anonymous user has a userId but does not have userIsAuthorized set if,
* as would typically be the case, credentials were not presented)
* - req.users: set for org-and-session-based logins, with list of profiles in session
*/
export async function addRequestUser(dbManager: HomeDBManager, permitStore: IPermitStore,
fallbackEmail: string|null,
req: Request, res: Response, next: NextFunction) {
const mreq = req as RequestWithLogin;
let profile: UserProfile|undefined;
// First, check for an apiKey
if (mreq.headers && mreq.headers.authorization) {
// header needs to be of form "Bearer XXXXXXXXX" to apply
const parts = String(mreq.headers.authorization).split(' ');
if (parts[0] === "Bearer") {
const user = parts[1] ? await dbManager.getUserByKey(parts[1]) : undefined;
if (!user) {
return res.status(401).send('Bad request: invalid API key');
}
if (user.id === dbManager.getAnonymousUserId()) {
// We forbid the anonymous user to present an api key. That saves us
// having to think through the consequences of authorized access to the
// anonymous user's profile via the api (e.g. how should the api key be managed).
return res.status(401).send('Credentials cannot be presented for the anonymous user account via API key');
}
mreq.user = user;
mreq.userId = user.id;
mreq.userIsAuthorized = true;
}
}
// Special permission header for internal housekeeping tasks
if (mreq.headers && mreq.headers.permit) {
const permitKey = String(mreq.headers.permit);
try {
const permit = await permitStore.getPermit(permitKey);
if (!permit) { return res.status(401).send('Bad request: unknown permit'); }
mreq.user = dbManager.getAnonymousUser();
mreq.userId = mreq.user.id;
mreq.specialPermit = permit;
} catch (err) {
log.error(`problem reading permit: ${err}`);
return res.status(401).send('Bad request: permit could not be read');
}
}
// If we haven't already been authenticated, and this is not a GET/HEAD/OPTIONS, then
// require that the X-Requested-With header field be set to XMLHttpRequest.
// This is trivial for legitimate web clients to do, and an obstacle to
// nefarious ones.
// https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#use-of-custom-request-headers
// https://markitzeroday.com/x-requested-with/cors/2017/06/29/csrf-mitigation-for-ajax-requests.html
if (!mreq.userId && !mreq.xhr && !['GET', 'HEAD', 'OPTIONS'].includes(mreq.method)) {
return res.status(401).send('Bad request (missing header)');
}
// A bit of extra info we'll add to the "Auth" log message when this request passes the check
// for custom-host-specific sessionID.
let customHostSession = '';
// If we haven't selected a user by other means, and have profiles available in the
// session, then select a user based on those profiles.
const session = mreq.session;
if (!mreq.userId && session && session.users && session.users.length > 0 &&
mreq.org !== undefined) {
// Prevent using custom-domain sessionID to authorize to a different domain, since
// custom-domain owner could hijack such sessions.
const allowedOrg = getAllowedOrgForSessionID(mreq.sessionID);
if (allowedOrg) {
if (allowHost(req, allowedOrg.host)) {
customHostSession = ` custom-host-match ${allowedOrg.host}`;
} else {
// We need an exception for internal forwarding from home server to doc-workers. These use
// internal hostnames, so we can't expect a custom domain. These requests do include an
// Organization header, which we'll use to grant the exception, but security issues remain.
// TODO Issue 1: an attacker can use a custom-domain request to get an API key, which is an
// open door to all orgs accessible by this user.
// TODO Issue 2: Organization header is easy for an attacker (who has stolen a session
// cookie) to include too; it does nothing to prove that the request is internal.
const org = req.header('organization');
if (org && org === allowedOrg.org) {
customHostSession = ` custom-host-fwd ${org}`;
} else {
// Log error and fail.
log.warn("Auth[%s]: sessionID for host %s org %s; wrong for host %s org %s", mreq.method,
allowedOrg.host, allowedOrg.org, mreq.get('host'), mreq.org);
return res.status(403).send('Bad request: invalid session ID');
}
}
}
mreq.users = getSessionProfiles(session);
// If we haven't set a maxAge yet, set it now.
if (session && session.cookie && !session.cookie.maxAge) {
session.cookie.maxAge = COOKIE_MAX_AGE;
}
// See if we have a profile linked with the active organization already.
// TODO: implement userSelector for rest API, to allow "sticky" user selection on pages.
let sessionUser: SessionUserObj|null = getSessionUser(session, mreq.org, '');
if (!sessionUser) {
// No profile linked yet, so let's elect one.
// Choose a profile that is no worse than the others available.
const option = await dbManager.getBestUserForOrg(mreq.users, mreq.org);
if (option) {
// Modify request session object to link the current org with our choice of
// profile. Express-session will save this change.
sessionUser = linkOrgWithEmail(session, option.email, mreq.org);
// In this special case of initially linking a profile, we need to look up the user's info.
mreq.user = await dbManager.getUserByLogin(option.email);
mreq.userId = option.id;
mreq.userIsAuthorized = true;
} else {
// No profile has access to this org. We could choose to
// link no profile, in which case user will end up
// immediately presented with a sign-in page, or choose to
// link an arbitrary profile (say, the first one the user
// logged in as), in which case user will end up with a
// friendlier page explaining the situation and offering to
// add an account to resolve it. We go ahead and pick an
// arbitrary profile.
sessionUser = session.users[0];
if (!session.orgToUser) { throw new Error("Session misconfigured"); }
// Express-session will save this change.
session.orgToUser[mreq.org] = 0;
}
}
profile = sessionUser && sessionUser.profile || undefined;
// If we haven't computed a userId yet, check for one using an email address in the profile.
// A user record will be created automatically for emails we've never seen before.
if (profile && !mreq.userId) {
const user = await dbManager.getUserByLoginWithRetry(profile.email, profile);
if (user) {
mreq.user = user;
mreq.userId = user.id;
mreq.userIsAuthorized = true;
}
}
}
if (!mreq.userId && fallbackEmail) {
const user = await dbManager.getUserByLogin(fallbackEmail);
if (user) {
mreq.user = user;
mreq.userId = user.id;
mreq.userIsAuthorized = true;
const fullUser = dbManager.makeFullUser(user);
mreq.users = [fullUser];
profile = fullUser;
}
}
// If no userId has been found yet, fall back on anonymous.
if (!mreq.userId) {
const anon = dbManager.getAnonymousUser();
mreq.user = anon;
mreq.userId = anon.id;
mreq.userIsAuthorized = false;
mreq.users = [dbManager.makeFullUser(anon)];
}
log.debug("Auth[%s]: id %s email %s host %s path %s org %s%s", mreq.method,
(core) support adding user characteristic tables for granular ACLs Summary: This is a prototype for expanding the conditions that can be used in granular ACLs. When processing ACLs, the following variables (called "characteristics") are now available in conditions: * UserID * Email * Name * Access (owners, editors, viewers) The set of variables can be expanded by adding a "characteristic" clause. This is a clause which specifies: * A tableId * The name of an existing characteristic * A colId The effect of the clause is to expand the available characteristics with all the columns in the table, with values taken from the record where there is a match between the specified characteristic and the specified column. Existing clauses are generalized somewhat to demonstrate and test the use these variables. That isn't the main point of this diff though, and I propose to leave generalizing+systematizing those clauses for a future diff. Issues I'm not dealing with here: * How clauses combine. (The scope on GranularAccessRowClause is a hack to save me worrying about that yet). * The full set of matching methods we'll allow. * Refreshing row access in clients when the tables mentioned in characteristic tables change. * Full CRUD permission control. * Default rules (part of combination). * Reporting errors in access rules. That said, with this diff it is possible to e.g. assign a City to editors by their email address or name, and have only rows for those Cities be visible in their client. Ability to modify those rows, and remain updates about them, remains under incomplete control. Test Plan: added tests Reviewers: dsagal Reviewed By: dsagal Differential Revision: https://phab.getgrist.com/D2642
2020-10-19 14:25:21 +00:00
mreq.userId, mreq.user?.loginEmail, mreq.get('host'), mreq.path, mreq.org,
customHostSession);
return next();
}
/**
* Returns a handler that redirects the user to a login or signup page.
*/
export function redirectToLoginUnconditionally(
getLoginRedirectUrl: (redirectUrl: URL) => Promise<string>,
getSignUpRedirectUrl: (redirectUrl: URL) => Promise<string>
) {
return async (req: Request, resp: Response, next: NextFunction) => {
const mreq = req as RequestWithLogin;
// Redirect to sign up if it doesn't look like the user has ever logged in (on
// this browser) After logging in, `users` will be set in the session. Even after
// logging out again, `users` will still be set.
const signUp: boolean = (mreq.session.users === undefined);
log.debug(`Authorizer: redirecting to ${signUp ? 'sign up' : 'log in'}`);
const redirectUrl = new URL(req.protocol + '://' + req.get('host') + req.originalUrl);
if (signUp) {
return resp.redirect(await getSignUpRedirectUrl(redirectUrl));
} else {
return resp.redirect(await getLoginRedirectUrl(redirectUrl));
}
};
}
/**
* Middleware to redirects user to a login page when the user is not
* logged in. If allowExceptions is set, then we make an exception
* for a team site allowing anonymous access, or a personal doc
* allowing anonymous access, or the merged org.
*/
export function redirectToLogin(
allowExceptions: boolean,
getLoginRedirectUrl: (redirectUrl: URL) => Promise<string>,
getSignUpRedirectUrl: (redirectUrl: URL) => Promise<string>,
dbManager: HomeDBManager
): RequestHandler {
const redirectUnconditionally = redirectToLoginUnconditionally(getLoginRedirectUrl,
getSignUpRedirectUrl);
return async (req: Request, resp: Response, next: NextFunction) => {
const mreq = req as RequestWithLogin;
mreq.session.alive = true; // This will ensure that express-session will set our cookie
// if it hasn't already - we'll need it if we redirect.
if (mreq.userIsAuthorized) { return next(); }
try {
// Otherwise it's an anonymous user. Proceed normally only if the org allows anon access.
if (mreq.userId && mreq.org && allowExceptions) {
// Anonymous user has qualified access to merged org.
if (dbManager.isMergedOrg(mreq.org)) { return next(); }
const result = await dbManager.getOrg({userId: mreq.userId}, mreq.org || null);
if (result.status === 200) { return next(); }
}
// In all other cases (including unknown org), redirect user to login or sign up.
return redirectUnconditionally(req, resp, next);
} catch (err) {
log.info("Authorizer failed to redirect", err.message);
return resp.status(401).send(err.message);
}
};
}
/**
* Sets mreq.docAuth if not yet set, and returns it.
*/
export async function getOrSetDocAuth(
mreq: RequestWithLogin, dbManager: HomeDBManager, urlId: string
): Promise<DocAuthResult> {
if (!mreq.docAuth) {
let effectiveUserId = getUserId(mreq);
if (mreq.specialPermit && mreq.userId === dbManager.getAnonymousUserId()) {
effectiveUserId = dbManager.getPreviewerUserId();
}
mreq.docAuth = await dbManager.getDocAuthCached({urlId, userId: effectiveUserId, org: mreq.org});
if (mreq.specialPermit && mreq.userId === dbManager.getAnonymousUserId() &&
mreq.specialPermit.docId === mreq.docAuth.docId) {
mreq.docAuth = {...mreq.docAuth, access: 'owners'};
}
}
return mreq.docAuth;
}
export interface ResourceSummary {
kind: 'doc';
id: string|number;
}
/**
*
* Handle authorization for a single document accessed by a given user.
*
*/
export interface Authorizer {
// get the id of user, or null if no authorization in place.
getUserId(): number|null;
(core) support adding user characteristic tables for granular ACLs Summary: This is a prototype for expanding the conditions that can be used in granular ACLs. When processing ACLs, the following variables (called "characteristics") are now available in conditions: * UserID * Email * Name * Access (owners, editors, viewers) The set of variables can be expanded by adding a "characteristic" clause. This is a clause which specifies: * A tableId * The name of an existing characteristic * A colId The effect of the clause is to expand the available characteristics with all the columns in the table, with values taken from the record where there is a match between the specified characteristic and the specified column. Existing clauses are generalized somewhat to demonstrate and test the use these variables. That isn't the main point of this diff though, and I propose to leave generalizing+systematizing those clauses for a future diff. Issues I'm not dealing with here: * How clauses combine. (The scope on GranularAccessRowClause is a hack to save me worrying about that yet). * The full set of matching methods we'll allow. * Refreshing row access in clients when the tables mentioned in characteristic tables change. * Full CRUD permission control. * Default rules (part of combination). * Reporting errors in access rules. That said, with this diff it is possible to e.g. assign a City to editors by their email address or name, and have only rows for those Cities be visible in their client. Ability to modify those rows, and remain updates about them, remains under incomplete control. Test Plan: added tests Reviewers: dsagal Reviewed By: dsagal Differential Revision: https://phab.getgrist.com/D2642
2020-10-19 14:25:21 +00:00
// get user profile if available.
getUser(): FullUser|null;
// get the id of the document.
getDocId(): string;
// get any link parameters in place when accessing the resource.
getLinkParameters(): Record<string, string>;
// Fetch the doc metadata from HomeDBManager.
getDoc(): Promise<Document>;
// Check access, throw error if the requested level of access isn't available.
assertAccess(role: 'viewers'|'editors'|'owners'): Promise<void>;
// Get the lasted access information calculated for the doc. This is useful
// for logging - but access control itself should use assertAccess() to
// ensure the data is fresh.
getCachedAuth(): DocAuthResult;
}
/**
*
* Handle authorization for a single document and user.
*
*/
export class DocAuthorizer implements Authorizer {
constructor(
private _dbManager: HomeDBManager,
private _key: DocAuthKey,
public readonly openMode: OpenDocMode,
public readonly linkParameters: Record<string, string>,
(core) support adding user characteristic tables for granular ACLs Summary: This is a prototype for expanding the conditions that can be used in granular ACLs. When processing ACLs, the following variables (called "characteristics") are now available in conditions: * UserID * Email * Name * Access (owners, editors, viewers) The set of variables can be expanded by adding a "characteristic" clause. This is a clause which specifies: * A tableId * The name of an existing characteristic * A colId The effect of the clause is to expand the available characteristics with all the columns in the table, with values taken from the record where there is a match between the specified characteristic and the specified column. Existing clauses are generalized somewhat to demonstrate and test the use these variables. That isn't the main point of this diff though, and I propose to leave generalizing+systematizing those clauses for a future diff. Issues I'm not dealing with here: * How clauses combine. (The scope on GranularAccessRowClause is a hack to save me worrying about that yet). * The full set of matching methods we'll allow. * Refreshing row access in clients when the tables mentioned in characteristic tables change. * Full CRUD permission control. * Default rules (part of combination). * Reporting errors in access rules. That said, with this diff it is possible to e.g. assign a City to editors by their email address or name, and have only rows for those Cities be visible in their client. Ability to modify those rows, and remain updates about them, remains under incomplete control. Test Plan: added tests Reviewers: dsagal Reviewed By: dsagal Differential Revision: https://phab.getgrist.com/D2642
2020-10-19 14:25:21 +00:00
private _docAuth?: DocAuthResult,
private _profile?: UserProfile
) {
}
public getUserId(): number {
return this._key.userId;
}
(core) support adding user characteristic tables for granular ACLs Summary: This is a prototype for expanding the conditions that can be used in granular ACLs. When processing ACLs, the following variables (called "characteristics") are now available in conditions: * UserID * Email * Name * Access (owners, editors, viewers) The set of variables can be expanded by adding a "characteristic" clause. This is a clause which specifies: * A tableId * The name of an existing characteristic * A colId The effect of the clause is to expand the available characteristics with all the columns in the table, with values taken from the record where there is a match between the specified characteristic and the specified column. Existing clauses are generalized somewhat to demonstrate and test the use these variables. That isn't the main point of this diff though, and I propose to leave generalizing+systematizing those clauses for a future diff. Issues I'm not dealing with here: * How clauses combine. (The scope on GranularAccessRowClause is a hack to save me worrying about that yet). * The full set of matching methods we'll allow. * Refreshing row access in clients when the tables mentioned in characteristic tables change. * Full CRUD permission control. * Default rules (part of combination). * Reporting errors in access rules. That said, with this diff it is possible to e.g. assign a City to editors by their email address or name, and have only rows for those Cities be visible in their client. Ability to modify those rows, and remain updates about them, remains under incomplete control. Test Plan: added tests Reviewers: dsagal Reviewed By: dsagal Differential Revision: https://phab.getgrist.com/D2642
2020-10-19 14:25:21 +00:00
public getUser(): FullUser|null {
return this._profile ? {id: this.getUserId(), ...this._profile} : null;
}
public getDocId(): string {
// We've been careful to require urlId === docId, see DocManager.
return this._key.urlId;
}
public getLinkParameters(): Record<string, string> {
return this.linkParameters;
}
public async getDoc(): Promise<Document> {
return this._dbManager.getDoc(this._key);
}
public async assertAccess(role: 'viewers'|'editors'|'owners'): Promise<void> {
const docAuth = await this._dbManager.getDocAuthCached(this._key);
this._docAuth = docAuth;
assertAccess(role, docAuth, {openMode: this.openMode});
}
public getCachedAuth(): DocAuthResult {
if (!this._docAuth) { throw Error('no cached authentication'); }
return this._docAuth;
}
}
export class DummyAuthorizer implements Authorizer {
constructor(public role: Role|null, public docId: string) {}
public getUserId() { return null; }
(core) support adding user characteristic tables for granular ACLs Summary: This is a prototype for expanding the conditions that can be used in granular ACLs. When processing ACLs, the following variables (called "characteristics") are now available in conditions: * UserID * Email * Name * Access (owners, editors, viewers) The set of variables can be expanded by adding a "characteristic" clause. This is a clause which specifies: * A tableId * The name of an existing characteristic * A colId The effect of the clause is to expand the available characteristics with all the columns in the table, with values taken from the record where there is a match between the specified characteristic and the specified column. Existing clauses are generalized somewhat to demonstrate and test the use these variables. That isn't the main point of this diff though, and I propose to leave generalizing+systematizing those clauses for a future diff. Issues I'm not dealing with here: * How clauses combine. (The scope on GranularAccessRowClause is a hack to save me worrying about that yet). * The full set of matching methods we'll allow. * Refreshing row access in clients when the tables mentioned in characteristic tables change. * Full CRUD permission control. * Default rules (part of combination). * Reporting errors in access rules. That said, with this diff it is possible to e.g. assign a City to editors by their email address or name, and have only rows for those Cities be visible in their client. Ability to modify those rows, and remain updates about them, remains under incomplete control. Test Plan: added tests Reviewers: dsagal Reviewed By: dsagal Differential Revision: https://phab.getgrist.com/D2642
2020-10-19 14:25:21 +00:00
public getUser() { return null; }
public getDocId() { return this.docId; }
public getLinkParameters() { return {}; }
public async getDoc(): Promise<Document> { throw new Error("Not supported in standalone"); }
public async assertAccess() { /* noop */ }
public getCachedAuth(): DocAuthResult {
return {
access: this.role,
docId: this.docId,
removed: false,
};
}
}
export function assertAccess(
role: 'viewers'|'editors'|'owners', docAuth: DocAuthResult, options: {
openMode?: OpenDocMode,
allowRemoved?: boolean,
} = {}) {
const openMode = options.openMode || 'default';
const details = {status: 403, accessMode: openMode};
if (docAuth.error) {
if ([400, 401, 403].includes(docAuth.error.status)) {
// For these error codes, we know our access level - forbidden. Make errors more uniform.
throw new ErrorWithCode("AUTH_NO_VIEW", "No view access", details);
}
throw docAuth.error;
}
if (docAuth.removed && !options.allowRemoved) {
throw new ErrorWithCode("AUTH_NO_VIEW", "Document is deleted", {status: 404});
}
// If docAuth has no error, the doc is accessible, but we should still check the level (in case
// it's possible to access the doc with a level less than "viewer").
if (!canView(docAuth.access)) {
throw new ErrorWithCode("AUTH_NO_VIEW", "No view access", details);
}
if (role === 'editors') {
// If opening in a fork or view mode, treat user as viewer and deny write access.
const access = (openMode === 'fork' || openMode === 'view') ?
getWeakestRole('viewers', docAuth.access) : docAuth.access;
if (!canEdit(access)) {
throw new ErrorWithCode("AUTH_NO_EDIT", "No write access", details);
}
}
}
/**
* Pull out headers to pass along to a proxied service. Focussed primarily on
* authentication.
*/
export function getTransitiveHeaders(req: Request): {[key: string]: string} {
const Authorization = req.get('Authorization');
const Cookie = req.get('Cookie');
const PermitHeader = req.get('Permit');
const Organization = (req as RequestWithOrg).org;
const XRequestedWith = req.get('X-Requested-With');
const Origin = req.get('Origin'); // Pass along the original Origin since it may
// play a role in granular access control.
return {
...(Authorization ? { Authorization } : undefined),
...(Cookie ? { Cookie } : undefined),
...(Organization ? { Organization } : undefined),
...(PermitHeader ? { Permit: PermitHeader } : undefined),
...(XRequestedWith ? { 'X-Requested-With': XRequestedWith } : undefined),
...(Origin ? { Origin } : undefined),
};
}