I'm working on a rust version of passwd and I would like to model the permissions of the user (whether he's root or not) using the typestate pattern. My first attempt is here, but I'm not entirely fond of it.
If a user is not root, he can only view his own shadow entry. If he's root, then he can view any user's shadow entry.
I want to keep a clear distinction between the operations allowed by the root user and the operations allowed by an unprivileged user. The typestate pattern allows me to define a single entry point into the root user's operations and this makes it easy to verify right there whether the user is root or not.
But if I keep the operations disjoint, then I need to duplicate common functionality: I can view my own shadow entry when I'm unprivileged, but the root user can also view my shadow entry.
In C, it's handled like this:
/*
* If the UID of the user does not match the current real UID,
* check if I'm root.
*/
if (!amroot && (pw->pw_uid != getuid ())) {
(void) fprintf (stderr,
_("%s: You may not view or modify password information for %s.\n"),
Prog, name);
SYSLOG ((LOG_WARN,
"%s: can't view or modify password information for %s",
Prog, name));
closelog ();
exit (E_NOPERM);
}
I would like to have a better model for this in Rust. This is what I have now:
if let Ok(shadow_file) = ShadowFile::new_as_root() {
let user_entry = shadow_file.get_user_entry(&username)?;
println!("{user_entry}");
} else {
let shadow_file = ShadowFile::new_as_user(&username)?;
let user_entry = shadow_file.get_user_entry();
println!("{user_entry}");
return Ok(());
}
First, I try the operation as root, and if it fails (because the user is not root, probably I need to check for a permission denied error here), I try the operation as the current user.
One way to avoid this duplication is by using polymorphism, defining a trait with two implementors: one privileged and one unprivileged. Then I can have a new function returning a trait object: if the user is privileged, it will return the privileged implementation, otherwise it will return the unprivileged one. But it feels awkward to implement a trait when clearly the unprivileged user can only implement a small subset of the privileged user's operations. The user's implementation would just return PERMISSION_DENIED (or this could be the default implementation of the trait's functions).
You can check out a POC with this approach here.
I would also like to avoid checks like:
if i_am_root {
....}
else {
...}
in my binary. I would like all the permission checks to be handled by the library.
The typestate pattern seems idiomatic and I wonder if there's something I can do to improve my current code or if you have any other ideas.