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 "googleauth/base_client"
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 = MultiJson.load 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]
        #
        def project_number
          segments = @audience.split "/"
          idx = segments.index "projects"
          return nil if idx.nil? || idx + 1 == segments.size
          segments[idx + 1]
        end

        def normalize_timestamp time
          case time
          when NilClass
            nil
          when Time
            time
          when String
            Time.parse time
          else
            raise "Invalid time value #{time}"
          end
        end

        def service_account_email
          return nil if @service_account_impersonation_url.nil?
          start_idx = @service_account_impersonation_url.rindex "/"
          end_idx = @service_account_impersonation_url.index ":generateAccessToken"
          if start_idx != -1 && end_idx != -1 && start_idx < end_idx
            start_idx += 1
            return @service_account_impersonation_url[start_idx..end_idx]
          end
          nil
        end
      end
    end
  end
end