gristlabs_grist-core/app/server/lib/AppSettings.ts
Paul Fitzpatrick 561d9696aa (core) clean up interaction of forward auth with session
Summary:
For self-hosted Grist, forward auth has proven useful, where
some proxy wrapped around Grist manages authentication, and
passes on user information to Grist in a trusted header.
The current implementation is adequate when Grist is the
only place where the user logs in or out, but is confusing
otherwise (see https://github.com/gristlabs/grist-core/issues/207).
Here we take some steps to broaden the scenarios Grist's
forward auth support can be used with:

  * When a trusted header is present and is blank, treat
    that as the user not being logged in, and don't look
    any further for identity information. Specifically,
    don't look in Grist's session information.
  * Add a `GRIST_IGNORE_SESSION` flag to entirely prevent
    Grist from picking up identity information from a cookie,
    in order to avoid confusion between multiple login methods.
  * Add tests for common scenarios.

Test Plan: added tests

Reviewers: georgegevoian

Reviewed By: georgegevoian

Differential Revision: https://phab.getgrist.com/D3482
2022-06-15 13:06:12 -04:00

216 lines
6.6 KiB
TypeScript

import { isAffirmative } from 'app/common/gutil';
/**
* A bundle of settings for the application. May contain
* a value directly, and/or via nested settings. Also
* may have some information about where we looked for
* the value, for reporting as a diagnostic.
*/
export class AppSettings {
private _value?: JSONValue;
private _children?: {[key: string]: AppSettings};
private _info?: AppSettingQueryResult;
public constructor(public readonly name: string) {}
/* access the setting - undefined if not set */
public get(): JSONValue|undefined {
return this._value;
}
/* access the setting as a boolean using isAffirmative - undefined if not set */
public getAsBool(): boolean|undefined {
return (this._value !== undefined) ? isAffirmative(this._value) : undefined;
}
/**
* Try to read the setting from the environment. Even if
* we fail, we record information about how we tried to
* find the setting, so we can report on that.
*/
public read(query: AppSettingQuery) {
this._value = undefined;
this._info = undefined;
let value = undefined;
let found = false;
const envVars = getEnvVarsFromQuery(query);
if (!envVars.length) {
throw new Error('could not find an environment variable to read');
}
let envVar = envVars[0];
for (const synonym of envVars) {
value = process.env[synonym];
if (value !== undefined) {
envVar = synonym;
found = true;
break;
}
}
this._info = {
envVar: found ? envVar : undefined,
found,
query,
};
if (value !== undefined) {
this._value = value;
} else if (query.defaultValue !== undefined) {
this._value = query.defaultValue;
}
return this;
}
/**
* As for read() but type the result as a string.
*/
public readString(query: AppSettingQuery): string|undefined {
this.read(query);
if (this._value === undefined) { return undefined; }
this._value = String(this._value);
return this._value;
}
/**
* As for readString() but fail if nothing was found.
*/
public requireString(query: AppSettingQuery): string {
const result = this.readString(query);
if (result === undefined) {
throw new Error(`missing environment variable: ${query.envVar}`);
}
return result;
}
/**
* As for read() but type (and store, and report) the result as
* a boolean.
*/
public readBool(query: AppSettingQuery): boolean|undefined {
this.readString(query);
const result = this.getAsBool();
this._value = result;
return result;
}
/* set this setting 'manually' */
public set(value: JSONValue): void {
this._value = value;
this._info = undefined;
}
/* access any nested settings */
public get nested(): {[key: string]: AppSettings} {
return this._children || {};
}
/**
* Add a named nested setting, returning an AppSettings
* object that can be used to access it. This method is
* named "section" to suggest that the nested setting
* will itself contain multiple settings, but doesn't
* require that.
*/
public section(fname: string): AppSettings {
if (!this._children) { this._children = {}; }
let child = this._children[fname];
if (!child) {
this._children[fname] = child = new AppSettings(fname);
}
return child;
}
/**
* Add a named nested setting, returning an AppSettings
* object that can be used to access it. This method is
* named "flag" to suggest that tthe nested setting will
* not iself be nested, but doesn't require that - it is
* currently just an alias for the section() method.
*/
public flag(fname: string): AppSettings {
return this.section(fname);
}
/**
* Produce a summary description of the setting and how it was
* derived.
*/
public describe(): AppSettingDescription {
return {
name: this.name,
value: (this._info?.query.censor && this._value !== undefined) ? '*****' : this._value,
foundInEnvVar: this._info?.envVar,
wouldFindInEnvVar: this._info?.query.preferredEnvVar || getEnvVarsFromQuery(this._info?.query)[0],
usedDefault: this._value !== undefined && this._info !== undefined && !this._info?.found,
};
}
/**
* As for describe(), but include all nested settings also.
* Used dotted notation for setting names. Omit settings that
* are undefined and without useful information about how they
* might be defined.
*/
public describeAll(): AppSettingDescription[] {
const inv: AppSettingDescription[] = [];
inv.push(this.describe());
if (this._children) {
for (const child of Object.values(this._children)) {
for (const item of child.describeAll()) {
inv.push({...item, name: this.name + '.' + item.name});
}
}
}
return inv.filter(item => item.value !== undefined ||
item.wouldFindInEnvVar !== undefined ||
item.usedDefault);
}
}
/**
* A global object for Grist application settings.
*/
export const appSettings = new AppSettings('grist');
/**
* Hints for how to define a setting, including possible
* environment variables and default values.
*/
export interface AppSettingQuery {
envVar: string|string[]; // environment variable(s) to check.
preferredEnvVar?: string; // "Canonical" environment variable to suggest.
// Should be in envVar (though this is not checked).
defaultValue?: JSONValue; // value to use if variable(s) unavailable.
censor?: boolean; // should the value of the setting be obscured when printed.
}
/**
* Result of a query specifying whether the setting
* was found, and if so in what environment variable, and using
* what query.
*/
export interface AppSettingQueryResult {
envVar?: string;
found: boolean;
query: AppSettingQuery;
}
/**
* Output of AppSettings.describe().
*/
interface AppSettingDescription {
name: string; // name of the setting.
value?: JSONValue; // value of the setting, if available.
foundInEnvVar?: string; // environment variable the setting was read from, if available.
wouldFindInEnvVar?: string; // environment variable that would be checked for the setting.
usedDefault: boolean; // whether a default value was used for the setting.
}
// Helper function to normalize the AppSettingQuery.envVar list.
function getEnvVarsFromQuery(q?: AppSettingQuery): string[] {
if (!q) { return []; }
return Array.isArray(q.envVar) ? q.envVar : [q.envVar];
}
// Keep app settings JSON-like, in case later we decide to load them from
// a JSON source.
type JSONValue = string | number | boolean | null | { [member: string]: JSONValue } | JSONValue[];