How to Use Service Accounts and OAuth2 in Google Apps Script
Het gebruik van service accounts is nodig als je users via je app wil verhoogde toegang geven tot bepaalde delen van je systeem, zonder alles te moeten open zetten.
De rechten in een service account worden op systeem niveau beperkt en het service account is niet gekoppeld aan één persoon, maar generiek te gebruiken in apps script.
Dit kan handig zijn in situaties waar je toegang wil geven vanuit een app naar FireStore (NoSQL en niet ACID compliant database van Google), BigQuery relationele database of Directory API of andere services buiten de gewone user permissies.
Denk er aan dat wanneer je met een service account, toegang moet hebben tot files op je drive, deze gedeeld moeten worden met dit service account (SA is geen super user die overal toegang tot heeft!)
Er dient een project aangemaakt te worden via de console.cloud.google.com (3 bolletje in balk/new project), of je kan gebruik maken van een bestaand project door het te selecteren.
Dit account ga je nadien gebruiken om de toegang te beheren.
Via IAM Admin/Service Account
Kies het project bovenaan en kies Create Service Account.
Geef het account een naam en een omschrijving.
De naam wordt gebruikt voor de Service ID.
In principe moet je nu de rol bepalen welke dit account gaat gebruiken zoals wanneer je een Firestore gebruikt (Datastore > Cloud Datastore Owner), maar voor de Directory API is er geen rol nodig.
PS: Aan een account kan je steeds rollen toevoegen, dus je zou perfect een service account kunnen maken voor de sysops, waardoor zij een beperkte toegang krijgen tot super-admin tools via de apps.
Wanneer een toepassing (zoals BigQuery) niet standaard een service account ondersteund, dien je gebruik te maken van de OAuth lib om de toegang te regelen.
Als je externe servers wil toegang geven tot GCP dan gebruik je best geen Service accounts maar Workload identity federation, het verdelen van de serviceaccount key zou in verkeerde handen kunnen komen.
Het SA dat we hier gebruiken blijft binnen de google omgeving.
Door het klikken op het service account kan je een key genereren in de tab keys.
Deze JSON file bewaar je op je computer om nadien in de app in te voeren.
Zoals bij het gebruik van de Directory API, heb je geen rol nodig voor het SA maar moet je de app “super-user” rechten geven voor een toepassing.
Deze verhoogde toegangsrechten stel je in via delegatie.
Klik op de 3 bolletjes naast het service account en kies Manage details.
Klik op de volgende link om de Domain Wide Delegation aan te passen: https://admin.google.com/ac/owl/domainwidedelegation
Kopieer de bovenstaande Client ID en vul deze in als client ID.
Geeft als OAuth scope het volgende op:
https://www.googleapis.com/auth/admin.directory.group
Omdat enkel de superuser toegang heeft tot de directory service in je domain, moet de app zich voordoen alsof het de SU is die ze gebruikt.
Dit kan je enkel doen door het email adres mee te geven van een SU in de app request
In de code kan je nu gebruik maken van de API en super-user om toegang te krijgen tot de API
https://github.com/googleworkspace/apps-script-oauth2
Het is normaal dat bij het uitvoeren van een script voor de eerste keer, er een OAuth consent wordt gevraagd.
Je doet dit best met een klein script zoals het volgende, waardoor de toegang tot de API geopend is.
Dit geeft je een resultaat wat je in de debugger kan zien en uitklappen:
De JSON Kind geeft je de string die je gevraagd hebt, de etag is een unieke ID en de data vind je onder Users als array.
Het aanpassen van bestaande code, zodat de OAuth wordt gebruikt en de juiste toegangsrechten worden verkregen alsook dat de data gegenereerd wordt met als eigenaar het service account, doe je als volgt.
Je kan de volgende generieke code toevoegen, zodat je nadien de functies hebt om verder te werken.(zie code snippets)
De private_key, client_email en client_id zal je bekomen in de download van je service account.
De API keys kan je terug vinden in GCP console
function getOAuthService(user) {
return OAuth2.createService("Service Account")
.setTokenUrl('https://accounts.google.com/o/oauth2/token')
.setPrivateKey(JSON.private_key)
.setIssuer(JSON.client_email)
.setSubject(user)
.setPropertyStore(PropertiesService.getScriptProperties())
.setParam('access_type', 'offline')
.setScope('https://www.googleapis.com/auth/drive');
}
function reset() {
var service = getOAuthService();
service.reset();
}
var CREDENTIALS = {
"private_key": "Your Private key",
"client_email": "Your Client email",
"client_id": "Your Client ID",
"user_email": "Your Email address",
"api_key": "Your API key"
};
De code die standaard gebruik heeft gemaakt van de API zonder automatische toekenning van rechten, vereist dat bij het eerste gebruik de user toestemming geeft om zijn account te gebruiken en vereist eveneens dat het Domain unrestricted access heeft tot de services voor alle gebruikers.
function copyTemplate(user, animal) {
var newPresentationName = user + ' ' + animal;
return DriveApp.getFileById(PARAMS.slidesTemplateId).makeCopy(newPresentationName, DriveApp.getFolderById(PARAMS.destinationFolderId)).getId();
}
We zien dat deze code gebruik maakt van de API DriveApp en hebben in voorgaande de DriveApp restricted gezet en voorzien van een API key.
In de generieke code hebben we deze sleutels van zowel het service account als de API key verwerkt.
De Apps script code wordt omgevormd tot een REST API call naar de API, waarbij bij iedere aanvraag een AccessToken wordt meegegeven (bearer), wat verkregen worden door de getOAuthService.
De instructies voor de driveapp worden omgevormd naar een URL en bijbehorende JSON object, waarvan het antwoord op de UrlFetchApp, ook een JSON object, terug wordt omgevormd naar normale data.
Het omvormen van code nadien is niet zo eenvoudig, omdat alle API calls moeten omgevormd worden naar de bijhorende REST API.
Ondanks de eenvoud van Apps Script door gebruik te maken van de OAuth die een User Consent vraagt (standaard Services open zetten voor alle users) loop je het risico dat de data verloren gaat bij het veranderen/wissen van die user.
Data die dient bewaard te worden, laat je best vanuit Shared drivers lopen en gebruik maken ervan, wat dit euvel reeds oplost.
Dit is een oplossing voor beperkte groepen gebruikers.(sysops, werkgroepen etc)
Wil je een app ontwikkelen die door alle users (klanten) kan gebruikt worden met strikte toegangscontrole, dan dien je bovenstaande stappen te gebruiken en de gegenereerde data eigendom te maken van een service account.
function copyTemplate(user, animal) {
var service = getOAuthService();
// Log de user terug uit van de service, zodat hij een ander account/keys kan gebruiken voor een andere service..
service.reset()
if (service.hasAccess()) {
var newPresentationName = user + ' ' + animal;
var url = 'https://www.googleapis.com/drive/v3/files/' + PARAMS.slidesTemplateId + '/copy?supportsAllDrives=true&key=' + CREDENTIALS.api_key;
var body = {
"name": newPresentationName,
"parents": [PARAMS.destinationFolderId]
};
var params = {
headers: {
Authorization: 'Bearer ' + service.getAccessToken()
},
method: 'post',
payload: JSON.stringify(body),
contentType: 'application/json',
muteHttpExceptions: true
};
var response = UrlFetchApp.fetch(url, params).getContentText();
Logger.log('response: ' + response);
var id = JSON.parse(response).id;
return id;
}
}