UNLOGGED_CHANGES = ['last_used_at', 'last_used_by_ip_address', 'updated_at']
def assign_random_api_token
- self.api_token ||= rand(2**256).to_s(36)
+ begin
+ self.api_token ||= rand(2**256).to_s(36)
+ rescue ActiveModel::MissingAttributeError
+ # Ignore the case where self.api_token doesn't exist, which happens when
+ # the select=[...] is used.
+ end
end
def owner_uuid
clnt
end
+ def self.check_anonymous_user_token(token:, remote:)
+ case token[0..2]
+ when 'v2/'
+ _, token_uuid, secret, optional = token.split('/')
+ unless token_uuid.andand.length == 27 && secret.andand.length.andand > 0 &&
+ token_uuid == Rails.configuration.ClusterID+"-gj3su-anonymouspublic"
+ # invalid v2 token, or v2 token for another user
+ return nil
+ end
+ else
+ # v1 token
+ secret = token
+ end
+
+ # Usually, the secret is salted
+ salted_secret = OpenSSL::HMAC.hexdigest('sha1', Rails.configuration.Users.AnonymousUserToken, remote)
+
+ # The anonymous token could be specified as a full v2 token in the config,
+ # but the config loader strips it down to the secret part.
+ # The anonymous token content and minimum length is verified in lib/config
+ if secret.length >= 0 && (secret == Rails.configuration.Users.AnonymousUserToken || secret == salted_secret)
+ return ApiClientAuthorization.new(user: User.find_by_uuid(anonymous_user_uuid),
+ uuid: Rails.configuration.ClusterID+"-gj3su-anonymouspublic",
+ api_token: secret,
+ api_client: anonymous_user_token_api_client,
+ scopes: ['GET /'])
+ else
+ return nil
+ end
+ end
+
def self.check_system_root_token token
if token == Rails.configuration.SystemRootToken
return ApiClientAuthorization.new(user: User.find_by_uuid(system_user_uuid),
return nil if token.nil? or token.empty?
remote ||= Rails.configuration.ClusterID
+ auth = self.check_anonymous_user_token(token: token, remote: remote)
+ if !auth.nil?
+ return auth
+ end
+
auth = self.check_system_root_token(token)
if !auth.nil?
return auth
remote_user_prefix = remote_user['uuid'][0..4]
- if token_uuid == ''
- # Use the same UUID as the remote when caching the token.
- begin
- remote_token = SafeJSON.load(
- clnt.get_content('https://' + host + '/arvados/v1/api_client_authorizations/current',
- {'remote' => Rails.configuration.ClusterID},
- {'Authorization' => 'Bearer ' + token}))
- token_uuid = remote_token['uuid']
- if !token_uuid.match(HasUuid::UUID_REGEX) || token_uuid[0..4] != upstream_cluster_id
- raise "remote cluster #{upstream_cluster_id} returned invalid token uuid #{token_uuid.inspect}"
- end
- rescue => e
- Rails.logger.warn "error getting remote token details for #{token.inspect}: #{e}"
- return nil
+ # Get token scope, and make sure we use the same UUID as the
+ # remote when caching the token.
+ remote_token = nil
+ begin
+ remote_token = SafeJSON.load(
+ clnt.get_content('https://' + host + '/arvados/v1/api_client_authorizations/current',
+ {'remote' => Rails.configuration.ClusterID},
+ {'Authorization' => 'Bearer ' + token}))
+ Rails.logger.debug "retrieved remote token #{remote_token.inspect}"
+ token_uuid = remote_token['uuid']
+ if !token_uuid.match(HasUuid::UUID_REGEX) || token_uuid[0..4] != upstream_cluster_id
+ raise "remote cluster #{upstream_cluster_id} returned invalid token uuid #{token_uuid.inspect}"
+ end
+ rescue HTTPClient::BadResponseError => e
+ if e.res.status != 401
+ raise
+ end
+ rev = SafeJSON.load(clnt.get_content('https://' + host + '/discovery/v1/apis/arvados/v1/rest'))['revision']
+ if rev >= '20010101' && rev < '20210503'
+ Rails.logger.warn "remote cluster #{upstream_cluster_id} at #{host} with api rev #{rev} does not provide token expiry and scopes; using scopes=['all']"
+ else
+ # remote server is new enough that it should have accepted
+ # this request if the token was valid
+ raise
end
+ rescue => e
+ Rails.logger.warn "error getting remote token details for #{token.inspect}: #{e}"
+ return nil
end
# Clusters can only authenticate for their own users.
user.last_name = "from cluster #{remote_user_prefix}"
end
- user.save!
+ begin
+ user.save!
+ rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique
+ Rails.logger.debug("remote user #{remote_user['uuid']} already exists, retrying...")
+ # Some other request won the race: retry fetching the user record.
+ user = User.find_by_uuid(remote_user['uuid'])
+ if !user
+ Rails.logger.warn("cannot find or create remote user #{remote_user['uuid']}")
+ return nil
+ end
+ end
if user.is_invited && !remote_user['is_invited']
# Remote user is not "invited" state, they should be unsetup, which
end
end
- # We will accept this token (and avoid reloading the user
- # record) for 'RemoteTokenRefresh' (default 5 minutes).
- # Possible todo:
- # Request the actual api_client_auth record from the remote
- # server in case it wants the token to expire sooner.
- auth = ApiClientAuthorization.find_or_create_by(uuid: token_uuid) do |auth|
- auth.user = user
- auth.api_client_id = 0
- end
# If stored_secret is set, we save stored_secret in the database
# but return the real secret to the caller. This way, if we end
# up returning the auth record to the client, they see the same
# secret they supplied, instead of the HMAC we saved in the
# database.
stored_secret = stored_secret || secret
+
+ # We will accept this token (and avoid reloading the user
+ # record) for 'RemoteTokenRefresh' (default 5 minutes).
+ exp = [db_current_time + Rails.configuration.Login.RemoteTokenRefresh,
+ remote_token.andand['expires_at']].compact.min
+ scopes = remote_token.andand['scopes'] || ['all']
+ begin
+ retries ||= 0
+ auth = ApiClientAuthorization.find_or_create_by(uuid: token_uuid) do |auth|
+ auth.user = user
+ auth.api_token = stored_secret
+ auth.api_client_id = 0
+ auth.scopes = scopes
+ auth.expires_at = exp
+ end
+ rescue ActiveRecord::RecordNotUnique
+ Rails.logger.debug("cached remote token #{token_uuid} already exists, retrying...")
+ # Some other request won the race: retry just once before erroring out
+ if (retries += 1) <= 1
+ retry
+ else
+ Rails.logger.warn("cannot find or create cached remote token #{token_uuid}")
+ return nil
+ end
+ end
auth.update_attributes!(user: user,
api_token: stored_secret,
api_client_id: 0,
- expires_at: db_current_time + Rails.configuration.Login.RemoteTokenRefresh)
- Rails.logger.debug "cached remote token #{token_uuid} with secret #{stored_secret} in local db"
+ scopes: scopes,
+ expires_at: exp)
+ Rails.logger.debug "cached remote token #{token_uuid} with secret #{stored_secret} and scopes #{scopes} in local db"
auth.api_token = secret
return auth
end
protected
def clamp_token_expiration
- if !current_user.andand.is_admin && Rails.configuration.API.MaxTokenLifetime > 0
+ if Rails.configuration.API.MaxTokenLifetime > 0
max_token_expiration = db_current_time + Rails.configuration.API.MaxTokenLifetime
- if self.new_record? && (self.expires_at.nil? || self.expires_at > max_token_expiration)
- self.expires_at = max_token_expiration
- elsif !self.new_record? && self.expires_at_changed? && (self.expires_at.nil? || self.expires_at > max_token_expiration)
+ if (self.new_record? || self.expires_at_changed?) && (self.expires_at.nil? || (self.expires_at > max_token_expiration && !current_user.andand.is_admin))
self.expires_at = max_token_expiration
end
end