drbservice 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data.tar.gz.sig +2 -0
- data/.gemtest +0 -0
- data/ChangeLog +249 -0
- data/History.rdoc +4 -0
- data/LICENSE +27 -0
- data/Manifest.txt +18 -0
- data/README.rdoc +74 -0
- data/Rakefile +38 -0
- data/examples/homedirservice.rb +110 -0
- data/examples/rubyversion.rb +26 -0
- data/lib/drb/authsslprotocol.rb +55 -0
- data/lib/drbservice.rb +208 -0
- data/lib/drbservice/ldapauth.rb +200 -0
- data/lib/drbservice/passwordauth.rb +58 -0
- data/lib/drbservice/utils.rb +426 -0
- data/spec/drb/authsslprotocol_spec.rb +76 -0
- data/spec/drbservice/ldapauth_spec.rb +382 -0
- data/spec/drbservice/passwordauth_spec.rb +141 -0
- data/spec/drbservice_spec.rb +168 -0
- data/spec/lib/helpers.rb +108 -0
- metadata +166 -0
- metadata.gz.sig +2 -0
@@ -0,0 +1,26 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'drbservice'
|
4
|
+
require 'drbservice/passwordauth'
|
5
|
+
|
6
|
+
# An example service that just returns the version of Ruby running on the
|
7
|
+
# current machine.
|
8
|
+
class RubyVersionService < DRbService
|
9
|
+
include DRbService::PasswordAuthentication
|
10
|
+
|
11
|
+
service_password '6d4bf8ac6490219f4f8807dad066d742f39a2d25501ae66d650cb647cd758979'
|
12
|
+
|
13
|
+
### Fetch the version of Ruby running this service as a vector of
|
14
|
+
### three network-byte-order shorts.
|
15
|
+
def ruby_version
|
16
|
+
return RUBY_VERSION.split( /\./, 3 ).map( &:to_i ).pack( 'n*' )
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
RubyVersionService.start(
|
22
|
+
:ip => '127.0.0.1',
|
23
|
+
:port => 4848,
|
24
|
+
:certfile => 'service.pem',
|
25
|
+
:keyfile => 'service.pem' )
|
26
|
+
|
@@ -0,0 +1,55 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'uri'
|
4
|
+
|
5
|
+
require 'drb'
|
6
|
+
require 'drb/ssl'
|
7
|
+
|
8
|
+
module DRb
|
9
|
+
|
10
|
+
# A DRb protocol implementation that provides an authenticated, encrypted channel
|
11
|
+
# for DRb services.
|
12
|
+
class DRbAuthenticatedSSLSocket < DRbSSLSocket
|
13
|
+
|
14
|
+
# The scheme of URIs which specify this protocol
|
15
|
+
SCHEME = 'drbauthssl'
|
16
|
+
|
17
|
+
|
18
|
+
### Parse a drbauthssl:// URI. Accepts a String, a URI, or any object that responds to
|
19
|
+
### #host, #port, and #query.
|
20
|
+
### Return the values from the URI as an Array of the form: [ host, port, optionhash ].
|
21
|
+
### Raises DRbBadScheme if the +uri+ is not a +drbauthssl+ URI.
|
22
|
+
### Raises DRbBadURI if the +uri+ is missing the port number.
|
23
|
+
def self::parse_uri( uri )
|
24
|
+
uri = URI( uri ) unless uri.respond_to?( :host )
|
25
|
+
raise DRbBadScheme, "not a #{SCHEME} URI: %p" % [ uri ] unless
|
26
|
+
uri.scheme == SCHEME
|
27
|
+
raise DRbBadURI, "missing the port number" unless
|
28
|
+
uri.port && uri.port.to_i.nonzero?
|
29
|
+
|
30
|
+
return [ uri.host, uri.port.to_i, uri.query ]
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
### Open a client connection to the server at +uri+, using configuration
|
35
|
+
### +config+. Return a protocol instance for this connection.
|
36
|
+
def self::open( uri, config )
|
37
|
+
host, port, option = self.parse_uri( uri )
|
38
|
+
host.untaint
|
39
|
+
port.untaint
|
40
|
+
soc = TCPSocket.open( host, port )
|
41
|
+
ssl_conf = DRb::DRbSSLSocket::SSLConfig.new( config )
|
42
|
+
ssl_conf.setup_ssl_context
|
43
|
+
ssl = ssl_conf.connect( soc )
|
44
|
+
self.new( uri, ssl, ssl_conf, true )
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
end # class DRbAuthenticatedSSLSocket
|
49
|
+
|
50
|
+
|
51
|
+
DRbProtocol.add_protocol( DRbAuthenticatedSSLSocket )
|
52
|
+
|
53
|
+
end # module DRb
|
54
|
+
|
55
|
+
|
data/lib/drbservice.rb
ADDED
@@ -0,0 +1,208 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'drb'
|
4
|
+
require 'drb/ssl'
|
5
|
+
|
6
|
+
|
7
|
+
# A base class for DRb-based services. Concrete subclasses must define the service API by
|
8
|
+
# declaring public methods. By default, any public methods are hidden until the client
|
9
|
+
# authenticates. You can optionally declare a subset of its API that is # accessible before
|
10
|
+
# authentication by wrapping them in an 'unguarded' block. # See DRbService::unguarded for
|
11
|
+
# more details.
|
12
|
+
class DRbService
|
13
|
+
require 'drbservice/utils'
|
14
|
+
include DRbUndumped,
|
15
|
+
DRbService::Logging
|
16
|
+
|
17
|
+
# Library version
|
18
|
+
VERSION = '1.0.4'
|
19
|
+
|
20
|
+
# Version-control revision
|
21
|
+
REVISION = %$Revision: 59c8e5acd8bb $
|
22
|
+
|
23
|
+
# The default IP address to listen on
|
24
|
+
DEFAULT_IP = '127.0.0.1'
|
25
|
+
|
26
|
+
# The default port to listen on
|
27
|
+
DEFAULT_PORT = 4848
|
28
|
+
|
29
|
+
# The default path to the service cert, relative to the current directory
|
30
|
+
DEFAULT_CERTNAME = 'service-cert.pem'
|
31
|
+
|
32
|
+
# The default path to the service key, relative to the current directory
|
33
|
+
DEFAULT_KEYNAME = 'service-key.pem'
|
34
|
+
|
35
|
+
# The default values for the drbservice config hash
|
36
|
+
DEFAULT_CONFIG = {
|
37
|
+
:ip => DEFAULT_IP,
|
38
|
+
:port => DEFAULT_PORT,
|
39
|
+
:certfile => DEFAULT_CERTNAME,
|
40
|
+
:keyfile => DEFAULT_KEYNAME,
|
41
|
+
}
|
42
|
+
|
43
|
+
# The container for obscured methods
|
44
|
+
class << self
|
45
|
+
attr_reader :real_methods
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
#################################################################
|
50
|
+
### C L A S S M E T H O D S
|
51
|
+
#################################################################
|
52
|
+
|
53
|
+
### Start the DRbService, using the ip, port, and cert information from the given +config+
|
54
|
+
### hash.
|
55
|
+
###
|
56
|
+
### [:ip] the ip to bind to
|
57
|
+
### [:port] the port to listen on
|
58
|
+
### [:certfile] the name of the server's SSL certificate file
|
59
|
+
### [:keyfile] the name of the server's SSL key file
|
60
|
+
###
|
61
|
+
def self::start( config={} )
|
62
|
+
config = DEFAULT_CONFIG.merge( config )
|
63
|
+
|
64
|
+
frontobj = self.new( config )
|
65
|
+
uri = "drbssl://%s:%d" % config.values_at( :ip, :port )
|
66
|
+
|
67
|
+
cert = OpenSSL::X509::Certificate.new( File.read(config[:certfile]) )
|
68
|
+
key = OpenSSL::PKey::RSA.new( File.read(config[:keyfile]) )
|
69
|
+
|
70
|
+
config = {
|
71
|
+
:safe_level => 1,
|
72
|
+
:verbose => true,
|
73
|
+
:SSLCertificate => cert,
|
74
|
+
:SSLPrivateKey => key,
|
75
|
+
}
|
76
|
+
|
77
|
+
DRbService.log.info "Starting %p as a DRbService at %s" % [ self, uri ]
|
78
|
+
server = DRb::DRbServer.new( uri, frontobj, config )
|
79
|
+
DRbService.log.debug " started. Joining the DRb thread."
|
80
|
+
$0 = "%s %s" % [ self.name, uri ]
|
81
|
+
server.thread.join
|
82
|
+
end
|
83
|
+
|
84
|
+
|
85
|
+
### Method-addition callback: Obscure the method +meth+ unless unguarded mode is enabled.
|
86
|
+
def self::method_added( meth )
|
87
|
+
super
|
88
|
+
|
89
|
+
unless self == ::DRbService || meth.to_sym == :initialize
|
90
|
+
if !self.public_instance_methods.collect( &:to_sym ).include?( meth )
|
91
|
+
DRbService.log.debug "Not obsuring %p#%s: not a public method" % [ self, meth ]
|
92
|
+
elsif self.unguarded_mode
|
93
|
+
DRbService.log.debug "Not obscuring %p#%s: unguarded mode." % [ self, meth ]
|
94
|
+
else
|
95
|
+
DRbService.log.debug "Obscuring %p#%s." % [ self, meth ]
|
96
|
+
@real_methods ||= {}
|
97
|
+
@real_methods[ meth.to_sym ] = self.instance_method( meth )
|
98
|
+
remove_method( meth )
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
|
104
|
+
### Inheritance callback: Add a per-class 'unguarded mode' flag to subclasses.
|
105
|
+
def self::inherited( subclass )
|
106
|
+
self.log.debug "Setting @unguarded_mode in %p" % [ subclass ]
|
107
|
+
subclass.instance_variable_set( :@unguarded_mode, false )
|
108
|
+
super
|
109
|
+
end
|
110
|
+
|
111
|
+
|
112
|
+
### Declare some service methods that can be called without authentication in
|
113
|
+
### the provided block.
|
114
|
+
def self::unguarded
|
115
|
+
self.unguarded_mode = true
|
116
|
+
yield
|
117
|
+
ensure
|
118
|
+
self.unguarded_mode = false
|
119
|
+
end
|
120
|
+
|
121
|
+
|
122
|
+
### Return the library's version string
|
123
|
+
def self::version_string( include_buildnum=false )
|
124
|
+
vstring = "%s %s" % [ self.name, VERSION ]
|
125
|
+
vstring << " (build %s)" % [ REVISION[/.*: ([[:xdigit:]]+)/, 1] || '0' ] if include_buildnum
|
126
|
+
return vstring
|
127
|
+
end
|
128
|
+
|
129
|
+
|
130
|
+
### Class accessors
|
131
|
+
class << self
|
132
|
+
|
133
|
+
# The unguarded mode flag -- instance methods defined while this flag is set
|
134
|
+
# will not be hidden
|
135
|
+
attr_accessor :unguarded_mode
|
136
|
+
|
137
|
+
end
|
138
|
+
|
139
|
+
|
140
|
+
#################################################################
|
141
|
+
### I N S T A N C E M E T H O D S
|
142
|
+
#################################################################
|
143
|
+
|
144
|
+
### Create a new instance of the service.
|
145
|
+
### Raises a ScriptError if DRbService is instantiated directly.
|
146
|
+
def initialize( config={} )
|
147
|
+
raise ScriptError,
|
148
|
+
"can't instantiate #{self.class} directly: please subclass it instead" if
|
149
|
+
self.class == DRbService
|
150
|
+
@authenticated = false
|
151
|
+
end
|
152
|
+
|
153
|
+
|
154
|
+
######
|
155
|
+
public
|
156
|
+
######
|
157
|
+
|
158
|
+
### Return a human-readable representation of the object.
|
159
|
+
def inspect
|
160
|
+
return "#<%s:0x%0x>" % [ self.class, self.__id__ * 2 ]
|
161
|
+
end
|
162
|
+
|
163
|
+
|
164
|
+
### Returns +true+ if the client has successfully authenticated.
|
165
|
+
def authenticated?
|
166
|
+
return @authenticated ? true : false
|
167
|
+
end
|
168
|
+
|
169
|
+
|
170
|
+
### Returns +true+ if the client has successfully authenticated and is authorized
|
171
|
+
### to use the service. By default, authentication is sufficient for authorization;
|
172
|
+
### to specify otherwise, override this method in your service's subclass or
|
173
|
+
### include an auth mixin that provides one.
|
174
|
+
def authorized?
|
175
|
+
return self.authenticated?
|
176
|
+
end
|
177
|
+
|
178
|
+
|
179
|
+
### Default authentication implementation -- always fails. You'll need to include
|
180
|
+
### one of the authentication modules or provide your own #authenticate method in
|
181
|
+
### your subclass.
|
182
|
+
def authenticate( *args )
|
183
|
+
self.log.error "authentication failure (fallback method)"
|
184
|
+
raise SecurityError, "authentication failure"
|
185
|
+
end
|
186
|
+
|
187
|
+
|
188
|
+
#########
|
189
|
+
protected
|
190
|
+
#########
|
191
|
+
|
192
|
+
### Handle calls to guarded methods by requiring the authentication flag be
|
193
|
+
### set if there is a password set.
|
194
|
+
def method_missing( sym, *args )
|
195
|
+
return super unless body = self.class.real_methods[ sym ]
|
196
|
+
|
197
|
+
if self.authorized?
|
198
|
+
return body.clone.bind( self ).call( *args )
|
199
|
+
else
|
200
|
+
self.log.error "Guarded method %p called without authentication!" % [ sym ]
|
201
|
+
raise SecurityError, "not authenticated"
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
|
206
|
+
end # class DRbService
|
207
|
+
|
208
|
+
|
@@ -0,0 +1,200 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# coding: utf-8
|
3
|
+
|
4
|
+
require 'treequel'
|
5
|
+
require 'drbservice'
|
6
|
+
|
7
|
+
# An authentication strategy for DRbService -- set a password via a
|
8
|
+
# class method.
|
9
|
+
module DRbService::LDAPAuthentication
|
10
|
+
|
11
|
+
### Methods added to including classes when LDAPAuthentication is
|
12
|
+
### mixed in.
|
13
|
+
module ClassMethods
|
14
|
+
|
15
|
+
# The default attributes of a search
|
16
|
+
DEFAULT_SEARCH = {
|
17
|
+
:filter => 'uid=%s',
|
18
|
+
:base => nil,
|
19
|
+
:scope => :sub,
|
20
|
+
}.freeze
|
21
|
+
|
22
|
+
|
23
|
+
### Extension callback -- add the necessary class instance variables
|
24
|
+
### to extended modules.
|
25
|
+
def self::extended( mod )
|
26
|
+
super
|
27
|
+
mod.instance_variable_set( :@ldap_uri, nil )
|
28
|
+
mod.instance_variable_set( :@ldap_dn, nil )
|
29
|
+
mod.instance_variable_set( :@ldap_dn_search, DEFAULT_SEARCH.dup )
|
30
|
+
mod.instance_variable_set( :@ldap_authz_callback, nil )
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
### Set the URI of the LDAP server to bind to for authentication
|
35
|
+
def ldap_uri( uri=nil )
|
36
|
+
@ldap_uri = uri if uri
|
37
|
+
return @ldap_uri
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
### Set the pattern to use when creating the DN to use when binding.
|
42
|
+
def ldap_dn( pattern=nil )
|
43
|
+
@ldap_dn = pattern if pattern
|
44
|
+
return @ldap_dn
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
### Set a filter that is used when searching for an account to bind
|
49
|
+
### as.
|
50
|
+
def ldap_dn_search( filter=nil, options={} )
|
51
|
+
if filter
|
52
|
+
@ldap_dn_search ||= {}
|
53
|
+
@ldap_dn_search[:filter] = filter
|
54
|
+
@ldap_dn_search[:base] = options[:base] if options[:base]
|
55
|
+
@ldap_dn_search[:scope] = options[:scope] if options[:scope]
|
56
|
+
end
|
57
|
+
|
58
|
+
return @ldap_dn_search
|
59
|
+
end
|
60
|
+
|
61
|
+
|
62
|
+
### Register a function to call when the user successfully binds to the
|
63
|
+
### directory to check for authorization. It will be called with the
|
64
|
+
### Treequel::Branch of the bound user and the Treequel::Directory they
|
65
|
+
### are bound to. Returning +true+ from this function will cause
|
66
|
+
### authorization to succeed, while returning a false value causes it to
|
67
|
+
### fail.
|
68
|
+
def ldap_authz_callback( callable=nil, &block )
|
69
|
+
if callable
|
70
|
+
@ldap_authz_callback = callable
|
71
|
+
elsif block
|
72
|
+
@ldap_authz_callback = block
|
73
|
+
end
|
74
|
+
|
75
|
+
return @ldap_authz_callback
|
76
|
+
end
|
77
|
+
|
78
|
+
end # module ClassMethods
|
79
|
+
|
80
|
+
|
81
|
+
### Overridden mixin callback -- add the ClassMethods to the including class
|
82
|
+
def self::included( klass )
|
83
|
+
super
|
84
|
+
klass.extend( ClassMethods )
|
85
|
+
end
|
86
|
+
|
87
|
+
|
88
|
+
### Set up some instance variables used by the mixin.
|
89
|
+
def initialize( *args )
|
90
|
+
super
|
91
|
+
@authenticated = false
|
92
|
+
@authuser = nil
|
93
|
+
@authuser_branch = nil
|
94
|
+
end
|
95
|
+
|
96
|
+
|
97
|
+
# the username of the authenticated user
|
98
|
+
attr_reader :authuser
|
99
|
+
|
100
|
+
# the Treequel::Branch of the authenticated user
|
101
|
+
attr_reader :authuser_branch
|
102
|
+
|
103
|
+
|
104
|
+
### Authenticate using the specified +password+, calling the provided block if
|
105
|
+
### authentication succeeds. Raises a SecurityError if authentication fails. If
|
106
|
+
### no password is set, the block is called regardless of what the +password+ is.
|
107
|
+
def authenticate( user, password )
|
108
|
+
uri = self.class.ldap_uri
|
109
|
+
self.log.debug "Connecting to %p for authentication" % [ uri ]
|
110
|
+
directory = Treequel.directory( uri )
|
111
|
+
self.log.debug " finding LDAP record for: %p" % [ user ]
|
112
|
+
user_branch = self.find_auth_user( directory, user ) or
|
113
|
+
return super
|
114
|
+
|
115
|
+
self.log.debug " binding as %p (%p)" % [ user, user_branch ]
|
116
|
+
directory.bind_as( user_branch, password )
|
117
|
+
self.log.debug " bound successfully..."
|
118
|
+
|
119
|
+
@authenticated = true
|
120
|
+
|
121
|
+
if cb = self.class.ldap_authz_callback
|
122
|
+
self.log.debug " calling authorization callback..."
|
123
|
+
|
124
|
+
unless self.call_authz_callback( cb, user_branch, directory )
|
125
|
+
msg = " authorization failed for: %s" % [ user_branch ]
|
126
|
+
self.log.debug( msg )
|
127
|
+
raise SecurityError, msg
|
128
|
+
end
|
129
|
+
|
130
|
+
self.log.debug " authorization succeeded."
|
131
|
+
end
|
132
|
+
|
133
|
+
@authuser = user
|
134
|
+
@authuser_branch = user
|
135
|
+
yield
|
136
|
+
|
137
|
+
rescue LDAP::ResultError => err
|
138
|
+
self.log.error " authentication failed for %p" % [ user_branch || user ]
|
139
|
+
raise SecurityError, "authentication failure"
|
140
|
+
|
141
|
+
ensure
|
142
|
+
@authuser = nil
|
143
|
+
@authuser_branch = nil
|
144
|
+
@authenticated = false
|
145
|
+
end
|
146
|
+
|
147
|
+
|
148
|
+
#########
|
149
|
+
protected
|
150
|
+
#########
|
151
|
+
|
152
|
+
### Find the specified +username+ entry in the given +directory+, which should be a
|
153
|
+
### Treequel::Directory. Returns the Treequel::Branch for the first found user, if one
|
154
|
+
### was found, or +nil+ if no such user was found.
|
155
|
+
def find_auth_user( directory, username )
|
156
|
+
self.log.debug "Finding the user to bind as."
|
157
|
+
|
158
|
+
if dnpattern = self.class.ldap_dn
|
159
|
+
self.log.debug " using DN pattern %p" % [ dnpattern ]
|
160
|
+
dn = dnpattern % [ username ]
|
161
|
+
user = Treequel::Branch.new( directory, dn )
|
162
|
+
return user.exists? ? user : nil
|
163
|
+
|
164
|
+
else
|
165
|
+
dnsearch = self.class.ldap_dn_search
|
166
|
+
usersearch = dnsearch[:base] ?
|
167
|
+
Treequel::Branch.new( directory, dnsearch[:base] ) :
|
168
|
+
directory.base
|
169
|
+
usersearch = usersearch.scope( dnsearch[:scope] ) if dnsearch[:scope]
|
170
|
+
usersearch = usersearch.filter( dnsearch[:filter] % [username] )
|
171
|
+
|
172
|
+
self.log.debug " using filter: %s" % [ usersearch ]
|
173
|
+
if user = usersearch.first
|
174
|
+
self.log.debug " search found: %s" % [ user ]
|
175
|
+
return user
|
176
|
+
else
|
177
|
+
self.log.error " search returned no entries" % [ usersearch ]
|
178
|
+
return nil
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
end
|
183
|
+
|
184
|
+
|
185
|
+
### Call the authorization callback with the given +user+ and +directory+ and
|
186
|
+
### return true if it indicates authorization was successful.
|
187
|
+
def call_authz_callback( callback, user, directory )
|
188
|
+
|
189
|
+
if callback.respond_to?( :call )
|
190
|
+
return true if callback.call( user, directory )
|
191
|
+
|
192
|
+
else callback = self.method( callback )
|
193
|
+
return true if callback.call( user, directory )
|
194
|
+
end
|
195
|
+
|
196
|
+
end
|
197
|
+
|
198
|
+
end # DRbService::LDAPAuthentication
|
199
|
+
|
200
|
+
|