8

Is there a way to programmatically access the email of the currently used Service Account on a GCP instance when no GOOGLE_APPLICATION_CREDENTIALS is set? (ie. when using the default Service Account)

I've looked through the GCP documentation, but the only resource I found can't be used with the default Service Account when no GOOGLE_APPLICATION_CREDENTIALS is set. I know that it is possible to do so using gcloud (see this SO question or documentation), however these solutions aren't applicable when running on a ContainerOptimisedOS. I've spent a couple of weeks back and forth with the GCP support team, but they concluded with not being able to help me and redirected me to Stack Overflow for help.

halfer
  • 19,824
  • 17
  • 99
  • 186
Voy
  • 5,286
  • 1
  • 49
  • 59
  • Clarify what you mean by accessing the service account. Do you mean the name, the contents or the OAuth tokens that a service account creates? The Google Client SDKs will detect that you are running on a Google service with a service account. There is nothing special for you to do unless you are writing custom code, calling REST APIs, etc. For that you can fetch an Access Token from the Metadata server. Edit your question to clearly state your goal and problem. – John Hanley Jan 21 '21 at 05:54
  • Thanks for your request John - I mean anything really: name, email, contents, anything that would point at the specific account that is being used. As requested I edited the question – Voy Jan 21 '21 at 05:57

5 Answers5

14

The solution of John works great, on any language without any external library. However, it works only on Google Cloud environment, when a metadata server is deployed. You can't perform this test on your computer.

I propose just bellow a piece of Python code (with Google OAuth library, but it works in other languages that have this library) to ask the library the current credential. If the credential is a service account (from GOOGLE_APPLICATION_CREDENTIALS on your computer, the ADC (Application Default Credential) or from the metadata server), you have the email printed, else, you have warning message because you use your user account credential

    import google.auth

    credentials, project_id = google.auth.default()

    if hasattr(credentials, "service_account_email"):
      print(credentials.service_account_email)
    else:
        print("WARNING: no service account credential. User account credential?")

Note that if the default service account is used this method will print default instead of the entire email address.


EDIT 1

    ctx := context.Background()
    credential,err := google.FindDefaultCredentials(ctx)
    content := map[string]interface{}{}

    json.Unmarshal(credential.JSON,&content)
    if content["client_email"] != nil {
      fmt.Println(content["client_email"])
    } else {
      fmt.Println("WARNING: no service account credential. User account credential?")
    }

guillaume blaquiere
  • 66,369
  • 2
  • 47
  • 76
  • 1
    This! I applaud you Guillaume, this is bullseye right what I needed and indeed returns the correct information that allows me to identify which Service Account is used. I've added a small typo correction and a small clarification to your answer - I hope that's ok. Once again, thank you, I wish the GCP support folks would be as swift and precise as you! – Voy Jan 22 '21 at 11:13
  • 2
    is there an equivalent lib in go-lang? thanks – Zanecola May 23 '21 at 13:41
  • 1
    Sure, I updated my answer with the GO code for you. – guillaume blaquiere May 23 '21 at 18:27
  • If its not a service account and its a user authing with oauth how do you get _that_ users email name? Is that not possible? – red888 Oct 14 '22 at 14:37
  • `The solution of John works great` -- I'm not seeing such a solution being posted by John on this page. Has it been deleted? Could you enlighten me on what solution you're referring to? – Vincent Yin Apr 12 '23 at 15:26
  • It's the first comment on the question. John is a great engineer and he often comment instead of answer, but it's awesome! – guillaume blaquiere Apr 12 '23 at 15:30
  • You said your code above works for `Application Default Credential` (`gcloud auth application-default login`), but I tested it and it doesn't work because this statement fails to pass: `if hasattr(credentials, "service_account_email"):` – Vincent Yin Apr 12 '23 at 16:10
  • Sadly, I'm not a python expert. I found that trick to test an attribute in the object. That value is set only if there is a service account, not a user credential. If you find a smarter workaround, I will be happy to learn from you! – guillaume blaquiere Apr 12 '23 at 16:18
  • Thanks for the replies above. After reading for hours and testing in my own Python code, I'm almost certain that (1) Your Python code above ONLY works when the env var `GOOGLE_APPLICATION_CREDENTIALS` is explicitly set. (2) This won't pass that `if` statement in Python: `gcloud auth application-default login`. (3) This won't pass that `if` statement in Python, either: `gcloud auth activate-service-account ...` This isn't a Python thing. This is a GCP thing -- the `credentials` object fundamentally doesn't have the email attribute in scenarios (2) and (3). – Vincent Yin Apr 12 '23 at 16:31
  • I just copy pasted the code in a Python editor, with my user credential, and it worked as expected. Python 3.8.10 and google-auth==2.14.0 – guillaume blaquiere Apr 12 '23 at 18:28
  • `with my user credential` -- meaning your individual user account such as `xxx@gmail.com` as in `gcloud config get-value account`? That will result in an `Exception` rather than branching to the `else:` part of the `if` statement (that's what happens to me). Anyway, that's not the primary point I was trying to make. I meant that if you **don't** set `GOOGLE_APPLICATION_CREDENTIALS` but rather do `gcloud auth application-default login`, then your code still won't enter the `if` branch of the logic -- it will enter the `else:` branch. I'm using Python 3.9.12 and `google-auth==2.17.2`. – Vincent Yin Apr 12 '23 at 20:37
  • Yes, your user credential, you go into the else. The if is for service account, not user account. – guillaume blaquiere Apr 12 '23 at 20:44
  • OK, this is too hard to explain in a comment. I just posted an "answer" (https://stackoverflow.com/a/76000116/13676217) in order to clearly show what I meant and to continue this conversation. – Vincent Yin Apr 12 '23 at 21:54
3

Just adding to the accepted answer. As stated in the answer this seems to return "default":

import google.auth

credentials, project_id = google.auth.default()
# returns "default"
print(credentials.service_account_email) 

I found to get the email name of the GSA currently active (via the metadata api) I had to manually refresh first:

import google.auth
import google.auth.transport.requests

credentials, project_id = google.auth.default()
request = google.auth.transport.requests.Request()
credentials.refresh(request=request)
# returns "mygsa@myproject.iam.gserviceaccount.com"
print(credentials.service_account_email) 

I'm using workload ID. I think maybe there is a race condition and I'm trying to read the service_account_email property before the creds get initialized for the first time when the pod starts.

The _retrieve_info() function is called when refresh() is called and it appears this is the function that grabs the email name.

If I had my script sleep for a few seconds on start up, I wonder if service_account_email would eventually be populated with the email name of the GSA.

halfer
  • 19,824
  • 17
  • 99
  • 186
red888
  • 27,709
  • 55
  • 204
  • 392
2

Querying the GCP metadata server should work on any GCP compute instance, including, ContainerOptimisedOS and without using gcloud SDK, as the OP requested.

Example:

curl --header "Metadata-Flavor: Google" "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/email"

Greg Dubicki
  • 5,983
  • 3
  • 55
  • 68
0

If you are interested in getting the exact email and not just the "default" alias (when you are using the compute engine), you can fetch it using the credentials metadata. This was particularly helpful in determining which service account is being used by AI Platform jobs.

    import google.auth
    from google.auth.transport.requests import Request
    from google.auth.compute_engine import _metadata
    
    if hasattr(credentials, "service_account_email"):
        print(credentials.service_account_email)

        # edits made on top of the top answer's code
        info = _metadata.get_service_account_info(Request(),
                service_account=credentials.service_account_email)

        print(f"Service account email: {info.email}")
        print(f"Service account aliases: {info.aliases}")
        print(f"Service account scopes: {info.scopes}")

    else:
        print("WARNING: no service account credentials available.")
0

Further exploring the Python code snippet posted by Guillaume Blaquiere...

I believe the code will throw an Exception instead of going to the else: branch if we supply an individual user account. Here's the demo setup:

$ python -V
Python 3.8.10


$ pip list | grep google
google-auth    2.14.0


$ gcloud config get-value account
...
xxx@xxx.com  <==== My individual (personal) email address


$ env | grep GOOGLE_APPLICATION_CREDENTIALS
    <==== Nothing, meaning this env var isn't set.


  ## I don't have the following file which means
  ## that I did NOT do `gcloud auth application-default login`
$ ls -l ~/.config/gcloud/application_default_credentials.json
ls: /Users/vyin/.config/gcloud/application_default_credentials.json: No such file or directory


$ cat test.py
import google.auth

credentials, project_id = google.auth.default()

if hasattr(credentials, "service_account_email"):
    print(credentials.service_account_email)
else:
    print("WARNING: no service account credential. User account credential?")

Now test it:

  ## Throws an Exception as I believe it should.
$ python test.py

Traceback (most recent call last):
  File "test.py", line 3, in <module>
    credentials, project_id = google.auth.default()
  File "/....../.venv/lib/python3.8/site-packages/google/auth/_default.py", line 643, in default
    raise exceptions.DefaultCredentialsError(_HELP_MESSAGE)
google.auth.exceptions.DefaultCredentialsError: 
Could not automatically determine credentials. 
Please set GOOGLE_APPLICATION_CREDENTIALS or explicitly create credentials and re-run the application. 
For more information, please see https://cloud.google.com/docs/authentication/getting-started


  ## Now create an ADC:
$ gcloud auth application-default login


  ## The above command created this file:
$ ls -l ~/.config/gcloud/application_default_credentials.json
-rw-------  ... Apr 12 17:48 /Users/vyin/.config/gcloud/application_default_credentials.json


  ## The Python code now enters the `else:` branch.

$ python test.py
WARNING: no service account credential. User account credential?

Conclusion:

  1. The else: branch isn't for user account at all -- a user account results in an exception.
  2. The else: branch is for the Application Default Credential (ADC).
Vincent Yin
  • 1,196
  • 5
  • 13
  • 1
    Arf, understood. In the first case, you have "no application credential". Neither with `gcloud auth application-credential login` nor with the env var `GOOGLE_APPLICATION_CREDENTIALS `. In the 2nd case, you have a credential and it works. So yes, my code snippet could be buggy if you have no application credential in your runtime environment. – guillaume blaquiere Apr 13 '23 at 07:14
  • 1
    Keep also in mind that there are different scope. The `gcloud auth login` as well as the `gcloud config get-value account` are set for the GCLOUD CLI only!! Those values aren't forwarded/read by the applications at runtime! That's a common trap! – guillaume blaquiere Apr 13 '23 at 07:15