Creating a spreadsheet file in a folder without using auth/drive scope - google-apps-script

I had asked a question before on creating a file and moving it to a folder, but now as I'm moving to publish this simple add-on I'm having issues with the auth/drive scope needed to move a file. auth/drive is a sensitive scope and I'd have to go through a review process just to publish. I tried changing the scope to auth/drive.file but apparently that doesn't apply to the moveTo method.
I find it frustrating that I have to jump through hoops just to create a file in the correct location. Are there any other ways to move create a file in a folder that doesn't require sensitive or restricted scopes?
Here is the function
function createSpreadsheet(form){
var spreadsheetName = [form.getTitle() + " Results"];
var thisFileId = form.getId();
var parentFolder = DriveApp.getFileById(thisFileId).getParents().next();
var spreadsheet = SpreadsheetApp.create(spreadsheetName);
var spreadsheetID = spreadsheet.getId();
var spreasheetFile = DriveApp.getFileById(spreadsheetID);
spreasheetFile.moveTo(parentFolder);
return spreadsheet;
}

I believe your goal as follows.
You want to narrow the scopes of your Google Apps Script.
You want to use https://www.googleapis.com/auth/drive.file for this.
Modification points:
In this case, when Drive service is used, the scope of https://www.googleapis.com/auth/drive is required to be used. So in order to narrow the scopes, I would like to propose to use Drive API instead of Drive service.
The flow of this modified script is as follows.
Retrieve the parent folder ID of "thisFileId".
In this case, the scope of https://www.googleapis.com/auth/drive.metadata.readonly is used.
Create new Spreadsheet to the specific folder.
In this case, the scope of https://www.googleapis.com/auth/drive.file is used.
The Spreadsheet is created and moved to the specific folder using the method of "Files: insert" by one API call.
Retrieve Spreadsheet object.
In this case, the scope of https://www.googleapis.com/auth/spreadsheets is used.
This flow is the same process with your script. When above flow is reflected to your script, it becomes as follows.
Modified script:
Before you use this script, please enable Drive API at Advanced Google services.
function createSpreadsheet(form){
var spreadsheetName = [form.getTitle() + " Results"];
var thisFileId = form.getId();
// 1. Retrieve the parent folder ID of "thisFileId".
// In this case, the scope of "https://www.googleapis.com/auth/drive.metadata.readonly" is used.
var folderId = Drive.Files.get(thisFileId).parents[0].id;
// 2. Create new Spreadsheet to the specific folder.
// In this case, the scope of "https://www.googleapis.com/auth/drive.file" is used.
var spreadsheetId = Drive.Files.insert({title: spreadsheetName, mimeType: MimeType.GOOGLE_SHEETS, parents: [{id: folderId}]}).id;
// 3. Retrieve Spreadsheet object.
// In this case, the scope of "https://www.googleapis.com/auth/spreadsheets" is used.
return SpreadsheetApp.openById(spreadsheetId);
}
Note:
This sample script is for the script in your question. So when you are using the methods for using the scope of https://www.googleapis.com/auth/drive in your other part, this script might not be useful. Please be careful this.
From form.getTitle() and form.getId(), the scope of https://www.googleapis.com/auth/forms might be required to be included. Please be careful this. And also, when you are using other methods for using other scopes, please include them.
The Spreadsheet is created with the scope of https://www.googleapis.com/auth/drive.file, the Spreadsheet can be used by the application using the scope. Please be careful this.
References:
Files: get
Files: insert

Drive API is an API with restricted scopes.
If you look at the list of restricted scopes for the Drive API, you can see that the https://www.googleapis.com/auth/drive.file is indeed not present in this list to be a restricted scope but depending on your application, it may qualify as a sensitive scope.
In order to check this, you can add the scope to your project via the Google Cloud Console and see if there will be a lock icon.
However, retrieving a folder and moving the file will require the use of other scopes which are part of the list, hence the verification process will be necessary.
There are also exceptions to the verification process; specifically for the add-on you are mentioning, these might apply:
If you want to deploy the add-on solely for internal use which means that the add on will be used only by people in your Google Workspace or Cloud Identity organization.
If you want to use the add-on domain wide which means that the add on will be used only by Google Workspace enterprise users within the domain.
For the whole list of exceptions, you can check this here.
Reference
OAuth API verification FAQs.

Related

How do I use the drive.file scope for a standalone google apps script

I have a standalone script that uses the Sheets API. There are only two calls:
SpreadsheetApp.openById(spreadsheetID).getSheetByName("Answers")
SpreadsheetApp.openById(otherspreadsheetID).getSheetByName("Questions").getDataRange().getValues()
So it reads from one file and writes to a different file. The script is set to run as a webapp as me, so on initial run, by default, this triggers a broad scope to view/edit/delete all sheets. I want to limit that. I see this is a scope I could manually set: https://www.googleapis.com/auth/drive.file (docs).
But I don't get how to then set my two spreadsheets as files that have been "opened or created" by the app so that the scope is valid for those files. I tried creating a function that creates two new sheets (I figured this counts as "a drive file that you created with this app") but even the Spreadsheetapp.create() function throws a "do not have permission" error.
I might be misunderstanding how this scoping works?
Issue and workaround:
When SpreadsheetApp.openById() is used, the scope of https://www.googleapis.com/auth/spreadsheets is used. It seems that this is the current specification.
So, as a workaround, how about using Sheets API? When the Sheets API is used, you can use the scope of https://www.googleapis.com/auth/drive.file.
As an important point, the official document of the scope of https://www.googleapis.com/auth/drive.file is as follows.
Per-file access to files created or opened by the app. File authorization is granted on a per-user basis and is revoked when the user deauthorizes the app.
So, when you want to retrieve the data from the Spreadsheet using the scope of https://www.googleapis.com/auth/drive.file, at first, the Spreadsheet is required to be created by the scope. In this answer, I would like to introduce the following flow.
Set the scope of https://www.googleapis.com/auth/drive.file to Google Apps Script project.
Create a new Spreadsheet.
Retrieve values from the Spreadsheet.
Usage:
1. Set scope.
Please create a new Google Apps Script. From your question, I understood that you are using the standalone type. For this, please set the scope of https://www.googleapis.com/auth/drive.file to the manifest file. Ref Please add "oauthScopes": ["https://www.googleapis.com/auth/drive.file"] to the file of appsscript.json. By this, the specific scope can be used.
2. Create a new Spreadsheet.
Before you use this script, please enable Sheets API at Advanced Google services. When you run the following script, a new Spreadsheet is created and you can see the spreadsheet ID at the log. Please copy the ID. This ID is used in the next script. By this, a new Spreadsheet is created by the scope of https://www.googleapis.com/auth/drive.file.
function createNewSpreadsheet() {
const id = Sheets.Spreadsheets.create({properties: {title: "sample"}}).spreadsheetId;
console.log(id)
}
When you try to retrieve the values from the existing Spreadsheet which is not created by this Google Apps Script project, an error of Requested entity was not found. occurs. So in this section, a new Spreadsheet is created by this Google Apps Script project.
3. Get values from Spreadsheet.
Before you use this script, please open the created Spreadsheet and check the sheet name. And, as a sample, please put the sample values to the cells. When you run the following script, the values are retrieved from the Spreadsheet and you can see them at the log.
function getValues() {
const spreadsheetId = "###"; // Please set the spreadsheet ID.
const obj = Sheets.Spreadsheets.Values.get(spreadsheetId, "Sheet1");
const values = obj.values;
console.log(values)
}
References:
Manifest structure
Method: spreadsheets.create
Method: spreadsheets.values.get

Creating a Google App Script web app to upload files to google drive with minimum scope

I want to create a Google App Script web app to allow users to upload files to a folder of my google drive.
The problem is that I've checked the drive scope list here,
in order to perform the upload action , this scope must be reviewed by the users
https://www.googleapis.com/auth/drive
and it comes with a scary description See, edit, create and delete all of your Google Drive files which is way too strong.
Is there any way to perform the upload action with a less stronger scope?
Thank you so much.
In your situation, for example, how about the following scope?
https://www.googleapis.com/auth/drive.file
The official document says as follows. Ref
Per-file access to files created or opened by the app. File authorization is granted on a per-user basis and is revoked when the user deauthorizes the app.
In this scope, only the files and folders created by this application can be accessed.
Reference:
Authenticate your users
Added:
From your following replying,
Thank you for replying too, I have an update. When I run the code in the editor , it throws an error : ' { [Exception: You do not have permission to call DriveApp.Folder.createFile. Required permissions: googleapis.com/auth/drive] name: 'Exception' }' So I guess there is no solution to my problem T.T
I understood that you are using createFile with the Drive service (DriveApp). In this case, the scope of https://www.googleapis.com/auth/drive is requierd to be used. It seems that this is the current specification. In order to use the scope of https://www.googleapis.com/auth/drive.file, how about using Drive API at Advanced Google services? By this, you can use the scope by setting to appsscript.json which is the manifest file.
At first, please add https://www.googleapis.com/auth/drive.file as "oauthScopes": ["https://www.googleapis.com/auth/drive.file"] to the manifest file of appsscript.json. Ref
Although I'm not sure about your actual script, the sample script for creating a Spreadsheet is as follows.
Sample script:
Before you use this script, please enable Drive API at Advanced Google services.
function myFunction() {
const obj = Drive.Files.insert({title: "sampleSpreadsheet", mimeType: MimeType.GOOGLE_SHEETS, parents: [{id: "###folderId###"}]});
console.log(obj.id)
}
When you run this script, new Spreadsheet is created to folderId by the scope of https://www.googleapis.com/auth/drive.file.
Note:
If you are required to use other scopes, please add them to oauthScopes.
References:
Manifests
Manifest structure
Related thread.
Can I use DocumentApp.openById() with read only permission?

GAS getSharingAccess() restriction scopes

Question
Is there any way to restrict the scope [drive.readonly] to only apply to current sheet?
I need to call getSharingAccess(), only on current sheet.
Background
We have built a public google addon thats calling our API with a customer uniqe API-Key.
To check that the sheet is not shared with others (must be private to user), we check if Sheet is shared.
If sheet is shared we give a warning to user.
var docId = SpreadsheetApp.getActiveSpreadsheet().getId();
var access = DriveApp.getFileById(docId).getSharingAccess();
Problem
The scope drive.readonly is giving full read access to all files on users drive.
We only need to check current sheet if this is shared.
We dont need access to all files on users drive.
Many users are afraid to give this full access to any addon.
https://www.googleapis.com/auth/drive.readonly
https://developers.google.com/apps-script/reference/drive/file#getsharingaccess
Thanks,
Br,
Henrik
Answer:
There's no way to get information of which types of Permission (anyone, domain, etc.) are associated with current spreadsheet while using a scope that is restricted to this file.
Explanation:
Using scope spreadsheets.currentonly, you can know whether the file has been shared with specific users (eg. Spreadsheet.getViewers()), but not if it has been shared with all users in a domain, or made public.
In order to get this information, you would at least need the scope drive.metadata.readonly, which is a restricted scope, the same as drive.readonly (see scopes).
drive.file cannot be used for that, since it only gives access to files created or opened by your project, and this doesn't seem to include container files for your bound script.
The easiest way to retrieve this information via drive.metadata.readonly (the more restricted scope you can use) is by doing the following:
Enable Advanced Drive Service.
Set the following explicit scopes in your manifest file (necessary to set a less permissive scope; otherwise Apps Script would automatically set drive.readonly or drive):
"oauthScopes": [
"https://www.googleapis.com/auth/spreadsheets.currentonly",
"https://www.googleapis.com/auth/drive.metadata.readonly"
]
Copy and run the following function to list the types of permission associated with this file:
function getCurrentFilePermissions() {
const ssid = SpreadsheetApp.getActive().getId();
const permissions = Drive.Permissions.list(ssid);
const permissionTypes = permissions["items"].map(permission => permission["type"]);
console.log(permissionTypes);
return permissionTypes;
}
Notes:
DriveApp.getFileById(id) requires drive or drive.readonly, metadata scopes cannot be used for that. That's why the Advanced Service is used.
Considering that methods for managing access for specific users (e.g. getEditors(), addViewer(user), I'd suggest filing a feature request in Issue Tracker for adding methods to manage other types of access (e.g. DOMAIN, ANYONE), using this template.
I have not tried this, but the Drive API has a get method that lets you retrieve a Files resource. The Files resource has a shared property that apparently indicates whether the file is shared or private to the owner.
It seems to be possible to specify the https://www.googleapis.com/auth/drive.file scope or the https://www.googleapis.com/auth/drive.metadata.readonly scope when sending a get request.
You can try the endpoint with the Try this API panel — enter a spreadsheet ID in the fileId box, shared in the fields box, and untick the Google OAuth 2.0 checkbox. The spreadsheet must be readable by the world for this test to work.
Another test to try against the get endpoint:
const response = UrlFetchApp.fetch(endpoint + '?' + parameters, { headers : { 'Authorization' : 'Bearer ' + ScriptApp.getOAuthToken() }});
It may also be worth trying DriveApp and the Advanced Drive Service and the https://www.googleapis.com/auth/drive.file scope.
As said, I have not tried any of the above, and you will have to do your own research to find if there is a solution there. Let us know how it goes.

How to transfer ownership of a Google spreadsheet and to be a viewer

Is it possible to hand over the owner permission to the other user and change the former owner to a viewer using apps script?
Transferring the owner permission to another user and being a viewer
I'm trying to make a script that hands over the owner to another user and changes the former owner to a viewer or prohibits the former owner from editing the Google Spreadsheet. The problem is the script is run by the former owner and the script cannot remove the edit permission of the former owner.
What I have tried
I tried setViewer(), sheet.protect(), and contentRestictions.readOnly but all of them was not a viable solution
removeEditors() and setViewer()
setViewer() method to an editors has no effects, and after applying removeEditors() to the former owner(after changing the owner, of course) the script cannot execute setViewer() since it does not have permission anymore.
sheet.protect()
the method gives the permission to edit the protected range for the owner and the user who runs the script. not eligible.
contentRestrictions.readOnly
the restriction can be unlocked by editors. not feasible.
Code for contentRestrictions:
function setReadOnly() {
var ss = SpreadsheetApp.getActive();
var supervisor = 'xxxxxxxx#gmail.com';
file = DriveApp.getFileById(ss.getId());
try {
Drive.Files.update({
'writersCanShare': false,
'copyRequiresWriterPermission': true,
'contentRestrictions': [{ 'readOnly': true }]
}, ss.getId());
file.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.NONE);
file.setOwner(supervisor);
} catch (e) {
console.log(e);
}
}
I recently found the contentRestrictions.readOnly has been implemented to Google Drive API. While I thought it would be a great way to restrict the editors from modifying the file content, I realized the users who are editors can "UNLOCK" the file with a click even it was locked by the owner.
I don't understand the use of this property if the restriction can be resolved by editors. The explanation in the docs says that we can use this field to prevent modifications to the title, uploading a new revision, and addition of comments. However, editors easily unlock the files, and the viewers cannot modify the file anyway.
I believe your current situation and your goal as follows.
You have a Google Spreadsheet and a script. You are the owner of them.
From your script, the script is the container-bound script of the Spreadsheet.
When you run the script, you want to transfer the owner of Spreadsheet to other user.
After the owner was transferred, you want to keep to have the permission for the Spreadsheet as the reader which is not the writer.
Modification points:
In this case, I thought that the methods of "Permissions: insert" and "Permissions: update" of Drive API (in this case, Drive API is used with Advanced Google services.) might be able to be used for achieving your goal.
The flow of my proposing script is as follows.
Retrieve the permission ID for you.
In the current stage, you are the owner of the file.
Transfer the owner of file (in your case, it's Spreadsheet.).
In the current stage, you are not the owner.3. Update your permission from writer to reader.
Create a shortcut of the owner-transferred Spreadsheet to the root folder.
Because, when the owner is transferred, the file can be seen at the folder of "Shared with me".
If you are not required to create the shortcut, please remove this part.
This flow is reflected to a script, it becomes as follows.
Modified script:
Please copy and paste the following script to the script editor of the container-bound script of Google Spreadsheet. And, before you use this script, please enable Drive API at Advanced Google services.
function myFunction() {
const email = "###"; // Please set the email. The owner of the file is transferred to this email.
const fileId = SpreadsheetApp.getActive().getId();
// 1. Retrieve the permission ID for you. In the current stage, you are the owner of the file.
const permissionId = Drive.Permissions.list(fileId).items[0].id;
// 2. Transfer the owner of file (in your case, it's Spreadsheet.). In the current stage, you are not the owner.
Drive.Permissions.insert({role: "owner", type: "user", value: email}, fileId, {sendNotificationEmail: false, moveToNewOwnersRoot: true});
// 3. Update your permission from `writer` to `reader`.
Drive.Permissions.update({role: "reader"}, fileId, permissionId);
// 4. Create a shortcut of the owner-transferred Spreadsheet to the root folder. Because, when the owner is transferred, the file can be seen at the folder of "Shared with me".
Drive.Files.insert({mimeType: MimeType.SHORTCUT, shortcutDetails: {targetId: fileId}}, null);
}
Note:
When above script is run the owner of Spreadsheet is changed to email. And your permission becomes the reader. So in this case, you cannot see the script of Spreadsheet. Please be careful this.
In this case, after the owner of Spreadsheet is changed, you cannot edit the Spreadsheet and the script. So please be careful this.
References:
Permissions: insert
Permissions: update
I tested my own file below, only me have access to the file.
Code:
function myFunction() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var ssId = ss.getId();
var file = DriveApp.getFileById(ssId);
var owner = file.getOwner();
var new_owner = 'new_owner#gmail.com';
file.setOwner(new_owner);
file.removeEditor(owner); // After this line, you can't view/edit the file
Logger.log(file.getOwner()); // Will fail
}
Things noted during testing:
Running removeEditor on yourself after setting another user as owner will restrict you from even viewing the file as per testing
Owner is immune with removeEditor (can't be removed by such)
Only the owner can set the ownership to other users
Please see a similar question that has an answer pointing to this, it will give you some insights regarding Google Apps Domain-Wide Delegation of Authority.

programmatically edit script in Google Drive document

I have a Google Drive document which writes values to a Google Drive spreadsheet using Google Apps Scripts.
The script associated with the document looks a lot like this:
// must change value to actual spreadsheet ID
RobProject.spreadsheetID = "spreadsheetID";
function onOpen()
{
// do stuff;
}
Each time I create a spreadsheet and its related documents, I manually change the value spreadsheetID to the spreadsheet's ID so the documents know to which spreadsheet they should write their values.
I would like a programmatic way to fill in the correct value for spreadsheetID into the Documents' scripts.
When I search for "edit scripts programmatically," I get tutorials for creating Google Apps Scripts, not editing scripts with scripts. Is there any way to edit Google Apps Scripts with a Google Apps Script?
If I understand correctly, you are working with a document and a spreadsheet. The document needs to know the Id of the spreadsheet.
There are some ways to access the Google Apps Script code through the API, but that is only for standalone projects, not for container-bound scripts (unfortunately).
You could consider using a naming convention for the document and spreadsheet so that you could use the Drive service to get from the document to the spreadsheet (DriveApp.getFilesByName()). Or possibly organize them by folder (DriveApp.getFoldersByName(), folder.getFiles()).
If you wanted to store the spreadsheet Id in a project property, you could build a UI in the document that let the user open up the list of files in Drive and pick the associated spreadsheet and then store the Id (ScriptProperties.setProperty('SpreadsheetId')).
Don't forget that onOpen has a parameter. You could use following code:
// Define global variable somewhere
RobProject = {};
function onOpen(e) {
RobProject.sSheet = e.source; // maybe the spreadsheet object is as useful as the ID
RobProject.spreadsheetID = e.source.getId();
// do stuff;
}
Please, for your own sake, don't try to write selfmodifying code.