Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BREAKING(dotenv): Fix dot env permissions #3578

Merged
merged 13 commits into from
Aug 29, 2023
Merged
168 changes: 62 additions & 106 deletions dotenv/mod.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
/**
* Load environment variables from a `.env` file.
* Load environment variables from a `.env` file. Loaded variables are accessible
* in a configuration object returned by the `load()` function, as well as optionally
* exporting them to the process environment using the `export` option.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for adding these lines.

*
* Inspired by the node modules [`dotenv`](https://github.com/motdotla/dotenv)
* and [`dotenv-expand`](https://github.com/motdotla/dotenv-expand).
Expand Down Expand Up @@ -37,19 +39,23 @@
* console.log(Deno.env.get("GREETING")); // hello world
* ```
*
* Run this with `deno run --allow-read --allow-env app.ts`.
*
* ## Files
* Dotenv supports a number of different files, all of which are optional.
* File names and paths are configurable.
*
* |File|Purpose|
* |----|-------|
* |.env|primary file for storing key-value environment entries
* |.env.example|this file does not set any values, but specifies env variables which must be present in the environment after loading dotenv
* |.env.example|this file does not set any values, but specifies env variables which must be present in the configuration object or process environment after loading dotenv
* |.env.defaults|specify default values for env variables to be used when there is no entry in the `.env` file
*
* ### Example file
*
* The purpose of the example file is to provide a list of environment
* variables which must be set or an exception will be thrown. These
* variables which must be set or already present in the process environment
* or an exception will be thrown. These
* variables may be set externally or loaded via the `.env` or
* `.env.defaults` files. A description may also be provided to help
* understand the purpose of the env variable. The values in this file
Expand Down Expand Up @@ -97,12 +103,11 @@
*
* |Option|Default|Description
* |------|-------|-----------
* |envPath|./.env|Path and filename of the `.env` file
* |defaultsPath|./.env.defaults|Path and filename of the `.env.defaults` file
* |examplePath|./.env.example|Path and filename of the `.env.example` file
* |export|false|This will export all environment variables in the `.env` and `.env.default` files to the process environment (e.g. for use by `Deno.env.get()`) but only if they are not already set. If a variable is already in the process, the `.env` value is ignored.
* |envPath|./.env|Path and filename of the `.env` file. Use null to prevent the .env file from being loaded.
* |defaultsPath|./.env.defaults|Path and filename of the `.env.defaults` file. Use null to prevent the .env.defaults file from being loaded.
* |examplePath|./.env.example|Path and filename of the `.env.example` file. Use null to prevent the .env.example file from being loaded.
* |export|false|When true, this will export all environment variables in the `.env` and `.env.default` files to the process environment (e.g. for use by `Deno.env.get()`) but only if they are not already set. If a variable is already in the process, the `.env` value is ignored.
* |allowEmptyValues|false|Allows empty values for specified env variables (throws otherwise)
* |restrictEnvAccessTo||Restrict which process environment variables are accessible in the `.env` or `.env.default` files
*
* ### Example configuration
* ```ts
Expand All @@ -116,6 +121,16 @@
* });
* ```
*
* ## Permissions
*
* At a minimum, loading the `.env` related files requires the `--allow-read` permission. Additionally, if
* you access the process environment, either through exporting your configuration or expanding variables
* in your `.env` file, you will need the `--allow-env` permission. E.g.
*
* ```sh
* deno run --allow-read=.env,.env.defaults,.env.example --allow-env=ENV1,ENV2 app.ts
* ```
*
* ## Parsing Rules
*
* The parsing engine currently supports the following rules:
Expand Down Expand Up @@ -159,21 +174,6 @@
* @module
*/

import { filterValues } from "../collections/filter_values.ts";
import { withoutAll } from "../collections/without_all.ts";

type StrictDotenvConfig<T extends ReadonlyArray<string>> =
& {
[key in T[number]]: string;
}
& Record<string, string>;

type StrictEnvVarList<T extends string> =
| Array<Extract<T, string>>
| ReadonlyArray<Extract<T, string>>;

type StringList = Array<string> | ReadonlyArray<string> | undefined;

export interface LoadOptions {
/**
* Optional path to `.env` file. To prevent the default value from being
Expand Down Expand Up @@ -223,11 +223,12 @@ export interface LoadOptions {
defaultsPath?: string | null;

/**
* List of Env variables to read from process. By default, the complete Env is
* looked up. This allows to permit access to only specific Env variables with
* `--allow-env=ENV_VAR_NAME`.
* @deprecated (will be removed in 0.205.0) This option has no effect now.
*
* This option has no effect now.
* See https://github.com/denoland/deno_std/pull/3578 for details.
*/
restrictEnvAccessTo?: StringList;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's keep this option only in typing in a few versions to prevent immediately breaking users code (in type checks)

restrictEnvAccessTo?: Array<string> | ReadonlyArray<string> | undefined;
}

type LineParseResult = {
Expand All @@ -245,10 +246,7 @@ const RE_KeyValue =
const RE_ExpandValue =
/(\${(?<inBrackets>.+?)(\:-(?<inBracketsDefault>.+))?}|(?<!\\)\$(?<notInBrackets>\w+)(\:-(?<notInBracketsDefault>.+))?)/g;

export function parse(
rawDotenv: string,
restrictEnvAccessTo?: StringList,
): Record<string, string> {
export function parse(rawDotenv: string): Record<string, string> {
const env: Record<string, string> = {};

let match;
Expand All @@ -270,36 +268,27 @@ export function parse(
}

//https://github.com/motdotla/dotenv-expand/blob/ed5fea5bf517a09fd743ce2c63150e88c8a5f6d1/lib/main.js#L23
const variablesMap = { ...env, ...readEnv(restrictEnvAccessTo) };
const variablesMap = { ...env };
keysForExpandCheck.forEach((key) => {
env[key] = expand(env[key], variablesMap);
});

return env;
}

export function loadSync(
options?: Omit<LoadOptions, "restrictEnvAccessTo">,
): Record<string, string>;
export function loadSync<TEnvVar extends string>(
options: Omit<LoadOptions, "restrictEnvAccessTo"> & {
restrictEnvAccessTo: StrictEnvVarList<TEnvVar>;
},
): StrictDotenvConfig<StrictEnvVarList<TEnvVar>>;
export function loadSync(
{
envPath = ".env",
examplePath = ".env.example",
defaultsPath = ".env.defaults",
export: _export = false,
allowEmptyValues = false,
restrictEnvAccessTo,
}: LoadOptions = {},
): Record<string, string> {
const conf = envPath ? parseFileSync(envPath, restrictEnvAccessTo) : {};
const conf = envPath ? parseFileSync(envPath) : {};

if (defaultsPath) {
const confDefaults = parseFileSync(defaultsPath, restrictEnvAccessTo);
const confDefaults = parseFileSync(defaultsPath);
for (const key in confDefaults) {
if (!(key in conf)) {
conf[key] = confDefaults[key];
Expand All @@ -308,8 +297,8 @@ export function loadSync(
}

if (examplePath) {
const confExample = parseFileSync(examplePath, restrictEnvAccessTo);
assertSafe(conf, confExample, allowEmptyValues, restrictEnvAccessTo);
const confExample = parseFileSync(examplePath);
assertSafe(conf, confExample, allowEmptyValues);
}

if (_export) {
Expand All @@ -322,31 +311,19 @@ export function loadSync(
return conf;
}

export function load(
options?: Omit<LoadOptions, "restrictEnvAccessTo">,
): Promise<Record<string, string>>;
export function load<TEnvVar extends string>(
options: Omit<LoadOptions, "restrictEnvAccessTo"> & {
restrictEnvAccessTo: StrictEnvVarList<TEnvVar>;
},
): Promise<StrictDotenvConfig<StrictEnvVarList<TEnvVar>>>;
export async function load(
{
envPath = ".env",
examplePath = ".env.example",
defaultsPath = ".env.defaults",
export: _export = false,
allowEmptyValues = false,
restrictEnvAccessTo,
}: LoadOptions = {},
): Promise<Record<string, string>> {
const conf = envPath ? await parseFile(envPath, restrictEnvAccessTo) : {};
const conf = envPath ? await parseFile(envPath) : {};

if (defaultsPath) {
const confDefaults = await parseFile(
defaultsPath,
restrictEnvAccessTo,
);
const confDefaults = await parseFile(defaultsPath);
for (const key in confDefaults) {
if (!(key in conf)) {
conf[key] = confDefaults[key];
Expand All @@ -355,11 +332,8 @@ export async function load(
}

if (examplePath) {
const confExample = await parseFile(
examplePath,
restrictEnvAccessTo,
);
assertSafe(conf, confExample, allowEmptyValues, restrictEnvAccessTo);
const confExample = await parseFile(examplePath);
assertSafe(conf, confExample, allowEmptyValues);
}

if (_export) {
Expand All @@ -374,10 +348,9 @@ export async function load(

function parseFileSync(
filepath: string,
restrictEnvAccessTo?: StringList,
): Record<string, string> {
try {
return parse(Deno.readTextFileSync(filepath), restrictEnvAccessTo);
return parse(Deno.readTextFileSync(filepath));
} catch (e) {
if (e instanceof Deno.errors.NotFound) return {};
throw e;
Expand All @@ -386,10 +359,9 @@ function parseFileSync(

async function parseFile(
filepath: string,
restrictEnvAccessTo?: StringList,
): Promise<Record<string, string>> {
try {
return parse(await Deno.readTextFile(filepath), restrictEnvAccessTo);
return parse(await Deno.readTextFile(filepath));
} catch (e) {
if (e instanceof Deno.errors.NotFound) return {};
throw e;
Expand All @@ -413,25 +385,27 @@ function assertSafe(
conf: Record<string, string>,
confExample: Record<string, string>,
allowEmptyValues: boolean,
restrictEnvAccessTo?: StringList,
) {
const currentEnv = readEnv(restrictEnvAccessTo);
const missingEnvVars: string[] = [];

// Not all the variables have to be defined in .env, they can be supplied externally
const confWithEnv = Object.assign({}, currentEnv, conf);

const missing = withoutAll(
Object.keys(confExample),
// If allowEmptyValues is false, filter out empty values from configuration
Object.keys(
allowEmptyValues ? confWithEnv : filterValues(confWithEnv, Boolean),
),
);
for (const key in confExample) {
if (key in conf) {
if (!allowEmptyValues && conf[key] === "") {
missingEnvVars.push(key);
}
} else if (Deno.env.get(key) !== undefined) {
if (!allowEmptyValues && Deno.env.get(key) === "") {
missingEnvVars.push(key);
}
} else {
missingEnvVars.push(key);
}
}

if (missing.length > 0) {
if (missingEnvVars.length > 0) {
const errorMessages = [
`The following variables were defined in the example file but are not present in the environment:\n ${
missing.join(
missingEnvVars.join(
", ",
)
}`,
Expand All @@ -442,32 +416,11 @@ function assertSafe(

throw new MissingEnvVarsError(
errorMessages.filter(Boolean).join("\n\n"),
missing,
missingEnvVars,
);
}
}

// a guarded env access, that reads only a subset from the Deno.env object,
// if `restrictEnvAccessTo` property is passed.
function readEnv(restrictEnvAccessTo: StringList) {
if (restrictEnvAccessTo && Array.isArray(restrictEnvAccessTo)) {
return restrictEnvAccessTo.reduce(
(
accessedEnvVars: Record<string, string>,
envVarName: string,
): Record<string, string> => {
if (Deno.env.get(envVarName)) {
accessedEnvVars[envVarName] = Deno.env.get(envVarName) as string;
}
return accessedEnvVars;
},
{},
);
}

return Deno.env.toObject();
}

export class MissingEnvVarsError extends Error {
missing: string[];
constructor(message: string, missing: string[]) {
Expand All @@ -491,8 +444,11 @@ function expand(str: string, variablesMap: { [key: string]: string }): string {
const expandValue = inBrackets || notInBrackets;
const defaultValue = inBracketsDefault || notInBracketsDefault;

return variablesMap[expandValue] ||
expand(defaultValue, variablesMap);
let value: string | undefined = variablesMap[expandValue];
if (value === undefined) {
value = Deno.env.get(expandValue);
}
return value === undefined ? expand(defaultValue, variablesMap) : value;
}),
variablesMap,
);
Expand Down
Loading