# frozen_string_literal: true require "fmrest/v1/connection" module FmRest module V1 # FM Data API authentication middleware using the credentials strategy # class TokenSession < Faraday::Middleware class NoSessionTokenSet < FmRest::Error; end HEADER_KEY = "Authorization" TOKEN_STORE_INTERFACE = [:load, :store, :delete].freeze LOGOUT_PATH_MATCHER = %r{\A(#{FmRest::V1::Connection::DATABASES_PATH}/[^/]+/sessions/)[^/]+\Z}.freeze # @param app [#call] # @param settings [FmRest::ConnectionSettings] def initialize(app, settings) super(app) @settings = settings end # Entry point for the middleware when sending a request # def call(env) return handle_logout(env) if is_logout_request?(env) set_auth_header(env) request_body = env[:body] # After failure env[:body] is set to the response body @app.call(env).on_complete do |response_env| if response_env[:status] == 401 # Unauthorized delete_token_store_key if @settings.autologin env[:body] = request_body set_auth_header(env) return @app.call(env) end end end end private def delete_token_store_key token_store.delete(token_store_key) # Sometimes we may want to pass the :token in settings manually, and # refrain from passing a :username. In that case the call to # #token_store_key above would fail as it tries to fetch :username, so # we purposely ignore that error. rescue FmRest::ConnectionSettings::MissingSetting end def handle_logout(env) token = @settings.token? ? @settings.token : token_store.load(token_store_key) raise NoSessionTokenSet, "Couldn't send logout request because no session token was set" unless token env.url.path = env.url.path.gsub(LOGOUT_PATH_MATCHER, "\\1#{token}") @app.call(env).on_complete do |response_env| delete_token_store_key if response_env[:status] == 200 end end def is_logout_request?(env) return false unless env.method == :delete return env.url.path.match?(LOGOUT_PATH_MATCHER) end def set_auth_header(env) env.request_headers[HEADER_KEY] = "Bearer #{token}" end # Uses the token given in connection settings if available, # otherwisek tries to get an existing token from the token store, # otherwise requests one through basic auth, # otherwise raises an exception. # def token return @settings.token if @settings.token? token = token_store.load(token_store_key) return token if token return nil unless @settings.autologin token = V1.request_auth_token!(auth_connection) token_store.store(token_store_key, token) token end # The key to use to store a token, uses the format host:database:username # def token_store_key @token_store_key ||= begin # Strip the host part to just the hostname (i.e. no scheme or port) host = @settings.host! host = URI(host).hostname if host =~ /\Ahttps?:\/\// identity_segment = if fmid_token = @settings.fmid_token require "digest" Digest::SHA256.hexdigest(fmid_token) else @settings.username! end "#{host}:#{@settings.database!}:#{identity_segment}" end end def token_store @token_store ||= begin if TOKEN_STORE_INTERFACE.all? { |method| token_store_option.respond_to?(method) } token_store_option elsif token_store_option.kind_of?(Class) if token_store_option.respond_to?(:instance) token_store_option.instance else token_store_option.new end else FmRest::TokenStore::Memory.new end end end def token_store_option @settings.token_store || FmRest.token_store end def auth_connection @auth_connection ||= V1.auth_connection(@settings) end end end end