Switching accounts in Google Apps Script HTML Service - google-apps-script

I'm trying to allow users of my Google Apps Script web app to switch google accounts. I've tried sending users to the Account Chooser via a hyperlink:
Switch Accounts
If the user chooses another account on that screen, the user returns to the web app still logged in under the original account.
What am I doing wrong, or is there another way of allowing users to switch accounts whilst on the web app?

While you might be needing Google Apps Script HTML Service to serve web pages that can interact with server-side Apps Script functions for your custom user interface, you might also need Admin SDK's Directory API to manage users.
As stated in the documentation,
The Admin SDK Directory service allows you to use the Admin SDK's Directory API in Apps Script. This API gives administrators of Google Apps domains (including resellers) the ability to manage devices, groups, users, and other entities in their domains.
First, you may use users: list to list all possible users. Example, you can list users in a domain which can be sorted by first name:
function listAllUsers() {
var pageToken, page;
do {
page = AdminDirectory.Users.list({
domain: 'example.com',
orderBy: 'givenName',
maxResults: 100,
pageToken: pageToken
});
var users = page.users;
if (users) {
for (var i = 0; i < users.length; i++) {
var user = users[i];
Logger.log('%s (%s)', user.name.fullName, user.primaryEmail);
}
} else {
Logger.log('No users found.');
}
pageToken = page.nextPageToken;
} while (pageToken);
}
Then, use users: get to retrieve a user.
Here's a sample code to get a user by email address and logs all of their data as a JSON string.
function getUser() {
var userEmail = 'liz#example.com';
var user = AdminDirectory.Users.get(userEmail);
Logger.log('User data:\n %s', JSON.stringify(user, null, 2));
}
Furthermore, please note that there are Directory API: Prerequisites and also needs authorization as described in Authorize requests in using this API.
This may seem to be a bit confusing so I suggest that you please go through the given links for more information.
Happy coding!

Related

how to access and modify google workspace domain user's google drive files as a domain administrator using google apps scripts

Objective:
as a google workspace domain admin for a school that uses google workspace education, I want to create a google apps script that given a google workspace user's email address (the current owner), the scritp should be able to get a list of all the user's folders and files in their google drive and then it should also be able to transfer the ownership of those folders and files to domain user and add the current owner as a viewer so they can only see the folders/files but can't modify them in any way.
things I tried:
DriveApp can access files/folders and change the ownership of the file/folder but only if you are the owner, and I want to do this as the domain admin, regardless which user owns the google drive and respective files/folders.
Drive API, seems to do the same as DriveApp as far you're the owner, I couldn't figure out how to give Drive API admin permissions so I can see every domain user google drive file list, if that's even possible.
GAM advance: I found this as management tool, I set it and it migh do what I need but it's bit complex for me, plus I was really hoping to be able to build the tool myself.
What worked halfway:
I found this: https://github.com/googleworkspace/apps-script-oauth2#using-service-accounts which refers to using a service account. It took a while but I manage to get a list of items that exist on a user's google drive with the script below. but I can't figure out how to access those files/folders so I can change the ownership or set viewers on them. I think I read that the service account will only give me read-only access so I'm doubting this is even possible.
Here's what I got so far:
function main(){
// Private key and client email of the service account.
var key = getJsonKey()
var clientEmail = 'service_account_email_setup_in_google_dev_console';
// Email address of the user to impersonate.
var userEmail = 'a_regular_domain_user#my_google_workspace_domain.com';
try{
var drive = getDriveService_(key,userEmail,clientEmail);
if (drive.hasAccess()) {
// this code gets me a json response with items that list id's and urls and other
//file metadata of the
// files that belongs to the domain user, this is as far as i got.
var url = 'https://www.googleapis.com/drive/v2/files';
var response = UrlFetchApp.fetch(url, {
headers: {
Authorization: 'Bearer ' + drive.getAccessToken()
}
});
var result = JSON.parse(response.getContentText());
//the following code returns a fileid in the user's google
//drive not shared with the admin
var fileid = JSON.stringify(result.items[0].id)
Logger.log(fileid);
//but the following code returns an error indicating that the
//file is not found (in reality it's not accessible by the
//admin account)
var file = Drive.Files.get(fileid);
//access a list of items and as I traverse it I'd like to
//change the ownership and
//add the the current user as a file viewer
//??
} else {
Logger.log(drive.getLastError());
}
}catch (e){
Logger.log(e)
}
}
// Load the JSON key file with private key for service account
function getJsonKey(){
var keyFile = DriveApp.getFileById("json_fileid_in_drive_obtained_from_googledevcons");
var key = JSON.parse(keyFile.getBlob().getDataAsString()).private_key;
return key
}
function reset() {
getDriveService__().reset();
}
//get the google drive from the domain user's email address
function getDriveService_(key,userEmail,clientEmail) {
return OAuth2.createService('GoogleDrive:' + userEmail)
.setTokenUrl('https://oauth2.googleapis.com/token')
.setPrivateKey(key)
.setIssuer(clientEmail)
.setSubject(userEmail)
.setPropertyStore(PropertiesService.getUserProperties())
.setCache(CacheService.getUserCache())
.setScope('https://www.googleapis.com/auth/drive');
}
Any help is appreciated :)
You are going in the right direction, the only part you are missing currently is setting up Domain Wide Delegation this will allow you to impersonate the users in your domain so you can make the changes on behalf of them by granting the service account permissions through the above mentioned DWD.
Since you have already created the Oauth2Service you will just need to send the user to impersonate through the OauthParams:
const oauthParams = {
serviceName: 'Nameofyourservice',
serviceAccount,
scopes: ['https://www.googleapis.com/auth/appsmarketplace.license', 'https://www.googleapis.com/auth/userinfo.email', 'https://mail.google.com', 'https://www.googleapis.com/auth/iam'],
userToImpersonate: 'usertoimpersonate#test.com',
};
The scopes were from the Marketplace API as an example.

Google Web Apps - Get user email but run scripts as owner

I've recently gravitated to google web apps and I have a bit of a dilemma. I'm trying to build an app that is open to very specific users and the data they are viewing filters based on their access group.
In a google sheet I'm listing the user emails and their respective access groups. Column A - email, Column B - access group
The issue
When the user accesses the web app I'm using this to grab their email:
var email = Session.getActiveUser().getEmail();
And then I run this code to get their access group:
function validate(email){
var sheet = SpreadsheetApp.openById(ID).getSheetByName(ssUserList);
try{
var group = getRowsData(sheet).find(e => e.userEmail === email).securityGroup;
return group;
} catch(e){
return "Not Authorized";
}
}
Because the user doesn't have access to my google sheet, they get an error when the function runs. And I can't deploy the web app to run as me because I need the user's email. I understand this very well.
What I've read:
Tons of other posts and articles about access tokens and credentials and urlFetchApps ... I don't understand any of it and to be honest I don't know which one makes more sense for my situation.
What I've tried:
I can't use the 1st usable option I've found which is to access web app 1 (which runs as user), then call web app 2 using the user email as a parameter because if they share that link from web app 2 then anyone could see the data and I'm working with really sensitive data.
I realize I could just put these parameters in a separate sheet and give them view only access and the scripts will run fine, but I'm extra and I want to do it right.
In reality I'm going to have a few other functions that will need to run as me. If you were in my shoes, where would you start? Or can someone explain it in layman's terms? Should I be looking into something like this? Any help is appreciated!
Summary
One of the possibilities, as suggested here, is to create a separate web application to handle access to SpreadSheets.
The client (the main web app) would make a request through UrlFetchApp to the middleware (web app in charge of consulting the SpreadSheet), the middleware would make the needed queries and would return them to the client. Finally, depending on the response obtained, one content or another would be rendered.
Minimal Example
Configuring the Project
First, we create two GAS projects, one we call Main and the other Middleware. The main point is that the Middleware would run as USER_DEPLOYING and the client as USER_ACCESSING. This allows access to the sheet without requiring additional permissions.
The appscripts.json file would look like this on the client. :
"oauthScopes": [
"https://www.googleapis.com/auth/script.external_request",
"https://www.googleapis.com/auth/userinfo.email"
],
"webapp": {
"executeAs": "USER_ACCESSING",
"access": "ANYONE"
}
And like this on the middleware:
"oauthScopes": [
"https://www.googleapis.com/auth/spreadsheets"
],
"webapp": {
"executeAs": "USER_DEPLOYING",
"access": "ANYONE_ANONYMOUS"
}
If you have any questions about editing or viewing appscript.json, check the Manifest and Scopes documentation.
Attention: "access": "ANYONE" and "access": "ANYONE_ANONYMOUS" are only being used for testing purposes. This is dangerous, and should be reviewed for the specific needs of your project.
Code Sample
As for the client, we only need to ask for the email of the user who is accessing through Session.getActiveUser().getEmail() and then send it to the middleware to obtain the response. Depending on the response obtained, we will render one content or another (I assume there are two roles present: USER and ADMIN)
Client
const doGet = () => {
var data = {email: Session.getActiveUser().getEmail()}
var options = {
'method': 'POST',
'contentType': 'application/json',
'payload': JSON.stringify(data)
}
var fetch = UrlFetchApp.fetch(URL_MIDDLEWARE, options)
var userAccess = JSON.parse(fetch).accessLevel
return HtmlService.createHtmlOutput(
userAccess === "ADMIN"
? `<h1>${data.email} - ADMIN USER</h1>`
: userAccess === "USER"
? `<h1>${data.email} - COMMON USER</h1>`
: "<h1>Unauthorized</h1>" )
}
For the middleware we need to obtain that email and compare it with our sheet to check the access level of the user. Then we return the result.
Middleware
const doPost = (request) => {
// destructuring the request
const { parameter, postData: { contents, type } = {} } = request;
const userEmail = JSON.parse(contents).email;
let userAccess = SpreadsheetApp.openById(SPREADSHEET_ID).getRange('A1:B2').getValues()
// This can be replaced by a filter function
let userAccessLevel;
for (let user of userAccess) { if (userEmail == user[0]) userAccessLevel = user[1] }
return ContentService.createTextOutput(Utilities.jsonStringify({
user: userEmail,
accessLevel: userAccessLevel
}))
};
Finally, you access the Main Web App to check that everything is working.
Remember that this is a test implementation, and should not be used in production. If you need more information on these topics, you can visit the following links:
Load data asynchronously, Best Practices
Request Parameters doGet() doPost()
ContentService Class

Access Admin SDK with Google App Maker

I'm practicing with the early access Google App Maker and want to create a simple app that allows an administrator to change the password of another user in the organisation.
Whenever I try to call the Admin SDK API with something that would have previously worked with App Script, I get an error. It seems to be that App Maker is not allowing access to the SDK API.
I've enabled the Advanced Services > Google Admin Directory API. Is this where I should be able to enable the Admin SDK API (required for changing passwords)
To test, I'm trying to run this simple function:
function listUsers() {
var response = AdminDirectory.Users.list(optionalArgs);
var users = response.users;
if (users && users.length > 0) {
Logger.log('Users:');
for (i = 0; i < users.length; i++) {
var user = users[i];
Logger.log('%s (%s)', user.primaryEmail, user.name.fullName);
}
} else {
Logger.log('No users found.');
}
}
The above code returns this error:
AdminDirectory is not defined at NewPage.Button1.onClick:2:18
I'm sure I must be missing something here.
Many Thanks.
AdminDirectory (As well as other advanced services) are available on server side only.
You should move the method to Server Script and call it with google.script.run on button's click.
Please use code completion to see available options.

How to get the list of members in a Google group in Google app script (Admin SDK)?

I would like to get a list of members in a Google group using Admin SDK.
But im not getting how to do this. I found below link - https://developers.google.com/admin-sdk/directory/v1/guides/manage-group-members
But I do not know how to use POST method in Google app script.
Can someone please guide me with an example?
UPDATED
I got the output as below, But i would like to access each element (role,email) separately for each member of the group. Is that possible??
{
"role": "OWNER",
"kind": "admin#directory#member",
"type": "USER",
"etag": "\"fdo0/1gUrEe8bli75zvzmqFHyH3cPzlQ\"",
"id": "107108832717913338955",
"email": "useremailid#domain.com",
"status": "ACTIVE"
}
Thanks in advance.
Please follow the steps,
Login your G drive using super admin account.
Create one Google sheet.
Go to sheet Tool menu -- script Editor.. Save Script project project. Enable admin SDK
Apps script editor click on Resources menu select Advance Google Services -- In Pop up on Admin Directory Apis then Click Google Developers Console link.
Developer Console -- click on Library --> search APIs box enter Admin sdk --> click on admin Sdk api link ==> ENABLE and close dev console.
=== At Apps script editor paste following code.
var onSheet = SpreadsheetApp.getActiveSpreadsheet();
var groupKey = "googlegroupid#domainName.com"
function MainGetUserList()
{
var rows = [];
var pageToken, page;
do {
page = AdminDirectory.Members.list(groupKey,
{
domainName: 'YOURDOMAINNAME.#com',
maxResults: 500,
pageToken: pageToken,
});
var members = page.members
if (members)
{
for (var i = 0; i < members.length; i++)
{
var member = members[i];
var row = [groupKey, member.email, member.role, member.status];
rows.push(row);
}
}
pageToken = page.nextPageToken;
} while (pageToken);
if (rows.length > 1)
{
var sheetData = onSheet.getSheetByName("Sheet1")
var header = ['Group Name', 'User Id', 'User role', 'User Status'];
sheetData.clear()
sheetData.appendRow(header).setFrozenRows(1);
sheetData.getRange(2, 1, rows.length, header.length).setValues(rows);
}
}
==> Run MainGetUserList() function allow the permission, open you Google Sheet refresh it.
Done.
I have tested this 20k members.
Thanks
Try to use Members:list to retrive list of all members in a group.
HTTP request
GET https://www.googleapis.com/admin/directory/v1/groups/groupKey/members
Every request your application sends to the Directory API must include an authorization token. The token also identifies your application to Google. Your application must use OAuth 2.0 to authorize requests.
HTTP response:
{
"kind": "admin#directory#members",
"etag": etag,
"members": [
members Resource
],
"nextPageToken": string
}

Calling a Google Apps Script web app with access token

I need to execute a GAS service on behalf of a user that is logged to my system. So I have her/his access token. I would like somehow to transfer the token to the web app and without having to authorize again the user to use it for some activities. Can this be accomplished? Thank you.
EDIT: I think I didn't explain right what I try to accomplish. Here is the work flow I try to achieve:
We authorize a user visiting our website using OAuth2 and Google;
We get hold of her/his access token that Google returns;
There is a Google Apps Script web app that is executed as the user running the web app;
We want to call this app (3) by providing the access token (2) so Google not to ask again for authorization;
Actually, we want to call this app (3) not by redirecting the user to it but by calling it as a web service.
Thanks
Martin's answer worked for me in the end, but when I was making a prototype there was a major hurdle.
I needed to add the following scope manually, as the "automatic scope detection system" of google apps script did not ask for it: "https://www.googleapis.com/auth/drive.readonly". This resulted in UrlFetchApp.fetch always giving 401 with additional information I did not understand. Logging this additional information would show html, including the following string
Sorry, unable to open the file at this time.</p><p> Please check the address and try again.
I still don't really understand why "https://www.googleapis.com/auth/drive.readonly" would be necessary. It may have to do with the fact that we can use the /dev url, but who may use the /dev url is managed is checked using the drive permissions of the script file.
That said, the following setup then works for me (it also works with doGet etc, but I chose doPost). I chose to list the minimally needed scopes explicitly in the manifest file, but you can also make sure the calling script will ask for permissions to access drive in different ways. We have two google apps script projects, Caller and WebApp.
In the manifest file of Caller, i.e. appsscript.json
{
...
"oauthScopes":
[
"https://www.googleapis.com/auth/drive.readonly",
"https://www.googleapis.com/auth/script.external_request"]
}
In Code.gs of Caller
function controlCallSimpleService(){
var webAppUrl ='https://script.google.com/a/DOMAIN/macros/s/id123123123/exec';
// var webAppUrl =
// 'https://script.google.com/a/DOMAIN/macros/s/id1212121212/dev'
var token = ScriptApp.getOAuthToken();
var options = {
'method' : 'post'
, 'headers': {'Authorization': 'Bearer '+ token}
, muteHttpExceptions: true
};
var response = UrlFetchApp.fetch(webAppUrl, options);
Logger.log(response.getContentText());
}
In Code.gs of WebApp (the web app being called)
function doPost(event){
return ContentService.createTextOutput("Hello World");
}
The hard answer is NO you can't use the built-in services of Apps Script with a service token. But if you already have the token for a user generated by a service account, access to the users data is pretty similar to any other language. All calls would be to the REST interface of the service your token is scoped for.
Take this small script for example. It will build a list of all the user's folders and return them as JSON:
function doGet(e){
var token = e.parameter.token;
var folderArray = [];
var pageToken = "";
var query = encodeURIComponent("mimeType = 'application/vnd.google-apps.folder'");
var params = {method:"GET",
contentType:'application/json',
headers:{Authorization:"Bearer "+token},
muteHttpExceptions:true
};
var url = "https://www.googleapis.com/drive/v2/files?q="+query;
do{
var results = UrlFetchApp.fetch(url,params);
if(results.getResponseCode() != 200){
Logger.log(results);
break;
}
var folders = JSON.parse(results.getContentText());
url = "https://www.googleapis.com/drive/v2/files?q="+query;
for(var i in folders.items){
folderArray.push({"name":folders.items[i].title, "id":folders.items[i].id})
}
pageToken = folders.nextPageToken;
url += "&pageToken="+encodeURIComponent(pageToken);
}while(pageToken != undefined)
var folderObj = {};
folderObj["folders"] = folderArray;
return ContentService.createTextOutput(JSON.stringify(folderObj)).setMimeType(ContentService.MimeType.JSON);
}
You do miss out on a lot of the convenience that makes Apps Script so powerful, mainly the built in services, but all functionality is available through the Google REST APIs.
I found a way! Just include the following header in the request:
Authorization: Bearer <user's_access_token>