-
Notifications
You must be signed in to change notification settings - Fork 589
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
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
ac9d45c
BREAKING: remove restrictEnvAccessTo option
cknight 64abb01
BREAKING: remove restrictEnvAccessTo option
cknight 271810c
chore: lint fix
cknight bfc157d
Merge branch 'main' of https://github.com/denoland/deno_std into fixD…
cknight 65cf9d2
chore: submodule update
cknight 995026c
Merge branch 'main' of https://github.com/denoland/deno_std into fixD…
cknight d1ed76c
Merge branch 'main' into fixDotEnvPermissions
kt3k 7988924
bump CI
cknight 6cba8f1
Merge branch 'fixDotEnvPermissions' of https://github.com/cknight/den…
cknight bf14fcc
test: fix incorrect import
cknight 8ede2a9
doc: small improvements
cknight b26e10a
bump CI
cknight a0b3a9f
keep the option for a while
kt3k File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. | ||
* | ||
* Inspired by the node modules [`dotenv`](https://github.com/motdotla/dotenv) | ||
* and [`dotenv-expand`](https://github.com/motdotla/dotenv-expand). | ||
|
@@ -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 | ||
|
@@ -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 | ||
|
@@ -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: | ||
|
@@ -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 | ||
|
@@ -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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 = { | ||
|
@@ -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; | ||
|
@@ -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]; | ||
|
@@ -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) { | ||
|
@@ -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]; | ||
|
@@ -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) { | ||
|
@@ -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; | ||
|
@@ -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; | ||
|
@@ -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( | ||
", ", | ||
) | ||
}`, | ||
|
@@ -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[]) { | ||
|
@@ -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, | ||
); | ||
|
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.