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