0
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2025-03-03 17:34:47 -05:00

Ergonomics: Prompt TTY for permission escalation (#1081)

This commit is contained in:
Ryan Dahl 2018-10-27 06:11:39 -07:00 committed by GitHub
parent 7f204b9803
commit 6adc87e3eb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 353 additions and 60 deletions

View file

@ -44,24 +44,25 @@ config("deno_config") {
}
main_extern = [
"$rust_build:atty",
"$rust_build:dirs",
"$rust_build:futures",
"$rust_build:getopts",
"$rust_build:hyper",
"$rust_build:hyper_rustls",
"$rust_build:futures",
"$rust_build:lazy_static",
"$rust_build:libc",
"$rust_build:log",
"$rust_build:rand",
"$rust_build:remove_dir_all",
"$rust_build:ring",
"$rust_build:tempfile",
"$rust_build:rand",
"$rust_build:tokio",
"$rust_build:tokio_io",
"$rust_build:tokio_fs",
"$rust_build:tokio_executor",
"$rust_build:tokio_fs",
"$rust_build:tokio_io",
"$rust_build:tokio_threadpool",
"$rust_build:url",
"$rust_build:remove_dir_all",
"$rust_build:dirs",
"$rust_build:getopts",
"//build_extra/flatbuffers/rust:flatbuffers",
":msg_rs",
]

View file

@ -9,6 +9,7 @@ name = "deno"
version = "0.0.0"
[dependencies]
atty = "0.2.11"
dirs = "1.0.4"
flatbuffers = { path = "third_party/flatbuffers/rust/flatbuffers/" }
futures = "0.1.25"

View file

@ -113,14 +113,15 @@ rust_crate("winapi") {
"basetsd",
"cfg",
"cfgmgr32",
"consoleapi",
"combaseapi",
"errhandlingapi",
"excpt",
"fileapi",
"guiddef",
"handleapi",
"inaddr",
"in6addr",
"inaddr",
"knownfolders",
"ktmtypes",
"libloaderapi",
@ -134,6 +135,7 @@ rust_crate("winapi") {
"objbase",
"objidl",
"objidlbase",
"processenv",
"processthreadsapi",
"profileapi",
"propidl",
@ -152,8 +154,10 @@ rust_crate("winapi") {
"vadefs",
"vcruntime",
"winbase",
"wincon",
"wincred",
"windef",
"wingdi",
"winerror",
"winnt",
"winreg",
@ -865,6 +869,14 @@ rust_crate("sct") {
]
}
rust_crate("atty") {
source_root = "$registry_github/atty-0.2.11/src/lib.rs"
extern = [
":libc",
":winapi",
]
}
rust_crate("base64") {
source_root = "$registry_github/base64-0.9.2/src/lib.rs"
extern = [

View file

@ -6,8 +6,10 @@
use deno_dir;
use errors::DenoError;
use errors::DenoResult;
use flags;
use libdeno;
use permissions::DenoPermissions;
use snapshot;
use futures::Future;
@ -56,6 +58,7 @@ pub struct Isolate {
pub struct IsolateState {
pub dir: deno_dir::DenoDir,
pub argv: Vec<String>,
pub permissions: Mutex<DenoPermissions>,
pub flags: flags::DenoFlags,
tx: Mutex<Option<mpsc::Sender<(i32, Buf)>>>,
pub metrics: Mutex<Metrics>,
@ -71,6 +74,21 @@ impl IsolateState {
tx.send((req_id, buf)).expect("tx.send error");
}
pub fn check_write(&self, filename: &str) -> DenoResult<()> {
let mut perm = self.permissions.lock().unwrap();
perm.check_write(filename)
}
pub fn check_env(&self) -> DenoResult<()> {
let mut perm = self.permissions.lock().unwrap();
perm.check_env()
}
pub fn check_net(&self, filename: &str) -> DenoResult<()> {
let mut perm = self.permissions.lock().unwrap();
perm.check_net(filename)
}
fn metrics_op_dispatched(
&self,
bytes_sent_control: u64,
@ -143,6 +161,7 @@ impl Isolate {
state: Arc::new(IsolateState {
dir: deno_dir::DenoDir::new(flags.reload, custom_root).unwrap(),
argv: argv_rest,
permissions: Mutex::new(DenoPermissions::new(&flags)),
flags,
tx: Mutex::new(Some(tx)),
metrics: Mutex::new(Metrics::default()),

View file

@ -31,6 +31,7 @@ mod http_util;
mod isolate;
mod libdeno;
pub mod ops;
mod permissions;
mod resources;
mod snapshot;
mod tokio_util;

View file

@ -1,6 +1,5 @@
// Copyright 2018 the Deno authors. All rights reserved. MIT license.
use errors;
use errors::permission_denied;
use errors::{DenoError, DenoResult, ErrorKind};
use fs as deno_fs;
use http_util;
@ -333,11 +332,9 @@ fn op_set_env(
let inner = base.inner_as_set_env().unwrap();
let key = inner.key().unwrap();
let value = inner.value().unwrap();
if !state.flags.allow_env {
return odd_future(permission_denied());
if let Err(e) = state.check_env() {
return odd_future(e);
}
std::env::set_var(key, value);
ok_future(empty_buf())
}
@ -350,8 +347,8 @@ fn op_env(
assert_eq!(data.len(), 0);
let cmd_id = base.cmd_id();
if !state.flags.allow_env {
return odd_future(permission_denied());
if let Err(e) = state.check_env() {
return odd_future(e);
}
let builder = &mut FlatBufferBuilder::new();
@ -399,8 +396,9 @@ fn op_fetch_req(
let id = inner.id();
let url = inner.url().unwrap();
if !state.flags.allow_net {
return odd_future(permission_denied());
// FIXME use domain (or use this inside check_net)
if let Err(e) = state.check_net(url) {
return odd_future(e);
}
let url = url.parse::<hyper::Uri>().unwrap();
@ -513,8 +511,9 @@ fn op_make_temp_dir(
let inner = base.inner_as_make_temp_dir().unwrap();
let cmd_id = base.cmd_id();
if !state.flags.allow_write {
return odd_future(permission_denied());
// FIXME
if let Err(e) = state.check_write("make_temp") {
return odd_future(e);
}
let dir = inner.dir().map(PathBuf::from);
@ -562,10 +561,9 @@ fn op_mkdir(
let mode = inner.mode();
let path = String::from(inner.path().unwrap());
if !state.flags.allow_write {
return odd_future(permission_denied());
if let Err(e) = state.check_write(&path) {
return odd_future(e);
}
blocking!(base.sync(), || {
debug!("op_mkdir {}", path);
deno_fs::mkdir(Path::new(&path), mode)?;
@ -583,8 +581,8 @@ fn op_chmod(
let _mode = inner.mode();
let path = String::from(inner.path().unwrap());
if !state.flags.allow_write {
return odd_future(permission_denied());
if let Err(e) = state.check_write(&path) {
return odd_future(e);
}
blocking!(base.sync(), || {
@ -766,11 +764,14 @@ fn op_remove(
) -> Box<Op> {
assert_eq!(data.len(), 0);
let inner = base.inner_as_remove().unwrap();
let path = PathBuf::from(inner.path().unwrap());
let path_ = inner.path().unwrap();
let path = PathBuf::from(path_);
let recursive = inner.recursive();
if !state.flags.allow_write {
return odd_future(permission_denied());
if let Err(e) = state.check_write(path.to_str().unwrap()) {
return odd_future(e);
}
blocking!(base.sync(), || {
debug!("op_remove {}", path.display());
let metadata = fs::metadata(&path)?;
@ -831,10 +832,11 @@ fn op_copy_file(
assert_eq!(data.len(), 0);
let inner = base.inner_as_copy_file().unwrap();
let from = PathBuf::from(inner.from().unwrap());
let to = PathBuf::from(inner.to().unwrap());
let to_ = inner.to().unwrap();
let to = PathBuf::from(to_);
if !state.flags.allow_write {
return odd_future(permission_denied());
if let Err(e) = state.check_write(&to_) {
return odd_future(e);
}
debug!("op_copy_file {} {}", from.display(), to.display());
@ -1015,14 +1017,13 @@ fn op_write_file(
data: &'static mut [u8],
) -> Box<Op> {
let inner = base.inner_as_write_file().unwrap();
if !state.flags.allow_write {
return odd_future(permission_denied());
}
let filename = String::from(inner.filename().unwrap());
let perm = inner.perm();
if let Err(e) = state.check_write(&filename) {
return odd_future(e);
}
blocking!(base.sync(), || -> OpResult {
debug!("op_write_file {} {}", filename, data.len());
deno_fs::write_file(Path::new(&filename), data, perm)?;
@ -1036,12 +1037,13 @@ fn op_rename(
data: &'static mut [u8],
) -> Box<Op> {
assert_eq!(data.len(), 0);
if !state.flags.allow_write {
return odd_future(permission_denied());
}
let inner = base.inner_as_rename().unwrap();
let oldpath = PathBuf::from(inner.oldpath().unwrap());
let newpath = PathBuf::from(inner.newpath().unwrap());
let newpath_ = inner.newpath().unwrap();
let newpath = PathBuf::from(newpath_);
if let Err(e) = state.check_write(&newpath_) {
return odd_future(e);
}
blocking!(base.sync(), || -> OpResult {
debug!("op_rename {} {}", oldpath.display(), newpath.display());
fs::rename(&oldpath, &newpath)?;
@ -1055,8 +1057,13 @@ fn op_symlink(
data: &'static mut [u8],
) -> Box<Op> {
assert_eq!(data.len(), 0);
if !state.flags.allow_write {
return odd_future(permission_denied());
let inner = base.inner_as_symlink().unwrap();
let oldname = PathBuf::from(inner.oldname().unwrap());
let newname_ = inner.newname().unwrap();
let newname = PathBuf::from(newname_);
if let Err(e) = state.check_write(&newname_) {
return odd_future(e);
}
// TODO Use type for Windows.
if cfg!(windows) {
@ -1065,10 +1072,6 @@ fn op_symlink(
"Not implemented".to_string(),
));
}
let inner = base.inner_as_symlink().unwrap();
let oldname = PathBuf::from(inner.oldname().unwrap());
let newname = PathBuf::from(inner.newname().unwrap());
blocking!(base.sync(), || -> OpResult {
debug!("op_symlink {} {}", oldname.display(), newname.display());
#[cfg(any(unix))]
@ -1118,13 +1121,14 @@ fn op_truncate(
) -> Box<Op> {
assert_eq!(data.len(), 0);
if !state.flags.allow_write {
return odd_future(permission_denied());
}
let inner = base.inner_as_truncate().unwrap();
let filename = String::from(inner.name().unwrap());
let len = inner.len();
if let Err(e) = state.check_write(&filename) {
return odd_future(e);
}
blocking!(base.sync(), || {
debug!("op_truncate {} {}", filename, len);
let f = fs::OpenOptions::new().write(true).open(&filename)?;
@ -1139,8 +1143,8 @@ fn op_listen(
data: &'static mut [u8],
) -> Box<Op> {
assert_eq!(data.len(), 0);
if !state.flags.allow_net {
return odd_future(permission_denied());
if let Err(e) = state.check_net("listen") {
return odd_future(e);
}
let cmd_id = base.cmd_id();
@ -1205,10 +1209,9 @@ fn op_accept(
data: &'static mut [u8],
) -> Box<Op> {
assert_eq!(data.len(), 0);
if !state.flags.allow_net {
return odd_future(permission_denied());
if let Err(e) = state.check_net("accept") {
return odd_future(e);
}
let cmd_id = base.cmd_id();
let inner = base.inner_as_accept().unwrap();
let server_rid = inner.rid();
@ -1232,10 +1235,9 @@ fn op_dial(
data: &'static mut [u8],
) -> Box<Op> {
assert_eq!(data.len(), 0);
if !state.flags.allow_net {
return odd_future(permission_denied());
if let Err(e) = state.check_net("dial") {
return odd_future(e);
}
let cmd_id = base.cmd_id();
let inner = base.inner_as_dial().unwrap();
let network = inner.network().unwrap();

86
src/permissions.rs Normal file
View file

@ -0,0 +1,86 @@
extern crate atty;
use flags::DenoFlags;
use errors::permission_denied;
use errors::DenoResult;
use std::io;
#[derive(Debug, Default, PartialEq)]
pub struct DenoPermissions {
pub allow_write: bool,
pub allow_net: bool,
pub allow_env: bool,
}
impl DenoPermissions {
pub fn new(flags: &DenoFlags) -> DenoPermissions {
DenoPermissions {
allow_write: flags.allow_write,
allow_env: flags.allow_env,
allow_net: flags.allow_net,
}
}
pub fn check_write(&mut self, filename: &str) -> DenoResult<()> {
if self.allow_write {
return Ok(());
};
// TODO get location (where access occurred)
let r = permission_prompt(format!(
"Deno requests write access to \"{}\".",
filename
));;
if r.is_ok() {
self.allow_write = true;
}
r
}
pub fn check_net(&mut self, domain_name: &str) -> DenoResult<()> {
if self.allow_net {
return Ok(());
};
// TODO get location (where access occurred)
let r = permission_prompt(format!(
"Deno requests network access to \"{}\".",
domain_name
));
if r.is_ok() {
self.allow_net = true;
}
r
}
pub fn check_env(&mut self) -> DenoResult<()> {
if self.allow_env {
return Ok(());
};
// TODO get location (where access occurred)
let r = permission_prompt(
"Deno requests access to environment variables.".to_string(),
);
if r.is_ok() {
self.allow_env = true;
}
r
}
}
fn permission_prompt(message: String) -> DenoResult<()> {
if !atty::is(atty::Stream::Stdin) || !atty::is(atty::Stream::Stderr) {
return Err(permission_denied());
};
// print to stderr so that if deno is > to a file this is still displayed.
eprint!("{} Grant? [yN] ", message);
let mut input = String::new();
let stdin = io::stdin();
let _nread = stdin.read_line(&mut input)?;
let ch = input.chars().next().unwrap();
let is_yes = ch == 'y' || ch == 'Y';
if is_yes {
Ok(())
} else {
Err(permission_denied())
}
}

@ -1 +1 @@
Subproject commit b93f9c8bd39a2548d60167043da6b947c023a830
Subproject commit 56c4acce2e8ffe979b2e7d52d2b3e6f613ed492e

143
tools/permission_prompt_test.py Executable file
View file

@ -0,0 +1,143 @@
#!/usr/bin/env python
import os
import pty
import select
import subprocess
from util import build_path, executable_suffix
PERMISSIONS_PROMPT_TEST_TS = "tools/permission_prompt_test.ts"
# This function is copied from:
# https://gist.github.com/hayd/4f46a68fc697ba8888a7b517a414583e
# https://stackoverflow.com/q/52954248/1240268
def tty_capture(cmd, bytes_input):
"""Capture the output of cmd with bytes_input to stdin,
with stdin, stdout and stderr as TTYs."""
mo, so = pty.openpty() # provide tty to enable line-buffering
me, se = pty.openpty()
mi, si = pty.openpty()
fdmap = {mo: 'stdout', me: 'stderr', mi: 'stdin'}
p = subprocess.Popen(
cmd, bufsize=1, stdin=si, stdout=so, stderr=se, close_fds=True)
os.write(mi, bytes_input)
timeout = .04 # seconds
res = {'stdout': b'', 'stderr': b''}
while True:
ready, _, _ = select.select([mo, me], [], [], timeout)
if ready:
for fd in ready:
data = os.read(fd, 512)
if not data:
break
res[fdmap[fd]] += data
elif p.poll() is not None: # select timed-out
break # p exited
for fd in [si, so, se, mi, mo, me]:
os.close(fd) # can't do it sooner: it leads to errno.EIO error
p.wait()
return p.returncode, res['stdout'], res['stderr']
class Prompt(object):
def __init__(self, deno_exe):
self.deno_exe = deno_exe
def run(self,
arg,
bytes_input,
allow_write=False,
allow_net=False,
allow_env=False):
"Returns (return_code, stdout, stderr)."
cmd = [self.deno_exe, PERMISSIONS_PROMPT_TEST_TS, arg]
if allow_write:
cmd.append("--allow-write")
if allow_net:
cmd.append("--allow-net")
if allow_env:
cmd.append("--allow-env")
return tty_capture(cmd, bytes_input)
def warm_up(self):
# ignore the ts compiling message
self.run('needsWrite', b'', allow_write=True)
def test_write_yes(self):
code, stdout, stderr = self.run('needsWrite', b'y\n')
assert code == 0
assert stdout == b''
assert b'Deno requests write access' in stderr
def test_write_arg(self):
code, stdout, stderr = self.run('needsWrite', b'', allow_write=True)
assert code == 0
assert stdout == b''
assert stderr == b''
def test_write_no(self):
code, stdout, stderr = self.run('needsWrite', b'N\n')
assert code == 1
# FIXME this error message should be in stderr
assert b'PermissionDenied: permission denied' in stdout
assert b'Deno requests write access' in stderr
def test_env_yes(self):
code, stdout, stderr = self.run('needsEnv', b'y\n')
assert code == 0
assert stdout == b''
assert b'Deno requests access to environment' in stderr
def test_env_arg(self):
code, stdout, stderr = self.run('needsEnv', b'', allow_env=True)
assert code == 0
assert stdout == b''
assert stderr == b''
def test_env_no(self):
code, stdout, stderr = self.run('needsEnv', b'N\n')
assert code == 1
# FIXME this error message should be in stderr
assert b'PermissionDenied: permission denied' in stdout
assert b'Deno requests access to environment' in stderr
def test_net_yes(self):
code, stdout, stderr = self.run('needsEnv', b'y\n')
assert code == 0
assert stdout == b''
assert b'Deno requests access to environment' in stderr
def test_net_arg(self):
code, stdout, stderr = self.run('needsNet', b'', allow_net=True)
assert code == 0
assert stdout == b''
assert stderr == b''
def test_net_no(self):
code, stdout, stderr = self.run('needsNet', b'N\n')
assert code == 1
# FIXME this error message should be in stderr
assert b'PermissionDenied: permission denied' in stdout
assert b'Deno requests network access' in stderr
def permission_prompt_test(deno_exe):
p = Prompt(deno_exe)
p.warm_up()
p.test_write_yes()
p.test_write_arg()
p.test_write_no()
p.test_env_yes()
p.test_env_arg()
p.test_env_no()
p.test_net_yes()
p.test_net_arg()
p.test_net_no()
if __name__ == "__main__":
deno_exe = os.path.join(build_path(), "deno" + executable_suffix)
permission_prompt_test(deno_exe)

View file

@ -0,0 +1,21 @@
import { args, listen, env, exit, makeTempDirSync } from "deno";
const name = args[1];
const test = {
needsWrite: () => {
makeTempDirSync();
},
needsEnv: () => {
env().home;
},
needsNet: () => {
listen("tcp", "127.0.0.1:4540");
}
}[name];
if (!test) {
console.log("Unknown test:", name);
exit(1);
}
test();

View file

@ -60,6 +60,13 @@ def main(argv):
check_output_test(deno_exe)
# TODO We currently skip testing the prompt in Windows completely.
# Windows does not support the pty module used for testing the permission
# prompt.
if os.name != 'nt':
from permission_prompt_test import permission_prompt_test
permission_prompt_test(deno_exe)
rmtree(deno_dir)
deno_dir_test(deno_exe, deno_dir)

View file

@ -125,8 +125,8 @@ def run_cargo():
# If the lockfile ends up in the git repo, it'll make cargo hang for everyone
# else who tries to run sync_third_party.
def delete_lockfile():
lockfiles = find_exts(
path.join(rust_crates_path, "registry/index"), '.cargo-index-lock')
lockfiles = find_exts([path.join(rust_crates_path, "registry/index")],
['.cargo-index-lock'])
for lockfile in lockfiles:
os.remove(lockfile)