Authorization is required after user has authorized the app - google-apps-script

I have a script that first presents a UI containing just a button to the user. Upon clicking that button, the script goes through their documents and changes the ownership of some of the files.
Upon loading my script, the user is greeted with the standard "Authorization Required" page with the red border. The user clicks the button to grant my app the needed permissions, and is taken to the page with the button.
However, after clicking the button, the user gets a dialog that says:
Error Encountered: Authorization is required to perform that action.
The app is set to execute as the user executing the script, and the script access is set to "anyone."
Any thoughts on what might be wrong?
My doGet():
function doGet(e) {
var app = UiApp.createApplication();
var button = app.createButton('Transfer');
app.add(button);
var handler = app.createServerHandler('init');
button.addClickHandler(handler);
return app;
}
The init() method goes through a specific folder (statically set by it's ID in the script), looks for files owned by the current user, and transfers the ownership to another user.
function init() {
// Create new log as a a Drive document
var log = DocsList.createFile("Ownership Transfer Log");
var user = Session.getActiveUser();
var targetEmail = "newemail#example.com";
transferFolderOwnership('[Drive folder ID]', user.getEmail(), targetEmail, log);
}
And transferFolderOwnership:
function transferFolderOwnership(folderId, userEmail, targetEmail, log) {
var rootFolder = DocsList.getFolderById(folderId);
var files = rootFolder.getFiles();
// Transfer files
var file;
for (var i = 0; i < files.length; i++) {
file = DriveApp.getFileById(files[i].getId());
if (file.getOwner() == userEmail) {
file.transferOwnership(targetEmail);
}
}
// Do the same for folders
}

The library you are using need an authorization that can only be granted when called from the script editor (see end of the library source code : function googleOAuth_()) . The first authorization you see is only for the "local" part of you script, the other looks like this :
This is a known issue and has a possible workaround described in one of the last issue tracker posts. I didn't try it yet but I guess it could be a way to go...

Related

Permissions API Call Failing when used in onOpen simple trigger

I've created a Google Sheet with an Apps Script to do issue and task tracking. One of the features I'm trying to implement is permissions based assigning of tasks. As a precursor to that, I have a hidden sheet populated with a list of users and their file permissions, using code similar to that in this StackOverflow question
When I manually run the code, it works fine. However, I want it to load every time the sheet is opened in case of new people entering the group or people leaving the group. So I made a call to my function in my onOpen simple trigger. However, when it is called via onOpen, I get the following:
GoogleJsonResponseException: API call to drive.permissions.list failed with error: Login Required
at getPermissionsList(MetaProject:382:33)
at onOpen(MetaProject:44:3)
Here are my functions:
My onOpen Simple Trigger:
function onOpen() {
//Constant Definitions for this function
const name_Access = 'ACCESS';
const row_header = 1;
const col_user = 1;
const col_level = 2;
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sht_Access = ss.getSheetByName(name_Access);
var ui = SpreadsheetApp.getUi();
ui.createMenu('MetaProject')
.addSubMenu(
ui.createMenu('View')
.addItem('Restore Default Sheet View', 'restoreDefaultView')
.addItem('Show All Sheets', 'showAllSheets')
)
.addSeparator()
.addToUi();
//Clear Contents, Leave Formatting
sht_Access.clearContents();
//Set Column Headers
var head_name = sht_Access.getRange(row_header,col_user);
var head_level = sht_Access.getRange(row_header,col_level);
head_name.setValue('User');
head_level.setValue('Level');
//Refresh User List for use in this instance
getPermissionsList();
}
Here is getPermissionsList:
function getPermissionsList() {
const fileId = "<REDACTED>"; // ID of your shared drive
const name_Sheet = 'ACCESS';
const row_start = 2;
const col_user = 1;
const col_access = 2;
var ss = SpreadsheetApp.getActiveSpreadsheet();
var thissheet = ss.getSheetByName(name_Sheet);
// THIS IS IMPORTANT! The default value is false, so the call won't
// work with shared drives unless you change this via optional arguments
const args = {
supportsAllDrives: true
};
// Use advanced service to get the permissions list for the shared drive
let pList = Drive.Permissions.list(fileId, args);
//Put email and role in an array
let editors = pList.items;
for (var i = 0; i < editors.length; i++) {
let email = editors[i].emailAddress;
let role = editors[i].role;
//Populate columns with users and access levels / role
thissheet.getRange(row_start + i,col_user).setValue(email);
thissheet.getRange(row_start + i,col_access).setValue(role);
}
I searched before I posted and I found the answer via some related questions in the comments, but I didn't see one with an actual answer. The answer is, you cannot call for Permissions or anything requiring authorization from a Simple Trigger. You have to do an Installable trigger.
In order to do that do the following:
Create your function or rename it something other than "onOpen".
NOTE: If you name it onOpen (as I originally did and posted in this answer) it WILL work, but you will actually run TWO triggers - both the installable trigger AND the simple trigger. The simple trigger will generate an error log, so a different name is recommended.
Click the clock (Triggers) icon on the left hand side in Apps Script.
Click "+ Add Trigger" in the lower right
Select the name of your function for "which function to run"
Select "onOpen" for "select event type"
Click "Save".
The exact same code I have above now runs fine.

Creating a view history log every time someone accesses a google doc and/or copies it using apps script

I wrote a script to log who and when opens a google doc. It works fine but the doc is a template for users to fill in so want to ensure whoever needs it will make a copy of the template through apps script.
I know users need edit access for the script to work but in terms of workarounds:
Is there a way for the script to still work if I give them a copy link or a template link?
Does it make more sense to use the google sheet as the base and pull from the google doc ID?
If that doesn't work than:
2) Is there a way to prompt them with a menu window to copy the file but delay it by x seconds on open?
//setups a count for the file
function setup() {
var propertyService = PropertiesService.getDocumentProperties();
propertyService.setProperty('viewCount', '0');
}
//logs the email and date of the user accessing the file
function onOpen(e) {
var count = parseInt(PropertiesService.getDocumentProperties().getProperty('viewCount'))+1;
PropertiesService.getDocumentProperties().setProperty('viewCount', count);
Logger.log(count);
var sheet = SpreadsheetApp.openById([spreadsheet ID]);
var user = Session.getActiveUser().getEmail();
var date = new Date();
sheet.getSheetByName('View Count').appendRow([user,date]);
}
You can add a menu item that creates a copy of the template for the user, logs the copy to the same sheet where you track who opens the file, and prompts the user to open their copy of the file to start working.
Steps
First: add these 3 functions.
The first, copyFile, creates a copy of the active document and names it the same plus the user's email. Then it logs the activity to the same change log, adding the new file's ID to the row. Finally, thanks to this nifty function it pops up a window with a link to the new file so the user can easily open it and start working.
The second, showAnchor, is the function that generates the HTML dialog box which presents the user a link to their new file.
The third, createMenu, adds a new menu item to the Google Docs nav bar, which prompts the user to copy the document. This, of course, triggers the copyFile function.
function copyFile() {
var thisDoc = DocumentApp.getActiveDocument()
var sheet = SpreadsheetApp.openById([spreadsheet ID]);
var user = Session.getActiveUser().getEmail();
var date = new Date();
var newCopy = DriveApp.getFileById(thisDoc.getId()).makeCopy(thisDoc.getName() + " " + user)
sheet.getSheetByName('View Count').appendRow([user,date,newCopy.getId()]);
var url = newCopy.getUrl()
showAnchor('Open Your File',url);
}
function showAnchor(name,url) {
var html = '<html><body>'+name+'</body></html>';
var ui = HtmlService.createHtmlOutput(html)
DocumentApp.getUi().showModelessDialog(ui," ");
}
function createMenu() {
const ui = DocumentApp.getUi();
const menu = ui.createMenu("Copy This Template");
menu.addItem("Copy", "copyFile");
menu.addToUi();
}
Then: add a call to createMenu() at the end of your onOpen script:
function onOpen(e) {
var count = parseInt(PropertiesService.getDocumentProperties().getProperty('viewCount'))+1;
PropertiesService.getDocumentProperties().setProperty('viewCount', count);
Logger.log(count);
var sheet = SpreadsheetApp.openById([spreadsheet ID]);
var user = Session.getActiveUser().getEmail();
var date = new Date();
sheet.getSheetByName('View Count').appendRow([user,date]);
createMenu();
}

Google Apps Script : is it possible to get a blob file stored in an user personal drive to insert it in a spreadsheet?

I've created a web application that ask users to sign manually a spreasheet document.
In order to do it I wrote the code below :
function saveSign(url){ // url = canvas.toDataURL("image/png");
const email = Session.getActiveUser().getEmail();
const base64 = url.split(",")[1];
const decoded = Utilities.base64Decode(base64);
const blob = Utilities.newBlob(decoded, MimeType.PNG, email);
const file = DriveApp.createFile(blob);
return true;
};
This code will create a file with the user email title in each users personal drive who use this web app.
All is well, but when I try to insert the picture just created or already created and stored in the user personal drive on the spreadsheet with the code below, an error occur.
const ssDA = SpreadsheetApp.openById("idSpreadsheet");
const wsDA = ssDA.getSheetByName("sheetName");
let uBlob;
const files = DriveApp.getFiles();
while (files.hasNext()) {
let file = files.next();
let fFileName = file.getName();
if(fFileName === ufileName){
uBlob = file.getBlob();
}
}
wsDA.insertImage(uBlob, 12, 32); // Error in this line
Console log Error :
Exception: Argument non valide : url
When I try to insert an image stored in my Drive all is well but when a colleague try to insert an image stored in his personal Drive the code above doen't work.
Indeed the while loop (var files) iterate in my personal folder and not in the user personal folder.
See below the web app deployement informations :
Execute the app as: User accessing the web app
Could you please help me ?
I don't know if you have your web app set up (permissions, if script is linked to a spreadsheet, standalone, if each user has to have their own spreadsheet) but here is an example similar to yours.
Instructions:
I have my primary account and a friend's account.
In primary account:
Created new script project.
Created new spreadsheet.
Made an image called "doggo.jpg" in my Drive.
Made a few functions:
function insertImg (fileName) {
const ssDA = SpreadsheetApp.openById("{spreadsheetid}"); // use your new spreadsheet id
const wsDA = ssDA.getSheetByName("{sheetname}");
const ufileName = fileName
const files = DriveApp.getFiles();
while (files.hasNext()) {
let file = files.next();
let fFileName = file.getName();
if(fFileName === ufileName){
uBlob = file.getBlob();
}
}
wsDA.insertImage(uBlob, 10, 10);
}
function testInsert() { // THIS IS THE TEST FUNCTION
insertImg("doggo.jpg")
}
function doGet(e) {
return HtmlService.createHtmlOutputFromFile("page"); // next we will make this page
}
Made a html file "page.html" in the script project with a button and a addEventListener to run the test function, in this example I am not including the boilerplate html (Doctype, head etc):
<body>
<h1>Hello</h1>
<button id="btn">Run Function</button>
<script>
document.getElementById("btn").addEventListener("click", doStuff);
function doStuff(){
google.script.run.insertImg("doggo.jpg"); // here is the running of the test function.
};
</script>
</body>
Ran the tests and ensured that I had given all the permissions to the script.
Published web app, Execute the app as: User accessing the web app, Who has access to the app: Anyone
Opened web app, pressed the button and it inserted the image to the spreadsheet. Up to here, same as you I think.
Shared the Google Sheet with friend's account. - very important.
Now friend logged into their account:
Got another image with same name "doggo.jpg" in friend's account's Drive.
Opened the URL of the web app and gave the necessary permissions.
Tested the button, and it worked!
Reference:
https://developers.google.com/apps-script/guides/web

Trying to set permissions for "anyone with link can view" on file uploaded via form "file upload" section

A "File upload" section is used in a form: I need the form user (not owner) to be able to share the file with someone other than the owner of the form.
It appears the results of the file-upload in the form puts the file into the current form user's Drive and provides a URL that the owner of the form can view. I need that URL to be sharable with others (via the script, not manual intervention).
Triggered by the form input, my script runs:
DriveApp.getFileById(id).setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.NONE);
... where id is derived from the URL provided from the form (see Easiest way to get file ID from URL on Google Apps Script ... as I know I'm getting the ID right).
As just user with a link to the form, I get no errors... nor do I see errors entering in the form as the owner.
But, the URL cannot be seen by anybody but the spreadsheet owner (not even the user who entered and owns the file can view the link)... so DriveApp.Access.ANYONE_WITH_LINK is not working. Non-owners of the form will be running this. I'm guessing setSharing is being run as the owner of the form, not the user, and therefore can't set it to be sharable? Is there a way for the script to make the file/link viewable?
But, even if the form owner runs the form, the URL can't be viewed by others. I don't think the ID changes once the file becomes sharable? So I must be calling setSharing incorrectly?
Maybe the ID in the URL provided by the form is n some "no-mans-land" where it's not the true ID of the file?
Example routine that just goes down column 4 of a spreadsheet with these URLs and makes them sharable:
`
function SetPermissions() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var maintsheet = ss.getSheetByName('Maintenance');
var lastmr = maintsheet.getLastRow();
for (var i = 2; i <= lastmr; i++) {
var fileURL = maintsheet.getRange(i,4).getValue();
Logger.log(fileURL);
for (var j in fileURL.split(",")) {
Logger.log(j);
Logger.log(fileURL.split(",")[j]);
var fileID = fileURL.split(",")[j].match(/[-\w]{25,}/)[0]
Logger.log(fileID);
DriveApp.getFileById(fileID).setSharing(DriveApp.Access.ANYONE, DriveApp.Permission.NONE);
}
}
}
`
... run by the owner of the form/spreadsheet, no errors, and the logged data looks correct. But, give the URL to another user, and they get a permissions error.
This fixed it:
function SetPermissions() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var maintsheet = ss.getSheetByName('Maintenance');
var lastmr = maintsheet.getLastRow();
for (var i = 2; i <= lastmr; i++) {
var fileURL = maintsheet.getRange(i,4).getValue();
Logger.log(fileURL);
for (var j in fileURL.split(",")) {
Logger.log(fileURL.split(",")[j]);
var fileID = fileURL.split(",")[j].match(/[-\w]{25,}/)[0]
Logger.log(fileID);
var file = DriveApp.getFileById(fileID);
file.setSharing(DriveApp.Access.ANYONE, DriveApp.Permission.VIEW);
var newURL = file.getUrl();
maintsheet.getRange(i, 4).setValue(newURL);
}
}
}
Doh: https://developers.google.com/apps-script/reference/drive/permission
... I was telling it: no permissions to anybody with the link! VIEW, is the right option!
Here's a function that given the sort of list of URLs the form will stick in the cell (the form can be setup to allow multiple pics per cell), set the permissions, and return the list of URLs back, this time as Google formats the URL in getURL():
function AnyoneWithLinkCanView(urls) {
var newURLs = "";
if ( urls.length > 0 ) {
for each (var j in urls.split(",")) {
var fileid = j.match(/[-\w]{25,}/)[0];
var file = DriveApp.getFileById(fileid);
file.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);
if ( newURLs.length > 0 ) {
newURLs += ", ";
}
newURLs += file.getUrl();
}
}
return newURLs;
}
In my case, as the URLs will form the message body of an email, my call looks like:
var message = AnyoneWithLinkCanView(dataSheet.getRange(lastr,i).getValue());
Form inputs run as whoever creates the form trigger. If you want the script to run as her give her access to the script and a macro that creates the form trigger.

Need help authorizing Google Sheets scripts for domain users

I have created three scripts (but I think they are all part of the same App?) deployed as a Sheets add-on for my company (all users at our domain). I am an administrator on our account and approved the app for all domain users.
First, users click a button that prompts them to properly name a copy of the current Google Sheet (a payment request form) and saves it in the correct folder on our drive. Then they open this new file and after filling it in, another button on the sheet opens a script that transfers ownership of the file to employee in charge of approving the request, and makes everyone else viewers only. That employee approves the request, then presses a button that runs a script to allow the accountant to edit the document.
The scripts work, but the second two scripts ask for authorization every time they run (even from me). I saw an earlier similar question on Authorizing a Google Apps Script but I think the 1st answer only applies when running the script as a Web App. I'm willing to do that, but I also barely understand this process (my first time writing a script). Thanks.
1.
function saveAsSpreadsheet() {
var sheet = SpreadsheetApp.getActive();
var folder = DriveApp.getFolderById('XXXXXXXX');
var ui = SpreadsheetApp.getUi();
Browser.msgBox("Please rename document. Wait for prompts.");
var fileName = Browser.inputBox("Rename file like this: YYYY.MM.DD Payee Name");
DriveApp.getFileById(sheet.getId()).makeCopy(fileName, folder);
}
2.
function setOwner() {
var sheet = SpreadsheetApp.getActive();
var file = DriveApp.getFileById(sheet.getId())
var owner = file.getOwner().getEmail();
file.setOwner("joe#ourdomain.com");
}
3.
function setSharing () {
var sheet = SpreadsheetApp.getActive();
var file = DriveApp.getFileById(sheet.getId())
var owner = file.getOwner().getEmail();
var user = file.getEditors();
if (user = "jill#ourdomain.com"){
file.addEditor("jill#ourdomain.com")
} else if (user = "sue#ourdomain.com"){
file.setSharing(DriveApp.Access.DOMAIN, DriveApp.Permission.VIEW);
} else {
file.setSharing(DriveApp.Access.DOMAIN, DriveApp.Permission.VIEW);
}}