8
votes

We want to download files from Google Storage in our application server. It is important to have read-only restricted access to a single bucket and nothing else.

At first I used a regular user account (not a service account) which have permissions to access all buckets in our Google Cloud project, and everything worked fine - my Java code opened buckets and downloaded files without problems.

Storage storage = StorageOptions.getDefaultInstance().getService();
Bucket b = storage.get( "mybucketname" );

Then I wanted to switch to use a specially created service account which has access to a single bucket only. So I created a service account, gave permissions to read a single bucket, and downloaded its key file. The permissions in Google Cloud Console are named as:

Storage Object Viewer (3 members) Read access to GCS objects.

gsutil command line utility works fine with this account - from the command line it allows accessing this bucket but not the others.

The initialization from the command line is done using the following command:

gcloud --project myprojectname auth activate-service-account [email protected] --key-file=/.../keyfilename.json

I even tried two different service accounts which have access to different buckets, and from the command line I can switch between them and gsutil gives access to a relevant bucket only, and for any other it returns the error:

"AccessDeniedException: 403 Caller does not have storage.objects.list access to bucket xxxxxxxxxx."

So, from the command line everything worked fine. But in Java there is some problem with the authentication.

The default authentication I previously used with a regular user account stopped working - it reports the error:

com.google.cloud.storage.StorageException: Anonymous users does not have storage.buckets.get access to bucket xxxxxxxxxx.

Then I've tried the following code (this is the simplest variant because it relies on the key json file, but I've already tried a number of other variants found in various forums, with no success):

FileInputStream fis = new FileInputStream( "/path/to/the/key-file.json" );
ServiceAccountCredentials credentials = ServiceAccountCredentials.fromStream( fis );
Storage storage = StorageOptions.newBuilder().setCredentials( credentials )
    .setProjectId( "myprojectid" ).build().getService();
Bucket b = storage.get( "mybucketname" );

And all I receive is this error:

com.google.cloud.storage.StorageException: Caller does not have storage.buckets.get access to bucket mybucketname. Caused by: com.google.api.client.googleapis.json.GoogleJsonResponseException: 403 Forbidden

The same error is returned no matter to what buckets I'm trying to access (even non-existing). What confuses me is that the same service account, initialized with the same JSON key file, works fine from the command line. So I think something is missing in Java code that ensures correct authentication.

2

2 Answers

11
votes

TL;DR - If you're using Application Default Credentials (which BTW you are when you do StorageOptions.getDefaultInstance().getService();), and if you need to use the credentials from a service account, you can do so without changing your code. All you need to do is set the GOOGLE_APPLICATION_CREDENTIALS environment variable to the full path of your service account json file and you are all set.

Longer version of the solution using Application Default Credentials

  • Use your original code as-is

    Storage storage = StorageOptions.getDefaultInstance().getService();
    Bucket b = storage.get( "mybucketname" );
    
  • Set the environment variable GOOGLE_APPLICATION_CREDENTIALS to the full path of your json file containing the service account credentials.

    export GOOGLE_APPLICATION_CREDENTIALS=/path/to/service_account_credentials.json
    
  • Run your java application once again to verify that it is working as expected.

Alternate solution using hard-coded Service Account Credentials

The code example you posted for initializing ServiceAccountCredentials looks valid to me on a quick glance. I tried the following code snippet and it is working for me as expected.

String SERVICE_ACCOUNT_JSON_PATH = "/path/to/service_account_credentials.json";

Storage storage =
    StorageOptions.newBuilder()
        .setCredentials(
            ServiceAccountCredentials.fromStream(
                new FileInputStream(SERVICE_ACCOUNT_JSON_PATH)))
        .build()
        .getService();
Bucket b = storage.get("mybucketname");

When specifying a service account credential, the project ID is automatically picked up from the information present in the json file. So you do not have to specify it once again. I'm not entirely sure though if this is related to the issue you're observing.

Application Default Credentials

Here is the full documentation regarding Application Default Credentials explaining which credentials are picked up based on your environment.

How the Application Default Credentials work

You can get Application Default Credentials by making a single client library call. The credentials returned are determined by the environment the code is running in. Conditions are checked in the following order:

  1. The environment variable GOOGLE_APPLICATION_CREDENTIALS is checked. If this variable is specified it should point to a file that defines the credentials. The simplest way to get a credential for this purpose is to create a Service account key in the Google API Console:

    a. Go to the API Console Credentials page.

    b. From the project drop-down, select your project.

    c. On the Credentials page, select the Create credentials drop-down, then select Service account key.

    d.From the Service account drop-down, select an existing service account or create a new one.

    e. For Key type, select the JSON key option, then select Create. The file automatically downloads to your computer.

    f. Put the *.json file you just downloaded in a directory of your choosing. This directory must be private (you can't let anyone get access to this), but accessible to your web server code.

    g. Set the environment variable GOOGLE_APPLICATION_CREDENTIALS to the path of the JSON file downloaded.

  2. If you have installed the Google Cloud SDK on your machine and have run the command gcloud auth application-default login, your identity can be used as a proxy to test code calling APIs from that machine.

  3. If you are running in Google App Engine production, the built-in service account associated with the application will be used.

  4. If you are running in Google Compute Engine production, the built-in service account associated with the virtual machine instance will be used.

  5. If none of these conditions is true, an error will occur.

IAM roles

I would recommend going over the IAM permissions and the IAM roles available for Cloud Storage. These provide control at project and bucket level. In addition, you can use ACLs to control permissions at the object level within the bucket.

  • If your use case involves just invoking storage.get(bucketName). This operation will require just storage.buckets.get permission and the best IAM role for just this permission is roles/storage.legacyObjectReader.

  • If you also want to grant the service account permissions to get (storage.objects.get) and list (storage.objects.list) individual objects, then also add the role roles/storage.objectViewer to the service account.

6
votes

Thanks to @Taxdude's long explanation, I understood that my Java code should be all right, and started looking at other possible reasons for the problem.

One of additional things I've tried were the permissions set to the service account, and there I've found the solution – it was unexpected, actually.

When a service account is created, it must not be given permissions to read from Google Storage, because then it will have read permissions to ALL buckets, and it is impossible to change that (not sure why), because the system marks these permissions as "inherited".

Therefore, you have to:

  • Create a "blank" service account with no permissions, and
  • Configure permissions from the bucket configuration

To do so:

  • Open Google Cloud Web console
  • Open Storage Browser
  • Select your bucket
  • Open the INFO PANEL with Permissions
  • Add the service account with the Storage Object Viewer permission, but there are also permissions named Storage Legacy Object Reader and Storage Legacy Bucket Reader

Because of the word "Legacy" I thought those should not be used – they look like something kept for backward compatibility. And after experimenting and adding these "legacy" permissions, all of a sudden the same code I was trying all the time started working properly.

I'm still not entirely sure what is the minimal set of permissions I should assign to a service account, but at least now it works with all three "read" permissions on the bucket – two "legacy" and one "normal".