From 1654a00bd67c14639368e51ac4f70b2edf633a5b Mon Sep 17 00:00:00 2001 From: dorasun Date: Thu, 16 Jul 2026 16:04:19 +0000 Subject: [PATCH 1/2] Changes needed for MPA support Change-Id: Iaefffce0a2955c3df4369affc6335fb9313151df --- .../invite_user_with_access_role.rb | 18 +- .../account_management/update_user_access.rb | 12 +- ...pprove_pending_multi_party_auth_reviews.rb | 190 ++++++++++++++++++ ...config.rb => google_ads_config.rb.template | 0 4 files changed, 213 insertions(+), 7 deletions(-) create mode 100644 examples/advanced_operations/fetch_and_approve_pending_multi_party_auth_reviews.rb rename google_ads_config.rb => google_ads_config.rb.template (100%) diff --git a/examples/account_management/invite_user_with_access_role.rb b/examples/account_management/invite_user_with_access_role.rb index d96de3e8f..4ce64fae7 100755 --- a/examples/account_management/invite_user_with_access_role.rb +++ b/examples/account_management/invite_user_with_access_role.rb @@ -38,11 +38,19 @@ def invite_user_with_access_role(customer_id, email_address, access_role) operation: operation, ) - # Prints out information of the created invitation. - puts "Customer user access invitation was sent for customerId = #{customer_id} " \ - "email address = '#{email_address}', " \ - "access role = '#{access_role}'." - # [END invite_user_with_access_role] + if !response.result.multi_party_auth_review.empty? + puts "A multi-party auth review was triggered. The MPA review resource " \ + "name is #{response.result.multi_party_auth_review}. Ask a second " \ + "administrator to approve this request to send the user access invitation. " \ + "See advanced_operations/fetch_and_approve_pending_multi_party_auth_reviews.rb " \ + "for an example on how to approve an MPA auth review using the API." + else + # Print out information of the created invitation. + puts "Customer user access invitation was sent for customerId = #{customer_id} " \ + "email address = '#{email_address}', " \ + "access role = '#{access_role}'. The invitation resource name is " \ + "#{response.result.resource_name}." + end end if __FILE__ == $PROGRAM_NAME diff --git a/examples/account_management/update_user_access.rb b/examples/account_management/update_user_access.rb index 8770082b2..468fe7bff 100755 --- a/examples/account_management/update_user_access.rb +++ b/examples/account_management/update_user_access.rb @@ -83,8 +83,16 @@ def modify_user_access(client, customer_id, user_id, access_role) operation: operation, ) - puts "Successfully updated customer user access with resource name " \ - "#{response.result.resource_name}." + if !response.result.multi_party_auth_review.empty? + puts "A multi-party auth review was triggered. The MPA review resource " \ + "name is #{response.result.multi_party_auth_review}. Ask a second " \ + "administrator to approve this request to make the requested user " \ + "access changes. See advanced_operations/fetch_and_approve_pending_multi_party_auth_reviews.rb " \ + "for an example on how to approve an MPA auth review using the API." + else + puts "Successfully updated customer user access with resource name " \ + "#{response.result.resource_name}." + end end if __FILE__ == $0 diff --git a/examples/advanced_operations/fetch_and_approve_pending_multi_party_auth_reviews.rb b/examples/advanced_operations/fetch_and_approve_pending_multi_party_auth_reviews.rb new file mode 100644 index 000000000..6ac30d116 --- /dev/null +++ b/examples/advanced_operations/fetch_and_approve_pending_multi_party_auth_reviews.rb @@ -0,0 +1,190 @@ +#!/usr/bin/env ruby +# Encoding: utf-8 +# +# Copyright 2026 Google LLC +# +# 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 +# +# https://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. +# +# This example fetches pending multi-party approvals and approves the first one. +# +# Multi-party authorization (MPA) reviews can only be resolved one at a time. +# In this code example, we illustrate approving the first pending review request. + +require 'optparse' +require 'google/ads/google_ads' + +def fetch_and_approve_pending_multi_party_auth_reviews(customer_id) + # GoogleAdsClient will read a config file from + # ENV['HOME']/google_ads_config.rb when called without parameters + client = Google::Ads::GoogleAds::GoogleAdsClient.new + + # Retrieve the list of pending MPA reviews. + pending_reviews = fetch_pending_mpa_reviews(client, customer_id) + + if !pending_reviews.empty? + # Multi-party auth reviews can only be resolved one at a time. In this + # code example, we illustrate approving the first pending review request. + approve_mpa_review(client, customer_id, pending_reviews.first) + end +end + +# [START fetch_mpa_review] +def fetch_pending_mpa_reviews(client, customer_id) + pending_reviews = [] + + # Create a query that will retrieve all the pending MPA reviews. + query = <<~QUERY + SELECT + multi_party_auth_review.resource_name, + multi_party_auth_review.multi_party_auth_review_id, + multi_party_auth_review.creation_date_time, + multi_party_auth_review.request_user_email, + multi_party_auth_review.operation_type, + multi_party_auth_review.justification, + multi_party_auth_review.target_resource, + multi_party_auth_review.customer_user_access_review.old_customer_user_access, + multi_party_auth_review.customer_user_access_review.new_customer_user_access, + multi_party_auth_review.customer_user_access_invitation_review.new_customer_user_access_invitation + FROM multi_party_auth_review + WHERE multi_party_auth_review.review_status = 'PENDING' + QUERY + + responses = client.service.google_ads.search_stream( + customer_id: customer_id, + query: query, + ) + + responses.each do |response| + response.results.each do |row| + mpa_review = row.multi_party_auth_review + puts "#{mpa_review.request_user_email} created a pending " \ + "multi-party auth review with ID #{mpa_review.multi_party_auth_review_id} " \ + "at #{mpa_review.creation_date_time}. This request is for target " \ + "resource type = #{mpa_review.target_resource} and operation type = " \ + "#{mpa_review.operation_type}. The justification is " \ + "\"#{mpa_review.justification}\"." + + if mpa_review.target_resource == :CUSTOMER_USER_ACCESS + access_review = mpa_review.customer_user_access_review + if mpa_review.operation_type == :UPDATE + # When updating a customer user access, only the new access level + # is populated. + puts "Old resource name: #{access_review.old_customer_user_access}, " \ + "new access role: #{access_review.new_customer_user_access.access_role}." + elsif mpa_review.operation_type == :REMOVE + puts "Old resource name: #{access_review.old_customer_user_access}." + end + elsif mpa_review.target_resource == :CUSTOMER_USER_ACCESS_INVITATION + new_invite = mpa_review.customer_user_access_invitation_review.new_customer_user_access_invitation + puts "Invitation email address: #{new_invite.email_address}, " \ + "Role: #{new_invite.access_role}." + end + + pending_reviews << mpa_review.resource_name + end + end + + pending_reviews +end +# [END fetch_mpa_review] + +# [START approve_mpa_review] +def approve_mpa_review(client, customer_id, pending_review) + # Currently, you can only approve one request at a time. In addition, the approvals + # can only be done by a second administrator. + response = client.service.multi_party_auth_review.resolve_multi_party_auth_review( + customer_id: customer_id, + operations: [ + { + multi_party_auth_review: pending_review, + new_status: :APPROVED, + } + ], + ) + + result_or_error = response.result_or_error.first + + if result_or_error.result + result = result_or_error.result + puts "Approved multi-party auth review: #{result.multi_party_auth_review}." + if !result.customer_user_access_invitation.empty? + puts "New user invitation created: #{result.customer_user_access_invitation}" + elsif !result.customer_user_access.empty? + puts "Affected customer user access resource: #{result.customer_user_access}" + end + elsif result_or_error.partial_failure_error + # Partial failure error + errors_count = 0 + failures = client.decode_partial_failure_error(result_or_error.partial_failure_error) + failures.each do |failure| + failure.errors.each do |error| + puts "\tError with message '#{error.message}'." + errors_count += 1 + end + end + puts "#{errors_count} partial failure error(s) occurred" + end +end +# [END approve_mpa_review] + +if __FILE__ == $PROGRAM_NAME + options = {} + # The following parameter(s) should be provided to run the example. You can + # either specify these by changing the INSERT_XXX_ID_HERE values below, or on + # the command line. + # + # Parameters passed on the command line will override any parameters set in + # code. + # + # Running the example with -h will print the command line usage. + options[:customer_id] = 'INSERT_CUSTOMER_ID_HERE' + + OptionParser.new do |opts| + opts.banner = sprintf('Usage: ruby %s [options]', File.basename(__FILE__)) + + opts.separator '' + opts.separator 'Options:' + + opts.on('-C', '--customer-id CUSTOMER-ID', String, 'Customer ID') do |v| + options[:customer_id] = v + end + + opts.separator '' + opts.separator 'Help:' + + opts.on_tail('-h', '--help', 'Show this message') do + puts opts + exit + end + end.parse! + + begin + fetch_and_approve_pending_multi_party_auth_reviews( + options.fetch(:customer_id).tr("-", "") + ) + rescue Google::Ads::GoogleAds::Errors::GoogleAdsError => e + e.failure.errors.each do |error| + STDERR.printf("Error with message: %s\n", error.message) + if error.location + error.location.field_path_elements.each do |field_path_element| + STDERR.printf("\tOn field: %s\n", field_path_element.field_name) + end + end + error.error_code.to_h.each do |k, v| + next if v == :UNSPECIFIED + STDERR.printf("\tType: %s\n\tCode: %s\n", k, v) + end + end + raise + end +end diff --git a/google_ads_config.rb b/google_ads_config.rb.template similarity index 100% rename from google_ads_config.rb rename to google_ads_config.rb.template From 7d2c9d36e85c2f09cb881bbaecce017ea3999afd Mon Sep 17 00:00:00 2001 From: dorasun Date: Thu, 16 Jul 2026 21:17:11 +0000 Subject: [PATCH 2/2] Respond to comments Change-Id: Ie7f15d5ef3968dd177b8eddfbf43b566f72fca3c --- .../invite_user_with_access_role.rb | 21 ++++++++++++++++++- .../account_management/update_user_access.rb | 18 +++++++++++++--- ...pprove_pending_multi_party_auth_reviews.rb | 20 +++++++++++++----- 3 files changed, 50 insertions(+), 9 deletions(-) diff --git a/examples/account_management/invite_user_with_access_role.rb b/examples/account_management/invite_user_with_access_role.rb index 4ce64fae7..fa742e176 100755 --- a/examples/account_management/invite_user_with_access_role.rb +++ b/examples/account_management/invite_user_with_access_role.rb @@ -54,6 +54,13 @@ def invite_user_with_access_role(customer_id, email_address, access_role) end if __FILE__ == $PROGRAM_NAME + ACCESS_ROLES = %w[ + ADMIN + STANDARD + READ_ONLY + EMAIL_ONLY + ] + options = {} # The following parameter(s) should be provided to run the example. You can # either specify these by changing the INSERT_XXX_ID_HERE values below, or on @@ -94,11 +101,23 @@ def invite_user_with_access_role(customer_id, email_address, access_role) end end.parse! + if options[:customer_id].nil? || options[:customer_id] == 'INSERT_CUSTOMER_ID_HERE' || + options[:email_address].nil? || options[:email_address] == 'INSERT_EMAIL_ADDRESS_HERE' || + options[:access_role].nil? || options[:access_role] == 'INSERT_ACCESS_ROLE_HERE' + puts "Missing required arguments. Customer ID (-C), Email Address (-E), and Access Role (-R) are required." + exit 1 + end + + unless ACCESS_ROLES.include?(options[:access_role]&.upcase) + puts "Invalid access role. Must be one of: #{ACCESS_ROLES.join(', ')}" + exit 1 + end + begin invite_user_with_access_role( options.fetch(:customer_id).tr("-", ""), options.fetch(:email_address), - options.fetch(:access_role).to_sym, + options.fetch(:access_role).upcase.to_sym, ) rescue Google::Ads::GoogleAds::Errors::GoogleAdsError => e e.failure.errors.each do |error| diff --git a/examples/account_management/update_user_access.rb b/examples/account_management/update_user_access.rb index 468fe7bff..9587704c6 100755 --- a/examples/account_management/update_user_access.rb +++ b/examples/account_management/update_user_access.rb @@ -93,9 +93,9 @@ def modify_user_access(client, customer_id, user_id, access_role) puts "Successfully updated customer user access with resource name " \ "#{response.result.resource_name}." end -end +end -if __FILE__ == $0 +if __FILE__ == $PROGRAM_NAME ACCESS_ROLES = %w[ ADMIN STANDARD @@ -143,11 +143,23 @@ def modify_user_access(client, customer_id, user_id, access_role) end end.parse! + if options[:customer_id].nil? || options[:customer_id] == 'INSERT_CUSTOMER_ID_HERE' || + options[:email_address].nil? || options[:email_address] == 'INSERT_EMAIL_ADDRESS_HERE' || + options[:access_role].nil? || options[:access_role] == 'INSERT_ACCESS_ROLE_HERE' + puts "Missing required arguments. Customer ID (-C), Email Address (-e), and Access Role (-a) are required." + exit 1 + end + + unless ACCESS_ROLES.include?(options[:access_role]&.upcase) + puts "Invalid access role. Must be one of: #{ACCESS_ROLES.join(', ')}" + exit 1 + end + begin update_user_access( options.fetch(:customer_id).tr("-", ""), options.fetch(:email_address), - options.fetch(:access_role), + options.fetch(:access_role).upcase, ) rescue Google::Ads::GoogleAds::Errors::GoogleAdsError => e e.failure.errors.each do |error| diff --git a/examples/advanced_operations/fetch_and_approve_pending_multi_party_auth_reviews.rb b/examples/advanced_operations/fetch_and_approve_pending_multi_party_auth_reviews.rb index 6ac30d116..8b126cc8f 100644 --- a/examples/advanced_operations/fetch_and_approve_pending_multi_party_auth_reviews.rb +++ b/examples/advanced_operations/fetch_and_approve_pending_multi_party_auth_reviews.rb @@ -35,6 +35,8 @@ def fetch_and_approve_pending_multi_party_auth_reviews(customer_id) # Multi-party auth reviews can only be resolved one at a time. In this # code example, we illustrate approving the first pending review request. approve_mpa_review(client, customer_id, pending_reviews.first) + else + puts "No pending multi-party auth reviews found for customer ID #{customer_id}." end end @@ -48,6 +50,7 @@ def fetch_pending_mpa_reviews(client, customer_id) multi_party_auth_review.resource_name, multi_party_auth_review.multi_party_auth_review_id, multi_party_auth_review.creation_date_time, + multi_party_auth_review.review_status, multi_party_auth_review.request_user_email, multi_party_auth_review.operation_type, multi_party_auth_review.justification, @@ -79,14 +82,14 @@ def fetch_pending_mpa_reviews(client, customer_id) if mpa_review.operation_type == :UPDATE # When updating a customer user access, only the new access level # is populated. - puts "Old resource name: #{access_review.old_customer_user_access}, " \ + puts "\tOld resource name: #{access_review.old_customer_user_access}, " \ "new access role: #{access_review.new_customer_user_access.access_role}." elsif mpa_review.operation_type == :REMOVE - puts "Old resource name: #{access_review.old_customer_user_access}." + puts "\tOld resource name: #{access_review.old_customer_user_access}." end elsif mpa_review.target_resource == :CUSTOMER_USER_ACCESS_INVITATION new_invite = mpa_review.customer_user_access_invitation_review.new_customer_user_access_invitation - puts "Invitation email address: #{new_invite.email_address}, " \ + puts "\tInvitation email address: #{new_invite.email_address}, " \ "Role: #{new_invite.access_role}." end @@ -114,7 +117,7 @@ def approve_mpa_review(client, customer_id, pending_review) result_or_error = response.result_or_error.first - if result_or_error.result + if result_or_error&.result result = result_or_error.result puts "Approved multi-party auth review: #{result.multi_party_auth_review}." if !result.customer_user_access_invitation.empty? @@ -122,7 +125,7 @@ def approve_mpa_review(client, customer_id, pending_review) elsif !result.customer_user_access.empty? puts "Affected customer user access resource: #{result.customer_user_access}" end - elsif result_or_error.partial_failure_error + elsif result_or_error&.partial_failure_error # Partial failure error errors_count = 0 failures = client.decode_partial_failure_error(result_or_error.partial_failure_error) @@ -133,6 +136,8 @@ def approve_mpa_review(client, customer_id, pending_review) end end puts "#{errors_count} partial failure error(s) occurred" + else + puts "No result or error returned." end end # [END approve_mpa_review] @@ -168,6 +173,11 @@ def approve_mpa_review(client, customer_id, pending_review) end end.parse! + if options[:customer_id].nil? || options[:customer_id] == 'INSERT_CUSTOMER_ID_HERE' + puts "Missing required argument: --customer-id (-C) is required." + exit 1 + end + begin fetch_and_approve_pending_multi_party_auth_reviews( options.fetch(:customer_id).tr("-", "")