2

In python, I'm trying to call the GMail API via a service account with Delegated domain-wide authority, without using SERVICE_ACCOUNT_FILE.

My objective is to avoid creating a secret Key for the service account. Instead, I gave the Service Account Token Creator role to the process owner (me in local dev, App Engine Service Account in prod).

In the code below I successfully get and use an access token for the service account, without any SERVICE_ACCOUNT_FILE.

from google.cloud.iam_credentials_v1 import IAMCredentialsClient
from google.oauth2.credentials import Credentials
import googleapiclient.discovery

tk = IAMCredentialsClient().generate_access_token(
    name=f'projects/-/serviceAccounts/{service_id}',
    scope=['https://www.googleapis.com/auth/gmail.insert'],
    # subject='admin@my.domain' doesn't work here :'(
)
service = googleapiclient.discovery.build('gmail', 'v1', credentials=Credentials(tk.access_token))
response = service.users().messages().insert(userId='user@my.domain', body=body).execute()

Problem is, after granting permissions to the service account in Google Admin of my.domain, I get the following error:

{'message': 'Precondition check failed.', 'domain': 'global', 'reason': 'failedPrecondition'}

I suspect that what I am missing is the subject, i.e. the email of an admin at my.domain.

I know I can provide the subject by constructing the credentials differently:

from google.oauth2 import service_account
credentials = service_account.Credentials.from_service_account_file(key_file, scopes=scopes)
delegated_credentials = credentials.with_subject('admin@my.domain'). # <- I need this
service = googleapiclient.discovery.build('gmail', 'v1', credentials=delegated_credentials)

But this requires the creation of a secret key, which I'd like to avoid.

Is there a way to pass the subject in the first code example above ?

poiuytrez
  • 21,330
  • 35
  • 113
  • 172
Arnaud P
  • 12,022
  • 7
  • 56
  • 67
  • Can you clarify why you want to avoid creating a secret key? – iansedano Aug 04 '21 at 08:38
  • It seems discouraged by Google, eg. [here: "Use service account keys only if there is no viable alternative"](https://cloud.google.com/blog/products/identity-security/how-to-authenticate-service-accounts-to-help-keep-applications-secure). In my case, there seems to be an alternative, since I manage to get an access token for the service without any user-managed key. Only impersonation of another user by that service is missing (which it can do, if authenticated with key). – Arnaud P Aug 04 '21 at 10:13
  • So you want to run an "attached" service account? Are you sure you are running this from a supported platform and have [attached](https://cloud.google.com/iam/docs/impersonating-service-accounts#binding-to-resources) it? – iansedano Aug 04 '21 at 11:04
  • I've come across the concept of attaching a service account to a resource. Unfortunately in our current workflow, we create one service account per customer (we are multi-tenant). IIUC there can be only one service account attached to our App Engine instance. – Arnaud P Aug 04 '21 at 16:25
  • I think this might be the answer to your question in that case - https://stackoverflow.com/questions/55354070/how-to-assign-multiple-service-account-credentials-to-google-cloud-functions - i.e. the service account identifies the app engine instance, so you can't attach more than one to it. – iansedano Aug 05 '21 at 08:01
  • That answer confirms the one-to-one attachment constraint yes. My problem would remain the same though: how do I get that attached service account to impersonate the user corresponding the inbox where I want to insert the email ? – Arnaud P Aug 05 '21 at 12:23
  • In other words, why is it I can only find the `with_subject()` method on credentials obtained by passing user-managed keys in python ? If this method was exposed on other types of service account Credentials, I guess the matter would be solved. – Arnaud P Aug 05 '21 at 12:24
  • Have you seen anywhere that this actually can be done? From what I can read it seems like to perform domain-wide delegation you need the `Key` whether its attached or not - "A service account key lets an application authenticate as a service account, similar to how a user might authenticate with a username and password." that quote is from your link above, and https://developers.google.com/identity/protocols/oauth2/service-account#delegatingauthority - its part of the required steps here and in other quickstarts for instance in the directory API. – iansedano Aug 06 '21 at 07:56
  • Right, I'm totally prepared to accept that this can't be done. Just couldn't see it written down explicitly anywhere. – Arnaud P Aug 06 '21 at 09:00

1 Answers1

1

AFAIK this is not possible

Would be happy to be proved wrong on this!

I place this as a disclaimer because you are right, it does not explicitly deny the possibility. Its not generally a requirement for documentation to explicitly express everything you can not do with it, though I do see the potential for confusion here so it might be worth "Sending Feedback" to the documentation page.

What the instructions say

Preparing to make an authorized API call

After you obtain the client email address and private key from the API Console ...

source

In no place does it say "this is an optional step". This is also true in other places like the Directory API instructions on domain-wide delegation, or the Reports API

The Recommendation to not use Keys

The following is from the article you linked:

Use service account keys only if there is no viable alternative

A service account key lets an application authenticate as a service account, similar to how a user might authenticate with a username and password.

source

When you attach a service account, for example to an App Engine instance, the instance is like the user and the service account is its user account. It serves as a sort of identity for the instance. More info on that on this other StackOverflow thread.

So the only way for a service account to act as-if it were another account, is with a key. Likewise the only way for a user to act as-if it were a service account is with a key. Therefore, if you are trying to enable domain-wide delegation, which presupposes that you want to have a service account act as-if it were other accounts, having a key is essential, whether the service account is attached to an App Engine instance or not.

Granted, its not mentioned explicitly that you can not achieve this without a key. Though it doesn't mention that you can either. From the tests that I have run, I have run into similar results as yours, so it would seem that you just can't. As mentioned in the disclaimer, I would be happy to be proved wrong, however, I suspect that it would be due to a vulnerability, not intended behavior.

You haven't mentioned why you need domain-wide delegation of authority, so you may want to evaluate if you really need it. Usually all that is needed is the OAuth flow, in which an app acts oh-behalf of a user, not as-if it were the user.

References

iansedano
  • 6,169
  • 2
  • 12
  • 24
  • 1
    Thank you for your research, I will accept this answer as there is no other challenger. As to why I need domain-wide delegation, I work for a phishing simulation company, and as part of our service we write emails in mailboxes of different companies (different Google Workspaces). Which brings [its own set of questions](https://stackoverflow.com/a/61823359/777285) – Arnaud P Aug 06 '21 at 13:30