X-Git-Url: https://git.arvados.org/arvados.git/blobdiff_plain/e8d73e8066b61f7704dc0f6cf200953cdf9a5e60..cb807029865aacbc54dc88b524ee55f3c5bfd327:/services/api/app/models/api_client_authorization.rb diff --git a/services/api/app/models/api_client_authorization.rb b/services/api/app/models/api_client_authorization.rb index 4218645d5d..52922d32b1 100644 --- a/services/api/app/models/api_client_authorization.rb +++ b/services/api/app/models/api_client_authorization.rb @@ -7,6 +7,7 @@ class ApiClientAuthorization < ArvadosModel include KindAndEtag include CommonApiTemplate extend CurrentApiClient + extend DbCurrentTime belongs_to :api_client belongs_to :user @@ -34,7 +35,12 @@ class ApiClientAuthorization < ArvadosModel 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 @@ -110,6 +116,37 @@ class ApiClientAuthorization < ArvadosModel 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), @@ -125,6 +162,11 @@ class ApiClientAuthorization < ArvadosModel 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 @@ -248,21 +290,34 @@ class ApiClientAuthorization < ArvadosModel 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. @@ -305,7 +360,17 @@ class ApiClientAuthorization < ArvadosModel 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 @@ -338,26 +403,43 @@ class ApiClientAuthorization < ArvadosModel 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: Time.now + 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 @@ -387,11 +469,9 @@ class ApiClientAuthorization < ArvadosModel protected def clamp_token_expiration - if !current_user.andand.is_admin && Rails.configuration.API.MaxTokenLifetime > 0 - max_token_expiration = Time.now + 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 Rails.configuration.API.MaxTokenLifetime > 0 + max_token_expiration = db_current_time + Rails.configuration.API.MaxTokenLifetime + 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