Su {block of code.}

Hi!

Scenario: on a Unix-like system I run a ruby program under a
non-privileged user. Occasionally I need to gain root privileges.

For external commands it’s just a matter of installing sudo, editing
/etc/sudoers properly, and going:

system "sudo command..."

But what if I want to do the same for a block of code?

Something like this would be really, really cool:

include Sudo
su do
  # ruby code...
end

There’s a way to get this? A gem? Any idea on how to implement it?

Thanks,
Guido

I’d suggest spawning a new Ruby instance which runs under sudo and
talking
to it with DRb

Tony A. wrote in post #955183:

I’d suggest spawning a new Ruby instance which runs under sudo and
talking
to it with DRb

Or use IO.popen and talk over stdin/stdout.

(It would be cool if the DRb protocol could be piped over stdin/stdout -
I looked into it once but it was actually not easy to modify the
existing DRb code to do that, and it gets hairy with callbacks anyway)

On 10/18/2010 06:32 PM, Guido De Rosa wrote:

But what if I want to do the same for a block of code?

Something like this would be really, really cool:

 include Sudo
 su do
   # ruby code...
 end

There’s a way to get this? A gem? Any idea on how to implement it?

You cannot do that in a single process since Unix permissions and user
identity are managed on a per process basis. And then there’s the issue
of authentication, i.e. you probably need a user to enter his password.

Kind regards

robert

Brian C. wrote in post #955241:

Tony A. wrote in post #955183:

I’d suggest spawning a new Ruby instance which runs under sudo and
talking
to it with DRb

Or use IO.popen and talk over stdin/stdout.

(It would be cool if the DRb protocol could be piped over stdin/stdout -
I looked into it once but it was actually not easy to modify the
existing DRb code to do that, and it gets hairy with callbacks anyway)

Well, if you worry about TCP not being optimal inside the same machine,
DRb may use unix sockets…

Tony A. wrote in post #955183:

I’d suggest spawning a new Ruby instance which runs under sudo and
talking
to it with DRb

My big concern comes directly from DRb doc:

“As blocks (or rather the Proc objects that represent them) are not
marshallable, the block executes in the local, not the remote, context.”

Following your suggestion, I thought about a solution based on a DRb
server run as root:

require ‘drb/drb’

URI=“druby://localhost:8787”

class Executor
def execute(&blk)
blk.call
end
end

DRb.start_service(URI, Executor.new)

DRb.thread.join

The client code being executed as a non-privileged user:

require ‘drb/drb’

SERVER_URI=“druby://localhost:8787”

DRb.start_service

executor = DRbObject.new_with_uri(SERVER_URI)

executor.execute do
# a file writable only by root
File.open(’/TEST’, ‘w’){|f| f.puts ‘hello!’}
end

BUT I get a permission denied!

On the other if I don’t use Procs, everything works fine:

server.rb:

class FileWriter
def write(file, str)
File.open(file, ‘w’){|f| f.puts str}
end
end

DRb.start_service(URI, FileWriter.new)

client.rb:

writer = DRbObject.new_with_uri(SERVER_URI)

writer.write ‘/TEST’, ‘hello!’

On Mon, Oct 18, 2010 at 4:18 PM, Guido De Rosa
[email protected]wrote:

Tony A. wrote in post #955183:

I’d suggest spawning a new Ruby instance which runs under sudo and
talking
to it with DRb

My big concern comes directly from DRb doc:

“As blocks (or rather the Proc objects that represent them) are not
marshallable, the block executes in the local, not the remote, context.”

That’s not an issue here. They’re describing how blocks execute in the
local
context. Here we specifically want a block to run in the scop of the
remote
object.

You could obtain an object over DRb and instance eval the block in the
scope
of the DRb object.

Any methods executed would be called on the DRb object.

Tony A.:

You could obtain an object over DRb and instance eval the block in the
scope
of the DRb object.

Any methods executed would be called on the DRb object.

Still doesn’t work.

server.rb, run as root:

DRb.start_service(URI, self) # export the ‘main’ Object

client.rb, as a normal user:

o = DRbObject.new_with_uri(SERVER_URI) #=> main

o.instance_eval do
::File.open(’/TEST’, ‘w’){|f| f.puts ‘hello’}
end

I keep getting a Permission Denied error (Errno::EACCES)

And if I change ‘/TEST’ into ‘/tmp/TEST’, It’s clearly seen that the
file has been created by the normal user, not by root.

G.

On Mon, Oct 18, 2010 at 5:20 PM, Guido De Rosa
[email protected]wrote:

o.instance_eval do
::File.open(‘/TEST’, ‘w’){|f| f.puts ‘hello’}
end

I keep getting a Permission Denied error (Errno::EACCES)

And if I change ‘/TEST’ into ‘/tmp/TEST’, It’s clearly seen that the
file has been created by the normal user, not by root.

Well yes, this isn’t going to work, because you’re talking to the File
singleton object here, not to the main object over DRb.

I guess my question is what exactly are you trying to accomplish? Do you
want a small DSL of commands to work with files as root, or are you
expecting everything to be executed in the context of the setuid root
VM?

If it’s the former, try this:

include FileUtils
cp “somefile”, “anotherfile”

That should operate as expected. Beyond that, you would need to use
ParseTree or ripper to extract the Ruby code you want executed on the
remote
VM or something like that, but then you need to ensure that all the
classes/objects it’s using are actually loaded on the new VM.

For practicality’s sake I’d suggest exposing a small DSL for doing what
you
want to do as root. FileUtils provides everything I’d think you need,
but
perhaps you have a use case I’m not envisioning.

Tony A. wrote in post #955286:

I guess my question is what exactly are you trying to accomplish? Do you
want a small DSL of commands to work with files as root, or are you
expecting everything to be executed in the context of the setuid root
VM?

My immediate, practical need is to deal with files; but in the longer
term it would be nice to develop something more general, as I wrote in
my first post. Or something intermediate, as you will read later.

If it’s the former, try this:

include FileUtils
cp “somefile”, “anotherfile”

Actually this works fine:

server.rb, run as root

DRb.start_service(URI, File)

client.rb, non-root

module Sudo
File = DRbObject.new_with_uri(SERVER_URI)
end

puts Sudo::File.read ‘/etc/shadow’ # only readable by root

It also works with FileUtils instead of Files and probably other classes
and modules.

But what if I want to distribute multiple classes/modules? In general,
what is the proper way to distribute multiple dRuby front objects?

The most obvious solution, to me, was an Array of objects as a front
object.

I tried this:

server.rb

DRb.start_service(URI, [File, FileUtils])

client.rb

module Sudo
File, FileUtils = DRbObject.new_with_uri(SERVER_URI)
end

but, again, It doesn’t work:

client.rb:8:in <module:Sudo>': can't convert DRb::DRbObject to Array (DRb::DRbObject#to_ary gives DRb::DRbUnknown) (TypeError) from client.rb:7:in

So I am compelled to run several DRb server instances?

That should operate as expected. Beyond that, you would need to use
ParseTree or ripper to extract the Ruby code you want executed on the
remote
VM or something like that, but then you need to ensure that all the
classes/objects it’s using are actually loaded on the new VM.

I see… looks like a lot of work…

For practicality’s sake I’d suggest exposing a small DSL for doing what
you
want to do as root. FileUtils provides everything I’d think you need,
but
perhaps you have a use case I’m not envisioning.

As a more flexible alternative, you should be able to say if you want to
Sudo-ize FileUtils or other modules/classes.

A possible API might look like this:

Sudo.autoload :MyClass, ‘mygem/myclass’
Sudo.require ‘fileutils’
Sudo.enable :File, :FileUtils, :MyClass

my_super_object = Sudo::MyClass.new

Sudo::File.open …

Sudo::FileUtils.cp

So you use superuser powers only explicitly when you really need them.

Well, I’ve just started this:

(documentation coming soon… :slight_smile:

G.

Guido De Rosa wrote in post #955389:

File, FileUtils = DRbObject.new_with_uri(SERVER_URI)

end

but, again, It doesn’t work:

client.rb:8:in <module:Sudo>': can't convert DRb::DRbObject to Array (DRb::DRbObject#to_ary gives DRb::DRbUnknown) (TypeError) from client.rb:7:in

That’s just a side-effect of the multiple-assignment syntax (implicit
splat), which only works on real Arrays. Try instead:

front = DRbObject.new(...)
File = front[0]
FileUtils = front[1]

Of course, you better be damned sure that your root DRb server is only
accessible by trusted processes; by default, any user on your machine
will be able to connect to it. (That’s the reason I’d prefer to talk to
the trusted process via a private pipe)

If you are sure you want a root DRb server, I’d be inclined to write one
which exposes a limited set of methods and sanitises their arguments
before doing anything with them (and possibly also requires
authentication) - rather than giving carte-blanche access to File and
FileUtils.

If you are running on a Unix system, then another option you have is to
open a file descriptor in one (trusted) process and pass that open file
descriptor across a socket. That avoids having DRb proxy objects at all.
Have a look at snailgun if you want some sample code which does that;
grep for send_io and recv_io.

Brian C. wrote in post #955751:

Guido De Rosa wrote in post #955389:

File, FileUtils = DRbObject.new_with_uri(SERVER_URI)

end

but, again, It doesn’t work:

client.rb:8:in <module:Sudo>': can't convert DRb::DRbObject to Array (DRb::DRbObject#to_ary gives DRb::DRbUnknown) (TypeError) from client.rb:7:in

That’s just a side-effect of the multiple-assignment syntax (implicit
splat), which only works on real Arrays. Try instead:

front = DRbObject.new(...)
File = front[0]
FileUtils = front[1]

Yep. Thanks :slight_smile:

Of course, you better be damned sure that your root DRb server is only
accessible by trusted processes; by default, any user on your machine
will be able to connect to it. (That’s the reason I’d prefer to talk to
the trusted process via a private pipe)

Yeah, nothing beats the security of anonymous, private pipe… Anyhow, I
set permissions of UNIX socket:

Moreover, I don’t keep a SUID daemon running; instead my approach is
based on starting a DRb server on demand and kill it as soon as it’s no
longer required.

This is not efficient, but imho there are no performance concerns here:
becoming root is something you do occasionally, this is not the
bottleneck.

The usage would look like this:

Sudo::Wrapper.new do |su|
# a sudoed DRb daemon is started under the hood…

puts su[File].read '/etc/shadow' # only readable by root
# ...

end # the daemon is killed

Anyway, if you need a long running thing:

su = Sudo::Wrapper.new

su[an_object].method # acts as root

su.close

If you are sure you want a root DRb server, I’d be inclined to write one
which exposes a limited set of methods and sanitises their arguments
before doing anything with them (and possibly also requires
authentication) - rather than giving carte-blanche access to File and
FileUtils.

See above but, yes, there’s a lot of work still TODO.

If you are running on a Unix system, then another option you have is to
open a file descriptor in one (trusted) process and pass that open file
descriptor across a socket. That avoids having DRb proxy objects at all.
Have a look at snailgun if you want some sample code which does that;
grep for send_io and recv_io.

Very interesting, thanks! And I certainly need to study Unix IPC deeper
and deeper… :slight_smile: