5.6 Permissions

The subsequent phase in executing code involves establishing permissions. Permissions stand out as a distinctive feature within the Deno runtime system. Deno provides an exceptional and safeguarded environment for running programs. This exclusive safeguarding is achieved through the mechanism of permissions. Deno's permissions framework allows users to control precisely what actions and resources their programs can access, adding an extra layer of security to the execution process.

let permissions = PermissionsContainer::new(Permissions::from_options(
    &cli_options.permissions_options(),
  )?);

Overview

Permissions play a crucial role in enabling Deno to provide a secure and distinctive sandboxed environment. Within Deno, a wide array of permissions and permission types exist, each serving a specific purpose. These permissions empower developers to control what their Deno programs can access and interact with. By categorizing permissions into types like read, write, env, and net, Deno tailors its security model to cater to different scenarios and use cases.

List of permissions

Deno offers a variety of permissions to enhance its functionality. As the number of permissions increases, the level of sandboxing becomes more finely tuned. Below is a compilation of frequently employed permissions along with their explanations:

  1. Read Permission: This permission allows Deno to access and read files from the local system. It is essential for tasks that involve reading data or configurations stored on the user's device.

  2. Write Permission: With the write permission, Deno gains the ability to modify and create files. This is crucial when your program needs to update files or generate new ones during its execution.

  3. Net Permission: The net permission permits Deno to initiate network connections. This is necessary for tasks like making API requests or establishing socket connections with remote servers.

  4. Environment Permission: This permission provides access to the environment variables of the system. It comes in handy when your application needs to gather information about the environment it's running in.

  5. Run Permission: Deno's run permission allows it to execute other programs or scripts. This is useful for scenarios where your code needs to interact with external executables or scripts.

  6. FFI Permission: The FFI (Foreign Function Interface) permission is more advanced. It enables Deno to interface with functions written in languages like C or Rust, expanding the range of capabilities your application can harness.

  7. Hrtime Permission: This permission is related to high-resolution time measurements. It permits Deno to access accurate timing information, which is crucial for tasks that require precise timing, such as benchmarking or performance optimization.

Certain types of permissions in Deno can be boiled down to a simple true or false value, making them boolean in nature. To put it plainly, these permissions are all about whether access is given or denied.

On the flip side, some permission types require a more nuanced approach. Deno maintains lists for these permissions, adding an extra layer of granularity to the access control. This meticulous handling of permissions results in a stricter form of sandboxing, where each action is examined under a magnifying glass.

Let's delve into the permission types that benefit from this meticulous treatment by having additional lists associated with them:

  1. Net Allow List: This permission type revolves around network-related actions. By maintaining an additional list, Deno ensures that only approved network interactions take place. This is especially crucial in environments where network security is paramount.

  2. Read Allow List: Reading files or data is a common task, but it can also be risky if not properly managed. Deno mitigates this risk by using a read allow list. Files specified in this list are the only ones that can be accessed, preventing unauthorized data exposure.

  3. Write Allow List: Writing to files or altering data demands even greater caution. Deno acknowledges this by employing a write allow list. Only the files granted explicit permission can be modified, maintaining data integrity and thwarting any potential malicious activities.

States

In the world of Deno, permissions come with three potential states. You can uncover these states by delving into Deno's inner workings or by examining TypeScript/JavaScript code written by users.

These three states are:

  1. Prompt: When a permission is in this state, Deno seeks the user's input to decide whether to grant or deny it.

  2. Granted: In this state, Deno has given the green light to the permission, allowing the code to carry out its intended actions without any hindrance.

  3. Denied: When a permission is denied, Deno firmly puts its foot down, preventing the code from exercising the particular action associated with the permission.

To witness the transition of a permission through these states, let's take a closer look:

  • If --allow-YYY is specified

    • The permission is granted

  • If --deny-YYY is specified

    • The permission is denied

  • Else

    • Prompt for permission

    • If prompt resulted in G (or grant)

      • The permission is granted

    • Else

      • The permission is denied

Building permissions

The function called "from_options" is rather straightforward. It works by going through the flags that have been analyzed and constructs an object related to permissions for each of the permissions that are supported. Certain permissions are binary in nature, meaning they can only be either "true" or "false." However, there are other permissions that might also come with a list of items.

Let's take a look at a code snippet that illustrates how to create a permission along with an optional list:

pub fn from_options(opts: &PermissionsOptions) -> Result<Self, AnyError> {
    Ok(Self {
      read: Permissions::new_read(
        &opts.allow_read,
        &opts.deny_read,
        opts.prompt,
      )?,
      write: Permissions::new_write(
        &opts.allow_write,
        &opts.deny_write,
        opts.prompt,
      )?,
      net: Permissions::new_net(&opts.allow_net, &opts.deny_net, opts.prompt)?,
      env: Permissions::new_env(&opts.allow_env, &opts.deny_env, opts.prompt)?,
      sys: Permissions::new_sys(&opts.allow_sys, &opts.deny_sys, opts.prompt)?,
      run: Permissions::new_run(&opts.allow_run, &opts.deny_run, opts.prompt)?,
      ffi: Permissions::new_ffi(&opts.allow_ffi, &opts.deny_ffi, opts.prompt)?,
      hrtime: Permissions::new_hrtime(opts.allow_hrtime, opts.deny_hrtime),
    })
}

pub fn new_net(
    allow_list: &Option<Vec<String>>,
    deny_list: &Option<Vec<String>>,
    prompt: bool,
  ) -> Result<UnaryPermission<NetDescriptor>, AnyError> {
    Ok(UnaryPermission::<NetDescriptor> {
      granted_global: global_from_option(allow_list),
      granted_list: parse_net_list(allow_list)?,
      flag_denied_global: global_from_option(deny_list),
      flag_denied_list: parse_net_list(deny_list)?,
      prompt,
      ..Default::default()
    })
}

In Deno, when it comes to actions involving reading, writing, and network operations, there are distinct permission levels to consider. These levels help control what the program is allowed to do. Let's break down the details:

  1. Global State Permission: This level of permission involves specifying whether the program has the authority to access global state information. It's like giving the program the key to the central vault where important data is kept. This permission is either set to true (allowed) or false (not allowed).

    • Granted List: If there is an allow list in place, the program's requested actions are reviewed against this list. It's akin to having a guest list at a party. The program's actions are compared to the list, and if the program's request matches an entry on the list, it's granted permission to proceed. This way, only invited actions get through.

    • Deny List: Conversely, if there's a deny list established, the program's requested actions are cross-checked with this list. Imagine a "do not enter" list at a restricted area. If the program's actions align with any entry on this list, access is denied. This ensures that certain actions are explicitly forbidden.

  2. Boolean Permission for Other Actions: For actions like handling environment variables, running programs, utilizing plugins, and measuring high-resolution time (hrtime), the permission structure is simpler. It's a straightforward yes-or-no scenario.

    These actions are governed by a boolean permission. It's like a simple switch: either the permission is specified (on) or not (off). There's no middle ground here. Either the program is allowed to carry out these actions, or it isn't.

Here is the code which converts any permission to boolean:

fn unit_permission_from_flag_bools(
  allow_flag: bool,
  deny_flag: bool,
  name: &'static str,
  description: &'static str,
  prompt: bool,
) -> UnitPermission {
  UnitPermission {
    name,
    description,
    state: if deny_flag {
      PermissionState::Denied
    } else if allow_flag {
      PermissionState::Granted
    } else {
      PermissionState::Prompt
    },
    prompt,
  }
}

If --deny-YYY is there, permission is denied. If --allow-YYY is not there, permission state is granted, otherwise prompt.

Permissions are typically provided at the beginning of the main program, unless Deno requests them during runtime. Once these permissions are established, they are consistently checked whenever needed.

Querying permissions

All the permissions usually get built at startup. For a production system, it'd be impractical to prompt for permissions at runtime. Although Deno supports prompting for permissions, it's unlikely that it'd ever get used for production code.

Query for permission

Obtaining a boolean permission using code in Deno is remarkably straightforward, as the process essentially involves retrieving the current state of the permission. Below, you'll find a few illustrative examples that showcase how this is done:

fn query_desc(
    &self,
    desc: &Option<T>,
    allow_partial: AllowPartial,
  ) -> PermissionState {
    if self.is_flag_denied(desc) || self.is_prompt_denied(desc) {
      PermissionState::Denied
    } else if self.is_granted(desc) {
      match allow_partial {
        AllowPartial::TreatAsGranted => PermissionState::Granted,
        AllowPartial::TreatAsDenied => {
          if self.is_partial_flag_denied(desc) {
            PermissionState::Denied
          } else {
            PermissionState::Granted
          }
        }
        AllowPartial::TreatAsPartialGranted => {
          if self.is_partial_flag_denied(desc) {
            PermissionState::GrantedPartial
          } else {
            PermissionState::Granted
          }
        }
      }
    } else if matches!(allow_partial, AllowPartial::TreatAsDenied)
      && self.is_partial_flag_denied(desc)
    {
      PermissionState::Denied
    } else {
      PermissionState::Prompt
    }
  }

Query for a permission with an optional list

Some permissions can either be boolean or a list. For example -

  • For read permission, input is path

  • For write permission, input is path

  • For net permission, input is URL

Here is the code for querying any list kind of permission:

fn is_granted(&self, desc: &Option<T>) -> bool {
    Self::list_contains(desc, self.granted_global, &self.granted_list)
}

fn list_contains(
    desc: &Option<T>,
    list_global: bool,
    list: &HashSet<T>,
  ) -> bool {
    match desc.as_ref() {
      Some(desc) => list_global || list.iter().any(|v| v.stronger_than(desc)),
      None => list_global,
    }
}

Request and revoke permissions

You can also ask for and withdraw permissions during the program's execution. Although this might not be very practical for real-world usage, Deno does allow for it. Let's take a brief look at these two aspects using a simple example involving the 'env' permission:

In some situations, you might need to change the permissions your Deno program has while it's running. For instance, you might want to access certain environment variables using the 'env' permission. This permission lets your program interact with the environment it's running in, like reading sensitive information such as API keys or configuration settings.

The following code requests a permission at runtime:

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

fn request_desc(
    &mut self,
    desc: &Option<T>,
    get_display_name: impl Fn() -> Option<String>,
  ) -> PermissionState {
    let state = self.query_desc(desc, AllowPartial::TreatAsPartialGranted);
    if state == PermissionState::Granted {
      self.insert_granted(desc.clone());
      return state;
    }
    if state != PermissionState::Prompt {
      return state;
    }
    let mut message = String::with_capacity(40);
    message.push_str(&format!("{} access", T::flag_name()));
    match get_display_name() {
      Some(display_name) => {
        message.push_str(&format!(" to \"{}\"", display_name))
      }
      None => match desc {
        Some(desc) => message.push_str(&format!(" to \"{}\"", desc.name())),
        None => {}
      },
    }
    match permission_prompt(
      &message,
      T::flag_name(),
      Some("Deno.permissions.request()"),
      true,
    ) {
      PromptResponse::Allow => {
        self.insert_granted(desc.clone());
        PermissionState::Granted
      }
      PromptResponse::Deny => {
        self.insert_prompt_denied(desc.clone());
        PermissionState::Denied
      }
      PromptResponse::AllowAll => {
        self.insert_granted(None);
        PermissionState::Granted
      }
    }
  }

Revoke is straightforward. It simply moves permission from granted to prompt.

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

fn revoke_desc(&mut self, desc: &Option<T>) -> PermissionState {
    match desc.as_ref() {
      Some(desc) => self.granted_list.retain(|v| !v.stronger_than(desc)),
      None => {
        self.granted_global = false;
        // Revoke global is a special case where the entire granted list is
        // cleared. It's inconsistent with the granular case where only
        // descriptors stronger than the revoked one are purged.
        self.granted_list.clear();
      }
    }
    self.query_desc(desc, AllowPartial::TreatAsPartialGranted)
}

The process of requesting permission in Deno involves a significant step that impacts how permissions are managed. When a permission is requested, it starts in a "prompt" state, awaiting a response. If the user responds to the prompt by denying the permission (typically indicated by the letter "D"), the permission transitions from the prompt state to the "denied" state. This means that the user's choice to deny the permission is acknowledged and recorded.

On the other hand, if the user responds by granting the permission (often indicated by the letter "G"), the permission undergoes a different transition. It shifts from the prompt state to the "granted" state. This transition signifies that the user has allowed the requested permission, and Deno is now authorized to proceed with the associated actions.

Last updated