mirror of
https://github.com/Dummi26/d26run.git
synced 2025-03-10 05:13:54 +01:00
initial commit for setuid/gid d26run
This commit is contained in:
commit
f859c7b528
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
target/*
|
||||||
|
Cargo.lock
|
8
Cargo.toml
Normal file
8
Cargo.toml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
[package]
|
||||||
|
name = "d26run"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
ctrlc = { version = "3.4.5", features = ["termination"] }
|
||||||
|
users = "0.11.0"
|
133
README.toml
Normal file
133
README.toml
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
# d26run
|
||||||
|
|
||||||
|
d26run execute commands defined in `/etc/d26run/exec` as other users without your sudo password.
|
||||||
|
using unix file permissions and groups, this gives you a simple way to do permission management, where every program may have different permissions.
|
||||||
|
for example, you may create a new user for your web browser and then use `d26run` to start the browser.
|
||||||
|
if you run another program as your normal user, it will not be able to access any of your web browser's data.
|
||||||
|
|
||||||
|
## execs
|
||||||
|
|
||||||
|
files in `/etc/d26run/exec` define what commands should be executed and who is allowed to execute them.
|
||||||
|
|
||||||
|
An example of such a file:
|
||||||
|
|
||||||
|
```
|
||||||
|
# my main user
|
||||||
|
allow user mark
|
||||||
|
|
||||||
|
# another user named browser is used for web browsing
|
||||||
|
user browser
|
||||||
|
group browser
|
||||||
|
|
||||||
|
env unset XDG_RUNTIME_DIR
|
||||||
|
exec /bin/firefox
|
||||||
|
```
|
||||||
|
|
||||||
|
## setup
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# compile d26run
|
||||||
|
cargo build --release
|
||||||
|
```
|
||||||
|
|
||||||
|
and then as `root`:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# copy the executable into your $PATH (doesn't have to be /bin/)
|
||||||
|
cp target/release/d26run /bin/
|
||||||
|
# set file permissions (setuid/setgid)
|
||||||
|
chown root:root /bin/d26run
|
||||||
|
chmod 775 /bin/d26run
|
||||||
|
chmod ug+s /bin/d26run
|
||||||
|
|
||||||
|
# create config directory
|
||||||
|
mkdir -p /etc/d26run/exec
|
||||||
|
```
|
||||||
|
|
||||||
|
then create at least one config, and you can start using d26run.
|
||||||
|
|
||||||
|
## execs
|
||||||
|
|
||||||
|
### with groups
|
||||||
|
|
||||||
|
The d26r-code group gives the program access to the `/code` directory.
|
||||||
|
Only some programs are allowed to see (or change!) code in my projects.
|
||||||
|
|
||||||
|
```
|
||||||
|
allow user mark
|
||||||
|
|
||||||
|
user d26r_code-main
|
||||||
|
group d26r_code-main
|
||||||
|
groups + d26r-code
|
||||||
|
groups + audio
|
||||||
|
|
||||||
|
env unset XDG_RUNTIME_DIR
|
||||||
|
exec /bin/terminal
|
||||||
|
arg -e
|
||||||
|
arg bash
|
||||||
|
arg -c
|
||||||
|
arg cd /code; tmux || bash || sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### running a command in a temporary user account
|
||||||
|
|
||||||
|
`/etc/d26run/exec/temp`:
|
||||||
|
|
||||||
|
```
|
||||||
|
allow anyone
|
||||||
|
|
||||||
|
user root
|
||||||
|
group root
|
||||||
|
|
||||||
|
env unset XDG_RUNTIME_DIR
|
||||||
|
exec /bin/bash
|
||||||
|
arg /etc/d26run/scripts/temp_command.sh
|
||||||
|
args all
|
||||||
|
```
|
||||||
|
|
||||||
|
A script creates a new user account, uses `sudo` to run the command, and, once the command exits, removes the user again.
|
||||||
|
It runs `pkill` to end any background processes spawned by the temporary user.
|
||||||
|
|
||||||
|
`/etc/d26run/scripts/temp_command.sh`:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
#!/bin/bash
|
||||||
|
my_id="$$"
|
||||||
|
mkdir -p /tmp/d26run-temphome
|
||||||
|
chmod 0755 /tmp/d26run-temphome
|
||||||
|
useradd --home-dir "/tmp/d26run-temphome/$my_id" --create-home --user-group --groups audio "d26r_temp_$my_id" 2>/dev/null
|
||||||
|
|
||||||
|
sudo -u "d26r_temp_$my_id" -D "/tmp/d26run-temphome/$my_id" -- "$@"
|
||||||
|
|
||||||
|
if userdel -r "d26r_temp_$my_id" 2>/dev/null; then exit; else printf '.'; fi
|
||||||
|
pkill -u "d26r_temp_$my_id"
|
||||||
|
sleep 2
|
||||||
|
if userdel -r "d26r_temp_$my_id" 2>/dev/null; then printf '\n'; exit; else printf '.'; fi
|
||||||
|
sleep 2
|
||||||
|
if userdel -r "d26r_temp_$my_id" 2>/dev/null; then printf '\n'; exit; else printf '.'; fi
|
||||||
|
sleep 2
|
||||||
|
if userdel -r "d26r_temp_$my_id" 2>/dev/null; then printf '\n'; exit; else printf '.'; fi
|
||||||
|
sleep 2
|
||||||
|
if userdel -r "d26r_temp_$my_id" 2>/dev/null; then printf '\n'; exit; else printf '.'; fi
|
||||||
|
sleep 2
|
||||||
|
if userdel -r "d26r_temp_$my_id" 2>/dev/null; then printf '\n'; exit; else printf '. '; fi
|
||||||
|
pkill -u "d26r_temp_$my_id" --signal kill
|
||||||
|
sleep 1
|
||||||
|
if userdel -r "d26r_temp_$my_id" 2>/dev/null; then printf '\n'; exit; else printf '.'; fi
|
||||||
|
sleep 1
|
||||||
|
if userdel -r "d26r_temp_$my_id" 2>/dev/null; then printf '\n'; exit; else printf '.'; fi
|
||||||
|
sleep 1
|
||||||
|
if userdel -r "d26r_temp_$my_id" 2>/dev/null; then printf '\n'; exit; else printf '.'; fi
|
||||||
|
sleep 1
|
||||||
|
if userdel -r "d26r_temp_$my_id" 2>/dev/null; then printf '\n'; exit; else printf '.'; fi
|
||||||
|
sleep 1
|
||||||
|
if userdel -r "d26r_temp_$my_id" 2>/dev/null; then printf '\n'; exit; else printf '.\n'; fi
|
||||||
|
userdel -rf "d26r_temp_$my_id"
|
||||||
|
```
|
||||||
|
|
||||||
|
to use `sudo -D <dir>`, add the following to a `sudo` config file (`/etc/sudoers` or `/etc/sudoers.d/...`):
|
||||||
|
(you can remove `-D <...>` from the script if you don't want to change your sudo config)
|
||||||
|
|
||||||
|
```
|
||||||
|
Defaults:root runcwd=*
|
||||||
|
```
|
2
rust-toolchain.toml
Normal file
2
rust-toolchain.toml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
[toolchain]
|
||||||
|
channel = "nightly"
|
306
src/main.rs
Normal file
306
src/main.rs
Normal file
@ -0,0 +1,306 @@
|
|||||||
|
#![feature(setgroups)]
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
env::args,
|
||||||
|
os::unix::process::CommandExt,
|
||||||
|
path::Path,
|
||||||
|
process::{exit, Command}, sync::{atomic::AtomicBool, Arc},
|
||||||
|
};
|
||||||
|
|
||||||
|
use users::os::unix::UserExt;
|
||||||
|
|
||||||
|
fn main() -> std::io::Result<()> {
|
||||||
|
let current_command = Arc::new(AtomicBool::new(false));
|
||||||
|
let current_command_h = Arc::clone(¤t_command);
|
||||||
|
let _ = ctrlc::set_handler(move || if !current_command_h.load(std::sync::atomic::Ordering::Relaxed) {
|
||||||
|
eprintln!("Received signal, exiting");
|
||||||
|
exit(255);
|
||||||
|
});
|
||||||
|
let mut cli_args = args().skip(1);
|
||||||
|
let exec = match cli_args.next() {
|
||||||
|
Some(v) => v,
|
||||||
|
None => {
|
||||||
|
eprintln!(
|
||||||
|
"Mandatory argument missing: exec (path in and relative to /etc/d26run/exec/)"
|
||||||
|
);
|
||||||
|
exit(255);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut exec_split = exec.split('/');
|
||||||
|
if let Some(ex) = exec_split.next() {
|
||||||
|
if let Some(file) = find_first_exec("/etc/d26run/exec/", ex, exec_split) {
|
||||||
|
match std::fs::read_to_string(file.path()) {
|
||||||
|
Ok(file) => {
|
||||||
|
let mut allowed = false;
|
||||||
|
let mut current_uid = None;
|
||||||
|
let mut current_uid =
|
||||||
|
|| *current_uid.get_or_insert_with(|| users::get_current_uid());
|
||||||
|
let mut current_groups = None;
|
||||||
|
let mut p_user = None;
|
||||||
|
let mut p_group = None;
|
||||||
|
let mut p_groups = vec![];
|
||||||
|
let mut working_dir = None;
|
||||||
|
let mut env_changes = vec![];
|
||||||
|
let mut execs = vec![];
|
||||||
|
for line in file.lines() {
|
||||||
|
if !(line.is_empty() || line.starts_with('#')) {
|
||||||
|
if line.starts_with("allow ") {
|
||||||
|
if let Some(user) = line.strip_prefix("allow user ") {
|
||||||
|
if !allowed {
|
||||||
|
if let Some(uid) = user.parse::<u32>().ok().or_else(|| {
|
||||||
|
users::get_user_by_name(user).map(|v| v.uid())
|
||||||
|
}) {
|
||||||
|
if uid == current_uid() {
|
||||||
|
allowed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if let Some(group) = line.strip_prefix("allow group ") {
|
||||||
|
if !allowed {
|
||||||
|
if let Some(gid) = group.parse::<u32>().ok().or_else(|| {
|
||||||
|
users::get_group_by_name(group).map(|v| v.gid())
|
||||||
|
}) {
|
||||||
|
if current_groups
|
||||||
|
.get_or_insert_with(|| {
|
||||||
|
users::group_access_list()
|
||||||
|
.map(|v| v.into_iter().map(|v| v.gid()).collect::<Vec<_>>())
|
||||||
|
})
|
||||||
|
.as_ref().is_ok_and(|v| v.contains(&gid)) {
|
||||||
|
allowed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if line == "allow anyone" {
|
||||||
|
allowed = true;
|
||||||
|
} else {
|
||||||
|
eprintln!(
|
||||||
|
"Invalid line: '{line}'. Expected 'allow user ...' or 'allow group ...'."
|
||||||
|
);
|
||||||
|
exit(255);
|
||||||
|
}
|
||||||
|
} else if line == "new" {
|
||||||
|
p_user = None;
|
||||||
|
p_group = None;
|
||||||
|
p_groups = vec![];
|
||||||
|
working_dir = None;
|
||||||
|
env_changes = vec![];
|
||||||
|
} else if let Some(u) = line.strip_prefix("user ") {
|
||||||
|
p_user = Some(u.parse::<u32>().map_err(|_| u.to_owned()));
|
||||||
|
} else if let Some(g) = line.strip_prefix("group ") {
|
||||||
|
p_group = Some(g.parse::<u32>().map_err(|_| g.to_owned()));
|
||||||
|
} else if let Some(g) = line.strip_prefix("groups = ") {
|
||||||
|
p_groups.clear();
|
||||||
|
for g in g.split_whitespace() {
|
||||||
|
p_groups.push(g.parse::<u32>().map_err(|_| g.to_owned()));
|
||||||
|
}
|
||||||
|
} else if let Some(g) = line.strip_prefix("groups + ") {
|
||||||
|
p_groups.push(g.parse::<u32>().map_err(|_| g.to_owned()));
|
||||||
|
} else if let Some(wd) = line.strip_prefix("working-dir ") {
|
||||||
|
working_dir = Some(wd.to_owned());
|
||||||
|
} else if let Some(env) = line.strip_prefix("env ") {
|
||||||
|
if env == "clear" {
|
||||||
|
env_changes.push(None);
|
||||||
|
} else if let Some(key) = env.strip_prefix("unset ") {
|
||||||
|
env_changes.push(Some((key.to_owned(), None)));
|
||||||
|
} else if let Some(pair) = env.strip_prefix("set ") {
|
||||||
|
let (key, val) = pair.split_once(' ').unwrap_or((pair, ""));
|
||||||
|
env_changes.push(Some((key.to_owned(), Some(val.to_owned()))));
|
||||||
|
} else {
|
||||||
|
eprintln!(
|
||||||
|
"Invalid line: '{line}'. Expected 'env clear', 'env unset <key>', or 'env set <key> <env value>'."
|
||||||
|
);
|
||||||
|
exit(255);
|
||||||
|
}
|
||||||
|
} else if let Some(exec) = line.strip_prefix("exec ") {
|
||||||
|
if let Some(u) = p_user.clone() {
|
||||||
|
if let Some(g) = p_group.clone() {
|
||||||
|
execs.push((
|
||||||
|
u,
|
||||||
|
g,
|
||||||
|
p_groups.clone(),
|
||||||
|
exec.to_owned(),
|
||||||
|
vec![],
|
||||||
|
working_dir.clone(),
|
||||||
|
env_changes.clone(),
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
eprintln!(
|
||||||
|
"Invalid line: '{line}'. Expected 'exec ...' only after 'group ...'."
|
||||||
|
);
|
||||||
|
exit(255);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
eprintln!(
|
||||||
|
"Invalid line: '{line}'. Expected 'exec ...' only after 'user ...'."
|
||||||
|
);
|
||||||
|
exit(255);
|
||||||
|
}
|
||||||
|
} else if let Some(arg) = line.strip_prefix("arg ") {
|
||||||
|
if let Some(exec) = execs.last_mut() {
|
||||||
|
exec.4.push(arg.to_owned());
|
||||||
|
} else {
|
||||||
|
eprintln!(
|
||||||
|
"Invalid line: '{line}'. Expected 'arg ...' only after 'exec ...'."
|
||||||
|
);
|
||||||
|
exit(255);
|
||||||
|
}
|
||||||
|
} else if let Some(args) = line.strip_prefix("args ") {
|
||||||
|
if let Some(exec) = execs.last_mut() {
|
||||||
|
if args == "all" {
|
||||||
|
exec.4.extend(&mut cli_args);
|
||||||
|
} else if let Some(v) = args.parse::<usize>().ok().filter(|v| *v > 0) {
|
||||||
|
for _ in 0..v {
|
||||||
|
if let Some(arg) = cli_args.next() {
|
||||||
|
exec.4.push(arg);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
eprintln!(
|
||||||
|
"Invalid line: '{line}'. Expected 'args all' or 'args <integer>', where <integer> >= 1."
|
||||||
|
);
|
||||||
|
exit(255);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
eprintln!(
|
||||||
|
"Invalid line: '{line}'. Expected 'args ...' only after 'exec ...'."
|
||||||
|
);
|
||||||
|
exit(255);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
eprintln!(
|
||||||
|
"Invalid line: '{line}'. Expected 'allow ...', 'user ...', 'group ...', 'groups = ...', 'groups + ...', 'working-dir ...', 'env ...', 'exec ...', 'arg ...', 'args', or 'new'."
|
||||||
|
);
|
||||||
|
exit(255);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if allowed {
|
||||||
|
for (u, g, gs, exec, args, working_dir, env_changes) in execs {
|
||||||
|
if let Some(u) = u.as_ref()
|
||||||
|
.map(|v| Some(*v))
|
||||||
|
.unwrap_or_else(|u| users::get_user_by_name(&u).map(|v| v.uid()))
|
||||||
|
{
|
||||||
|
if let Some(g) = g.as_ref().map(|v| Some(*v)).unwrap_or_else(|g| {
|
||||||
|
users::get_group_by_name(&g).map(|v| v.gid())
|
||||||
|
}) {
|
||||||
|
if Path::new(&exec).is_absolute() {
|
||||||
|
let user = users::get_user_by_uid(u);
|
||||||
|
let mut cmd = Command::new(&exec);
|
||||||
|
cmd.uid(u).gid(g).args(&args);
|
||||||
|
if let Some(user) = &user {
|
||||||
|
cmd.env("HOME", user.home_dir());
|
||||||
|
} else {
|
||||||
|
cmd.env_remove("HOME");
|
||||||
|
}
|
||||||
|
cmd.groups(gs.iter().filter_map(|g| if let Some(g) = g.as_ref().map(|v| Some(*v)).unwrap_or_else(|g| users::get_group_by_name(g).map(|g| g.gid())) { Some(g) } else {
|
||||||
|
eprintln!("Group '{}' not found, not adding it to this exec.", g.as_ref().unwrap_err());
|
||||||
|
None
|
||||||
|
}).collect::<Vec<_>>().as_slice());
|
||||||
|
if let Some(wd) = working_dir {
|
||||||
|
cmd.current_dir(wd);
|
||||||
|
} else if let Some(user) = &user {
|
||||||
|
cmd.current_dir(user.home_dir());
|
||||||
|
}
|
||||||
|
for env_change in env_changes {
|
||||||
|
match env_change {
|
||||||
|
None => {
|
||||||
|
cmd.env_clear();
|
||||||
|
}
|
||||||
|
Some((key, None)) => {
|
||||||
|
cmd.env_remove(key);
|
||||||
|
}
|
||||||
|
Some((key, Some(val))) => {
|
||||||
|
cmd.env(key, val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
current_command.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||||
|
match cmd.status() {
|
||||||
|
Ok(_) => (),
|
||||||
|
Err(e) => {
|
||||||
|
let exec_ws = if exec.contains(char::is_whitespace) {
|
||||||
|
"\""
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
};
|
||||||
|
eprintln!(
|
||||||
|
"Error running command '{exec_ws}{}{exec_ws}{}': {e}", exec.replace('\\', "\\\\").replace('"', "\\\""), args.iter().map(|a| {
|
||||||
|
let arg_ws = if a.contains(char::is_whitespace) {
|
||||||
|
"\""
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
};
|
||||||
|
format!(" {arg_ws}{}{arg_ws}", a.replace('\\', "\\\\").replace('"', "\\\""))
|
||||||
|
|
||||||
|
}).collect::<String>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
current_command.store(false, std::sync::atomic::Ordering::Relaxed);
|
||||||
|
} else {
|
||||||
|
eprintln!("Exec path '{exec}' must be an absolute path! skipping this exec.");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
eprintln!(
|
||||||
|
"Group '{}' not found, skipping this exec!",
|
||||||
|
g.unwrap_err()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
eprintln!(
|
||||||
|
"User '{}' not found, skipping this exec!",
|
||||||
|
u.unwrap_err()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
eprintln!("You are not allowed to use this exec!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Couldn't read exec file '{exec}': {e}");
|
||||||
|
exit(255);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
eprintln!("Exec file '{exec}' not found");
|
||||||
|
exit(255);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
eprintln!(
|
||||||
|
"Mandatory argument missing or empty: exec (path in and relative to /etc/d26run/exec/)"
|
||||||
|
);
|
||||||
|
exit(255);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_first_exec(
|
||||||
|
path: &(impl AsRef<Path> + ?Sized),
|
||||||
|
ex: &str,
|
||||||
|
mut exec: std::str::Split<char>,
|
||||||
|
) -> Option<std::fs::DirEntry> {
|
||||||
|
for conf in match std::fs::read_dir(path.as_ref()) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!(
|
||||||
|
"Couldn't list files in {:?}: {e}",
|
||||||
|
path.as_ref().to_string_lossy()
|
||||||
|
);
|
||||||
|
exit(255);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.filter_map(Result::ok)
|
||||||
|
{
|
||||||
|
if conf.file_name().as_os_str() == std::ffi::OsStr::new(ex) {
|
||||||
|
if let Some(ex2) = exec.next() {
|
||||||
|
return find_first_exec(&conf.path(), ex2, exec);
|
||||||
|
} else {
|
||||||
|
return Some(conf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user