diff --git a/examples/account_management/invite_user_with_access_role.rb b/examples/account_management/invite_user_with_access_role.rb index d96de3e8f..fa742e176 100755 --- a/examples/account_management/invite_user_with_access_role.rb +++ b/examples/account_management/invite_user_with_access_role.rb @@ -38,14 +38,29 @@ 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 + 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 @@ -86,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 8770082b2..9587704c6 100755 --- a/examples/account_management/update_user_access.rb +++ b/examples/account_management/update_user_access.rb @@ -83,11 +83,19 @@ 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}." -end + 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 +if __FILE__ == $PROGRAM_NAME ACCESS_ROLES = %w[ ADMIN STANDARD @@ -135,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 new file mode 100644 index 000000000..8b126cc8f --- /dev/null +++ b/examples/advanced_operations/fetch_and_approve_pending_multi_party_auth_reviews.rb @@ -0,0 +1,200 @@ +#!/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) + else + puts "No pending multi-party auth reviews found for customer ID #{customer_id}." + 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.review_status, + 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 "\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 "\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 "\tInvitation 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" + else + puts "No result or error returned." + 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! + + 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("-", "") + ) + 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