lib/mongo/functional/authentication.rb in mongo-1.11.1 vs lib/mongo/functional/authentication.rb in mongo-1.12.0.rc0
- old
+ new
@@ -16,12 +16,15 @@
module Mongo
module Authentication
DEFAULT_MECHANISM = 'MONGODB-CR'
- MECHANISMS = ['GSSAPI', 'MONGODB-CR', 'MONGODB-X509', 'PLAIN']
- EXTRA = { 'GSSAPI' => [:gssapi_service_name, :canonicalize_host_name] }
+ MECHANISMS = ['GSSAPI', 'MONGODB-CR', 'MONGODB-X509', 'PLAIN', 'SCRAM-SHA-1']
+ MECHANISM_ERROR = "Must use one of #{MECHANISMS.join(', ')} " +
+ "authentication mechanisms."
+ EXTRA = { 'GSSAPI' => [:service_name, :canonicalize_host_name,
+ :service_realm] }
# authentication module methods
class << self
# Helper to validate an authentication mechanism and optionally
# raise an error if invalid.
@@ -47,19 +50,17 @@
# @param auth [Hash] A hash containing the credential set.
#
# @raise [MongoArgumentError] if the credential set is invalid.
# @return [Hash] The validated credential set.
def validate_credentials(auth)
- # set the default auth mechanism if not defined
- auth[:mechanism] ||= DEFAULT_MECHANISM
-
# set the default auth source if not defined
auth[:source] = auth[:source] || auth[:db_name] || 'admin'
- if (auth[:mechanism] == 'MONGODB-CR' || auth[:mechanism] == 'PLAIN') && !auth[:password]
+ if password_required?(auth[:mechanism]) && !auth[:password]
raise MongoArgumentError,
- "When using the authentication mechanism #{auth[:mechanism]} " +
+ "When using the authentication mechanism " +
+ "#{auth[:mechanism].nil? ? 'MONGODB-CR or SCRAM-SHA-1' : auth[:mechanism]} " +
"both username and password are required."
end
# if extra opts exist, validate them
allowed_keys = EXTRA[auth[:mechanism]]
if auth[:extra] && !auth[:extra].empty?
@@ -90,10 +91,23 @@
#
# @return [String] The hashed password value.
def hash_password(username, password)
Digest::MD5.hexdigest("#{username}:mongo:#{password}")
end
+
+ private
+
+ # Does the authentication require a password?
+ #
+ # @param [ String ] mech The authentication mechanism.
+ #
+ # @return [ true, false ] If a password is required.
+ #
+ # @since 1.12.0
+ def password_required?(mech)
+ mech == 'MONGODB-CR' || mech == 'PLAIN' || mech == 'SCRAM-SHA-1' || mech.nil?
+ end
end
# Saves a cache of authentication credentials to the current
# client instance. This method is called automatically by DB#authenticate.
#
@@ -102,11 +116,11 @@
# @param password [String] (nil) The users's password (not required for
# all authentication mechanisms).
# @param source [String] (nil) The authentication source database
# (if different than the current database).
# @param mechanism [String] (nil) The authentication mechanism being used
- # (default: 'MONGODB-CR').
+ # (default: 'MONGODB-CR' or 'SCRAM-SHA-1' if server version >= 2.7.8).
# @param extra [Hash] (nil) A optional hash of extra options to be stored with
# the credential set.
#
# @raise [MongoArgumentError] Raised if the database has already been used
# for authentication. A log out is required before additional auths can
@@ -129,12 +143,12 @@
"'#{auth[:source]}' and multiple authentications are not " +
"permitted. Please logout first."
end
begin
- socket = self.checkout_reader(:mode => :primary_preferred)
- self.issue_authentication(auth, :socket => socket)
+ socket = checkout_reader(:mode => :primary_preferred)
+ issue_authentication(auth, :socket => socket)
ensure
socket.checkin if socket
end
@auths << auth
@@ -146,11 +160,14 @@
# @param db_name [String] The database name.
#
# @return [Boolean] The result of the operation.
def remove_auth(db_name)
return false unless @auths
- @auths.reject! { |a| a[:source] == db_name } ? true : false
+ auths = @auths.to_a
+ removed = auths.reject! { |a| a[:source] == db_name }
+ @auths = Set.new(auths)
+ !!removed
end
# Remove all authentication information stored in this connection.
#
# @return [Boolean] result of the operation.
@@ -188,19 +205,26 @@
# @option opts [Socket] socket Socket instance to use.
#
# @raise [AuthenticationError] Raised if the authentication fails.
# @return [Boolean] Result of the authentication operation.
def issue_authentication(auth, opts={})
+ # set the default auth mechanism if not defined
+ auth[:mechanism] ||= default_mechanism
+
+ raise MongoArgumentError,
+ MECHANISM_ERROR unless MECHANISMS.include?(auth[:mechanism])
result = case auth[:mechanism]
when 'MONGODB-CR'
issue_cr(auth, opts)
when 'MONGODB-X509'
issue_x509(auth, opts)
when 'PLAIN'
issue_plain(auth, opts)
when 'GSSAPI'
issue_gssapi(auth, opts)
+ when 'SCRAM-SHA-1'
+ issue_scram(auth, opts)
end
unless Support.ok?(result)
raise AuthenticationError,
"Failed to authenticate user '#{auth[:username]}' " +
@@ -210,10 +234,90 @@
true
end
private
+ def default_mechanism
+ max_wire_version >= 3 ? 'SCRAM-SHA-1' : DEFAULT_MECHANISM
+ end
+
+ # Handles copying a database with SCRAM-SHA-1 authentication.
+ #
+ # @api private
+ #
+ # @param [ String ] username The user to authenticate on the
+ # 'from' database.
+ # @param [ String ] password The password for the user authenticated
+ # on the 'from' database.
+ # @param [ String ] from_host The host of the 'from' database.
+ # @param [ String ] from_db Name of the database to copy from.
+ # @param [ String ] to_db Name of the database to copy to.
+ #
+ # @return [ Hash ] The result of the copydb operation.
+ #
+ # @since 1.12.0
+ def copy_db_scram(username, password, from_host, from_db, to_db)
+ auth = { :db_name => from_db,
+ :username => username,
+ :password => password }
+
+ socket = checkout_reader(:mode => :primary_preferred)
+
+ copy_db = { :from_host => from_host, :from_db => from_db, :to_db => to_db }
+ scram = SCRAM.new(auth, Authentication.hash_password(username, password),
+ { :copy_db => copy_db })
+ result = auth_command(scram.copy_db_start, socket, 'admin').first
+ result = auth_command(scram.copy_db_continue(result), socket, 'admin').first
+ until result['done']
+ result = auth_command(scram.copy_db_continue(result), socket, 'admin').first
+ end
+ socket.checkin
+ result
+ end
+
+ # Handles copying a database with MONGODB-CR authentication.
+ #
+ # @api private
+ #
+ # @param [ String ] username The user to authenticate on the
+ # 'from' database.
+ # @param [ String ] password The password for the user authenticated
+ # on the 'from' database.
+ # @param [ String ] from_host The host of the 'from' database.
+ # @param [ String ] from_db Name of the database to copy from.
+ # @param [ String ] to_db Name of the database to copy to.
+ #
+ # @return [ Hash ] The result of the copydb operation.
+ #
+ # @since 1.12.0
+ def copy_db_mongodb_cr(username, password, from_host, from_db, to_db)
+ oh = BSON::OrderedHash.new
+ oh[:copydb] = 1
+ oh[:fromhost] = from_host
+ oh[:fromdb] = from_db
+ oh[:todb] = to_db
+
+ socket = checkout_reader(:mode => :primary_preferred)
+
+ if username || password
+ unless username && password
+ raise MongoArgumentError,
+ 'Both username and password must be supplied for authentication.'
+ end
+ nonce_cmd = BSON::OrderedHash.new
+ nonce_cmd[:copydbgetnonce] = 1
+ nonce_cmd[:fromhost] = from_host
+ result = auth_command(nonce_cmd, socket, 'admin').first
+ oh[:nonce] = result['nonce']
+ oh[:username] = username
+ oh[:key] = Authentication.auth_key(username, password, oh[:nonce])
+ end
+ result = auth_command(oh, socket, 'admin').first
+ socket.checkin
+ result
+ end
+
# Handles issuing authentication commands for the MONGODB-CR auth mechanism.
#
# @param auth [Hash] The authentication credentials to be used.
# @param opts [Hash] Hash of optional settings and configuration values.
#
@@ -283,9 +387,32 @@
# @param opts [Hash] Hash of optional settings and configuration values.
#
# @private
def issue_gssapi(auth, opts={})
raise "In order to use Kerberos, please add the mongo-kerberos gem to your dependencies"
+ end
+
+ # Handles issuing SCRAM-SHA-1 authentication.
+ #
+ # @api private
+ #
+ # @param [ Hash ] auth The authentication credentials.
+ # @param [ Hash ] opts The options.
+ #
+ # @options opts [ Socket ] socket The Socket instance to use.
+ #
+ # @return [ Hash ] The result of the authentication operation.
+ #
+ # @since 1.12.0
+ def issue_scram(auth, opts = {})
+ db_name = auth[:source]
+ scram = SCRAM.new(auth, Authentication.hash_password(auth[:username], auth[:password]))
+ result = auth_command(scram.start, opts[:socket], db_name).first
+ result = auth_command(scram.continue(result), opts[:socket], db_name).first
+ until result['done']
+ result = auth_command(scram.finalize(result), opts[:socket], db_name).first
+ end
+ result
end
# Helper to fetch a nonce value from a given database instance.
#
# @param database [Mongo::DB] The DB instance to use for issue the nonce command.