drbservice 1.0.4
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|