reauthenticate on reconnect

This commit is contained in:
Kyle Banker 2010-02-25 14:58:32 -05:00
parent c05503d42d
commit c0e8a525bd
8 changed files with 198 additions and 39 deletions

View File

@ -81,6 +81,11 @@ namespace :test do
t.verbose = true t.verbose = true
end end
Rake::TestTask.new(:authentication) do |t|
t.test_files = FileList['test/auxillary/authentication_test.rb']
t.verbose = true
end
task :drop_databases do |t| task :drop_databases do |t|
puts "Dropping test database..." puts "Dropping test database..."
require File.join(File.dirname(__FILE__), 'lib', 'mongo') require File.join(File.dirname(__FILE__), 'lib', 'mongo')

View File

@ -90,8 +90,10 @@ module Mongo
# #
# @core connections # @core connections
def initialize(pair_or_host=nil, port=nil, options={}) def initialize(pair_or_host=nil, port=nil, options={})
@auths = []
if block_given? if block_given?
@nodes, @auths = yield self @nodes = yield self
else else
@nodes = format_pair(pair_or_host, port) @nodes = format_pair(pair_or_host, port)
end end
@ -126,10 +128,7 @@ module Mongo
@options = options @options = options
should_connect = options[:connect].nil? ? true : options[:connect] should_connect = options[:connect].nil? ? true : options[:connect]
if should_connect connect_to_master if should_connect
connect_to_master
authenticate_databases if @auths
end
end end
# Initialize a paired connection to MongoDB. # Initialize a paired connection to MongoDB.
@ -154,7 +153,7 @@ module Mongo
# Block returns an array, the first element being an array of nodes and the second an array # Block returns an array, the first element being an array of nodes and the second an array
# of authorizations for the database. # of authorizations for the database.
new(nil, nil, opts) do |con| new(nil, nil, opts) do |con|
[[con.pair_val_to_connection(nodes[0]), con.pair_val_to_connection(nodes[1])], []] [con.pair_val_to_connection(nodes[0]), con.pair_val_to_connection(nodes[1])]
end end
end end
@ -172,12 +171,68 @@ module Mongo
end end
end end
# Apply each of the saved database authentications.
#
# @return [Boolean] returns true if authentications exist and succeeed, false
# if none exists.
#
# @raise [AuthenticationError] raises an exception if any one
# authentication fails.
def apply_saved_authentication
return false if @auths.empty?
@auths.each do |auth|
self[auth['db_name']].authenticate(auth['username'], auth['password'], false)
end
true
end
# Save an authentication to this connection. When connecting,
# the connection will attempt to re-authenticate on every db
# specificed in the list of auths.
#
# @param [String] db_name
# @param [String] username
# @param [String] password
#
# @return [Hash] a hash representing the authentication just added.
def add_auth(db_name, username, password)
remove_auth(db_name)
auth = {}
auth['db_name'] = db_name
auth['username'] = username
auth['password'] = password
@auths << auth
auth
end
# Remove a saved authentication for this connection.
#
# @param [String] db_name
#
# @return [Boolean]
def remove_auth(db_name)
return unless @auths
if @auths.reject! { |a| a['db_name'] == db_name }
true
else
false
end
end
# Remove all authenication information stored in this connection.
#
# @return [true] this operation return true because it always succeeds.
def clear_auths
@auths = []
true
end
# Return a hash with all database names # Return a hash with all database names
# and their respective sizes on disk. # and their respective sizes on disk.
# #
# @return [Hash] # @return [Hash]
def database_info def database_info
doc = self['admin'].command(:listDatabases => 1) doc = self['admin'].command({:listDatabases => 1}, false, true)
returning({}) do |info| returning({}) do |info|
doc['databases'].each { |db| info[db['name']] = db['sizeOnDisk'].to_i } doc['databases'].each { |db| info[db['name']] = db['sizeOnDisk'].to_i }
end end
@ -232,7 +287,7 @@ module Mongo
oh[:fromhost] = from_host oh[:fromhost] = from_host
oh[:fromdb] = from oh[:fromdb] = from
oh[:todb] = to oh[:todb] = to
self["admin"].command(oh) self["admin"].command(oh, false, true)
end end
# Increment and return the next available request id. # Increment and return the next available request id.
@ -250,7 +305,7 @@ module Mongo
# #
# @return [Hash] # @return [Hash]
def server_info def server_info
db("admin").command({:buildinfo => 1}, {:admin => true, :check_response => true}) self["admin"].command({:buildinfo => 1}, false, true)
end end
# Get the build version of the current server. # Get the build version of the current server.
@ -365,6 +420,7 @@ module Mongo
result = self['admin'].command({:ismaster => 1}, false, false, socket) result = self['admin'].command({:ismaster => 1}, false, false, socket)
if result['ok'] == 1 && ((is_master = result['ismaster'] == 1) || @slave_ok) if result['ok'] == 1 && ((is_master = result['ismaster'] == 1) || @slave_ok)
@host, @port = host, port @host, @port = host, port
apply_saved_authentication
end end
# Note: slave_ok can be true only when connecting to a single node. # Note: slave_ok can be true only when connecting to a single node.
@ -470,13 +526,13 @@ module Mongo
raise MongoArgumentError, "MongoDB URI must include all three of username, password, " + raise MongoArgumentError, "MongoDB URI must include all three of username, password, " +
"and db if any one of these is specified." "and db if any one of these is specified."
else else
auths << [uname, pwd, db] add_auth(db, uname, pwd)
end end
nodes << [host, port] nodes << [host, port]
end end
[nodes, auths] nodes
end end
private private
@ -654,19 +710,5 @@ module Mongo
end end
message message
end end
# Authenticate for any auth info provided on instantiating the connection.
# Only called when a MongoDB URI has been used to instantiate the connection, and
# when that connection specifies databases and authentication credentials.
#
# @raise [MongoDBError]
def authenticate_databases
@auths.each do |auth|
user = auth[0]
pwd = auth[1]
db_name = auth[2]
self.db(db_name).authenticate(user, pwd)
end
end
end end
end end

View File

@ -82,7 +82,7 @@ module Mongo
# @raise [AuthenticationError] # @raise [AuthenticationError]
# #
# @core authenticate authenticate-instance_method # @core authenticate authenticate-instance_method
def authenticate(username, password) def authenticate(username, password, save_authorization=true)
doc = command(:getnonce => 1) doc = command(:getnonce => 1)
raise "error retrieving nonce: #{doc}" unless ok?(doc) raise "error retrieving nonce: #{doc}" unless ok?(doc)
nonce = doc['nonce'] nonce = doc['nonce']
@ -92,8 +92,14 @@ module Mongo
auth['user'] = username auth['user'] = username
auth['nonce'] = nonce auth['nonce'] = nonce
auth['key'] = Digest::MD5.hexdigest("#{nonce}#{username}#{hash_password(username, password)}") auth['key'] = Digest::MD5.hexdigest("#{nonce}#{username}#{hash_password(username, password)}")
ok?(command(auth)) || if ok?(command(auth))
raise(MongoDBError::AuthenticationError, "Failed to authenticate user '#{username}' on db '#{self.name}'") if save_authorization
@connection.add_auth(@name, username, password)
end
true
else
raise(Mongo::AuthenticationError, "Failed to authenticate user '#{username}' on db '#{self.name}'")
end
end end
# Adds a user to this database for use with authentication. If the user already # Adds a user to this database for use with authentication. If the user already
@ -125,15 +131,21 @@ module Mongo
end end
end end
# Deauthorizes use for this database for this connection. # Deauthorizes use for this database for this connection. Also removes
# any saved authorization in the connection class associated with this
# database.
# #
# @raise [MongoDBError] if logging out fails. # @raise [MongoDBError] if logging out fails.
# #
# @return [Boolean] # @return [Boolean]
def logout def logout
doc = command(:logout => 1) doc = command(:logout => 1)
return true if ok?(doc) if ok?(doc)
raise MongoDBError, "error logging out: #{doc.inspect}" @connection.remove_auth(@name)
true
else
raise MongoDBError, "error logging out: #{doc.inspect}"
end
end end
# Get an array of collection names in this database. # Get an array of collection names in this database.

View File

@ -320,7 +320,7 @@ module Mongo
if @safe if @safe
@client_md5 = @local_md5.hexdigest @client_md5 = @local_md5.hexdigest
if @local_md5 != @server_md5 if @local_md5 != @server_md5
raise @local_md5 != @server_md5, "File on server failed MD5 check" raise GridError, "File on server failed MD5 check"
end end
else else
@server_md5 @server_md5

View File

@ -0,0 +1,68 @@
$:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
require 'mongo'
require 'test/unit'
require 'test/test_helper'
# NOTE: This test requires bouncing the server.
# It also requires that a user exists on the admin database.
class AuthenticationTest < Test::Unit::TestCase
include Mongo
def setup
@conn = Mongo::Connection.new
@db1 = @conn.db('mongo-ruby-test-auth1')
@db2 = @conn.db('mongo-ruby-test-auth2')
@admin = @conn.db('admin')
end
def teardown
@db1.authenticate('user1', 'secret')
@db2.authenticate('user2', 'secret')
@conn.drop_database('mongo-ruby-test-auth1')
@conn.drop_database('mongo-ruby-test-auth2')
end
def test_authenticate
@admin.authenticate('bob', 'secret')
@db1.add_user('user1', 'secret')
@db2.add_user('user2', 'secret')
@admin.logout
assert_raise Mongo::OperationFailure do
@db1['stuff'].insert({:a => 2}, :safe => true)
end
assert_raise Mongo::OperationFailure do
@db2['stuff'].insert({:a => 2}, :safe => true)
end
@db1.authenticate('user1', 'secret')
@db2.authenticate('user2', 'secret')
assert @db1['stuff'].insert({:a => 2}, :safe => true)
assert @db2['stuff'].insert({:a => 2}, :safe => true)
puts "Please bounce the server."
gets
# Here we reconnect.
begin
@db1['stuff'].find.to_a
rescue Mongo::ConnectionFailure
end
assert @db1['stuff'].insert({:a => 2}, :safe => true)
assert @db2['stuff'].insert({:a => 2}, :safe => true)
@db1.logout
assert_raise Mongo::OperationFailure do
@db1['stuff'].insert({:a => 2}, :safe => true)
end
@db2.logout
assert_raise Mongo::OperationFailure do
assert @db2['stuff'].insert({:a => 2}, :safe => true)
end
end
end

View File

@ -29,7 +29,7 @@ class AutoreconnectTest < Test::Unit::TestCase
begin begin
@coll.find.to_a @coll.find.to_a
rescue Mongo::ConnectionFailure rescue Mongo::ConnectionFailure
end end
results = [] results = []
@ -38,5 +38,4 @@ class AutoreconnectTest < Test::Unit::TestCase
assert results.any? {|r| r['a'] == a}, "Could not find record for a => #{a}" assert results.any? {|r| r['a'] == a}, "Could not find record for a => #{a}"
end end
end end
end end

View File

@ -69,8 +69,6 @@ class TestConnection < Test::Unit::TestCase
assert_kind_of Array, names assert_kind_of Array, names
assert names.length >= 1 assert names.length >= 1
assert names.include?('ruby-mongo-info-test') assert names.include?('ruby-mongo-info-test')
@mongo.drop_database('ruby-mongo-info-test')
end end
def test_logging def test_logging
@ -80,7 +78,7 @@ class TestConnection < Test::Unit::TestCase
db = Connection.new(@host, @port, :logger => logger).db('ruby-mongo-test') db = Connection.new(@host, @port, :logger => logger).db('ruby-mongo-test')
assert output.string.include?("admin.$cmd.find") assert output.string.include?("admin.$cmd.find")
end end
def test_connection_logger def test_connection_logger
output = StringIO.new output = StringIO.new
logger = Logger.new(output) logger = Logger.new(output)
@ -124,6 +122,38 @@ class TestConnection < Test::Unit::TestCase
assert_equal ['foo', 123], nodes[1] assert_equal ['foo', 123], nodes[1]
end end
context "Saved authentications" do
setup do
@conn = Mongo::Connection.new
@auth = {'db_name' => 'test', 'username' => 'bob', 'password' => 'secret'}
@conn.add_auth(@auth['db_name'], @auth['username'], @auth['password'])
end
should "save the authentication" do
assert_equal @auth, @conn.auths[0]
end
should "replace the auth if given a new auth for the same db" do
auth = {'db_name' => 'test', 'username' => 'mickey', 'password' => 'm0u53'}
@conn.add_auth(auth['db_name'], auth['username'], auth['password'])
assert_equal 1, @conn.auths.length
assert_equal auth, @conn.auths[0]
end
should "remove auths by database" do
@conn.remove_auth('non-existent database')
assert_equal 1, @conn.auths.length
@conn.remove_auth('test')
assert_equal 0, @conn.auths.length
end
should "remove all auths" do
@conn.clear_auths
assert_equal 0, @conn.auths.length
end
end
context "Connection exceptions" do context "Connection exceptions" do
setup do setup do
@conn = Mongo::Connection.new('localhost', 27017, :pool_size => 10, :timeout => 10) @conn = Mongo::Connection.new('localhost', 27017, :pool_size => 10, :timeout => 10)

View File

@ -75,8 +75,10 @@ class ConnectionTest < Test::Unit::TestCase
@conn = Connection.from_uri("mongodb://kyle:s3cr3t@localhost:27017/app,mickey:m0u5e@mydb.com:27018/dsny", :connect => false) @conn = Connection.from_uri("mongodb://kyle:s3cr3t@localhost:27017/app,mickey:m0u5e@mydb.com:27018/dsny", :connect => false)
assert_equal ['localhost', 27017], @conn.nodes[0] assert_equal ['localhost', 27017], @conn.nodes[0]
assert_equal ['mydb.com', 27018], @conn.nodes[1] assert_equal ['mydb.com', 27018], @conn.nodes[1]
assert_equal ['kyle', 's3cr3t', 'app'], @conn.auths[0] auth_hash = {'username' => 'kyle', 'password' => 's3cr3t', 'db_name' => 'app'}
assert_equal ['mickey', 'm0u5e', 'dsny'], @conn.auths[1] assert_equal auth_hash, @conn.auths[0]
auth_hash = {'username' => 'mickey', 'password' => 'm0u5e', 'db_name' => 'dsny'}
assert_equal auth_hash, @conn.auths[1]
end end
should "attempt to connect" do should "attempt to connect" do
@ -86,6 +88,7 @@ class ConnectionTest < Test::Unit::TestCase
admin_db = new_mock_db admin_db = new_mock_db
admin_db.expects(:command).returns({'ok' => 1, 'ismaster' => 1}) admin_db.expects(:command).returns({'ok' => 1, 'ismaster' => 1})
@conn.expects(:[]).with('admin').returns(admin_db) @conn.expects(:[]).with('admin').returns(admin_db)
@conn.expects(:apply_saved_authentication)
@conn.connect_to_master @conn.connect_to_master
end end