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
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|
puts "Dropping test database..."
require File.join(File.dirname(__FILE__), 'lib', 'mongo')

View File

@ -90,8 +90,10 @@ module Mongo
#
# @core connections
def initialize(pair_or_host=nil, port=nil, options={})
@auths = []
if block_given?
@nodes, @auths = yield self
@nodes = yield self
else
@nodes = format_pair(pair_or_host, port)
end
@ -126,10 +128,7 @@ module Mongo
@options = options
should_connect = options[:connect].nil? ? true : options[:connect]
if should_connect
connect_to_master
authenticate_databases if @auths
end
connect_to_master if should_connect
end
# 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
# of authorizations for the database.
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
@ -172,12 +171,68 @@ module Mongo
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
# and their respective sizes on disk.
#
# @return [Hash]
def database_info
doc = self['admin'].command(:listDatabases => 1)
doc = self['admin'].command({:listDatabases => 1}, false, true)
returning({}) do |info|
doc['databases'].each { |db| info[db['name']] = db['sizeOnDisk'].to_i }
end
@ -232,7 +287,7 @@ module Mongo
oh[:fromhost] = from_host
oh[:fromdb] = from
oh[:todb] = to
self["admin"].command(oh)
self["admin"].command(oh, false, true)
end
# Increment and return the next available request id.
@ -250,7 +305,7 @@ module Mongo
#
# @return [Hash]
def server_info
db("admin").command({:buildinfo => 1}, {:admin => true, :check_response => true})
self["admin"].command({:buildinfo => 1}, false, true)
end
# Get the build version of the current server.
@ -365,6 +420,7 @@ module Mongo
result = self['admin'].command({:ismaster => 1}, false, false, socket)
if result['ok'] == 1 && ((is_master = result['ismaster'] == 1) || @slave_ok)
@host, @port = host, port
apply_saved_authentication
end
# 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, " +
"and db if any one of these is specified."
else
auths << [uname, pwd, db]
add_auth(db, uname, pwd)
end
nodes << [host, port]
end
[nodes, auths]
nodes
end
private
@ -654,19 +710,5 @@ module Mongo
end
message
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

View File

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

View File

@ -320,7 +320,7 @@ module Mongo
if @safe
@client_md5 = @local_md5.hexdigest
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
else
@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
@coll.find.to_a
rescue Mongo::ConnectionFailure
rescue Mongo::ConnectionFailure
end
results = []
@ -38,5 +38,4 @@ class AutoreconnectTest < Test::Unit::TestCase
assert results.any? {|r| r['a'] == a}, "Could not find record for a => #{a}"
end
end
end

View File

@ -69,8 +69,6 @@ class TestConnection < Test::Unit::TestCase
assert_kind_of Array, names
assert names.length >= 1
assert names.include?('ruby-mongo-info-test')
@mongo.drop_database('ruby-mongo-info-test')
end
def test_logging
@ -80,7 +78,7 @@ class TestConnection < Test::Unit::TestCase
db = Connection.new(@host, @port, :logger => logger).db('ruby-mongo-test')
assert output.string.include?("admin.$cmd.find")
end
def test_connection_logger
output = StringIO.new
logger = Logger.new(output)
@ -124,6 +122,38 @@ class TestConnection < Test::Unit::TestCase
assert_equal ['foo', 123], nodes[1]
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
setup do
@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)
assert_equal ['localhost', 27017], @conn.nodes[0]
assert_equal ['mydb.com', 27018], @conn.nodes[1]
assert_equal ['kyle', 's3cr3t', 'app'], @conn.auths[0]
assert_equal ['mickey', 'm0u5e', 'dsny'], @conn.auths[1]
auth_hash = {'username' => 'kyle', 'password' => 's3cr3t', 'db_name' => 'app'}
assert_equal auth_hash, @conn.auths[0]
auth_hash = {'username' => 'mickey', 'password' => 'm0u5e', 'db_name' => 'dsny'}
assert_equal auth_hash, @conn.auths[1]
end
should "attempt to connect" do
@ -86,6 +88,7 @@ class ConnectionTest < Test::Unit::TestCase
admin_db = new_mock_db
admin_db.expects(:command).returns({'ok' => 1, 'ismaster' => 1})
@conn.expects(:[]).with('admin').returns(admin_db)
@conn.expects(:apply_saved_authentication)
@conn.connect_to_master
end