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

feat(cli/config): Add permissions support #12767

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions cli/config_file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,7 @@ pub struct ConfigFileJson {
pub compiler_options: Option<Value>,
pub lint: Option<Value>,
pub fmt: Option<Value>,
pub permissions: Option<Value>,
}

#[derive(Clone, Debug)]
Expand Down Expand Up @@ -410,6 +411,20 @@ impl ConfigFile {
}
}

pub fn to_permissions_options(
&self,
) -> Result<Option<deno_runtime::permissions::PermissionsOptions>, AnyError>
{
if let Some(config) = self.json.permissions.clone() {
let permissions: deno_runtime::permissions::PermissionsOptions =
serde_json::from_value(config)
.context("Failed to parse \"permissions\" configuration")?;
Ok(Some(permissions))
} else {
Ok(None)
}
}

/// If the configuration file contains "extra" modules (like TypeScript
/// `"types"`) options, return them as imports to be added to a module graph.
pub fn to_maybe_imports(&self) -> MaybeImportsResult {
Expand Down
16 changes: 12 additions & 4 deletions cli/flags.rs
Original file line number Diff line number Diff line change
Expand Up @@ -338,9 +338,17 @@ impl Flags {
}
}

impl From<Flags> for PermissionsOptions {
fn from(flags: Flags) -> Self {
Self {
impl TryFrom<Flags> for PermissionsOptions {
type Error = deno_core::error::AnyError;

fn try_from(flags: Flags) -> Result<Self, Self::Error> {
let config = if let Some(path) = flags.config_path.as_ref() {
super::config_file::ConfigFile::read(path)?.to_permissions_options()?
} else {
None
};

Ok(config.unwrap_or(Self {
allow_env: flags.allow_env,
allow_hrtime: flags.allow_hrtime,
allow_net: flags.allow_net,
Expand All @@ -349,7 +357,7 @@ impl From<Flags> for PermissionsOptions {
allow_run: flags.allow_run,
allow_write: flags.allow_write,
prompt: flags.prompt,
}
}))
}
}

Expand Down
13 changes: 7 additions & 6 deletions cli/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,8 @@ async fn install_command(
let mut preload_flags = flags.clone();
preload_flags.inspect = None;
preload_flags.inspect_brk = None;
let permissions = Permissions::from_options(&preload_flags.clone().into());
let permissions =
Permissions::from_options(&preload_flags.clone().try_into()?);
let ps = ProcState::build(preload_flags).await?;
let main_module = resolve_url_or_path(&install_flags.module_url)?;
let mut worker =
Expand Down Expand Up @@ -600,7 +601,7 @@ async fn eval_command(
// type, and so our "fake" specifier needs to have the proper extension.
let main_module =
resolve_url_or_path(&format!("./$deno$eval.{}", eval_flags.ext)).unwrap();
let permissions = Permissions::from_options(&flags.clone().into());
let permissions = Permissions::from_options(&flags.clone().try_into()?);
let ps = ProcState::build(flags.clone()).await?;
let mut worker =
create_main_worker(&ps, main_module.clone(), permissions, None);
Expand Down Expand Up @@ -931,7 +932,7 @@ async fn format_command(

async fn run_repl(flags: Flags, repl_flags: ReplFlags) -> Result<(), AnyError> {
let main_module = resolve_url_or_path("./$deno$repl.ts").unwrap();
let permissions = Permissions::from_options(&flags.clone().into());
let permissions = Permissions::from_options(&flags.clone().try_into()?);
let ps = ProcState::build(flags.clone()).await?;
let mut worker =
create_main_worker(&ps, main_module.clone(), permissions, None);
Expand All @@ -945,7 +946,7 @@ async fn run_repl(flags: Flags, repl_flags: ReplFlags) -> Result<(), AnyError> {

async fn run_from_stdin(flags: Flags) -> Result<(), AnyError> {
let ps = ProcState::build(flags.clone()).await?;
let permissions = Permissions::from_options(&flags.clone().into());
let permissions = Permissions::from_options(&flags.clone().try_into()?);
let main_module = resolve_url_or_path("./$deno$stdin.ts").unwrap();
let mut worker =
create_main_worker(&ps.clone(), main_module.clone(), permissions, None);
Expand Down Expand Up @@ -1128,8 +1129,8 @@ async fn run_with_watch(flags: Flags, script: String) -> Result<(), AnyError> {

let operation = |(ps, main_module): (ProcState, ModuleSpecifier)| {
let flags = flags.clone();
let permissions = Permissions::from_options(&flags.clone().into());
async move {
let permissions = Permissions::from_options(&flags.clone().try_into()?);
// We make use an module executor guard to ensure that unload is always fired when an
// operation is called.
let mut executor = FileWatcherModuleExecutor::new(
Expand Down Expand Up @@ -1166,7 +1167,7 @@ async fn run_command(
// probably call `ProcState::resolve` instead
let main_module = resolve_url_or_path(&run_flags.script)?;
let ps = ProcState::build(flags.clone()).await?;
let permissions = Permissions::from_options(&flags.clone().into());
let permissions = Permissions::from_options(&flags.clone().try_into()?);
let mut worker =
create_main_worker(&ps, main_module.clone(), permissions, None);

Expand Down
71 changes: 71 additions & 0 deletions cli/schemas/config-file.v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,77 @@
}
}
}
},
"permissions": {
"description": "permissions configuration",
"type": ["boolean", "object"],
"default": false,
"properties": {
"env": {
"description": "Allow environment access",
"type": ["boolean", "array"],
"items": {
"type": "string"
},
"uniqueItems": true,
"default": false
},
"ffi": {
"description": "Allow loading dynamic libraries",
"type": ["boolean", "array"],
"items": {
"type": "string"
},
"uniqueItems": true,
"default": false
},
"hr-time": {
"description": "Allow high resolution time measurement",
"type": "boolean",
"default": false
},
"net": {
"description": "Allow network access",
"type": ["boolean", "array"],
"items": {
"type": "string"
},
"uniqueItems": true,
"default": false
},
"read": {
"description": "Allow file system read access",
"type": ["boolean", "array"],
"items": {
"type": "string"
},
"uniqueItems": true,
"default": false
},
"run": {
"description": "Allow running subprocesses",
"type": ["boolean", "array"],
"items": {
"type": "string"
},
"uniqueItems": true,
"default": false
},
"write": {
"description": "Allow file system write access",
"type": ["boolean", "array"],
"items": {
"type": "string"
},
"uniqueItems": true,
"default": false
},
"prompt": {
"description": "Fallback to prompt if required permission wasn't passed",
"type": "boolean",
"default": false
}
}
}
}
}
2 changes: 1 addition & 1 deletion cli/tools/standalone.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ pub fn create_standalone_binary(
unstable: flags.unstable,
seed: flags.seed,
location: flags.location.clone(),
permissions: flags.clone().into(),
permissions: flags.clone().try_into()?,
v8_flags: flags.v8_flags.clone(),
unsafely_ignore_certificate_errors: flags
.unsafely_ignore_certificate_errors
Expand Down
4 changes: 2 additions & 2 deletions cli/tools/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -974,7 +974,7 @@ pub async fn run_tests(
concurrent_jobs: NonZeroUsize,
) -> Result<(), AnyError> {
let ps = ProcState::build(flags.clone()).await?;
let permissions = Permissions::from_options(&flags.clone().into());
let permissions = Permissions::from_options(&flags.clone().try_into()?);
let specifiers_with_mode = fetch_specifiers_with_test_mode(
ps.clone(),
include.unwrap_or_else(|| vec![".".to_string()]),
Expand Down Expand Up @@ -1032,7 +1032,7 @@ pub async fn run_tests_with_watch(
concurrent_jobs: NonZeroUsize,
) -> Result<(), AnyError> {
let ps = ProcState::build(flags.clone()).await?;
let permissions = Permissions::from_options(&flags.clone().into());
let permissions = Permissions::from_options(&flags.clone().try_into()?);

let lib = if flags.unstable {
emit::TypeLib::UnstableDenoWindow
Expand Down
152 changes: 151 additions & 1 deletion runtime/permissions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1114,7 +1114,7 @@ impl Default for Permissions {
}
}

#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
#[derive(Clone, Debug, PartialEq, Default, Serialize)]
pub struct PermissionsOptions {
pub allow_env: Option<Vec<String>>,
pub allow_hrtime: bool,
Expand All @@ -1126,6 +1126,156 @@ pub struct PermissionsOptions {
pub prompt: bool,
}

impl<'de> Deserialize<'de> for PermissionsOptions {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct UnaryPermissionsOptions(Option<Vec<String>>);
impl<'de> Deserialize<'de> for UnaryPermissionsOptions {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct UnaryPermissionsOptionsVisitor;
impl<'de> de::Visitor<'de> for UnaryPermissionsOptionsVisitor {
type Value = Option<Vec<String>>;

fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("boolean or string[]")
}

fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
where
E: de::Error,
{
if v {
Ok(Some(vec![]))
} else {
Ok(None)
}
}

fn visit_seq<V>(self, mut v: V) -> Result<Self::Value, V::Error>
where
V: de::SeqAccess<'de>,
{
let mut granted_list = vec![];
while let Some(value) = v.next_element::<String>()? {
granted_list.push(value);
}
Ok(Some(granted_list))
}
Comment on lines +1159 to +1168
Copy link
Member Author

@crowlKats crowlKats Nov 15, 2021

Choose a reason for hiding this comment

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

should encountering an empty seq return None? because

"permissions": {
    "net": [],
  }

is the same as "net": true, though an empty array seems more like "net": false behaviour would be expected

}

deserializer
.deserialize_any(UnaryPermissionsOptionsVisitor)
.map(UnaryPermissionsOptions)
}
}

struct PermissionsOptionsVisitor;
impl<'de> de::Visitor<'de> for PermissionsOptionsVisitor {
type Value = PermissionsOptions;

fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("boolean or object")
}

fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
where
E: de::Error,
{
if v {
Ok(PermissionsOptions {
allow_env: Some(vec![]),
allow_hrtime: true,
allow_net: Some(vec![]),
allow_ffi: Some(vec![]),
allow_read: Some(vec![]),
allow_run: Some(vec![]),
allow_write: Some(vec![]),
prompt: false,
})
} else {
Ok(PermissionsOptions::default())
}
}

fn visit_map<V>(self, mut v: V) -> Result<Self::Value, V::Error>
where
V: de::MapAccess<'de>,
{
let mut permissions = PermissionsOptions::default();
while let Some((key, value)) =
v.next_entry::<String, serde_json::Value>()?
{
if key == "env" {
let arg = UnaryPermissionsOptions::deserialize(value);
permissions.allow_env = arg
.map_err(|e| {
de::Error::custom(format!("(deno.permissions.env) {}", e))
})?
.0;
} else if key == "hrtime" {
let arg = serde_json::from_value::<bool>(value);
permissions.allow_hrtime = arg.map_err(|e| {
de::Error::custom(format!("(deno.permissions.hrtime) {}", e))
})?;
} else if key == "net" {
let arg = UnaryPermissionsOptions::deserialize(value);
permissions.allow_net = arg
.map_err(|e| {
de::Error::custom(format!("(deno.permissions.net) {}", e))
})?
.0;
} else if key == "ffi" {
let arg = UnaryPermissionsOptions::deserialize(value);
permissions.allow_ffi = arg
.map_err(|e| {
de::Error::custom(format!("(deno.permissions.ffi) {}", e))
})?
.0
.map(|v| v.into_iter().map(PathBuf::from).collect());
} else if key == "read" {
let arg = UnaryPermissionsOptions::deserialize(value);
permissions.allow_read = arg
.map_err(|e| {
de::Error::custom(format!("(deno.permissions.read) {}", e))
})?
.0
.map(|v| v.into_iter().map(PathBuf::from).collect());
} else if key == "run" {
let arg = UnaryPermissionsOptions::deserialize(value);
permissions.allow_run = arg
.map_err(|e| {
de::Error::custom(format!("(deno.permissions.run) {}", e))
})?
.0;
} else if key == "write" {
let arg = UnaryPermissionsOptions::deserialize(value);
permissions.allow_write = arg
.map_err(|e| {
de::Error::custom(format!("(deno.permissions.write) {}", e))
})?
.0
.map(|v| v.into_iter().map(PathBuf::from).collect());
} else if key == "prompt" {
let arg = serde_json::from_value::<bool>(value);
permissions.prompt = arg.map_err(|e| {
de::Error::custom(format!("(deno.permissions.prompt) {}", e))
})?;
} else {
return Err(de::Error::custom("unknown permission name"));
}
}
Ok(permissions)
}
}
deserializer.deserialize_any(PermissionsOptionsVisitor)
}
}

impl Permissions {
pub fn new_read(
state: &Option<Vec<PathBuf>>,
Expand Down