Using the typestate pattern to design a privilege model

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.

If the implementation of the common functions are literally identical, then you can model it like this playground example

This assumes that all of the superuser info is present at runtime even for a regular user.
Does this fit your use case?

Thanks for the idea, but I would like to avoid storing all the superuser info for regular users. In my case, Root and User are not empty enums, but rather structs which contain data. So I store the superuser data separately from the user data.
Maybe the current design is ok because it makes it clear what operations are possible as a superuser and what operations are possible as user.

That pattern can also work with non-empty types, since it's wrapped in PhantomData to avoid having an actual instance. The empty enum is a common trick to prevent creating instances of the market types when they don't represent any data on their own. That said, it may still be a good idea to keep separate data storing types and markers for semantical reasons. You can have Root for storing data and RootTag for tagging types, for example.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.