Last post I introduced some code that allowed external commands to be run conveniently in any user context desired. Specifically, in the case of that code, I allowed it to be run with a given effective group and user ID as well as a passed-in path.
This post extends on the previous by cleaning up the code and by rendering the provided class more Ruby-like.
The problem with the code provided is that it doesn't do some things well, and it does other things well, but not in the Ruby way. The new code is attached in its entirety to the bottom of this post and we'll be referring to it in pieces here.
So, first problem: this code simply won't work properly if you're not the root user. I'd rather have this reported up-front when making an execution context rather than deeper in the code when trying to use it. I'd also like the error to be specific to the problem rather than something useless like "process failed". So the new initialize function is this:
def initialize username, path = nil
raise SecurityError , "must be used from root privileges", caller if Process.euid != 0
require 'etc'
@path = path
@username = username
@userinfo = Etc.getpwnam(username)
end
Note the other change here. I no longer have a specific function to extract just the uid and gid of the given user. I instead take back all the stuff from
getpwnam() and store it. This was done mostly to just get rid of an unnecessary function given that the user info doesn't take much space and it's just as easy to set, say, a
gid to
@userinfo.gid as it is to set it to
@gid.
I'm all about the reduced typing.
Another (cosmetic) change is that the
store and
restore methods have been made private. Further the actual code that swapped the context for execution has been moved to a method and made private as well. This is simple reorganisation stuff, however. Here's the biggest change:
def execute
store
switch_context
begin
yield
ensure
restore
end
end
This code is far more the
Ruby Way than my previous efforts (although I will undoubtedly find other ways to improve on it as time goes on). Instead of taking a string, swapping context and then using the
system call to execute it, this code works with a block. It stores the current execution context, switches to the target context, then yields to the block ensuring that anything run is run in the target's context. When done, forced through the
ensure clause, it puts the context back to where it's supposed to be and continues on its merry way.
This has a couple of benefits. First, passing in a block to process is far more
Ruby-like than evaluating strings with function calls. Second, it permits more than one statement to be executed at a time in the new context and avoids all the swapping in and out of contexts.
There is a drawback, however, for my purposes (I wrote this, recall, specifically to execute external utilities in arbitrary permissions). Now I have to make explicit calls to
system in my passed-in block. This, to me, isn't that great a problem. If it annoys me, I'll go digging for a
Ruby-like solution to that one as well. (Note the supreme confidence that there is a convenient way in
Ruby to do this....)
module UserWrapper
class Context
def initialize username, path = nil
raise SecurityError , "must be used from root privileges", caller if Process.euid != 0
require 'etc'
@path = path
@username = username
@userinfo = Etc.getpwnam(username)
end
def to_s
"username: #{@username}\nuid: #{@userinfo.uid}, gid: #{@userinfo.gid}\npath: #{@path}"
end
def execute
store
switch_context
begin
yield
ensure
restore
end
end
private
def store
@cuid = Process::Sys.geteuid
@cgid = Process::Sys.getegid
@cpath = ENV["PATH"]
end
def switch_context
Process::Sys.setegid @userinfo.gid
Process::Sys.seteuid @userinfo.uid
ENV["PATH"] = @path if @path
end
def restore
Process::Sys.seteuid @cuid
Process::Sys.setegid @cgid
ENV["PATH"] = @cpath
end
end
end