### Copyright 2019 Pixar ### ### Licensed under the Apache License, Version 2.0 (the "Apache License") ### with the following modification; you may not use this file except in ### compliance with the Apache License and the following modification to it: ### Section 6. Trademarks. is deleted and replaced with: ### ### 6. Trademarks. This License does not grant permission to use the trade ### names, trademarks, service marks, or product names of the Licensor ### and its affiliates, except as required to comply with Section 4(c) of ### the License and to reproduce the content of the NOTICE file. ### ### You may obtain a copy of the Apache License at ### ### http://www.apache.org/licenses/LICENSE-2.0 ### ### Unless required by applicable law or agreed to in writing, software ### distributed under the Apache License with the above modification is ### distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY ### KIND, either express or implied. See the Apache License for the specific ### language governing permissions and limitations under the Apache License. ### ### module JSSTestHelper # auth module Auth KEYCHAIN_LABEL_BASE = 'com.pixar.ruby-jss.testing'.freeze KEYCHAIN_JSS_LABEL = KEYCHAIN_LABEL_BASE + '.api' KEYCHAIN_DB_LABEL = KEYCHAIN_LABEL_BASE + '.db' KEYCHAIN_DIST_LABEL = KEYCHAIN_LABEL_BASE + '.distribution' KEYCHAIN_DIST_ACCT = 'From JSS'.freeze KEYCHAIN_LABELS = { jss: KEYCHAIN_JSS_LABEL, db: KEYCHAIN_DB_LABEL, dist: KEYCHAIN_DIST_LABEL }.freeze module_function # return the user's API password, prompting for it if we don't already have # it # def api_pw(server: nil, port: nil, user: nil) return @api_pw if @api_pw # If we're here, we need to get the pw via prompt @api_pw = prompt_for_password prompt: "Enter the API password for #{user}@#{server}:#{port}" do |apw| begin JSS.api.disconnect JSS.api.connect user: user, pw: apw, server: server, port: port true rescue JSS::AuthenticationError false end # begin end # do apw raise JSS::AuthenticationError, "Incorrect API password for #{user}@#{server}:#{port}" unless @api_pw @api_pw end # return the server, port, and user, which we might have gotten fromthe keychain def connect_to_api(server: nil, user: nil, port: nil, pw: nil) return if JSS.api.connected? JSS.api.connect server: server, port: port, user: user, pw: pw @api_pw = pw { server: JSS.api.hostname, port: JSS.api.port, user: JSS.api.jss_user } rescue JSS::AuthenticationError # If we're here, we need to prompt for the pw pw = api_pw server: server, port: port, user: user ensure # store the pw save_rw_credentials label: KEYCHAIN_JSS_LABEL, acct: JSS.api.jss_user, server: JSS.api.hostname, port: JSS.api.port, pw: pw if JSS.api.connected? end # TODO: update this as API above so there's only one per keychain. def db_pw return @db_pw if @db_pw # If we're here, we need to get the passwd for the user @db_pw = prompt_for_password prompt: "Enter the MySQL password for #{@db_user}@#{@db_server}" do |apw| begin JSS::DB_CNX.disconnect JSS::DB_CNX.connect server: @db_server, user: @db_user, pw: apw true rescue Mysql::ServerError::AccessDeniedError false end # begin end # do apw raise JSS::AuthenticationError, "Incorrect MySQL password for #{@db_user}@#{@db_server}" unless @db_pw @db_pw end # TODO: update this as API above so there's only one per keychain. def connect_to_db(server: nil, user: nil) return if JSS::DB_CNX.connected? @db_server = server ? server : JSS::CONFIG.api_server_name @db_user = user raise 'No db_server_name defined in /etc/ruby-jss.conf. Please specify with --db-server' unless @db_server # look in the keychain keychain_creds = rw_credentials_from_keychain(:db, @db_server, @db_user) # did we get a user/pw? Use it if keychain_creds @db_user ||= keychain_creds[:user] begin JSS::DB_CNX.connect server: @db_server, user: @db_user, pw: keychain_creds[:password] return rescue Mysql::ServerError::AccessDeniedError # re-prompt if bad pw in keychain say "Stored MySQL password for #{@db_user}@#{@db_server} is incorrect" end # begin else # Couldn't find a stored user for server, and no user given on CLI - error. raise "No user stored for MySQL server '#{@db_server}'. Please specify with --db-user" unless @db_user end # If we're here, we need to get the passwd for the user pw = db_pw # store the pw save_rw_credentials KEYCHAIN_DB_LABEL, @db_user, @db_server, pw end # TODO: pass in the API server for which we want the dist point pw # must be connected to api # and make it like API connection above, 1 per keychain. def dist_point_pw keychain_creds = rw_credentials_from_keychain(:db, @api_server, KEYCHAIN_DIST_ACCT) if keychain_creds @distpw = keychain_creds[:password] return @distpw if JSS::DistributionPoint.master_distribution_point.check_pw :rw, distpw end # if we're here, there was no stored pw or it was incorrect # If we're here, we need to get the passwd for the master dist point prmpt = "Enter the RW password for the Master Distribution Point on JSS #{@api_server}" @distpw = prompt_for_password prompt: prmpt do |apw| JSS::DistributionPoint.master_distribution_point.check_pw :rw, apw end # do apw raise JSS::AuthenticationError, "Incorrect RW password for the Master Distribution Point on JSS #{@api_server}" unless @distpw # store the pw save_rw_credentials KEYCHAIN_DIST_LABEL, KEYCHAIN_DIST_ACCT, @api_server, @distpw @distpw end # Fetch read-write credentials from the login keychain # # If the login keychain is locked, the user will be prompted # to unlock it in the GUI. # # @param kind[Symbol] which kind of credentials? :jss, :db, or :dist # # @return [Hash{Symbol => String}, nil] A Hash with :server, :user and :password # values, nil if no matching keychain item. # def rw_credentials_from_keychain(kind) Keychain.user_interaction_allowed = true unlock_keychain label = KEYCHAIN_LABELS[kind] raise JSS::InvalidDataError, "argument must be one of :#{RW_CREDENTIAL_KINDS.join ', :'}" unless label pw_search = { label: label } pw_item = Keychain.default.internet_passwords.where(pw_search).first return nil unless pw_item { server: pw_item.server, user: pw_item.account, pw: pw_item.password, port: pw_item.port } end # Save the credentials in the login keychain # # @param user[String] the username to save # # @param pw[String] the password to save with the username # # @return [void] # def save_rw_credentials(label: nil, acct: nil, server: nil, pw: nil, port: nil) Keychain.default.internet_passwords.where(label: label).all.each(&:delete) item = Keychain.default.internet_passwords.create label: label, account: acct, server: server, password: pw, port: port item.save! end # Prompt the user to unlock the default keychain # # @return [void] # def unlock_keychain return true unless Keychain.default.locked? unlocked = prompt_for_password prompt: 'Enter your keychain password' do |pw| begin Keychain.default.unlock! pw true rescue Keychain::AuthFailedError false end # begin end # prompt for pass raise JSS::AuthenticationError, 'Failed to unlock default keychain' unless unlocked end # unlock keychain # Prompt the user for a password in the shell # Returns: # - a valid password, or # - false if no valid password after the max number of tries # # Displays the provided promp. Passes what the user types to the block you # provide, which must return a Boolean indicating if the password was correct. # # @param prompt[String] The message asking for the password # # @option max_tries[Integer] The max number of times to let the # user enter the passwd. Defaults to 3 # # @option retry_msg[String] the text to display after a failure, # but before :max_tries failures. Defaults to "Try again: " followed # by the text arg. # # @option failed_msg[String] The message to display after :max_tries # failures. Defaults to "Too many failed attempts" # # @yield The block should figure out if the user typed the correct password # # @yieldparam pw [String] The password typed by the user in the input field # # @yieldreturn [Boolean] Was the password correct? # # @return [String] The validated password # @return [false] The user failed after max_tries: attempts. # # def prompt_for_password(**options) colon = ':' # set defaults options[:prompt] ||= 'Enter your password:' options[:prompt] << colon unless options[:prompt].end_with? colon options[:max_tries] ||= 3 options[:retry_msg] ||= "Try Again. #{options[:prompt]}" options[:failed_msg] ||= 'Too many failed attempts' tries = 0 prompt = options[:prompt] while tries < options[:max_tries] print "#{prompt} " system 'stty -echo' pw = $stdin.gets.chomp puts system 'stty echo' return pw if yield pw prompt = options[:retry_msg] tries += 1 end puts options[:failed_msg] false ensure # make sure terminal is usable at the end of this system 'stty echo' end # prompt_for_password end # module auth end # module JSSTestHelper