lib/conjur/base.rb in conjur-api-4.28.1 vs lib/conjur/base.rb in conjur-api-4.29.0

- old
+ new

@@ -115,15 +115,10 @@ # for the identity using {.new_from_key}. This method is useful when you are performing authorization checks # given a token. For example, a Conjur gateway that requires you to prove that you can 'read' a resource named # 'super-secret' might get the token from a request header, create an {Conjur::API} instance with this method, # and use {Conjur::Resource#permitted?} to decide whether to accept and forward the request. # - # Note that Conjur tokens are issued as JSON. This method expects to get the token as a parsed JSON Hash. - # When sending tokens as headers, you will normally use base64 encoded strings. Authorization headers - # used by Conjur have the form `'Token token="#{b64encode token.to_json}"'`, but this format is in no way - # required. - # # @example A simple gatekeeper # RESOURCE_NAME = 'protected-service' # # def handle_request request # token_header = request.header 'X-Conjur-Token' @@ -139,10 +134,25 @@ # @param [String] remote_ip the optional IP address to be recorded in the audit record. # @return [Conjur::API] an api that will authenticate with the token def new_from_token(token, remote_ip = nil) self.new.init_from_token token, remote_ip end + + # Create a new {Conjur::API} instance from a file containing a token issued by the + # {http://developer.conjur.net/reference/services/authentication Conjur authentication service}. + # The file is read the first time that a token is required. It is also re-read + # whenever the API decides that the token it already has is getting close to expiration. + # + # This method is useful when an external process, such as a sidecar container, is continuously + # obtaining fresh tokens and writing them to a known file. + # + # @param [String] token_file the file path containing an authentication token as parsed JSON. + # @param [String] remote_ip the optional IP address to be recorded in the audit record. + # @return [Conjur::API] an api that will authenticate with the tokens provided in the file. + def new_from_token_file(token_file, remote_ip = nil) + self.new.init_from_token_file token_file, remote_ip + end def encode_audit_ids(ids) ids.collect{|id| CGI::escape(id)}.join('&') end @@ -251,54 +261,148 @@ # Ensure that all resource ids are fully qualified api.audit_resources = resource_ids.collect { |id| api.resource(id).resourceid } end end + module MonotonicTime + def monotonic_time + Process.clock_gettime Process::CLOCK_MONOTONIC + rescue + # fall back to normal clock if there's no CLOCK_MONOTONIC + Time.now.to_f + end + end + + module TokenExpiration + include MonotonicTime + + # The four minutes is to work around a bug in Conjur < 4.7 causing a 404 on + # long-running operations (when the token is used right around the 5 minute mark). + TOKEN_STALE = 4.minutes + + attr_accessor :token_born + + def needs_token_refresh? + token_age > TOKEN_STALE + end + + def token_age + gettime - token_born + end + end + + # When the API is constructed with an API key, the token can be refreshed using + # the username and API key. This authenticator assumes that the token was + # minted immediately before the API instance was created. + class APIKeyAuthenticator + include TokenExpiration + + attr_reader :username, :api_key + + def initialize username, api_key + @username = username + @api_key = api_key + update_token_born + end + + def refresh_token + Conjur::API.authenticate(username, api_key).tap do + update_token_born + end + end + + def update_token_born + self.token_born = gettime + end + + def gettime + monotonic_time + end + end + + # When the API is constructed with a token, the token cannot be refreshed. + class UnableAuthenticator + include MonotonicTime + + def refresh_token + raise "Unable to re-authenticate using an access token" + end + + def needs_token_refresh? + false + end + end + + # Obtains fresh tokens by reading them from a file. Some other process is assumed + # to be acquiring tokens and storing them to the file on a regular basis. + # + # This authenticator assumes that the token was created immediately before + # it was written to the file. + class TokenFileAuthenticator + attr_reader :token_file + + def initialize token_file + @token_file = token_file + end + + attr_reader :last_mtime + + def mtime + File.mtime token_file + end + + def refresh_token + # There's a race condition here in which the file could be updated + # after we read the mtime but before we read the file contents. So to be + # conservative, use the oldest possible mtime. + mtime = self.mtime + File.open token_file, 'r' do |f| + JSON.load(f.read).tap { @last_mtime = mtime } + end + end + + def needs_token_refresh? + mtime != last_mtime + end + end + def init_from_key username, api_key, remote_ip = nil @username = username @api_key = api_key @remote_ip = remote_ip + @authenticator = APIKeyAuthenticator.new(username, api_key) self end def init_from_token token, remote_ip = nil @token = token @remote_ip = remote_ip + @authenticator = UnableAuthenticator.new self end + def init_from_token_file token_file, remote_ip = nil + @remote_ip = remote_ip + @authenticator = TokenFileAuthenticator.new(token_file) + self + end + + attr_reader :authenticator + private - attr_accessor :token_born # Tries to refresh the token if possible. # # @return [Hash, false] false if the token couldn't be refreshed due to # unavailable API key; otherwise, the new token. def refresh_token - return false unless @api_key - self.token_born = gettime - @token = Conjur::API.authenticate(@username, @api_key) + @token = @authenticator.refresh_token end - # The four minutes is to work around a bug in Conjur < 4.7 causing a 404 on - # long-running operations (when the token is used right around the 5 minute mark). - TOKEN_STALE = 4.minutes - # Checks if the token is old (or not present). # # @return [Boolean] def needs_token_refresh? - !@token || ((token_age || 0) > TOKEN_STALE) - end - - def gettime - Process.clock_gettime Process::CLOCK_MONOTONIC - rescue - # fall back to normal clock if there's no CLOCK_MONOTONIC - Time.now.to_f - end - - def token_age - token_born && (gettime - token_born) + !@token || @authenticator.needs_token_refresh? end end end