current commit
261013.10 · permission: keep explicit fs grants when dropping a covering directory
#include "fs_permission.h"
#include "debug_utils-inl.h"
#include "env.h"
#include "path.h"

#include <fcntl.h>
#include <algorithm>
#include <climits>
#include <cstdlib>
#include <string>
#include <string_view>
#include <vector>

namespace {

std::string WildcardIfDir(const std::string& res) noexcept {
  uv_fs_t req;
  int rc = uv_fs_stat(nullptr, &req, res.c_str(), nullptr);
  const bool is_dir =
      rc == 0 &&
      (static_cast<const uv_stat_t*>(req.ptr)->st_mode & S_IFMT) == S_IFDIR;
  uv_fs_req_cleanup(&req);
  if (is_dir) {
    // add wildcard when directory
    if (res.back() == node::kPathSeparator) {
      return res + "*";
    }
    return res + node::kPathSeparator + "*";
  }
  return res;
}

bool is_tree_granted(
    node::Environment* env,
    const node::permission::FSPermission::RadixTree* granted_tree,
    std::string_view param) {
  std::string resolved_param = node::PathResolve(env, {param});
#ifdef _WIN32
  // Remove leading "\\?\" from UNC path
  if (resolved_param.starts_with("\\\\?\\")) {
    resolved_param.erase(0, 4);
  }

  // Remove leading "UNC\" from UNC path
  if (resolved_param.starts_with("UNC\\")) {
    resolved_param.erase(0, 4);
  }
  // Remove leading "//" from UNC path
  if (resolved_param.starts_with("//")) {
    resolved_param.erase(0, 2);
  }
#endif
  auto _is_granted = granted_tree->Lookup(resolved_param, true);
  node::Debug(env,
              node::DebugCategory::PERMISSION_MODEL,
              "Access %d to %s\n",
              _is_granted,
              param);

  return _is_granted;
}

static const char* kBoxDrawingsLightUpAndRight = "└─ ";
static const char* kBoxDrawingsLightVerticalAndRight = "├─ ";

void PrintTree(const node::permission::FSPermission::RadixTree::Node* node,
               size_t depth = 0,
               const std::string& branch_prefix = "",
               bool is_last = true) {
  if (node == nullptr) {
    return;
  }

  if (depth > 0 || (node->prefix.length() > 0)) {
    std::string indent;

    if (depth > 0) {
      indent = branch_prefix;
      if (is_last) {
        indent += kBoxDrawingsLightUpAndRight;
      } else {
        indent += kBoxDrawingsLightVerticalAndRight;
      }
    }

    node::per_process::Debug(
        node::DebugCategory::PERMISSION_MODEL, "%s%s\n", indent, node->prefix);
  }

  if (!node->children.empty()) {
    size_t count = 0;
    size_t total = node->children.size();

    std::string next_branch_prefix;
    if (depth > 0) {
      next_branch_prefix = branch_prefix;
      if (is_last) {
        next_branch_prefix += "   ";
      } else {
        next_branch_prefix += "│  ";
      }
    }

    for (const auto& [label, child] : node->children) {
      count++;
      bool child_is_last = (count == total);
      PrintTree(child.get(), depth + 1, next_branch_prefix, child_is_last);
    }
  }
}

}  // namespace

namespace node {

namespace permission {

// allow = '*'
// allow = '/tmp/,/home/example.js'
void FSPermission::Apply(Environment* env,
                         std::span<const std::string> allow,
                         PermissionScope scope) {
  RwLock::ScopedWriteLock lock(lock_);
  for (const std::string& res : allow) {
    if (res == "*") {
      if (scope == PermissionScope::kFileSystemRead) {
        deny_all_in_ = false;
        allow_all_in_ = true;
      } else {
        deny_all_out_ = false;
        allow_all_out_ = true;
      }
      return;
    }
    GrantAccess(scope, PathResolve(env, {res}));
  }
}

void FSPermission::Drop(Environment* env,
                        PermissionScope scope,
                        std::string_view param) {
  RwLock::ScopedWriteLock lock(lock_);
  if (param.empty()) {
    // Drop all access for this scope
    if (scope == PermissionScope::kFileSystemRead ||
        scope == PermissionScope::kFileSystem) {
      deny_all_in_ = true;
      allow_all_in_ = false;
      granted_in_fs_.Clear();
      granted_paths_in_.clear();
    }
    if (scope == PermissionScope::kFileSystemWrite ||
        scope == PermissionScope::kFileSystem) {
      deny_all_out_ = true;
      allow_all_out_ = false;
      granted_out_fs_.Clear();
      granted_paths_out_.clear();
    }
    return;
  }

  // When allowed with *, you can only drop * (no specific paths)
  std::string resolved = PathResolve(env, {param});
  if (scope == PermissionScope::kFileSystemRead ||
      scope == PermissionScope::kFileSystem) {
    if (!allow_all_in_) {
      RevokeAccess(PermissionScope::kFileSystemRead, resolved);
    }
  }
  if (scope == PermissionScope::kFileSystemWrite ||
      scope == PermissionScope::kFileSystem) {
    if (!allow_all_out_) {
      RevokeAccess(PermissionScope::kFileSystemWrite, resolved);
    }
  }
}

void FSPermission::RevokeAccess(PermissionScope perm, const std::string& res) {
  const std::string path = WildcardIfDir(res);
  if (perm == PermissionScope::kFileSystemRead) {
    auto it =
        std::find(granted_paths_in_.begin(), granted_paths_in_.end(), path);
    if (it != granted_paths_in_.end()) {
      granted_paths_in_.erase(it);
      RebuildTree(PermissionScope::kFileSystemRead);
    }
  } else if (perm == PermissionScope::kFileSystemWrite) {
    auto it =
        std::find(granted_paths_out_.begin(), granted_paths_out_.end(), path);
    if (it != granted_paths_out_.end()) {
      granted_paths_out_.erase(it);
      RebuildTree(PermissionScope::kFileSystemWrite);
    }
  }
}

void FSPermission::RebuildTree(PermissionScope scope) {
  if (scope == PermissionScope::kFileSystemRead) {
    granted_in_fs_.Clear();
    if (granted_paths_in_.empty()) {
      deny_all_in_ = true;
    } else {
      for (const auto& path : granted_paths_in_) {
        granted_in_fs_.Insert(path);
      }
    }
  } else if (scope == PermissionScope::kFileSystemWrite) {
    granted_out_fs_.Clear();
    if (granted_paths_out_.empty()) {
      deny_all_out_ = true;
    } else {
      for (const auto& path : granted_paths_out_) {
        granted_out_fs_.Insert(path);
      }
    }
  }
}

void FSPermission::GrantAccess(PermissionScope perm, const std::string& res) {
  const std::string path = WildcardIfDir(res);
  // Track every explicit grant, even when already covered by another one,
  // so that it still applies if the covering grant is dropped later.
  if (perm == PermissionScope::kFileSystemRead) {
    if (std::find(granted_paths_in_.begin(), granted_paths_in_.end(), path) ==
        granted_paths_in_.end()) {
      granted_paths_in_.push_back(path);
    }
    if (!granted_in_fs_.Lookup(path)) {
      granted_in_fs_.Insert(path);
    }
    deny_all_in_ = false;
  } else if (perm == PermissionScope::kFileSystemWrite) {
    if (std::find(granted_paths_out_.begin(), granted_paths_out_.end(), path) ==
        granted_paths_out_.end()) {
      granted_paths_out_.push_back(path);
    }
    if (!granted_out_fs_.Lookup(path)) {
      granted_out_fs_.Insert(path);
    }
    deny_all_out_ = false;
  }
}

bool FSPermission::is_granted(Environment* env,
                              PermissionScope perm,
                              std::string_view param = "") const {
  RwLock::ScopedReadLock lock(lock_);
  switch (perm) {
    case PermissionScope::kFileSystem:
      return allow_all_in_ && allow_all_out_;
    case PermissionScope::kFileSystemRead:
      if (param.empty()) {
        return allow_all_in_;
      }
      return !deny_all_in_ &&
             (allow_all_in_ || is_tree_granted(env, &granted_in_fs_, param));
    case PermissionScope::kFileSystemWrite:
      if (param.empty()) {
        return allow_all_out_;
      }
      return !deny_all_out_ &&
             (allow_all_out_ || is_tree_granted(env, &granted_out_fs_, param));
    default:
      return false;
  }
}

FSPermission::RadixTree::RadixTree() : root_node_(std::make_unique<Node>("")) {}

FSPermission::RadixTree::~RadixTree() = default;

void FSPermission::RadixTree::Clear() {
  root_node_->children.clear();
  root_node_->wildcard_child.reset();
  root_node_->is_leaf = false;
}

bool FSPermission::RadixTree::Lookup(std::string_view s,
                                     bool when_empty_return) const {
  FSPermission::RadixTree::Node* current_node = root_node_.get();
  if (current_node->children.empty()) {
    return when_empty_return;
  }
  size_t parent_node_prefix_len = current_node->prefix.length();
  auto path_len = s.length();

  while (true) {
    if (parent_node_prefix_len == path_len && current_node->IsEndNode()) {
      return true;
    }

    auto node = current_node->NextNode(s, parent_node_prefix_len);
    if (node == nullptr) {
      return false;
    }

    current_node = node;
    parent_node_prefix_len += current_node->prefix.length();
    if (current_node->wildcard_child != nullptr &&
        path_len >= (parent_node_prefix_len - 2 /* slash* */)) {
      return true;
    }
  }
}

void FSPermission::RadixTree::Insert(const std::string& path) {
  FSPermission::RadixTree::Node* current_node = root_node_.get();

  size_t parent_node_prefix_len = current_node->prefix.length();
  size_t path_len = path.length();

  for (size_t i = 1; i <= path_len; ++i) {
    bool is_wildcard_node = path[i - 1] == '*';
    bool is_last_char = i == path_len;

    if (is_wildcard_node || is_last_char) {
      std::string node_path = path.substr(parent_node_prefix_len, i);
      current_node = current_node->CreateChild(node_path);
    }

    if (is_wildcard_node) {
      current_node = current_node->CreateWildcardChild();
      parent_node_prefix_len = i;
    }
  }

  if (per_process::enabled_debug_list.enabled(DebugCategory::PERMISSION_MODEL))
      [[unlikely]] {
    per_process::Debug(DebugCategory::PERMISSION_MODEL, "Inserting %s\n", path);
    PrintTree(root_node_.get());
  }
}

}  // namespace permission
}  // namespace node