Using the BigQuery API on APP Scritps with a Service Account - google-apps-script

I'm trying execute a job on BigQuery on a VPC project using App Scripts.
My goal is store the result in an array to create a dynamic prompt for DataStudio using community connectors
Using the following code:
function runQuery() {
var sql = "SELECT Distinct ss_cd FROM `vf-pt-ngbi-dev-gen-03.AEAD_DataSet_test.d_customer` WHERE end_dttm IS NOT NULL";
var queryResults;
var projectNumber = 'projectNumber'
// Inserts a Query Job
try {
var queryRequest = BigQuery.newQueryRequest();
queryRequest.setQuery(sql).setTimeoutMs(100000);
queryResults = BigQuery.Jobs.query(queryRequest, projectNumber);
}
catch (err) {
Logger.log(err);
return;
}
Since this is a VPC project I need to use a Service Account to perform this request?
However, I would like to know how to add this authorization?
Or exists another approach to execute a BigQuery job on a VPC project and store the results in an array?

You can get the service account token in apps script (see reference) then use that token for the REST API via UrlFetchApp.
Sample:
function runQuery() {
// ...
var service = getService();
if (service.hasAccess()) {
sendQuery(service);
}
// ...
}
function sendQuery(service){
var projectId = 'projectID';
var url = 'https://bigquery.googleapis.com/bigquery/v2/projects/' + projectId + '/queries';
// see request body for reference
// https://cloud.google.com/bigquery/docs/reference/rest/v2/jobs/query#QueryRequest
var body = {
// ...
}
var options = {
"method": "post",
"headers": {
"Authorization": "Bearer " + service.getAccessToken()
},
"contentType": "application/json",
"payload": JSON.stringify(body)
};
var response = UrlFetchApp.fetch(url, options);
}
// direclty copied from https://github.com/googleworkspace/apps-script-oauth2/blob/master/samples/GoogleServiceAccount.gs
function getService() {
return OAuth2.createService('BigQuery:' + USER_EMAIL)
// Set the endpoint URL.
.setTokenUrl('https://oauth2.googleapis.com/token')
// Set the private key and issuer.
.setPrivateKey(PRIVATE_KEY)
.setIssuer(CLIENT_EMAIL)
// Set the name of the user to impersonate. This will only work for
// Google Apps for Work/EDU accounts whose admin has setup domain-wide
// delegation:
// https://developers.google.com/identity/protocols/OAuth2ServiceAccount#delegatingauthority
.setSubject(USER_EMAIL)
// Set the property store where authorized tokens should be persisted.
.setPropertyStore(PropertiesService.getScriptProperties())
// Set the scope. This must match one of the scopes configured during the
// setup of domain-wide delegation.
.setScope('https://www.googleapis.com/auth/bigquery');
}
References:
BigQuery REST API
Creation of Service Account
Get Service Account Token
Note:
See a question containing a code what yours should look like.

Related

Google Ads API with Google Apps Script no access token received

I am trying to use Google Ads on Google Apps Script using this guide. I created client id and client secret on Google Console's API & Services.
Not too sure if this configuration is correct but the account is linked to Google Apps Script as I have pagespeed insights as well and there are some requests on the dashboard. I added https://www.googleapis.com/auth/drive as the scope. Again not too sure if I should add Google Ads to the scope. Lastly, got my refresh token from Google Auth playground. When I run the script above I got the following error:
Error: No access token received: {
"error": "invalid_client",
"error_description": "Unauthorized"
}
authenticate_ # test.gs:120
withRefreshToken # test.gs:144
initializeOAuthClient # test.gs:28
Honestly not too sure what I am doing wrong here so any help would be very much appreciated. Thank you.
Edit
Codes:
//From Google Console API & Services
var CLIENT_ID = '"MY_CLIENT_ID';
var CLIENT_SECRET = 'MY_CLIENT_SECRET';
//From Google Authplayground
var REFRESH_TOKEN = 'REFRESH_TOKEN';
// Enter scopes which should match scopes in File > Project properties
// For this project, e.g.: https://www.googleapis.com/auth/drive
var SCOPES = "https://www.googleapis.com/auth/adwords";
// Script ID taken from 'File > Project Properties'
var SCRIPT_ID = 'MY_SCRIPT_ID';
var authUrlFetch;
// Call this function just once, to initialize the OAuth client.
function initializeOAuthClient() {
if (typeof OAuth2 === 'undefined') {
var libUrl = 'https://developers.google.com/google-ads/scripts/docs/examples/oauth20-library';
throw Error('OAuth2 library not found. Please take a copy of the OAuth2 ' +
'library from ' + libUrl + ' and append to the bottom of this script.');
}
var tokenUrl = 'https://accounts.google.com/o/oauth2/token';
authUrlFetch = OAuth2.withRefreshToken(tokenUrl, CLIENT_ID, CLIENT_SECRET,
REFRESH_TOKEN, SCOPES);
}
/**
* Execute a remote function.
* #param {string} remoteFunctionName The name of the function to execute.
* #param {Object[]} functionParams An array of JSON objects to pass to the
* remote function.
* #return {?Object} The return value from the function.
*/
function executeRemoteFunction(remoteFunctionName, functionParams) {
var apiParams = {
'function': remoteFunctionName,
'parameters': functionParams
};
var httpOptions = {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
payload: JSON.stringify(apiParams)
};
var url = 'https://script.googleapis.com/v1/scripts/' + SCRIPT_ID + ':run';
var response = authUrlFetch.fetch(url, httpOptions);
var data = JSON.parse(response.getContentText());
// Retrieve the value that has been returned from the execution.
if (data.error) {
throw Error('There was an error: ' + response.getContentText());
}
return data.response.result;
}
// Paste in OAuth2 library here, from:
// https://developers.google.com/google-ads/scripts/docs/examples/oauth20-library
I have pasted the oauth2.0 library under the codes above.
Edit 2
I fixed the part of function initializeOAuthClient. It now shows execution complete, but when I try to run function executeRemoteFunction I am getting TypeError: Cannot read property 'fetch' of undefined. I am guessing I have to input remoteFunctionName and functionParams but where do I find them?

Google Sheets AddOn - AppScript & BigQuery Integration via Service Account

I have a Google Sheets workspace addon and recently did some work to integrate BigQuery. Essentially BigQuery hold a record of books each of which has an author, title etc and my Addon allows people to pull the books that they have read into their sheet. The first column in the sheet allows people to choose from all the authors in the DB, based on that selection the second column is populated with data from BigQuery with all books by that author etc etc. There is no need for my AddOn to access a user's BigQuery, they only access 'my' BgQuery.
This all works fine, but when I submitted my addon for approval I was told
Unfortunately, we cannot approve your request for the use of the following scopes
https://www.googleapis.com/auth/bigquery
We recommend using service accounts for this type of information exchange.
This seems fair and reading up on Service Accounts it seems a much better fit for my use case. I've gone through the process of creating the service accounts and downloaded my security details json file, however I just can't figure out how to actually query BigQuery from AppScript.
In my non-service account method I have the BigQuery Library installed in AppScript and basically run
var queryResults = BigQuery.Jobs.query(request, projectId);
I've been trying to work from an example at https://developers.google.com/datastudio/solution/blocks/using-service-accounts
function getOauthService() {
var serviceAccountKey = getServiceAccountCreds('SERVICE_ACCOUNT_KEY');// from private_key not private_key_id of JSON file
var serviceAccountEmail = getServiceAccountCreds('SERVICE_ACCOUNT_EMAIL');
return OAuth2.createService('RowLevelSecurity')
.setAuthorizationBaseUrl('https://accounts.google.com/o/oauth2/auth')
.setTokenUrl('https://accounts.google.com/o/oauth2/token')
.setPrivateKey(serviceAccountKey)
.setIssuer(serviceAccountEmail)
.setPropertyStore(scriptProperties)
.setCache(CacheService.getScriptCache())
.setScope(['https://www.googleapis.com/auth/bigquery.readonly']);
}
function getData(request) {
var accessToken = getOauthService().getAccessToken();
var billingProjectId = getServiceAccountCreds('BILLING_PROJECT_ID');
// var email = Session.getEffectiveUser().getEmail();
// return cc
// .newBigQueryConfig()
// .setAccessToken(accessToken)
// .setBillingProjectId(billingProjectId)
// .setUseStandardSql(true)
// .setQuery(BASE_SQL)
// .addQueryParameter('email', bqTypes.STRING, email)
// .build();
}
I've commented out the code in the above which relates to
var cc = DataStudioApp.createCommunityConnector();
in the above tutorial since I'm not using DataStudio but I'm really not sure what to replace it with so I can query BigQuery with AppScript via a Service Account. Can anyone offer any advice?
Based on the advice from #TheAddonDepot in the comments above my revised code now looks like:
function getBigQueryService() {
return (
OAuth2.createService('BigQuery')
// Set the endpoint URL.
.setTokenUrl('https://accounts.google.com/o/oauth2/token')
// Set the private key and issuer.
.setPrivateKey(JSON_CREDS.private_key) // from the json file downloaded when you create service account
.setIssuer(JSON_CREDS.client_email). // from the json file downloaded when you create service account
// Set the property store where authorized tokens should be persisted.
.setPropertyStore(PropertiesService.getScriptProperties())
// Caching
.setCache(CacheService.getUserCache())
// Locking
.setLock(LockService.getUserLock())
// Set the scopes.
.setScope(['https://www.googleapis.com/auth/bigquery.readonly'])
// .setScope('https://www.googleapis.com/auth/bigquery')
)
}
function queryData(){
const bigQueryService = getBigQueryService()
if (!bigQueryService.hasAccess()) {
Logger.log("BQ ERROR IS "+ bigQueryService.getLastError())
}
//const projectId = bigqueryCredentials.project_id
var projectId = "<yourprojectid>"
let url = 'https://bigquery.googleapis.com/bigquery/v2/projects/<yourprojectid>/queries'; //projectID is taken from the security json file for the service account, although it doesn't seem to matter if you use the project code
const headers = {
Authorization: `Bearer ${bigQueryService.getAccessToken()}`,
'Content-Type': 'application/json',
}
var data = {query:"<your query>",useLegacySql:false};
const options = {
method: 'post',
headers,
//contentType: 'application/json',
payload: JSON.stringify(data),
muteHttpExceptions: true // on for debugging
}
try {
const response = UrlFetchApp.fetch(url, options)
const result = JSON.parse(response.getContentText())
Logger.log("here is result "+ JSON.stringify(result))
} catch (err) {
console.error(err)
}
}

Upload to Google Cloud storage from App Script

I've been bashing my head at this for over a day, but I can't figure out how to upload data to Google Cloud Storage via an app script attached to a google sheet. I've been running into issues with authorisation. I've copied the getService method from here (pasted below) but the service keeps failing to receive authorisation. service.hasAccess() always returns false.
function uploadFileToGCS(dataJSON) {
var service = getService();
if (!service.hasAccess()) {
Browser.msgBox("Failed to grant service access")
return;
}
var url = 'https://www.googleapis.com/upload/storage/v1/b/BUCKET/o?uploadType=media&name=FILE'
.replace("BUCKET", params.BUCKET_NAME)
.replace("FILE", encodeURIComponent(params.FILE_PATH));
var response = UrlFetchApp.fetch(url, {
method: "POST",
payload: dataJSON,
contentType: "application/json",
headers: {
Authorization: 'Bearer ' + service.getAccessToken()
}
});
var result = JSON.parse(response.getContentText());
Logger.log(JSON.stringify(result, null, 2));
}
function getService() {
return OAuth2.createService('ctrlq')
.setAuthorizationBaseUrl('https://accounts.google.com/o/oauth2/auth')
.setTokenUrl('https://accounts.google.com/o/oauth2/token')
.setClientId(params.CLIENT_ID)
.setClientSecret(params.CLIENT_SECRET)
.setCallbackFunction('authCallback')
.setPropertyStore(PropertiesService.getUserProperties())
.setScope('https://www.googleapis.com/auth/devstorage.read_write')
.setParam('access_type', 'offline')
.setParam('approval_prompt', 'force')
.setParam('login_hint', Session.getActiveUser().getEmail());
}
function authCallback(request) {
var service = getService();
var authorized = service.handleCallback(request);
if (authorized) {
return HtmlService.createHtmlOutput('Connected to Google Cloud Storage');
} else {
return HtmlService.createHtmlOutput('Access Denied');
}
}
I've created OAUTH credentials for a web-app on the Google Cloud Console. I've also enabled the Cloud Storage API and Google Cloud Storage JSON API. I'm unsure however on the redirect URL. (Ideally, I'd like to use a service account because I just want to take the values from the spreadsheet and upload them as a JSON file.)
Anyway, appreciate the help!
I think google app script project and google cloud project should be linked. You need to create a google cloud project to use this API.
I encountered a similar problem when I wanted to duplicate an object in Cloud Storage. Not sure if this is the solution for you, but I'm putting it here in case someone need it.
Just use the XML-formmatted REST API with OAuth2 token, and the code looks like this:
var objectSource = "SOURCE_BUCKET_NAME/SOURCE_OBJECT_NAME";
var url = "https://storage.googleapis.com/DESTINATION_BUCKET_NAME/NAME_OF_COPY";
var resp = UrlFetchApp.fetch(url, {
method: "PUT",
headers: {
Authorization: 'Bearer '+ OAUTH2_TOKEN,
"x-goog-copy-source": objectSource,
},
'muteHttpExceptions': true,
});
Check out the Cloud Storage's document for differences between copy and upload.

How to authenticate Google Sheets using Service Account within Google Apps Script

I am trying to create a Google app that will search a keyword in a Google Sheets, request (Column A) and return the response (Column B). The script's working well, but whenever the app is called by an end user it is requesting permission to access the Sheets.
I want to know how end users can access the app without permission and how can I provide the app to use service account to read the spreadsheet within the code?
function ReadExcelFile()
{
var spreadsheetId = <MySpreadSheetID>;
var sheet = SpreadsheetApp.openById(spreadsheetId);
var SearchText='Hello';
var data = sheet.getRange('A:B').getValues();
for(var i in data){
if(i>0){
var row = data[i][0];
Logger.log('Searching for a KeyWord: '+SearchText+' From the SpreetSheet Value : '+row);
var SearchCount=SearchText.indexOf(row);
if(SearchCount>-1){
var row2=data[i][1];
Logger.log(SearchCount+' '+row2);
break;
}
}
}
}
Request | Response
Hello | Hi Welcome!!
Hey | Hey Welcome!!
Output would be..
Hi Welcome!!
I also faced this problem, I want to read the container bound spreadsheet data and reply back as messages to the end user when they messages to the google chatbot, Finally I solved by this way, it may help.
Here is my appscript.json code
{
"exceptionLogging": "STACKDRIVER",
"runtimeVersion": "V8",
"dependencies": {
"libraries": [
{
"userSymbol": "OAuth2",
"libraryId": "1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF",
"version": "26"
}
],
"enabledAdvancedServices": [
{
"userSymbol": "Sheets",
"version": "v4",
"serviceId": "sheets"
}
]
},
"chat": {
"addToSpaceFallbackMessage": "Thank you for adding me"
}
}
and here is the oauth, token access, and query api code
var PRIVATE_KEY = 'your service account private key generated Google Cloud Project (GCP) console ';
var CLIENT_EMAIL = 'clien-email';
var ssId = 'Spreadsheet Id which you want to read or write data from end user';
/**
* Configures the Chatbot service.
*/
function getChatbotService() {
return OAuth2.createService("sathibhai-bot")
// Set the endpoint URL.
.setTokenUrl("https://accounts.google.com/o/oauth2/token")
// Set the private key and issuer.
.setPrivateKey(PRIVATE_KEY)
.setIssuer(CLIENT_EMAIL)
// Set the property store where authorized tokens should be persisted.
.setPropertyStore(PropertiesService.getScriptProperties())
// Set the scope.
.setScope("https://www.googleapis.com/auth/chat.bot");
}
/**
* Configures the spreadsheet service.
*/
function getSpreasheetService() {
return OAuth2.createService("spreadsheet")
// Set the endpoint URL.
.setTokenUrl("https://accounts.google.com/o/oauth2/token")
// Set the private key and issuer.
.setPrivateKey(PRIVATE_KEY)
.setIssuer(CLIENT_EMAIL)
// Set the property store where authorized tokens should be persisted.
.setPropertyStore(PropertiesService.getScriptProperties())
// Set the scope.
.setScope("https://www.googleapis.com/auth/spreadsheets");
}
function readSheet(){
var service = getSpreasheetService();
var range = 'Sheet1!A1:D';
var url = 'https://sheets.googleapis.com/v4/spreadsheets/' + ssId +'/values/' + range;
var response = UrlFetchApp.fetch(url, { headers: {Authorization: 'Bearer ' + service.getAccessToken() } });
var rep = JSON.parse(response.getContentText());
var values = rep.values;
for(row in values)
Logger.log(values[row][0] + ":" + values[row][1] + ":" + values[row][2] + ":" + values[row][3]); //you can use these data as your requirements
}
Note that:
Share your spreadsheet with the service account i.i your-service-account#sys-..............iam.gserviceaccount.com (and no need to share to any body except service account)
Enable Spreadsheet API in GCP console.

Google Apps Script to add an alias to Gmail

I have a Google Apps Script that gives me the error "Delegation denied for jasonjurotich#school.edu.mx", and is not working in order to add an alias (another email) to an account within a domain. It may be because of Token headers or some URL that is missing that authorizes something, but I cannot find enough documentation that clarifies how to add it.
This should not be confused with creating an alias in the Google Admin console for the same email. Rather, this is adding another separate account to the first account so as to send emails on behalf of the second account.
All the necessary permissions have been activated (OAuth2, a Google Service account with domain-wide delegation, necessary APIs are activated, etc.)
The script I have is the following:
var JSON = {
"private_key": "key",
"client_email": "email",
"client_id": "ID",
"user_email": "teststudent#school.edu.mx"
};
function getOAuthService(user) {
return OAuth2.createService('Service Account')
.setAuthorizationBaseUrl('https://accounts.google.com/o/oauth2/auth')
.setTokenUrl('https://accounts.google.com/o/oauth2/token')
.setClientId('ID')
.setPrivateKey(JSON.private_key)
.setIssuer(JSON.client_email)
.setSubject(JSON.user_email)
.setPropertyStore(PropertiesService.getScriptProperties())
.setParam('access_type', 'offline')
.setParam('approval_prompt', 'force')
.setScope('https://www.googleapis.com/auth/script.external_request https://www.googleapis.com/auth/gmail.settings.sharing');
}
function changeEmail() {
var service = getOAuthService();
service.reset();
if (service.hasAccess()) {
var userEmail = 'teststudent#school.edu.mx';
var alias = {
sendAsEmail: 'aliastest1#school.edu.mx',
displayName: 'TS',
replyToAddress : 'aliastest1#school.edu.mx',
treatAsAlias: true
};
Gmail.Users.Settings.SendAs.create(alias, userEmail);
}
}
It seems that you are using the older version of OAuth2 library for Google Apps Script. Please check the source code and make sure it doesn't invoke ScriptApp.getProjectKey(). For example, the version below utilizes ScriptApp.getScriptId() instead of the deprecated method:
https://github.com/gsuitedevs/apps-script-oauth2/blob/master/dist/OAuth2.gs
Try connecting the new version to your GAS project as a library or manually add the files to your script and see if that fixes things.
UPDATE
I believe what happens is that you override a permissive authorization scope with a more restrictive one. Looking at the source code of the 'setScope()' method, it doesn't look like you can call it in succession.
Service_.prototype.setScope = function(scope, optSeparator) {
var separator = optSeparator || ' ';
this.params_.scope = Array.isArray(scope) ? scope.join(separator) : scope;
return this;
};
Rather, you must provide the list of scopes and the optional separator (the default one is space). As a result, the only authorization scope your script ends up using is https://www.googleapis.com/auth/gmail.settings.basic.
Bottom line: call 'setScope()' once, passing the space-separated list of required scopes.
Instead of 2 separate calls, try this:
setScope('https://www.googleapis.com/auth/gmail.settings.basic https://www.googleapis.com/auth/gmail.settings.sharing');
A colleague of mine posted something similar to this question here: Creates a custom "from" send-as alias with GAS and APIs. The following modified code is what finally worked taking into account what was put here and what they put there.
var service_account = {
"private_key": "-----BEGIN PRIVATE KEY-----",
"client_email": "changealiastest4#project-id-[].iam.gserviceaccount.com",
"client_id": "ID",
"user_email": "teststudent#school.edu.mx"
};
function getOAuthService(user) {
return OAuth2.createService('Service Account')
.setAuthorizationBaseUrl('https://accounts.google.com/o/oauth2/auth')
.setTokenUrl('https://accounts.google.com/o/oauth2/token')
.setClientId('ID')
.setPrivateKey(service_account.private_key)
.setIssuer(service_account.client_email)
.setSubject(service_account.user_email)
.setPropertyStore(PropertiesService.getScriptProperties())
.setParam('access_type', 'offline')
.setParam('approval_prompt', 'force')
.setScope('https://www.googleapis.com/auth/script.external_request https://www.googleapis.com/auth/gmail.settings.sharing https://www.googleapis.com/auth/gmail.settings.basic');
}
function changeEmail() {
var userEmail = 'teststudent#school.edu.mx';
var aliasEmail = 'aliastest1#school.edu.mx';
var aliasName = 'TS';
var service = getOAuthService();
service.reset();
if (service.hasAccess()) {
var url = 'https://www.googleapis.com/gmail/v1/users/me/settings/sendAs'
var headers ={
"Authorization": 'Bearer ' + service.getAccessToken(),
"Accept":"application/json",
"Content-Type":"application/json",
};
var resource ={
sendAsEmail: aliasEmail,
displayName: aliasName,
replyToAddress : aliasEmail,
treatAsAlias: true,
verificationStatus: 'accepted'
};
var options = {
'headers': headers,
'method': 'POST',
'payload': JSON.stringify(resource),
'muteHttpExceptions': true
};
Logger.log(options);
var response = UrlFetchApp.fetch(url, options);
Logger.log(response.getContentText());
}
}