1# Copyright 2015 Google Inc.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#      http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Helpers for reading the Google Cloud SDK's configuration."""
16
17import json
18import os
19import subprocess
20
21import six
22
23from google.auth import environment_vars
24from google.auth import exceptions
25
26
27# The ~/.config subdirectory containing gcloud credentials.
28_CONFIG_DIRECTORY = "gcloud"
29# Windows systems store config at %APPDATA%\gcloud
30_WINDOWS_CONFIG_ROOT_ENV_VAR = "APPDATA"
31# The name of the file in the Cloud SDK config that contains default
32# credentials.
33_CREDENTIALS_FILENAME = "application_default_credentials.json"
34# The name of the Cloud SDK shell script
35_CLOUD_SDK_POSIX_COMMAND = "gcloud"
36_CLOUD_SDK_WINDOWS_COMMAND = "gcloud.cmd"
37# The command to get the Cloud SDK configuration
38_CLOUD_SDK_CONFIG_COMMAND = ("config", "config-helper", "--format", "json")
39# The command to get google user access token
40_CLOUD_SDK_USER_ACCESS_TOKEN_COMMAND = ("auth", "print-access-token")
41# Cloud SDK's application-default client ID
42CLOUD_SDK_CLIENT_ID = (
43    "764086051850-6qr4p6gpi6hn506pt8ejuq83di341hur.apps.googleusercontent.com"
44)
45
46
47def get_config_path():
48    """Returns the absolute path the the Cloud SDK's configuration directory.
49
50    Returns:
51        str: The Cloud SDK config path.
52    """
53    # If the path is explicitly set, return that.
54    try:
55        return os.environ[environment_vars.CLOUD_SDK_CONFIG_DIR]
56    except KeyError:
57        pass
58
59    # Non-windows systems store this at ~/.config/gcloud
60    if os.name != "nt":
61        return os.path.join(os.path.expanduser("~"), ".config", _CONFIG_DIRECTORY)
62    # Windows systems store config at %APPDATA%\gcloud
63    else:
64        try:
65            return os.path.join(
66                os.environ[_WINDOWS_CONFIG_ROOT_ENV_VAR], _CONFIG_DIRECTORY
67            )
68        except KeyError:
69            # This should never happen unless someone is really
70            # messing with things, but we'll cover the case anyway.
71            drive = os.environ.get("SystemDrive", "C:")
72            return os.path.join(drive, "\\", _CONFIG_DIRECTORY)
73
74
75def get_application_default_credentials_path():
76    """Gets the path to the application default credentials file.
77
78    The path may or may not exist.
79
80    Returns:
81        str: The full path to application default credentials.
82    """
83    config_path = get_config_path()
84    return os.path.join(config_path, _CREDENTIALS_FILENAME)
85
86
87def _run_subprocess_ignore_stderr(command):
88    """ Return subprocess.check_output with the given command and ignores stderr."""
89    with open(os.devnull, "w") as devnull:
90        output = subprocess.check_output(command, stderr=devnull)
91    return output
92
93
94def get_project_id():
95    """Gets the project ID from the Cloud SDK.
96
97    Returns:
98        Optional[str]: The project ID.
99    """
100    if os.name == "nt":
101        command = _CLOUD_SDK_WINDOWS_COMMAND
102    else:
103        command = _CLOUD_SDK_POSIX_COMMAND
104
105    try:
106        # Ignore the stderr coming from gcloud, so it won't be mixed into the output.
107        # https://github.com/googleapis/google-auth-library-python/issues/673
108        output = _run_subprocess_ignore_stderr((command,) + _CLOUD_SDK_CONFIG_COMMAND)
109    except (subprocess.CalledProcessError, OSError, IOError):
110        return None
111
112    try:
113        configuration = json.loads(output.decode("utf-8"))
114    except ValueError:
115        return None
116
117    try:
118        return configuration["configuration"]["properties"]["core"]["project"]
119    except KeyError:
120        return None
121
122
123def get_auth_access_token(account=None):
124    """Load user access token with the ``gcloud auth print-access-token`` command.
125
126    Args:
127        account (Optional[str]): Account to get the access token for. If not
128            specified, the current active account will be used.
129
130    Returns:
131        str: The user access token.
132
133    Raises:
134        google.auth.exceptions.UserAccessTokenError: if failed to get access
135            token from gcloud.
136    """
137    if os.name == "nt":
138        command = _CLOUD_SDK_WINDOWS_COMMAND
139    else:
140        command = _CLOUD_SDK_POSIX_COMMAND
141
142    try:
143        if account:
144            command = (
145                (command,)
146                + _CLOUD_SDK_USER_ACCESS_TOKEN_COMMAND
147                + ("--account=" + account,)
148            )
149        else:
150            command = (command,) + _CLOUD_SDK_USER_ACCESS_TOKEN_COMMAND
151
152        access_token = subprocess.check_output(command, stderr=subprocess.STDOUT)
153        # remove the trailing "\n"
154        return access_token.decode("utf-8").strip()
155    except (subprocess.CalledProcessError, OSError, IOError) as caught_exc:
156        new_exc = exceptions.UserAccessTokenError(
157            "Failed to obtain access token", caught_exc
158        )
159        six.raise_from(new_exc, caught_exc)
160