lib/googleauth/external_account/external_account_utils.rb



# Copyright 2023 Google, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.require "time"

require "json"
require "googleauth/base_client"
require "googleauth/errors"
require "googleauth/helpers/connection"
require "googleauth/oauth2/sts_client"

module Google
  # Module Auth provides classes that provide Google-specific authorization
  # used to access Google APIs.
  module Auth
    module ExternalAccount
      # Authenticates requests using External Account credentials, such
      # as those provided by the AWS provider or OIDC provider like Azure, etc.
      module ExternalAccountUtils
        # Cloud resource manager URL used to retrieve project information.
        CLOUD_RESOURCE_MANAGER = "https://cloudresourcemanager.googleapis.com/v1/projects/".freeze

        ##
        # Retrieves the project ID corresponding to the workload identity or workforce pool.
        # For workforce pool credentials, it returns the project ID corresponding to the workforce_pool_user_project.
        # When not determinable, None is returned.
        #
        # The resource may not have permission (resourcemanager.projects.get) to
        # call this API or the required scopes may not be selected:
        # https://cloud.google.com/resource-manager/reference/rest/v1/projects/get#authorization-scopes
        #
        # @return [String, nil] The project ID corresponding to the workload identity
        #   pool or workforce pool if determinable
        #
        def project_id
          return @project_id unless @project_id.nil?
          project_number = self.project_number || @workforce_pool_user_project

          # if we missing either project number or scope, we won't retrieve project_id
          return nil if project_number.nil? || @scope.nil?

          url = "#{CLOUD_RESOURCE_MANAGER}#{project_number}"
          response = connection.get url do |req|
            req.headers["Authorization"] = "Bearer #{@access_token}"
            req.headers["Content-Type"] = "application/json"
          end

          if response.status == 200
            response_data = JSON.parse response.body, symbolize_names: true
            @project_id = response_data[:projectId]
          end

          @project_id
        end

        ##
        # Retrieve the project number corresponding to workload identity pool
        # STS audience pattern:
        #     `//iam.googleapis.com/projects/$PROJECT_NUMBER/locations/...`
        #
        # @return [String, nil] The project number extracted from the audience string,
        #   or nil if it cannot be determined
        #
        def project_number
          segments = @audience.split "/"
          idx = segments.index "projects"
          return nil if idx.nil? || idx + 1 == segments.size
          segments[idx + 1]
        end

        # Normalizes a timestamp value to a Time object
        #
        # @param time [Time, String, nil] The timestamp to normalize
        # @return [Time, nil] The normalized timestamp or nil if input is nil
        # @raise [Google::Auth::CredentialsError] If the time value is not nil, Time, or String
        def normalize_timestamp time
          case time
          when NilClass
            nil
          when Time
            time
          when String
            Time.parse time
          else
            raise CredentialsError, "Invalid time value #{time}"
          end
        end

        # Extracts the service account email from the impersonation URL
        #
        # @return [String, nil] The service account email extracted from the
        #   service_account_impersonation_url, or nil if it cannot be determined
        def service_account_email
          return nil if @service_account_impersonation_url.nil?
          match = @service_account_impersonation_url.match %r{serviceAccounts/([^:]+):generateAccessToken$}
          match[1] if match
        end
      end
    end
  end
end