GAS getSharingAccess() restriction scopes - google-apps-script

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.

Related

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?

Creating a spreadsheet file in a folder without using auth/drive scope

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.

How can I protect my Apps Script code / hide database login details?

I'd like to pull data into a Google doc from a MySQL database (quote / invoice data) via a Google Apps Script. The doc will be used by several people (in a small agency). However these project managers should not be able to see the login details that are embedded in the Apps Script Code. I don't care if they see the Code. So is there a trick to hide or lock the login details for the database? Any advice??
Google apps script has a concept called properties services. You can use the Script properties or the user properties to stored your credentials:
Script properties are shared among all users of a script, add-on, or web app. They are typically used for app-wide configuration data, like the username and password for the developer's external database.
Usage:
var scriptProperties = PropertiesService.getScriptProperties();
//setting
scriptProperties.setProperty('SERVER_URL', 'http://www.example.com/');
//retrieving
var units = userProperties.getProperty('DISPLAY_UNITS');
User properties are shared among the current user of a script, add-on, or web app and typically used for User-specific settings, like metric or imperial units.
Usage:
var userProperties = PropertiesService.getUserProperties();
//setting
userProperties.setProperty('DISPLAY_UNITS', 'metric');
//retrieving
var units = userProperties.getProperty('DISPLAY_UNITS');
You can also manually set, view or remove these properties by going to File->Properties and the relevant tab.
There is also the third kind of property called document properties about which you can find out here.
As some of you mentioned, the problem is that a user could read out the login details stored externally with an appropriate script. So my solution for now is to run the SQL select in a standalone script embedded as web app and to let the standalone script write the query result into a spreadsheet. The editable doc will then Import the data from there.

Set file permissions with Google App Script

I regularly need to change file share permissions on large numbers of files on Google Drive. I would rather not have to do this manually on a file by file basis. I have no issues extracting the File IDs, but can't seem to find any information associated with setting permissions for specific users.
Suppose I have a file with ID - 1132asdfasdf5sdf564sdf
Suppose I have a user named John Smith whose email address is jsmith#email.com and this user is a Google user.
Here's a sample script:
function myFunction() {
// this grabs the ID of the source file
var sourcefile=DriveApp.getFileById("1132asdfasdf5sdf564sdf");
}
Question. Once I grab the file by ID, is it possible to set either edit or view permissions for this specific user?
I haven't tried this, but I would imagine you could use addviewer(email) or addeditor(email). There is also an option for multiple emails.
source: https://developers.google.com/apps-script/reference/drive/file#methods
You can try to use the Drive Service. This service allows scripts to create, find, and modify files and folders in Google Drive.
// Log the name of every file in the user's Drive.
var files = DriveApp.getFiles();
while (files.hasNext()) {
var file = files.next();
Logger.log(file.getName());
}
By using the enum permission, that represents the permissions granted to users who can access a file or folder, besides any individual users who have been explicitly given access. These properties can be accessed from DriveApp.Permission.
VIEW - Users who can access the file or folder are able only to view it or copy it. Passing this value to File.setSharing(accessType, permissionType) throws an exception if the type of file does not support it.
EDIT - Users who can access the file or folder are able to edit it. Unless File.setShareableByEditors(shareable) is set to false, users can also change the sharing settings. Passing this value to File.setSharing(accessType, permissionType) throws an exception if the type of file does not support it.
Try also to check this SO question for more information.
Are you replacing old users with new ones in the permission scheme? If you are, you need to invoke the File or Document class methods removeEditor(s) or removeViewer(s), and then follow up with addEditor(s) or addViewer(s).
The problem with using built in methods (as far as I can tell) is that it is difficult to stop a blizzard of email notifications going out to the users if you are doing bulk permission inserts. You can use the flexibility of the Drive API to specifically stop email notifications, but may experience with calling the Drive API is that it eats up a lot of time.

Accessing a (new-style, public) Google sheet as JSON

How can I access the contents of a (new-style) Google sheet a JSON? My aim is to access the values from JavaScript, so I need to be able to download the JSON via HTTP.
Example: how can I download the data from this sheet as JSON?
I tried to find the answer via a web search, but ultimately failed:
Many tutorials on the web start with the instruction to find the key=... value in the url. The URL I got when I exported the sheet is https://docs.google.com/spreadsheets/d/1mhv1lpzufTruZhzrY-ms0ciDVKYiFJLWCMpi4OOuqvI/pubhtml?gid=1822753188&single=true and has no key=... in it.
The answer to "Accessing A Public Google Sheet" seems to indicate that I should try https://docs.google.com/spreadsheets/d/1mhv1lpzufTruZhzrY-ms0ciDVKYiFJLWCMpi4OOuqvI/export?format=csv&id=1mhv1lpzufTruZhzrY-ms0ciDVKYiFJLWCMpi4OOuqvI&gid=1822753188 to get a CSV version, but this does not work for me: I get a sign-in page instead of the data.
I found approaches using Google Apps Scripts, but these seem to require some user action in the browser instead of giving a download link.
If you want to use the latest API (v4), you'll need to do the following:
Generate a spreadsheets API key (see instructions below).
Make your sheet publicly accessible.
Use a request of the form:
https://sheets.googleapis.com/v4/spreadsheets/SPREADSHEET_ID/values/RANGE?key=API_KEY
You'll then get a clean JSON response back:
{
"range": "Sheet1!A1:D5",
"majorDimension": "ROWS",
"values": [
["Item", "Cost", "Stocked", "Ship Date"],
["Wheel", "$20.50", "4", "3/1/2016"],
["Door", "$15", "2", "3/15/2016"],
["Engine", "$100", "1", "30/20/2016"],
["Totals", "$135.5", "7", "3/20/2016"]
],
}
Note that if you want to specify the entire contents of a page, an identifier such as Sheet1 is perfectly valid.
See Basic Reading for more information.
As of v4 API, all requests must be accompanied by an identifier (e.g. API key):
Requests to the Google Sheets API for public data must be accompanied by an identifier, which can be an API key or an access token.
Follow the steps in the linked document to create an API key on the credentials page.
Make sure to:
Create a new app on Google Cloud Platform.
Create a new API key.
Add the Google Sheets API. (API Manager > Dashboard > Enable API)
Note that you can still access public data without forcing the user to log in:
In the new Sheets API v4, there is no explicit declaration of visibility. API calls are made using spreadsheet IDs. If the application does not have permission to access specified spreadsheet, an error is returned. Otherwise the call proceeds.
Note that you do not need to publish the sheet to the web. All you need to do is make sure anyone with the link can access the sheet.
(I.e. when you click Create credentials on the Google Sheets API, choose Other non-UI, User data, and it says "User data cannot be accessed from a platform without a UI because it requires user interaction for sign-in." you can safely ignore that message. The API Key is all you really need, since this is public data.)
Common error messages:
The request is missing a valid API key.
You didn't include the key= param in your call.
API key not valid. Please pass a valid API key.
Google developers console
You supplied an incorrect API key. Make sure that you typed in your key correctly. If you don't have a key yet, go to the Google developers console and create one.
API Key not found. Please pass a valid API key.
Google developer console API key
Your API Key is probably correct, but you most likely didn't add the Google Sheets permission. Go to the Google developer console API key page and add the sheets permission.
The caller does not have permission
Your sheet isn't set to be publicly accessible.
I have finally (kind of) solved my problem. Just for future reference, and in case somebody else runs into the same troubles, here the solution I came up with:
To make the worksheet publicly accessible, one needs to make the worksheet publicly accessible. This is done in the Google Sheets web interface, using the menu entries File > Publish to the web ... > link > publish. It is possible to either publish the whole spreadsheet or individual worksheets.
An API to access data from Google Sheets programmatically is described on the Google Sheets API web pages. This API uses URLS of the form https://spreadsheets.google.com/feeds/.../key/worksheetId/.... Slightly oddly, the meaning of key and worksheetId seems not to be explained in the API documentation.
My experiments show that the key value can be found by taking part of the URLs used to access the sheet via the web interface (see also here). The key is everything after the /d/, until the next slash. For the spreadsheet in the question, the key is thus 1mhv1lpzufTruZhzrY-ms0ciDVKYiFJLWCMpi4OOuqvI. The worksheetId seems to be an integer, giving the position of the worksheet in the spreadsheet. For the example in the question one has to know that the sheet shown is the second worksheet, the worksheetId in this case is 2.
The API defined public and private requests. To access an exported resource without authentication, public requests must be used.
The API calls to get data from the spreadsheet are explained in the section "Retrieving a list-based feed" (click on the "Protocol" tab in the examples). The URL required extract the data from the spreadsheet in the question is
https://spreadsheets.google.com/feeds/list/1mhv1lpzufTruZhzrY-ms0ciDVKYiFJLWCMpi4OOuqvI/2/public/full
A HTTP GET request to this URL returns that data as XML. (I have not found a way to get the data as JSON.)
The usual protections agains cross-site requests make it difficult to access the data via JavaScript XML RPC calls in a web app. One way around this problem is to proxy the API calls through the web server (e.g. using nginx's proxy_pass directive).
The above steps are at least a partial solution to the problem in the question. The only difficulty is that the data is returned as XML rather than as JSON. Since the API documentation does not mention JSON, maybe it is not possible any more to extract the data in this format?
Edit: (Aug 17, 2021) With the rollout of Sheets v4, the endpoint in the original answer has been deprecated. The updated endpoint and sample script included below:
Updated solution
Credits to the original answer here.
"https://docs.google.com/spreadsheets/d/" + spreadsheetId + "/gviz/tq?tqx=out:json&gid=0";
You don't technically have to include the gid if you just want the first sheet, but you can specify another sheet if you'd like using that parameter.
Here's a sample script to retrieve values of Spreadsheet as JSON, and then parsed as header row and values.
var sf = "https://docs.google.com/spreadsheets/d/1l7VfPOI3TYtPuBZlZ-JMMiZW1OK6rzIBt8RFd6KmwbA/gviz/tq?tqx=out:json";
$.ajax({url: sf, type: 'GET', dataType: 'text'})
.done(function(data) {
const r = data.match(/google\.visualization\.Query\.setResponse\(([\s\S\w]+)\)/);
if (r && r.length == 2) {
const obj = JSON.parse(r[1]);
const table = obj.table;
const header = table.cols.map(({label}) => label);
const rows = table.rows.map(({c}) => c.map(({v}) => v));
console.log(header);
console.log(rows);
}
})
.fail((e) => console.log(e.status));
Original solution
Note: This no longer works as Sheets v3 was deprecated in August 2021.
Here's how to get the JSON using those same URL parameters:
"https://spreadsheets.google.com/feeds/list/" + spreadsheetID + "/od6/public/values?alt=json";
Creds to #jochen on the answer with the path all the way up to XML "https://spreadsheets.google.com/feeds/list/" + spreadsheetID + "/od6/public/" + sheetID;
As #jochen's answer explains, this sheetID is based on the order of the sheets in the spreadsheet.
A faster solution here is to use this https://gist.github.com/ronaldsmartin/47f5239ab1834c47088e to wrap around your existing spreadsheet.
You first need to change your sheet access to Anyone with link can View
Add the id and sheet html param to the URL below.
https://script.google.com/macros/s/AKfycbzGvKKUIaqsMuCj7-A2YRhR-f7GZjl4kSxSN1YyLkS01_CfiyE/exec
Eg: your id is your sheet id which is
1mhv1lpzufTruZhzrY-ms0ciDVKYiFJLWCMpi4OOuqvI
and your sheet which is
Sheet2
In your case you can actually see your data here as json at
https://script.google.com/macros/s/AKfycbzGvKKUIaqsMuCj7-A2YRhR-f7GZjl4kSxSN1YyLkS01_CfiyE/exec?id=1mhv1lpzufTruZhzrY-ms0ciDVKYiFJLWCMpi4OOuqvI&sheet=Sheet2
To be safe, you should deploy the code sheetAsJson.gs in the github gist above as your own in your Google Drive.
Here is the solution
Note your sheet id in the document url (don't use the published url to find the id!)
Publish your sheet, just as html page
Use the id from step 1,
and put it in this url https://spreadsheets.google.com/feeds/cells/{id}/1/public/full?alt=json
The /1 indicates the first sheet in your document