17

I'm using the Google Sheets API to obtain sheet data for a Java project. All works as expected locally, but I'm using the verbose permissions scope https://www.googleapis.com/auth/spreadsheets which "Allows read/write access to the user's sheets and their properties.". I would prefer to not provide this app access to all of the spreadsheets in my Google Drive (have only done so locally temporarily).

Ideally, I'd like to request permission for read/write access to a file using a file's ID. Is this possible?

If not possible, I'm guessing that the https://www.googleapis.com/auth/drive.file scope which provides "Per-file access to files created or opened by the app." is the closest I can get. I haven't managed to find a way to open the file with this app. How would I go about doing that?

Or if both of the above solutions aren't ideal or possible, let me know what you'd recommend.

Thank you!

Linda Lawton - DaImTo
  • 106,405
  • 32
  • 180
  • 449
Daniel Hunt
  • 343
  • 2
  • 12
  • Daniel, did you figure this out? I'm thinking I'll have to create a new google user that only has access to the file(s) for my specific project. – Matt Dec 04 '18 at 03:50
  • @Matt I did the same as you're thinking and created a new Google account for the drive files. Definitely the easiest and most problem-free route. You can probably use the Picker API in some way to do it, but I decided it wasn't worth the hassle: https://developers.google.com/picker/docs/ – Daniel Hunt Dec 05 '18 at 11:34

3 Answers3

7

I know this was posted quite a while ago, but I'll give my answer to help out future developers coming across this in the future.

I think using service accounts would give you the functionality you are looking for here. Service accounts are kind of like "bot" users that users can share documents with, and then your server can login to this service account to access those documents. Instead of having to request access to a user's entire google drive or google sheets, you can have them share the documents with you manually, which I think would be more comfortable for most users.

Here is an example of how to set this up in Node.js, but the ideas should translate fairly readily to Java.

LiamM
  • 161
  • 2
  • 6
2

Scopes grant you access across an api there is no way to limit it to a single file or group of files.

Google Sheets API, v4 Scopes

There is no way to limit permissions to a single file. Assuming that the file you are editing was created by your application then https://www.googleapis.com/auth/drive.file should be a valid option

Sample

Java quickstart

import com.google.api.client.auth.oauth2.Credential;
import com.google.api.client.extensions.java6.auth.oauth2.AuthorizationCodeInstalledApp;
import com.google.api.client.extensions.jetty.auth.oauth2.LocalServerReceiver;
import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow;
import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets;
import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.api.client.util.store.FileDataStoreFactory;
import com.google.api.services.sheets.v4.Sheets;
import com.google.api.services.sheets.v4.SheetsScopes;
import com.google.api.services.sheets.v4.model.ValueRange;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.security.GeneralSecurityException;
import java.util.Collections;
import java.util.List;

public class SheetsQuickstart {
    private static final String APPLICATION_NAME = "Google Sheets API Java Quickstart";
    private static final JsonFactory JSON_FACTORY = JacksonFactory.getDefaultInstance();
    private static final String TOKENS_DIRECTORY_PATH = "tokens";

    /**
     * Global instance of the scopes required by this quickstart.
     * If modifying these scopes, delete your previously saved tokens/ folder.
     */
    private static final List<String> SCOPES = Collections.singletonList(SheetsScopes.SPREADSHEETS_READONLY);
    private static final String CREDENTIALS_FILE_PATH = "/credentials.json";

    /**
     * Creates an authorized Credential object.
     * @param HTTP_TRANSPORT The network HTTP Transport.
     * @return An authorized Credential object.
     * @throws IOException If the credentials.json file cannot be found.
     */
    private static Credential getCredentials(final NetHttpTransport HTTP_TRANSPORT) throws IOException {
        // Load client secrets.
        InputStream in = SheetsQuickstart.class.getResourceAsStream(CREDENTIALS_FILE_PATH);
        GoogleClientSecrets clientSecrets = GoogleClientSecrets.load(JSON_FACTORY, new InputStreamReader(in));

        // Build flow and trigger user authorization request.
        GoogleAuthorizationCodeFlow flow = new GoogleAuthorizationCodeFlow.Builder(
                HTTP_TRANSPORT, JSON_FACTORY, clientSecrets, SCOPES)
                .setDataStoreFactory(new FileDataStoreFactory(new java.io.File(TOKENS_DIRECTORY_PATH)))
                .setAccessType("offline")
                .build();
        LocalServerReceiver receier = new LocalServerReceiver.Builder().setPort(8888).build();
        return new AuthorizationCodeInstalledApp(flow, receier).authorize("user");
    }

    /**
     * Prints the names and majors of students in a sample spreadsheet:
     * https://docs.google.com/spreadsheets/d/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms/edit
     */
    public static void main(String... args) throws IOException, GeneralSecurityException {
        // Build a new authorized API client service.
        final NetHttpTransport HTTP_TRANSPORT = GoogleNetHttpTransport.newTrustedTransport();
        final String spreadsheetId = "1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms";
        final String range = "Class Data!A2:E";
        Sheets service = new Sheets.Builder(HTTP_TRANSPORT, JSON_FACTORY, getCredentials(HTTP_TRANSPORT))
                .setApplicationName(APPLICATION_NAME)
                .build();
        ValueRange response = service.spreadsheets().values()
                .get(spreadsheetId, range)
                .execute();
        List<List<Object>> values = response.getValues();
        if (values == null || values.isEmpty()) {
            System.out.println("No data found.");
        } else {
            System.out.println("Name, Major");
            for (List row : values) {
                // Print columns A and E, which correspond to indices 0 and 4.
                System.out.printf("%s, %s\n", row.get(0), row.get(4));
            }
        }
    }
}

Update 2020

There is a way to grant per file access.

https://www.googleapis.com/auth/drive.file 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.

Linda Lawton - DaImTo
  • 106,405
  • 32
  • 180
  • 449
  • Thanks so much for the response. Is there documentation on how to request to open a file from the Sheets/Drive API? – Daniel Hunt Oct 24 '18 at 11:44
  • there is a sample here https://developers.google.com/sheets/api/quickstart/java Just remember to change the scope. You may need to grab the google drive api as well in order to get the drive scopes. SheetsScopes.DRIVE_FILE might work – Linda Lawton - DaImTo Oct 24 '18 at 11:53
  • Thanks again for the response. Any documentation on opening a file (or requesting to open a file)? That Quickstart guide (from what I can see) only shows how to read. When using the DRIVE_FILE scope to try to read/request a file via ID, I get a 404 error: `{ "code" : 404, "errors" : [ { "domain" : "global", "message" : "Requested entity was not found.", "reason" : "notFound" } ], "message" : "Requested entity was not found.", "status" : "NOT_FOUND" }` – Daniel Hunt Oct 24 '18 at 12:06
  • This is an api. That is what it does. It does NOT open Google sheets for you. The Google sheets api gives you access to the data contained within the sheet. Google Drive api gives you acccess to download the files. Nothing is going to open the file for you. You will probably need import com.google.api.services.drive.v3.DriveScopes; – Linda Lawton - DaImTo Oct 24 '18 at 12:07
  • 1
    My bad, should've been clearer. The scope permits access to files that have been "Opened" by the app, I assume this suggests the user has used the "Open with" option in Google Drive. What I am asking is if there is any way to invoke this via code (as there's no app in the "Open with" option). By "Open", I'm referring to what Google Picker does. You need to "Open" the file in order to gain access to it. I can't use the DRIVE_FILE scope to access the file without opening it first. I don't mean literally open the file. – Daniel Hunt Oct 24 '18 at 12:13
  • Your question is tagged with API and your title contains API. I would have assumed that you did some research into what the api does. Drive API gives you file store access to the files. Google sheets API gives you access to the data within a sheet. None of these apis are going to OPEN a file in an application for you. Thats not what they are designed to do. I know of nothing that will. DRIVE_FILE wont work unless the file was created by your application even then its only going to give you access to the data in the file. – Linda Lawton - DaImTo Oct 24 '18 at 12:21
  • My bad, I thought based on your first response to my comment that this was the case, must've misunderstood. My understanding was that these APIs are used within apps, and as the action of "Opening" a file is mentioned many times in the documentation, I thought it would/should be possible to invoke that via code to gain access to the file. I believe Google Picker allows per-file access via the action of "Opening". In this case, it seems I'll have to create a separate Google account/Drive and access the files there. Thanks for your help! – Daniel Hunt Oct 24 '18 at 12:34
  • 2
    Hey Daniel! Did you manage to get through this? I’m trying to see how I can reduce the scopes asked and do what you wanted to do :) Thing is that I need access to other users (app is distributed) – Frenchcooc Jan 02 '20 at 17:44
-1

I think, this is what you're requesting for. ( https://developers.google.com/sheets/api/quickstart/java , https://www.youtube.com/watch?v=zDxTSUWaZs4 ) I am using this code to access a google sheet via ID

public class ConnectToDatabase extends AsyncTask<Object, Integer, Long> {

    private static final String APPLICATION_NAME = "Google Sheets API Java Quickstart";
    private static final JsonFactory JSON_FACTORY = JacksonFactory.getDefaultInstance();
    private static final String TOKENS_DIRECTORY_PATH = "tokens";
    private static String SPREADSHEET_ID = INSERTYOURIDHERE;
    private static MainActivity main_Activity = null;
    /**
     * Global instance of the scopes required by this quickstart.
     * If modifying these scopes, delete your previously saved tokens/ folder.
     */
    private static final List<String> SCOPES = Collections.singletonList(SheetsScopes.SPREADSHEETS_READONLY);
    private static final String CREDENTIALS_FILE_PATH = "credentials.json";


    public ConnectToDatabase(MainActivity mainActivity) {
        this.main_Activity = mainActivity;
    }

    private static Credential getCredentials(final NetHttpTransport HTTP_TRANSPORT) throws IOException {

        // Load client secrets.
        InputStream in =
                main_Activity.getAssets().open("credentials.json");
        //new FileInputStream(CREDENTIALS_FILE_PATH);
        ConnectToDatabase.class.getResourceAsStream(CREDENTIALS_FILE_PATH);
        GoogleClientSecrets clientSecrets = GoogleClientSecrets.load(JSON_FACTORY, new InputStreamReader(in));


        // Build flow and trigger user authorization request.

        GoogleAuthorizationCodeFlow flow = new GoogleAuthorizationCodeFlow.Builder(
                HTTP_TRANSPORT, JSON_FACTORY, clientSecrets, SCOPES)
                .setDataStoreFactory(new FileDataStoreFactory(main_Activity.getDir(TOKENS_DIRECTORY_PATH, Context.MODE_APPEND)))
                .setAccessType("offline")
                .build();


        AuthorizationCodeInstalledApp ab = new AuthorizationCodeInstalledApp(flow, new LocalServerReceiver()){
            protected void onAuthorization(AuthorizationCodeRequestUrl authorizationUrl) throws IOException {
                String url = (authorizationUrl.build());
                        /*flow.newAuthorizationUrl()
                        .setScopes(flow.getScopes())
                        .setAccessType("offline")
                        .setClientId(clientSecrets.getDetails().getClientId())
                        .setRedirectUri("/oauth2-callback")
                        .toString();
*/

                Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
                main_Activity.startActivity(browserIntent);
            }
        };
        Credential a = ab.authorize("user");


        return a;
    }

    /**
     * Prints the names and majors of students in a sample spreadsheet:
     * https://docs.google.com/spreadsheets/d/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms/edit
     */
    public static void main(String[] args) throws IOException, GeneralSecurityException {
    // Build a new authorized API client service.
            final NetHttpTransport HTTP_TRANSPORT = new com.google.api.client.http.javanet.NetHttpTransport();
            final String range = "A1:H";
            Sheets service = null;
            try {
                service = new Sheets.Builder(HTTP_TRANSPORT, JSON_FACTORY, getCredentials(HTTP_TRANSPORT))
                        .setApplicationName(APPLICATION_NAME)
                        .build();
            } catch (IOException e) {
                e.printStackTrace();
            }
            ValueRange response = null;

            try {
                response = service.spreadsheets().values()
                        .get(SPREADSHEET_ID, range)
                        .execute();
            } catch (IOException e) {
                e.printStackTrace();
            }

            List<List<Object>> values = response.getValues();
            [...]
    }
}

You might need to modify this code snippet according to your needs a bit. I used this as a part of an app of mine.

So what will this code do? On execution, this code will connect to google with your personal API key, which I named credentials.json. (Create your own at: https://developers.google.com/+/web/api/rest/oauth ) After successfull authentication, you will be able to access a google sheet with a particular ID.

dnsiv
  • 520
  • 4
  • 20
  • 2
    Thanks for the response. Unfortunately, as this uses the SheetsScopes.SPREADSHEETS_READONLY, it'd still provide read access to all of my Drive sheets. Really do appreciate the response though, thank you. – Daniel Hunt Oct 24 '18 at 11:51