Repository: denoland/deno
Pull request: fix(ext/node): implement `process:seteuid()`

Closes #31144

Changed files:
filename: ext/node/lib.rs
patch: @@ -458,6 +458,7 @@ deno_core::extension!(deno_node,
     ops::ipc::op_node_ipc_unref,
     ops::process::op_node_process_kill,
     ops::process::op_node_process_setegid<P>,
+    ops::process::op_node_process_seteuid<P>,
     ops::process::op_process_abort,
     ops::tls::op_get_root_certificates,
     ops::tls::op_tls_peer_certificate,

filename: ext/node/ops/process.rs
patch: @@ -9,6 +9,10 @@ use deno_permissions::PermissionsContainer;
 use nix::unistd::Gid;
 #[cfg(unix)]
 use nix::unistd::Group;
+#[cfg(unix)]
+use nix::unistd::Uid;
+#[cfg(unix)]
+use nix::unistd::User;
 
 use crate::NodePermissions;
 
@@ -153,3 +157,55 @@ where
 {
   Err(ProcessError::NotSupported)
 }
+
+#[cfg(not(any(target_os = "android", target_os = "windows")))]
+fn get_user_id(name: &str) -> Result<Uid, ProcessError> {
+  let user = User::from_name(name)?;
+
+  if let Some(user) = user {
+    Ok(user.uid)
+  } else {
+    Err(ProcessError::UnknownCredential(
+      "User".to_string(),
+      name.to_string(),
+    ))
+  }
+}
+
+#[cfg(not(any(target_os = "android", target_os = "windows")))]
+#[op2(fast, stack_trace)]
+pub fn op_node_process_seteuid<P, 'a>(
+  scope: &mut v8::PinScope<'a, '_>,
+  state: &mut OpState,
+  id: v8::Local<'a, v8::Value>,
+) -> Result<(), ProcessError>
+where
+  P: NodePermissions + 'static,
+{
+  {
+    let permissions = state.borrow_mut::<P>();
+    permissions.check_sys("seteuid", "node:process.seteuid")?;
+  }
+
+  let uid = match serialize_id(scope, id)? {
+    Id::Number(number) => Uid::from_raw(number),
+    Id::Name(name) => get_user_id(&name)?,
+  };
+
+  nix::unistd::seteuid(uid)?;
+
+  Ok(())
+}
+
+#[cfg(any(target_os = "android", target_os = "windows"))]
+#[op2(fast, stack_trace)]
+pub fn op_node_process_seteuid<P>(
+  _scope: &mut v8::PinScope<'_, '_>,
+  _state: &mut OpState,
+  _id: v8::Local<'_, v8::Value>,
+) -> Result<(), ProcessError>
+where
+  P: NodePermissions + 'static,
+{
+  Err(ProcessError::NotSupported)
+}

filename: ext/node/polyfills/process.ts
patch: @@ -13,6 +13,7 @@ import {
   op_node_load_env_file,
   op_node_process_kill,
   op_node_process_setegid,
+  op_node_process_seteuid,
   op_process_abort,
 } from "ext:core/ops";
 
@@ -378,7 +379,7 @@ export function kill(pid: number, sig: string | number = "SIGTERM") {
   return true;
 }
 
-let getgid, getuid, getegid, geteuid, setegid;
+let getgid, getuid, getegid, geteuid, setegid, seteuid;
 
 function wrapIdSetter(
   syscall: string,
@@ -408,10 +409,11 @@ if (!isWindows) {
 
   if (!isAndroid) {
     setegid = wrapIdSetter("setegid", op_node_process_setegid);
+    seteuid = wrapIdSetter("seteuid", op_node_process_seteuid);
   }
 }
 
-export { getegid, geteuid, getgid, getuid, setegid };
+export { getegid, geteuid, getgid, getuid, setegid, seteuid };
 
 const ALLOWED_FLAGS = buildAllowedFlags();
 
@@ -823,6 +825,9 @@ process.geteuid = geteuid;
 /** This method is removed on Windows */
 process.setegid = setegid;
 
+/** This method is removed on Windows */
+process.seteuid = seteuid;
+
 process.getBuiltinModule = getBuiltinModule;
 
 // TODO(kt3k): Implement this when we added -e option to node compat mode

filename: runtime/permissions/lib.rs
patch: @@ -2441,7 +2441,7 @@ impl SysDescriptor {
       "hostname" | "inspector" | "osRelease" | "osUptime" | "loadavg"
       | "networkInterfaces" | "systemMemoryInfo" | "uid" | "gid" | "cpus"
       | "homedir" | "getegid" | "statfs" | "getPriority" | "setPriority"
-      | "userInfo" | "setegid" => Ok(Self(kind)),
+      | "userInfo" | "setegid" | "seteuid" => Ok(Self(kind)),
 
       // the underlying permission check changed to `userInfo` to better match the API,
       // alias this to avoid breaking existing projects with `--allow-sys=username`

filename: tests/unit_node/process_test.ts
patch: @@ -15,6 +15,7 @@ import process, {
   pid as importedPid,
   platform as importedPlatform,
   setegid,
+  seteuid,
 } from "node:process";
 
 import { Readable } from "node:stream";
@@ -43,6 +44,12 @@ function getGroupNameFromSystem(gid: number): string {
   return groupInfo;
 }
 
+function getUserNameFromSystem(uid: number): string {
+  const stdout = execSync(`id -un ${uid}`).toString();
+  const name = stdout.trim();
+  return name;
+}
+
 Deno.test({
   name: "process.cwd and process.chdir success",
   fn() {
@@ -1384,3 +1391,37 @@ Deno.test({
     assertEquals(process.setegid, undefined);
   },
 });
+
+Deno.test({
+  name: "process.seteuid()",
+  ignore: Deno.build.os === "windows" || Deno.build.os === "android",
+  fn() {
+    const originalEuid = geteuid!();
+    const userName = getUserNameFromSystem(originalEuid);
+
+    // calling with original euid to ensure it doesn't throw
+    seteuid!(originalEuid);
+    seteuid!(userName);
+  },
+});
+
+Deno.test({
+  name: "process.seteuid() throws on invalid user",
+  ignore: Deno.build.os === "windows" || Deno.build.os === "android",
+  fn() {
+    assertThrows(
+      () => {
+        seteuid!("67?!");
+      },
+      "User identifier does not exist: 67?!",
+    );
+  },
+});
+
+Deno.test({
+  name: "process.seteuid() should be undefined on unsupported platforms",
+  ignore: Deno.build.os !== "windows" && Deno.build.os !== "android",
+  fn() {
+    assertEquals(process.seteuid, undefined);
+  },
+});