Skip to content

Commit

Permalink
feat(permission): support suffix wildcards in --allow-env flag (#25255
Browse files Browse the repository at this point in the history
)

This commit adds support for suffix wildcard for `--allow-env` flag.

Specifying flag like `--allow-env=DENO_*` will enable access to all
environmental variables starting with `DENO_*`.

Closes #24847

---------

Co-authored-by: Bartek Iwańczuk <[email protected]>
Co-authored-by: David Sherret <[email protected]>
  • Loading branch information
3 people authored Nov 20, 2024
1 parent 56f3162 commit b729bf0
Show file tree
Hide file tree
Showing 7 changed files with 250 additions and 20 deletions.
203 changes: 183 additions & 20 deletions runtime/permissions/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ impl UnitPermission {
/// A normalized environment variable name. On Windows this will
/// be uppercase and on other platforms it will stay as-is.
#[derive(Clone, Eq, PartialEq, Hash, Debug)]
struct EnvVarName {
pub struct EnvVarName {
inner: String,
}

Expand Down Expand Up @@ -1114,15 +1114,37 @@ impl ImportDescriptor {
pub struct EnvDescriptorParseError;

#[derive(Clone, Eq, PartialEq, Hash, Debug)]
pub struct EnvDescriptor(EnvVarName);
pub enum EnvDescriptor {
Name(EnvVarName),
PrefixPattern(EnvVarName),
}

impl EnvDescriptor {
pub fn new(env: impl AsRef<str>) -> Self {
Self(EnvVarName::new(env))
if let Some(prefix_pattern) = env.as_ref().strip_suffix('*') {
Self::PrefixPattern(EnvVarName::new(prefix_pattern))
} else {
Self::Name(EnvVarName::new(env))
}
}
}

#[derive(Clone, Eq, PartialEq, Hash, Debug)]
enum EnvQueryDescriptorInner {
Name(EnvVarName),
PrefixPattern(EnvVarName),
}

#[derive(Clone, Eq, PartialEq, Hash, Debug)]
pub struct EnvQueryDescriptor(EnvQueryDescriptorInner);

impl EnvQueryDescriptor {
pub fn new(env: impl AsRef<str>) -> Self {
Self(EnvQueryDescriptorInner::Name(EnvVarName::new(env)))
}
}

impl QueryDescriptor for EnvDescriptor {
impl QueryDescriptor for EnvQueryDescriptor {
type AllowDesc = EnvDescriptor;
type DenyDesc = EnvDescriptor;

Expand All @@ -1131,19 +1153,45 @@ impl QueryDescriptor for EnvDescriptor {
}

fn display_name(&self) -> Cow<str> {
Cow::from(self.0.as_ref())
Cow::from(match &self.0 {
EnvQueryDescriptorInner::Name(env_var_name) => env_var_name.as_ref(),
EnvQueryDescriptorInner::PrefixPattern(env_var_name) => {
env_var_name.as_ref()
}
})
}

fn from_allow(allow: &Self::AllowDesc) -> Self {
allow.clone()
match allow {
Self::AllowDesc::Name(s) => {
Self(EnvQueryDescriptorInner::Name(s.clone()))
}
Self::AllowDesc::PrefixPattern(s) => {
Self(EnvQueryDescriptorInner::PrefixPattern(s.clone()))
}
}
}

fn as_allow(&self) -> Option<Self::AllowDesc> {
Some(self.clone())
Some(match &self.0 {
EnvQueryDescriptorInner::Name(env_var_name) => {
Self::AllowDesc::Name(env_var_name.clone())
}
EnvQueryDescriptorInner::PrefixPattern(env_var_name) => {
Self::AllowDesc::PrefixPattern(env_var_name.clone())
}
})
}

fn as_deny(&self) -> Self::DenyDesc {
self.clone()
match &self.0 {
EnvQueryDescriptorInner::Name(env_var_name) => {
Self::DenyDesc::Name(env_var_name.clone())
}
EnvQueryDescriptorInner::PrefixPattern(env_var_name) => {
Self::DenyDesc::PrefixPattern(env_var_name.clone())
}
}
}

fn check_in_permission(
Expand All @@ -1156,29 +1204,94 @@ impl QueryDescriptor for EnvDescriptor {
}

fn matches_allow(&self, other: &Self::AllowDesc) -> bool {
self == other
match other {
Self::AllowDesc::Name(n) => match &self.0 {
EnvQueryDescriptorInner::Name(env_var_name) => n == env_var_name,
EnvQueryDescriptorInner::PrefixPattern(env_var_name) => {
env_var_name.as_ref().starts_with(n.as_ref())
}
},
Self::AllowDesc::PrefixPattern(p) => match &self.0 {
EnvQueryDescriptorInner::Name(env_var_name) => {
env_var_name.as_ref().starts_with(p.as_ref())
}
EnvQueryDescriptorInner::PrefixPattern(env_var_name) => {
env_var_name.as_ref().starts_with(p.as_ref())
}
},
}
}

fn matches_deny(&self, other: &Self::DenyDesc) -> bool {
self == other
match other {
Self::AllowDesc::Name(n) => match &self.0 {
EnvQueryDescriptorInner::Name(env_var_name) => n == env_var_name,
EnvQueryDescriptorInner::PrefixPattern(env_var_name) => {
env_var_name.as_ref().starts_with(n.as_ref())
}
},
Self::AllowDesc::PrefixPattern(p) => match &self.0 {
EnvQueryDescriptorInner::Name(env_var_name) => {
env_var_name.as_ref().starts_with(p.as_ref())
}
EnvQueryDescriptorInner::PrefixPattern(env_var_name) => {
p == env_var_name
}
},
}
}

fn revokes(&self, other: &Self::AllowDesc) -> bool {
self == other
match other {
Self::AllowDesc::Name(n) => match &self.0 {
EnvQueryDescriptorInner::Name(env_var_name) => n == env_var_name,
EnvQueryDescriptorInner::PrefixPattern(env_var_name) => {
env_var_name.as_ref().starts_with(n.as_ref())
}
},
Self::AllowDesc::PrefixPattern(p) => match &self.0 {
EnvQueryDescriptorInner::Name(env_var_name) => {
env_var_name.as_ref().starts_with(p.as_ref())
}
EnvQueryDescriptorInner::PrefixPattern(env_var_name) => {
p == env_var_name
}
},
}
}

fn stronger_than_deny(&self, other: &Self::DenyDesc) -> bool {
self == other
match other {
Self::AllowDesc::Name(n) => match &self.0 {
EnvQueryDescriptorInner::Name(env_var_name) => n == env_var_name,
EnvQueryDescriptorInner::PrefixPattern(env_var_name) => {
env_var_name.as_ref().starts_with(n.as_ref())
}
},
Self::AllowDesc::PrefixPattern(p) => match &self.0 {
EnvQueryDescriptorInner::Name(env_var_name) => {
env_var_name.as_ref().starts_with(p.as_ref())
}
EnvQueryDescriptorInner::PrefixPattern(env_var_name) => {
p == env_var_name
}
},
}
}

fn overlaps_deny(&self, _other: &Self::DenyDesc) -> bool {
false
}
}

impl AsRef<str> for EnvDescriptor {
impl AsRef<str> for EnvQueryDescriptor {
fn as_ref(&self) -> &str {
self.0.as_ref()
match &self.0 {
EnvQueryDescriptorInner::Name(env_var_name) => env_var_name.as_ref(),
EnvQueryDescriptorInner::PrefixPattern(env_var_name) => {
env_var_name.as_ref()
}
}
}
}

Expand Down Expand Up @@ -1749,20 +1862,20 @@ impl UnaryPermission<ImportDescriptor> {
}
}

impl UnaryPermission<EnvDescriptor> {
impl UnaryPermission<EnvQueryDescriptor> {
pub fn query(&self, env: Option<&str>) -> PermissionState {
self.query_desc(
env.map(EnvDescriptor::new).as_ref(),
env.map(EnvQueryDescriptor::new).as_ref(),
AllowPartial::TreatAsPartialGranted,
)
}

pub fn request(&mut self, env: Option<&str>) -> PermissionState {
self.request_desc(env.map(EnvDescriptor::new).as_ref())
self.request_desc(env.map(EnvQueryDescriptor::new).as_ref())
}

pub fn revoke(&mut self, env: Option<&str>) -> PermissionState {
self.revoke_desc(env.map(EnvDescriptor::new).as_ref())
self.revoke_desc(env.map(EnvQueryDescriptor::new).as_ref())
}

pub fn check(
Expand All @@ -1771,7 +1884,7 @@ impl UnaryPermission<EnvDescriptor> {
api_name: Option<&str>,
) -> Result<(), PermissionDeniedError> {
skip_check_if_is_permission_fully_granted!(self);
self.check_desc(Some(&EnvDescriptor::new(env)), false, api_name)
self.check_desc(Some(&EnvQueryDescriptor::new(env)), false, api_name)
}

pub fn check_all(&mut self) -> Result<(), PermissionDeniedError> {
Expand Down Expand Up @@ -1905,7 +2018,7 @@ pub struct Permissions {
pub read: UnaryPermission<ReadQueryDescriptor>,
pub write: UnaryPermission<WriteQueryDescriptor>,
pub net: UnaryPermission<NetDescriptor>,
pub env: UnaryPermission<EnvDescriptor>,
pub env: UnaryPermission<EnvQueryDescriptor>,
pub sys: UnaryPermission<SysDescriptor>,
pub run: UnaryPermission<RunQueryDescriptor>,
pub ffi: UnaryPermission<FfiQueryDescriptor>,
Expand Down Expand Up @@ -4564,6 +4677,56 @@ mod tests {
assert_eq!(perms.env.revoke(Some("HomE")), PermissionState::Prompt);
}

#[test]
fn test_env_wildcards() {
set_prompter(Box::new(TestPrompter));
let _prompt_value = PERMISSION_PROMPT_STUB_VALUE_SETTER.lock();
let mut perms = Permissions::allow_all();
perms.env = UnaryPermission {
granted_global: false,
..Permissions::new_unary(
Some(HashSet::from([EnvDescriptor::new("HOME_*")])),
None,
false,
)
};
assert_eq!(perms.env.query(Some("HOME")), PermissionState::Prompt);
assert_eq!(perms.env.query(Some("HOME_")), PermissionState::Granted);
assert_eq!(perms.env.query(Some("HOME_TEST")), PermissionState::Granted);

// assert no privilege escalation
let parser = TestPermissionDescriptorParser;
assert!(perms
.env
.create_child_permissions(
ChildUnaryPermissionArg::GrantedList(vec!["HOME_SUB".to_string()]),
|value| parser.parse_env_descriptor(value).map(Some),
)
.is_ok());
assert!(perms
.env
.create_child_permissions(
ChildUnaryPermissionArg::GrantedList(vec!["HOME*".to_string()]),
|value| parser.parse_env_descriptor(value).map(Some),
)
.is_err());
assert!(perms
.env
.create_child_permissions(
ChildUnaryPermissionArg::GrantedList(vec!["OUTSIDE".to_string()]),
|value| parser.parse_env_descriptor(value).map(Some),
)
.is_err());
assert!(perms
.env
.create_child_permissions(
// ok because this is a subset of HOME_*
ChildUnaryPermissionArg::GrantedList(vec!["HOME_S*".to_string()]),
|value| parser.parse_env_descriptor(value).map(Some),
)
.is_ok());
}

#[test]
fn test_check_partial_denied() {
let parser = TestPermissionDescriptorParser;
Expand Down
26 changes: 26 additions & 0 deletions tests/specs/permission/process_env_permissions/__test__.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"tempDir": true,
"tests": {
"deno_env_wildcard_tests": {
"envs": {
"MYAPP_HELLO": "Hello\tworld,",
"MYAPP_GOODBYE": "farewell",
"OTHER_VAR": "ignore"
},
"steps": [
{
"args": "run --allow-env=MYAPP_* main.js",
"output": "Hello\tworld,\nfarewell\ndone\n"
},
{
"args": "run --allow-env main.js",
"output": "Hello\tworld,\nfarewell\ndone\n"
},
{
"args": "run --allow-env=MYAPP_HELLO,MYAPP_GOODBYE,MYAPP_TEST,MYAPP_DONE main.js",
"output": "Hello\tworld,\nfarewell\ndone\n"
}
]
}
}
}
5 changes: 5 additions & 0 deletions tests/specs/permission/process_env_permissions/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
console.log(Deno.env.get("MYAPP_HELLO"));
console.log(Deno.env.get("MYAPP_GOODBYE"));
Deno.env.set("MYAPP_TEST", "done");
Deno.env.set("MYAPP_DONE", "done");
console.log(Deno.env.get("MYAPP_DONE"));
10 changes: 10 additions & 0 deletions tests/specs/run/allow_env_wildcard_worker/__test__.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"envs": {
"DENO_HELLO": "hello",
"DENO_BYE": "bye",
"AWS_HELLO": "aws"
},
"args": "run --allow-env --allow-read --unstable-worker-options main.js",
"output": "main.out",
"exitCode": 1
}
12 changes: 12 additions & 0 deletions tests/specs/run/allow_env_wildcard_worker/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
console.log("main1", Deno.env.get("DENO_HELLO"));
console.log("main2", Deno.env.get("DENO_BYE"));
console.log("main3", Deno.env.get("AWS_HELLO"));

new Worker(import.meta.resolve("./worker.js"), {
type: "module",
deno: {
permissions: {
env: ["DENO_*"],
},
},
});
11 changes: 11 additions & 0 deletions tests/specs/run/allow_env_wildcard_worker/main.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
main1 hello
main2 bye
main3 aws
worker1 hello
worker2 bye
error: Uncaught (in worker "") (in promise) NotCapable: Requires env access to "AWS_HELLO", run again with the --allow-env flag
console.log("worker3", Deno.env.get("AWS_HELLO"));
^
[WILDCARD]
error: Uncaught (in promise) Error: Unhandled error in child worker.
[WILDCARD]
3 changes: 3 additions & 0 deletions tests/specs/run/allow_env_wildcard_worker/worker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
console.log("worker1", Deno.env.get("DENO_HELLO"));
console.log("worker2", Deno.env.get("DENO_BYE"));
console.log("worker3", Deno.env.get("AWS_HELLO"));

0 comments on commit b729bf0

Please sign in to comment.