I\'d like to run a function that requires authorization when a user clicks/selects a cell in a given range. A simple onSelectionChange(e) trigger doesn\'t work because of th
For functions that don't involve ui
/HtmlService
, Simple triggers can be used to run some functions that require authorization(say privileged functions) by weakening security:
The flow: Trigger => onSelectionChange(no auth to fetch/execute privileged functions) => trigger custom functions(gain auth to fetch/no auth to execute privileged functions) => fetch/post => webapp(full auth to run privileged functions)
This solution is inspired by this, which directly uses installable triggers and plain access tokens to authorize custom functions. This is not recommended from a security perspective.
Although efforts have been taken to ensure security and privacy of user executing the following script, all attack vectors haven't been considered. The script might be vulnerable in a lot of areas, especially given the lack of crypto module support in the platform. Use at your own risk, where alternate solutions are infeasible.
In most cases, alternate solutions using menu/button/time triggers/installable triggers(which always runs under full auth) is preferred. A similar flow can be achieved using onEdit installable trigger + checkbox
To use the sample script, follow the following steps:
Set necessary scopes in the manifest file. For the sample script,
"oauthScopes": ["https://www.googleapis.com/auth/script.send_mail"],
Publish a webapp for the explicit purpose of executing a function that requires authorization
Create a service account with no roles/permissions for the explicit purpose of authorizing webapps from custom functions
Create a service account key and copy it to the creds
object in the sample script.
Share your project/spreadsheet with the service account (client_email
)
Install Oauth2 library to create/sign jwt tokens for service account
Create a hiddenSheet
for setting a custom function, which will be set to set to A1 of this sheet onSelectionChange
The following script sends email, when someone touches anything in your spreadsheet.
/**
* Gets Oauth2 service based on service account with drive scope
* Drive scope needed to access webapp with access:anyone
* This does not grant access to the user's drive but the service
* account's drive, which will only contain the file shared with it
*/
function getService_() {
const creds = {
private_key: '[PRIVATE_KEY]',
client_email: '[CLIENT_EMAIL]',
};
const PRIVATE_KEY = creds['private_key'];
const CLIENT_EMAIL = creds['client_email'];
return OAuth2.createService('GoogleDrive:')
.setTokenUrl('https://oauth2.googleapis.com/token')
.setPrivateKey(PRIVATE_KEY)
.setIssuer(CLIENT_EMAIL)
.setPropertyStore(PropertiesService.getUserProperties())
.setScope('https://www.googleapis.com/auth/drive');
}
/**
* @returns {string} base64 encoded string of SHA_512 digest of random uuidstring
*/
const getRandHashKey_ = () =>
Utilities.base64EncodeWebSafe(
Utilities.computeDigest(
Utilities.DigestAlgorithm.SHA_512,
Utilities.getUuid() //type 4 advertised crypto secure
)
);
/**
* @param {GoogleAppsScript.Events.SheetsOnSelectionChange} e
*/
const onSelectionChange = e => {
const sCache = CacheService.getScriptCache();
e.rangestr = e.range.getSheet().getName() + '!' + e.range.getA1Notation();
const hashRandom = getRandHashKey_();
sCache.put(hashRandom, JSON.stringify(e), 20);//expires in 20 seconds
e.source
.getSheetByName('hiddenSheet')
.getRange('A1')
.setValue(`=CALLWEBAPP("${hashRandom}")`);
};
/**
* Calls published webapp(Access:Anyone) with service account token
* @customfunction
* @returns void
*/
const callwebapp = randomHash => {
const webAppScriptId = '[SCRIPT_ID]';
UrlFetchApp.fetch(
`https://script.google.com/macros/s/${webAppScriptId}/exec`,
{
method: 'post',
payload: { e: randomHash },
headers: { Authorization: `Bearer ${getService_().getAccessToken()}` },
}
);
};
/**
* @param {GoogleAppsScript.Events.AppsScriptHttpRequestEvent} e
*/
const doPost = e => {
const hashRandom = e.parameter.e;
const sCache = CacheService.getScriptCache();
const encodedSelectionEvent = sCache.get(hashRandom);
if (encodedSelectionEvent) {
const selectionEvent = JSON.parse(encodedSelectionEvent);
MailApp.sendEmail(
'[EMAIL_TO_SEND_NOTIFICATION_TO]',
'Someone touched your spreadsheet',
`Wanna take a look? ${selectionEvent.rangestr} was touched without your permission`
);
}
};
So I ended up adding a column with the value "Clear to run function" on each row, with a simple onEdit(e) that triggers my function if one value in this column in different from "Clear to run function".
From a UX perspective, it means clearing a cell to run the function - not ideal, but it works.