# The MIT License (MIT) # Copyright (c) 2023 Mike DeAngelo Google, Inc. # Permission is hereby granted, free of charge, to any person obtaining a copy of # this software and associated documentation files (the "Software"), to deal in # the Software without restriction, including without limitation the rights to # use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of # the Software, and to permit persons to whom the Software is furnished to do so, # subject to the following conditions: # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # frozen_string_literal: true require 'json' require 'pastel' require 'tty-reader' require_relative '../../gzr' module Gzr module Session def pastel @pastel ||= Pastel.new end def say_ok(data, output: $stdout) output.puts pastel.green data end def say_warning(data, output: $stdout) output.puts pastel.yellow data end def say_error(data, output: $stdout) output.puts pastel.red data end @versions = [] @current_version = nil def sufficient_version?(given_version, minimum_version) return true unless (given_version && minimum_version) versions = @versions.sort !versions.drop_while {|v| v < minimum_version}.reverse.drop_while {|v| v > given_version}.empty? end def token_file "#{ENV["HOME"]}/.gzr_auth" end def read_token_data return nil unless File.exist?(token_file) s = File.stat(token_file) if !(s.mode.to_s(8)[3..5] == "600") say_error "#{token_file} mode is #{s.mode.to_s(8)[3..5]}. It must be 600. Ignoring." return nil end token_data = nil file = nil begin file = File.open(token_file) token_data = JSON.parse(file.read,{:symbolize_names => true}) ensure file.close if file end token_data end def write_token_data(token_data) file = nil begin file = File.new(token_file, "wt") file.chmod(0600) file.write JSON.pretty_generate(token_data) ensure file.close if file end end def build_connection_hash(api_version=nil) conn_hash = Hash.new conn_hash[:api_endpoint] = "http#{@options[:ssl] ? "s" : ""}://#{@options[:host]}:#{@options[:port]}/api/#{api_version||@current_version||""}" if @options[:http_proxy] conn_hash[:connection_options] ||= {} conn_hash[:connection_options][:proxy] = { :uri => @options[:http_proxy] } end if @options[:ssl] conn_hash[:connection_options] ||= {} if @options[:verify_ssl] then conn_hash[:connection_options][:ssl] = { :verify => true, :verify_mode => (OpenSSL::SSL::VERIFY_PEER | OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT) } else conn_hash[:connection_options][:ssl] = { :verify => false, :verify_mode => (OpenSSL::SSL::VERIFY_NONE) } end end if @options[:timeout] conn_hash[:connection_options] ||= {} conn_hash[:connection_options][:request] = { :timeout => @options[:timeout] } end conn_hash[:user_agent] = "Gazer #{Gzr::VERSION}" return conn_hash if @options[:token] || @options[:token_file] if @options[:client_id] then conn_hash[:client_id] = @options[:client_id] if @options[:client_secret] then conn_hash[:client_secret] = @options[:client_secret] else reader = TTY::Reader.new @secret ||= reader.read_line("Enter your client_secret:", echo: false) conn_hash[:client_secret] = @secret end else conn_hash[:netrc] = true conn_hash[:netrc_file] = "~/.netrc" end conn_hash end def login(min_api_version="4.0") if !@options[:token] && !@options[:token_file] if (@options[:client_id].nil? && ENV["LOOKERSDK_CLIENT_ID"]) @options[:client_id] = ENV["LOOKERSDK_CLIENT_ID"] end if (@options[:client_secret].nil? && ENV["LOOKERSDK_CLIENT_SECRET"]) @options[:client_secret] = ENV["LOOKERSDK_CLIENT_SECRET"] end end if (@options[:verify_ssl] && ENV["LOOKERSDK_VERIFY_SSL"]) @options[:verify_ssl] = !(/^f(alse)?$/i =~ ENV["LOOKERSDK_VERIFY_SSL"]) end if ((@options[:host] == 'localhost') && ENV["LOOKERSDK_BASE_URL"]) base_url = ENV["LOOKERSDK_BASE_URL"] @options[:ssl] = !!(/^https/ =~ base_url) @options[:host] = /^https?:\/\/([^:\/]+)/.match(base_url)[1] md = /:([0-9]+)\/?$/.match(base_url) @options[:port] = md[1] if md end say_ok("using options #{@options.select { |k,v| k != 'client_secret' }.map { |k,v| "#{k}=>#{v}" }}") if @options[:debug] @secret = nil begin conn_hash = build_connection_hash sawyer_options = { :links_parser => Sawyer::LinkParsers::Simple.new, :serializer => LookerSDK::Client::Serializer.new(JSON), :faraday => Faraday.new(conn_hash[:connection_options]) do |conn| conn.use LookerSDK::Response::RaiseError if @options[:persistent] conn.adapter :net_http_persistent end end } endpoint = conn_hash[:api_endpoint] endpoint_uri = URI.parse(endpoint) root = endpoint.slice(0..-endpoint_uri.path.length) agent = Sawyer::Agent.new(root, sawyer_options) do |http| http.headers[:accept] = 'application/json' http.headers[:user_agent] = conn_hash[:user_agent] end begin versions_response = agent.call(:get,"/versions") @versions = versions_response.data.supported_versions.map {|v| v.version} @current_version = versions_response.data.current_version.version || "4.0" rescue Faraday::SSLError => e raise Gzr::CLI::Error, "SSL Certificate could not be verified\nDo you need the --no-verify-ssl option or the --no-ssl option?" rescue Faraday::ConnectionFailed => cf raise Gzr::CLI::Error, "Connection Failed.\nDid you specify the --no-ssl option for an ssl secured server?\nYou may need to use --port=443 in some cases as well." rescue LookerSDK::NotFound => nf say_warning "endpoint #{root}/versions was not found" end end say_warning "API current_version #{@current_version}" if @options[:debug] say_warning "Supported API versions #{@versions}" if @options[:debug] api_version = [min_api_version, @current_version].max raise Gzr::CLI::Error, "Operation requires API v#{api_version}, which is not available from this host" if api_version && !@versions.any? {|v| v == api_version} conn_hash = build_connection_hash(api_version) @secret = nil say_ok("connecting to #{conn_hash.map { |k,v| "#{k}=>#{(k == :client_secret) ? '*********' : v}" }}") if @options[:debug] begin faraday = Faraday.new(conn_hash[:connection_options]) do |conn| conn.use LookerSDK::Response::RaiseError if @options[:persistent] conn.adapter :net_http_persistent end end @sdk = LookerSDK::Client.new(conn_hash.merge(faraday: faraday)) unless @sdk say_ok "check for connectivity: #{@sdk.alive?}" if @options[:debug] if @options[:token_file] entry = read_token_data&.fetch(@options[:host].to_sym,nil)&.fetch(@options[:su]&.to_sym || :default,nil) if entry.nil? say_error "No token found for host #{@options[:host]} and user #{@options[:su] || "default"}" say_error "login with `gzr session login --host #{@options[:host]}` to set a token" raise LookerSDK::Unauthorized.new end (day, time, tz) = entry[:expiration].split(' ') day_parts = day.split('-') time_parts = time.split(':') date_time_parts = day_parts + time_parts + [tz] expiration = Time.new(*date_time_parts) if expiration < (Time.now + 300) if expiration < Time.now say_error "token expired at #{expiration}" else say_error "token expires at #{expiration}, which is in the next 5 minutes" end say_error "login again with `gzr session login --host #{@options[:host]}`" raise LookerSDK::Unauthorized.new end @sdk.access_token = entry[:token] elsif @options[:token] @sdk.access_token = @options[:token] end say_ok "verify authentication: #{@sdk.authenticated?}" if @options[:debug] rescue LookerSDK::Unauthorized => e say_error "Unauthorized - credentials are not valid" raise rescue LookerSDK::Error => e say_error "Unable to connect" say_error e say_error e.errors if e.respond_to?(:errors) && e.errors raise end raise Gzr::CLI::Error, "Invalid credentials" unless @sdk.authenticated? if @options[:su] && !(@options[:token] || @options[:token_file])then say_ok "su to user #{@options[:su]}" if @options[:debug] @access_token_stack.push(@sdk.access_token) begin @sdk.access_token = @sdk.login_user(@options[:su]).access_token say_warning "verify authentication: #{@sdk.authenticated?}" if @options[:debug] rescue LookerSDK::Error => e say_error "Unable to su to user #{@options[:su]}" say_error e say_error e.errors if e.respond_to?(:errors) && e.errors raise end end @sdk end def logout_all pastel = Pastel.new(enabled: true) say_ok "logout" if @options[:debug] begin @sdk.logout rescue LookerSDK::Error => e say_error "Unable to logout" say_error e say_error e.errors if e.respond_to?(:errors) && e.errors end if @sdk loop do token = @access_token_stack.pop break unless token say_ok "logout the parent session" if @options[:debug] @sdk.access_token = token begin @sdk.logout rescue LookerSDK::Error => e say_error "Unable to logout" say_error e say_error e.errors if e.respond_to?(:errors) && e.errors end end end def with_session(min_api_version="4.0") return nil unless block_given? begin login(min_api_version) unless @sdk yield rescue LookerSDK::Error => e say_error e.errors if e.respond_to?(:errors) && e.errors e.backtrace.each { |b| say_error b } if @options[:debug] raise Gzr::CLI::Error, e.message ensure logout_all unless @options[:token] || @options[:token_file] end end def update_auth(workspace_id) body = {} body[:workspace_id] = workspace_id begin @sdk.update_session(body)&.to_attrs rescue LookerSDK::Error => e say_error "Unable to run update_session(#{JSON.pretty_generate(body)})" say_error e end end end end