Skip to content

Commit

Permalink
feat: Persistent tokens for login (#182)
Browse files Browse the repository at this point in the history
* feat: persistent sessions

* feat: expiration tracking for tokens

* feat: force new login if token expires in next 5 minutes

* feat: improved error handling around token file
  • Loading branch information
drstrangelooker authored Apr 25, 2023
1 parent bda38eb commit 482c00f
Show file tree
Hide file tree
Showing 5 changed files with 227 additions and 7 deletions.
5 changes: 5 additions & 0 deletions lib/gzr/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ def self.exit_on_failure?
class_option :su, type: :string, desc: 'After connecting, change to user_id given'
class_option :width, type: :numeric, default: nil, desc: 'Width of rendering for tables'
class_option :persistent, type: :boolean, default: false, desc: 'Use persistent connection to communicate with host'
class_option :token, type: :string, default: nil, desc: "Access token to use for authentication"
class_option :token_file, type: :boolean, default: false, desc: "Use access token stored in file for authentication"

# Error raised by this runner
Error = Class.new(StandardError)
Expand Down Expand Up @@ -96,5 +98,8 @@ def version

require_relative 'commands/folder'
register Gzr::Commands::Folder, 'folder', 'folder [SUBCOMMAND]', 'Commands pertaining to folders'

require_relative 'commands/session'
register Gzr::Commands::Session, 'session', 'session [SUBCOMMAND]', 'Commands pertaining to sessions'
end
end
56 changes: 56 additions & 0 deletions lib/gzr/commands/session.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# 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 'thor'

module Gzr
module Commands
class Session < Thor

namespace :session

desc 'login', 'Create a persistent session'
method_option :text, type: :boolean, default: false,
desc: 'output token to screen instead of file'
def login(*)
if options[:help]
invoke :help, ['login']
else
require_relative 'session/login'
Gzr::Commands::Session::Login.new(options).execute
end
end

desc 'logout', 'End a persistent session'
def logout(*)
if options[:help]
invoke :help, ['logout']
else
require_relative 'session/logout'
Gzr::Commands::Session::Logout.new(options).execute
end
end

end
end
end
50 changes: 50 additions & 0 deletions lib/gzr/commands/session/login.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# 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_relative '../../command'

module Gzr
module Commands
class Session
class Login < Gzr::Command
def initialize(options)
super()
@options = options
end

def execute(input: $stdin, output: $stdout)
say_warning(@options) if @options[:debug]
login
if @options[:text]
puts @sdk.access_token
return
end
token_data = read_token_data || {}
token_data[@options[:host].to_sym] ||= {}
token_data[@options[:host].to_sym][@options[:su]&.to_sym || :default] = { token: @sdk.access_token, expiration: @sdk.access_token_expires_at }
write_token_data(token_data)
end
end
end
end
end
47 changes: 47 additions & 0 deletions lib/gzr/commands/session/logout.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# 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_relative '../../command'

module Gzr
module Commands
class Session
class Logout < Gzr::Command
def initialize(options)
super()
@options = options
end

def execute(input: $stdin, output: $stdout)
say_warning(@options) if @options[:debug]
login
@sdk.logout
token_data = read_token_data || {}
token_data[@options[:host].to_sym] ||= {}
token_data[@options[:host].to_sym]&.delete(@options[:su]&.to_sym || :default)
write_token_data(token_data)
end
end
end
end
end
76 changes: 69 additions & 7 deletions lib/gzr/modules/session.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,38 @@ def sufficient_version?(given_version, minimum_version)
!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
Expand Down Expand Up @@ -86,6 +118,9 @@ def build_connection_hash(api_version=nil)
}
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
Expand All @@ -103,12 +138,14 @@ def build_connection_hash(api_version=nil)
end

def login(min_api_version="4.0")
if (@options[:client_id].nil? && ENV["LOOKERSDK_CLIENT_ID"])
@options[:client_id] = ENV["LOOKERSDK_CLIENT_ID"]
end
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"]
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"])
Expand Down Expand Up @@ -183,6 +220,31 @@ def login(min_api_version="4.0")
@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"
Expand All @@ -196,7 +258,7 @@ def login(min_api_version="4.0")
raise Gzr::CLI::Error, "Invalid credentials" unless @sdk.authenticated?


if @options[:su] then
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
Expand Down Expand Up @@ -247,7 +309,7 @@ def with_session(min_api_version="4.0")
e.backtrace.each { |b| say_error b } if @options[:debug]
raise Gzr::CLI::Error, e.message
ensure
logout_all
logout_all unless @options[:token] || @options[:token_file]
end
end
end
Expand Down

0 comments on commit 482c00f

Please sign in to comment.