# Copyright (C) 2009-2013 MongoDB, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. require 'digest/md5' module Mongo module Authentication DEFAULT_MECHANISM = 'MONGODB-CR' MECHANISMS = ['GSSAPI', 'MONGODB-CR', 'MONGODB-X509', 'PLAIN'] # authentication module methods class << self # Helper to validate an authentication mechanism and optionally # raise an error if invalid. # # @param mechanism [String] [description] # @param raise_error [Boolean] [description] # # @raise [ArgumentError] if raise_error and not a valid auth mechanism. # @return [Boolean] returns the validation result. def validate_mechanism(mechanism, raise_error=false) return true if MECHANISMS.include?(mechanism.upcase) if raise_error raise ArgumentError, "Invalid authentication mechanism provided. Must be one of " + "#{Mongo::Authentication::MECHANISMS.join(', ')}." end false end # Helper to validate and normalize credential sets. # # @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] raise MongoArgumentError, "When using the authentication mechanism #{auth[:mechanism]} " + "both username and password are required." end auth end # Generate an MD5 for authentication. # # @param username [String] The username. # @param password [String] The user's password. # @param nonce [String] The nonce value. # # @return [String] MD5 key for db authentication. def auth_key(username, password, nonce) Digest::MD5.hexdigest("#{nonce}#{username}#{hash_password(username, password)}") end # Return a hashed password for auth. # # @param username [String] The username. # @param password [String] The users's password. # # @return [String] The hashed password value. def hash_password(username, password) Digest::MD5.hexdigest("#{username}:mongo:#{password}") end end # Saves a cache of authentication credentials to the current # client instance. This method is called automatically by DB#authenticate. # # @param db_name [String] The current database name. # @param username [String] The current username. # @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'). # # @raise [MongoArgumentError] Raised if the database has already been used # for authentication. A log out is required before additional auths can # be issued against a given database. # @raise [AuthenticationError] Raised if authentication fails. # @return [Hash] a hash representing the authentication just added. def add_auth(db_name, username, password=nil, source=nil, mechanism=nil) auth = Authentication.validate_credentials({ :db_name => db_name, :username => username, :password => password, :source => source, :mechanism => mechanism }) if @auths.any? {|a| a[:source] == auth[:source]} raise MongoArgumentError, "Another user has already authenticated to the database " + "'#{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) ensure socket.checkin if socket end @auths << auth auth end # Remove a saved authentication for this connection. # # @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 end # Remove all authentication information stored in this connection. # # @return [Boolean] result of the operation. def clear_auths @auths = Set.new true end # Method to handle and issue logout commands. # # @note This method should not be called directly. Use DB#logout. # # @param db_name [String] The database name. # @param opts [Hash] Hash of optional settings and configuration values. # # @option opts [Socket] socket (nil) Optional socket instance to use. # # @raise [MongoDBError] Raised if the logout operation fails. # @return [Boolean] The result of the logout operation. def issue_logout(db_name, opts={}) doc = db(db_name).command({:logout => 1}, :socket => opts[:socket]) unless Support.ok?(doc) raise MongoDBError, "Error logging out on DB #{db_name}." end true # somewhat pointless, but here to preserve the existing API end # Method to handle and issue authentication commands. # # @note This method should not be called directly. Use DB#authenticate. # # @param auth [Hash] The authentication credentials to be used. # @param opts [Hash] Hash of optional settings and configuration values. # # @option opts [Socket] socket (nil) Optional socket instance to use. # # @raise [AuthenticationError] Raised if the authentication fails. # @return [Boolean] Result of the authentication operation. def issue_authentication(auth, opts={}) 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) end unless Support.ok?(result) raise AuthenticationError, "Failed to authenticate user '#{auth[:username]}' " + "on db '#{auth[:source]}'." end true end private # 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. # # @option opts [Socket] socket (nil) Optional socket instance to use. # # @return [Boolean] Result of the authentication operation. # # @private def issue_cr(auth, opts={}) database = db(auth[:source]) nonce = get_nonce(database, opts) # build auth command document cmd = BSON::OrderedHash.new cmd['authenticate'] = 1 cmd['user'] = auth[:username] cmd['nonce'] = nonce cmd['key'] = Authentication.auth_key(auth[:username], auth[:password], nonce) database.command(cmd, :check_response => false, :socket => opts[:socket]) end # Handles issuing authentication commands for the MONGODB-X509 auth mechanism. # # @param auth [Hash] The authentication credentials to be used. # @param opts [Hash] Hash of optional settings and configuration values. # # @private def issue_x509(auth, opts={}) database = db('$external') cmd = BSON::OrderedHash.new cmd[:authenticate] = 1 cmd[:mechanism] = auth[:mechanism] cmd[:user] = auth[:username] database.command(cmd, :check_response => false, :socket => opts[:socket]) end # Handles issuing authentication commands for the PLAIN auth mechanism. # # @param auth [Hash] The authentication credentials to be used. # @param opts [Hash] Hash of optional settings and configuration values. # # @option opts [Socket] socket (nil) Optional socket instance to use. # # @return [Boolean] Result of the authentication operation. # # @private def issue_plain(auth, opts={}) database = db(auth[:source]) payload = "\x00#{auth[:username]}\x00#{auth[:password]}" cmd = BSON::OrderedHash.new cmd[:saslStart] = 1 cmd[:mechanism] = auth[:mechanism] cmd[:payload] = BSON::Binary.new(payload) cmd[:autoAuthorize] = 1 database.command(cmd, :check_response => false, :socket => opts[:socket]) end # Handles issuing authentication commands for the GSSAPI auth mechanism. # # @param auth [Hash] The authentication credentials to be used. # @param opts [Hash] Hash of optional settings and configuration values. # # @private def issue_gssapi(auth, opts={}) raise NotImplementedError, "The #{auth[:mechanism]} authentication mechanism is not yet supported." end # Helper to fetch a nonce value from a given database instance. # # @param db [Mongo::DB] The DB instance to use for issue the nonce command. # @param opts [Hash] Hash of optional settings and configuration values. # # @option opts [Socket] socket (nil) Optional socket instance to use. # # @raise [MongoDBError] Raised if there is an error executing the command. # @return [String] Returns the nonce value. # # @private def get_nonce(database, opts={}) doc = database.command({:getnonce => 1}, :check_response => false, :socket => opts[:socket]) unless Support.ok?(doc) raise MongoDBError, "Error retrieving nonce: #{doc}" end doc['nonce'] end end end