How to authenticate Google Sheets using Service Account within Google Apps Script - 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.

Related

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.

Container bound apps script deploy from manifest can allow other user read data from container spreadsheet without sharing file

I am developing chatbot through container (Spreadsheet) bound apps script deploy from manifest for may internal organization members, the members can request info stored on the sheets by simply typing and sending through google chat.
Members can get the info automatically by the chatbot if I share the spreadsheet, otherwise the bot is not responding message replied.
When I share the file, the shard copy also appear to all of the members drive too. I want make that content read by user without sharing the Container spreadsheet.
What code I have to add to my script so that user get the permission to read my file during chatting. Is this possible?
My manifest code is as follows
{
"dependencies": {
"enabledAdvancedServices": [
{
"userSymbol": "Sheets",
"serviceId": "sheets",
"version": "v4"
}
],
"libraries": [
{
"userSymbol": "OAuth2",
"libraryId": "1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF",
"version": "40"
}
]
},
"exceptionLogging": "STACKDRIVER",
"runtimeVersion": "V8",
"chat": {
"addToSpaceFallbackMessage": "Hi ! Thanks for adding me. Type help to get more.."
}
}
and user oauth2 ..
/**
* Configures the Chatbot service.
*/
function getChatbotService() {
return OAuth2.createService("chat-sheet-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 https://www.googleapis.com/auth/spreadsheets");
}
Finally I have solved the problem as follows:
added get service method with spreadsheet scope as
/**
* 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");
}
added method read sheet to get data from spreadsheet by the service account using spreadsheet api
function readSheet(){
var service = getSpreasheetService();
var ssID = 'your Spreadsheet Id'
var range = 'Sheet1!A3:E';
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][2] + ":" + values[row][3] + ":" + values[row][4]);
//now i can use these data to reply back to end user as message
}
I shared the spreadsheet to the service account which looks like "your-service-account#sys................iam.gserviceaccount.com"
I enabled the Sheet API in Google Cloud Project console on API & Services
oh! it works with great charm.

Google Apps Script Wants Unrestricted Scope

Simple function here in Google Apps Script:
function driveSearch() {
// Log the name of every file in the user's Drive whose visibility is anyonewithLink or anyonecanfind
var files = DriveApp.searchFiles(
'visibility = "anyoneWithLink" or visibility = "anyoneCanFind"');
while (files.hasNext()) {
var file = files.next();
var owner = file.getOwner().getName();
var sa = file.getSharingAccess();
Logger.log(file.getName());
Logger.log('Owner:'+owner);
Logger.log("SharingAccess:"+sa);
}
}
It want to find shared files in my gsuite drive.
However, it says I don't have permissions to run DriveApp
My permissions are set and requested correctly, like so:
{
"timeZone": "America/New_York",
"dependencies": {
},
"exceptionLogging": "STACKDRIVER",
"oauthScopes": [
"https://www.googleapis.com/auth/drive.readonly",
"https://www.googleapis.com/auth/drive.metadata.readonly"
]
}
The error is
You do not have permission to call DriveApp.searchFiles. Required permissions: https://www.googleapis.com/auth/drive (line 3, file "Code")
Why does searchFiles require delete permissions? I am very worried about inadvertent deletion, I don't want the full scope. Is there something else I'm doing wrong?
Lastly, yes my Gsuite allows Google Apps Script.
How about this workaround? In this workaround, the endpoint of Drive API is directly requested by UrlFetchApp, and the following 2 scopes are used.
https://www.googleapis.com/auth/drive.metadata.readonly
This is used to the access token which is used for using Drive API.
https://www.googleapis.com/auth/script.external_request
This is used for using UrlFetchApp.
Enable Drive API at API console
Before you use this script, please enable Drive API as follows.
On script editor
Resources -> Cloud Platform project
View API console
At Getting started, click "Explore and enable APIs".
At left side, click Library.
At "Search for APIs & services", input "Drive". And click Drive API.
Click Enable button.
If API has already been enabled, please don't turn off.
Sample script:
function myFunction() {
var baseUrl = "https://www.googleapis.com/drive/v3/files";
var q = 'visibility = "anyoneWithLink" or visibility = "anyoneCanFind"';
var fields = "files(id,name,owners,permissions),nextPageToken";
var pageToken = "";
var results = [];
var params = {headers: {Authorization: "Bearer " + ScriptApp.getOAuthToken()}};
do {
var endpoint = baseUrl + "?pageSize=1000&q=" + encodeURIComponent(q) + "&fields=" + encodeURIComponent(fields) + "&pageToken=" + pageToken;
var res = UrlFetchApp.fetch(endpoint, params);
res = JSON.parse(res.getContentText());
Array.prototype.push.apply(results, res.files);
pageToken = res.nextPageToken || "";
} while (pageToken);
results.forEach(function(e) {
Logger.log(e.name);
Logger.log('Owner: ' + e.owners.map(function(f) {return f.displayName}).join(","));
Logger.log("SharingAccess: " + e.permissions.map(function(f) {return f.id}).join(","));
});
}
Note:
In this sample script, the log is the same with your script.
References:
Drive API
list
UrlFetchApp
If this was not the result you want, I apologize.

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

Transfer ownership of a file to another user in Google Apps Script

I have created a little upload UI which I include in a Google Sites page. It let's a user upload a document. Since the script is published it runs under my account and the uploaded file is uploaded to my google drive. I can from apps script add the person that uploaded the file as an editor, but what I would like to do is make them the owner of a file. (from apps script you can do a .getOwner() on a file... but not a .setOwner() ..... does anyone know a workaround?
Unfortunately changing the owner of a file isn't supported in Apps Script. Issue 74 is a feature request to add this ability, and if you star the issue you'll show your support for the feature and get notified of updates.
------EDITED------
Now there is a handy method called setOwner, which can be found here: https://developers.google.com/apps-script/reference/drive/file#setOwner(User)
There's also the possibility to pass the new owner's email address instead of the User object, which is even more handy in some cases:
https://developers.google.com/apps-script/reference/drive/file#setOwner(String)
Updated 12 Sep 2016:
This code uses a service account that's been granted Google Apps Domain-Wide Delegation of Authority. This lets you change the owner of any file owned by any user on the domain. This code uses the Google apps-script-oauth2 library by Eric Koleda.
var PRIVATE_KEY = PropertiesService.getScriptProperties().getProperty('PRIVATE_KEY');
var CLIENT_EMAIL = PropertiesService.getScriptProperties().getProperty('CLIENT_EMAIL');
/**
* Transfers ownership of a file or folder to another account on the domain.
*
* #param {String} fileId The id of the file or folder
* #param {String} ownerEmail The email address of the new owner.
*/
function transferOwnership(fileId, ownerEmail) {
var file = DriveApp.getFileById(fileId);
var currentOwnerEmail = file.getOwner().getEmail(); //the user that we need to impersonate
var service = getService(currentOwnerEmail);
if (service.hasAccess()) {
var url = 'https://www.googleapis.com/drive/v2/files/' + fileId + '/permissions';
var payload = {value: ownerEmail,
type: 'user',
role: 'owner'};
var options = {method: "post",
contentType: "application/json",
headers : {
Authorization: 'Bearer ' + service.getAccessToken()
},
payload: JSON.stringify(payload)//,
,muteHttpExceptions: true
};
//debugger;
//throw 'test my errors';
var response = UrlFetchApp.fetch(url, options);
if (response.getResponseCode() === 200 ||
(response.getResponseCode() === 400 && response.getContentText().indexOf('were successfully shared'))
) {
return response.getContentText();
}
if (response.getResponseCode() === 401 && response.getContentText().indexOf('Invalid Credentials')) {
throw 'Unable to transfer ownership from owner ' + currentOwnerEmail + ' ... ' +
'Please make sure the file\'s owner has a Google Apps license (and not a Google Apps Vault - Former Employee license) and try again.';
}
throw response.getContentText();
} else {
throw service.getLastError();
}
}
/**
* Reset the authorization state, so that it can be re-tested.
*/
function reset() {
var service = getService();
service.reset();
}
/**
* Configures the service.
*/
function getService(userEmail) {
return OAuth2.createService('GoogleDrive:' + userEmail)
// 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 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(userEmail)
// 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/drive');
}
Old answer (obsolete now as the Document List API is deprecated):
You can achieve this in Google Apps Script by accessing the Document List API using the raw atom xml protocol. You need to be a super admin of the domain. Here's an example that works for me:
/**
* Change Owner of a file or folder
* Run this as admin and authorise first in the script editor.
*/
function changeOwner(newOwnerEmail, fileOrFolderId){
var file = DocsList.getFileById(fileOrFolderId);
var oldOwnerEmail = file.getOwner().getEmail();
if (oldOwnerEmail === newOwnerEmail) {
return;
}
file.removeEditor(newOwnerEmail);
var base = 'https://docs.google.com/feeds/';
var fetchArgs = googleOAuth_('docs', base);
fetchArgs.method = 'POST';
var rawXml = "<entry xmlns='http://www.w3.org/2005/Atom' xmlns:gAcl='http://schemas.google.com/acl/2007'>"
+"<category scheme='http://schemas.google.com/g/2005#kind' "
+"term='http://schemas.google.com/acl/2007#accessRule'/>"
+"<gAcl:role value='owner'/>"
+"<gAcl:scope type='user' value='"+newOwnerEmail+"'/>"
+"</entry>";
fetchArgs.payload = rawXml;
fetchArgs.contentType = 'application/atom+xml';
var url = base + encodeURIComponent(oldOwnerEmail) + '/private/full/'+fileOrFolderId+'/acl?v=3&alt=json';
var content = UrlFetchApp.fetch(url, fetchArgs).getContentText();
}
//Google oAuth
function googleOAuth_(name,scope) {
var oAuthConfig = UrlFetchApp.addOAuthService(name);
oAuthConfig.setRequestTokenUrl("https://www.google.com/accounts/OAuthGetRequestToken?scope="+scope);
oAuthConfig.setAuthorizationUrl("https://www.google.com/accounts/OAuthAuthorizeToken");
oAuthConfig.setAccessTokenUrl("https://www.google.com/accounts/OAuthGetAccessToken");
oAuthConfig.setConsumerKey("anonymous");
oAuthConfig.setConsumerSecret("anonymous");
return {oAuthServiceName:name, oAuthUseToken:"always"};
}
Try this out! (at least it worked for me)
var newFolder = DriveApp.createFolder(folderName).addEditor(ownerEmail).setOwner(ownerEmail).addEditor(group.getEmail()).removeEditor(Session.getActiveUser().getEmail());
The above creates a folder and adds a new editor, sets the new editor to be the owner, adds another group to the editors and finally removes the user who has just created the folder from the editors.
I figured out a work-around -- not sure if it's exactly what you all are looking for. Simply give access to the account you'd like to switch ownership to. Access the document/form/etc. from that account and then make a copy of the said document. You are now the owner. You'll have to re-invite others.