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

···

--
Posted via http://www.ruby-forum.com/.

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

···

On Mon, Oct 18, 2010 at 10:32 AM, Guido De Rosa <guidoderosa@gmail.com>wrote:

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

--
Posted via http://www.ruby-forum.com/\.

--
Tony Arcieri
Medioh! A Kudelski Brand

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

···

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

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?

Tony Arcieri 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)

···

--
Posted via http://www.ruby-forum.com/\.

Tony Arcieri 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!'

--
Posted via http://www.ruby-forum.com/\.

Brian Candler wrote in post #955241:

Tony Arcieri 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...

···

--
Posted via http://www.ruby-forum.com/\.

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.

···

On Mon, Oct 18, 2010 at 4:18 PM, Guido De Rosa <guidoderosa@gmail.com>wrote:

Tony Arcieri 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."

--
Tony Arcieri
Medioh! A Kudelski Brand

Tony Arcieri:

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.

···

--
Posted via http://www.ruby-forum.com/\.

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.

···

On Mon, Oct 18, 2010 at 5:20 PM, Guido De Rosa <guidoderosa@gmail.com>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.

--
Tony Arcieri
Medioh! A Kudelski Brand

Tony Arcieri 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 `<main>'

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.

···

--
Posted via http://www.ruby-forum.com/\.

Well, I've just started this:

(documentation coming soon... :slight_smile:

G.

···

--
Posted via http://www.ruby-forum.com/.

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 `<main>'

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.

···

--
Posted via http://www.ruby-forum.com/\.

Brian Candler 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 `<main>'

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:

···

--
Posted via http://www.ruby-forum.com/\.