su {block of code.}

G

Guido De Rosa

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
 
T

Tony Arcieri

[Note: parts of this message were removed to make it a legal post.]

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

Brian Candler

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)
 
R

Robert Klemme

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?

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
 
G

Guido De Rosa

Brian Candler wrote in post #955241:
Tony Arcieri wrote in post #955183:

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...
 
G

Guido De Rosa

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!'
 
T

Tony Arcieri

[Note: parts of this message were removed to make it a legal post.]

Tony Arcieri wrote in post #955183:

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.
 
G

Guido De Rosa

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.
 
T

Tony Arcieri

[Note: parts of this message were removed to make it a legal post.]

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.
 
G

Guido De Rosa

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.
 
B

Brian Candler

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.
 
G

Guido De Rosa

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 :)
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:

http://github.com/gderosa/rubysu/blob/5fab1503fdaac85cb3876b76cd16e3422e83df73/libexec/server.rb#L13

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... :)
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,982
Messages
2,570,186
Members
46,739
Latest member
Clint8040

Latest Threads

Top