Google Sheets AddOn - AppScript & BigQuery Integration via Service Account - google-apps-script

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)
}
}

Related

OAuth2 access token coming back as Null in Google App Script

Apologies if this is a silly question, I am very new to programming in general and I have never worked with OAuth before.
I am currently trying to build a Google App Script which interacts with an external service and authenticates using OAuth2.
I am having a lot of trouble with OAuth2... for starters, I'm not sure I am using the correct library for this. I am going off the one recommended by Google for ads script - https://developers.google.com/google-ads/scripts/docs/examples/oauth20-library
I know there is another popular one available in GitHub https://github.com/googleworkspace/apps-script-oauth2
I was not able to use it because everything comes out as 'is not a function', no matter how I add the library, manually or through the built-in feature.
I started building the API call, based on the first library and I had some partial success, I started getting back a 500 error message and I realized my accessToken is null.
function SHICall(){
var tokenUrl = "X/token";
const scriptProperties = PropertiesService.getScriptProperties();
var clientId = scriptProperties.getProperty('CLIENT_ID');
var clientSecret = scriptProperties.getProperty('SECRET');
var opt_scope = "CustomerAPI.Public";
// Access token is obtained and cached.
const authUrlFetch = OAuth2.withClientCredentials(tokenUrl, clientId, clientSecret, opt_scope);
const url = "X";
Logger.log(authUrlFetch);
var options = {
headers: { 'Content-Type': "application/json", 'Accept': "application/json"},
muteHttpExceptions: true,
method: "GET",
contentType: "application/json",
validateHttpsCertificates: false,
};
// Use access token in each request
const response = authUrlFetch.fetch(url, options);
// ... use response
Logger.log(response);
}
Any clue why the token is coming back as null? I based my API Call on google's documentation again https://developers.google.com/google-ads/scripts/docs/features/third-party-apis#oauth_2

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?

What is the benefit of calling google web app with access token and what are the disadvantages of not using it

client (imitating google sheets add-on) :
function getReqParam(type){
//DriveApp.getFiles() - commented row needed for the Drive scope request
let data = { param2: "value2" ,param1: Session.getActiveUser()};
var params = {
method: type,
contentType: "application/json",
headers: { Authorization: "Bearer " + ScriptApp.getOAuthToken() },
payload: JSON.stringify(data),
muteHttpExceptions: true,
};
return params;
}
function sendPostReq(){
let res = UrlFetchApp.fetch(url, getReqParam("POST"));
console.log("sendPostReq response: ", res.getResponseCode());
console.log("sendPostReq response: ", res.getContentText());
}
webapp:
function doPost(e){
console.log("doPost",e);
try{
saveRegToDB(JSON.parse(e.postData.contents));
e.method = "POST";
//return regisration status
e.regStatus = {status: "active"};
}catch(err){
e.regStatus = {status: "error"};
e.err = JSON.stringify(err);
e.errMSg = JSON.stringify(err.message);
console.error("doPost exception",err);
console.error("doPost exception",err.stack);
}
return ContentService.createTextOutput(JSON.stringify(e))
.setMimeType(ContentService.MimeType.JSON);
}
function saveRegToDB({param1,param2}={}){
let rowID = -1;
if (param1 && param2){
let ss = getSS();
let sheetName = "Registrations";
let s = ss.getSheetByName(sheetName);
let r = s.getLastRow() + 1;
rowID = r;
let rVals = [param1,param2,getTimeStamp()];
let vals = [rVals]; //2 dimensions array
console.log("reg saveToDB ",vals,r);
//write to sheet
s.getRange(r,1,1,rVals.length).setValues(vals);
}
console.log("saveRegToDB:" ,rowID);
return rowID;
}
function getSS(){
let ssid = getSSid();
let ss = SpreadsheetApp.openById(ssid);
SpreadsheetApp.setActiveSpreadsheet(ss);
return ss;
}
function getSSid(){
return PropertiesService.getScriptProperties()
.getProperty("regSSID");
}
function getTimeStamp() {
return Utilities.formatDate(new Date(), Session.getScriptTimeZone(), "yyyy-MM-dd HH:mm:ss");
}
I have a simple google web app. The web app is supposed to serve a google sheets addon, to store add-on related information to a private SS (owned by the add-on account), for example: users' registrations. I am wondering about the Who has access setting I should use in order to deploy the the web app, and the usage of access token that comes with it as explained here.
The Execute as (legacy editor: Execute the app as to Me
When deploying webapp with Who has access: Anyone with Google account (Anyone in legacy editor) web app should be called with access token
headers: { Authorization: "Bearer " + ScriptApp.getOAuthToken() }
There are 2 problems with that approach:
here it is claimed that webapp script project should be shared with all the users that might use it. In my case every user that will install the addon, that is: everyone. This is a disatvantage for itself. Despite that, another user (say, user2) can access successfully the web app.
In order the use access token the client has to request for Drive scopes, even though web app doesn't use any of the Drive API. More over, even of calling the web app with access code, it fails to access the user drive files, due to missing permissions Exception: You do not have permission to access the requested document.
So not only, client is enforced to ask for Drive scope web app does not need, even if trying to "use" Drive API to access client drive - it is blocked.
I have tested it by setting the regSSID script property read in getSSid() to spreadsheet owned by the user2, and also executing client from user2 account script editor
What is the benefit of deploying web app with Anyone with Google account (Anyone in legacy editor) and using access token for that?
When deploying webapp with Who has access: Anyone (Anyone, even anonymous in legacy editor) no access token is required, thus no need to ask for Drive scope. This is good thing. The question is what might the disadvantages of deploying the web app to Anyone, especially in terms of security. I guess web app can filtered out all calls from unwanted access by validating applicative post request parameter. Is this a real security issue?

Using the BigQuery API on APP Scritps with a Service Account

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.

ScriptApp.getOAuthToken not getting the right permissions for drive through url fetch app?

Trying to explore this with a very simple script but I'm getting an insufficient permissions error:
function mini(){
var gdriveId = "1hp8ncIG4Ww7FH8wi7HjJzzzzzzz";
var options = {
method: "GET",
headers: {
'Authorization': 'Bearer ' + ScriptApp.getOAuthToken()
},
}
var url = "https://www.googleapis.com/drive/v2/files/"+gdriveId+"/children";
var response = JSON.parse(UrlFetchApp.fetch( url, options).getContentText());
}
I tried enabling the v2 drive api in the advanced google services dropdown but that didn't work.
I believe your situation and goal as follows.
From gdriveId in your script, I thought that you want to retrieve the folder list in the root folder of gdriveId using the method of "Children: list" in Drive API v2.
You have already enabled Drive API at Advanced Google Services.
For this, how about this answer?
Modification points:
When your script is put to new GAS project and Drive API is enabled at Advanced Google Services, the scopes of the project is only https://www.googleapis.com/auth/script.external_request. The required scope can be automatically detected by the script editor. But, even when Drive API is only enabled, it seems that no scopes are added. I think that the reason of your issue is this.
Under above situation, if you want to retrieve the access token including the required scopes, in order to make the script editor automatically detect the scope of https://www.googleapis.com/auth/drive.readonly, for example, please put // DriveApp.getFiles() to the script as a comment line.
In this case, when you use the methods for other scopes in your script, those scopes can be automatically detected and added by the script editor.
Modified script 1:
When your script is modified, it becomes as follows.
function mini(){
var gdriveId = "1hp8ncIG4Ww7FH8wi7HjJzzzzzzz";
var options = {
method: "GET",
headers: {
'Authorization': 'Bearer ' + ScriptApp.getOAuthToken()
},
}
var url = "https://www.googleapis.com/drive/v2/files/"+gdriveId+"/children";
var response = JSON.parse(UrlFetchApp.fetch( url, options).getContentText());
}
// DriveApp.getFiles() // <--- Added this comment line. By this, the scope of https://www.googleapis.com/auth/drive.readonly is added.
Modified script 2:
When the method of Advanced Google service is used, the scope of https://www.googleapis.com/auth/drive is automatically added. By this, the following script works.
function test() {
var gdriveId = "1hp8ncIG4Ww7FH8wi7HjJzzzzzzz";
var res = Drive.Children.list(gdriveId);
console.log(res)
}
Other pattern:
From June 1, 2020, the files and folders in the shared Drive can be retrieved by Drive service. So you can also use the following script.
function myFunction() {
const getFolderList = (id, folders = []) => {
const f = DriveApp.getFolderById(id);
const fols = f.getFolders();
let temp = [];
while (fols.hasNext()) {
const fol = fols.next();
temp.push({name: fol.getName(), id: fol.getId(), parent: f.getName()});
}
if (temp.length > 0) {
folders.push(temp);
temp.forEach((e) => getFolderList(e.id, folders));
}
return folders.flat();
};
var gdriveId = "###"; // Please set the Drive ID.
const res = getFolderList(gdriveId);
console.log(res);
}
References:
Advanced Google services
Children: list of Drive API v2
Authorization Scopes
If you want to give permission to write with ScriptApp.getOAuthToken(), just add the following code in a commented out form and authorize it at runtime. If you don't do this, you'll only be able to download and browse.
//DriveApp.addFile("test");
Reference URL:https://00m.in/UeeOB