• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python
2#
3# Copyright 2016 - The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#         http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16"""Module for handling Authentication.
17
18Possible cases of authentication are noted below.
19
20--------------------------------------------------------
21     account                   | authentcation
22--------------------------------------------------------
23
24google account (e.g. gmail)*   | normal oauth2
25
26
27service account*               | oauth2 + private key
28
29--------------------------------------------------------
30
31* For now, non-google employees (i.e. non @google.com account) or
32  non-google-owned service account can not access Android Build API.
33  Only local build artifact can be used.
34
35* Google-owned service account, if used, needs to be allowed by
36  Android Build team so that acloud can access build api.
37"""
38
39import logging
40import os
41
42import httplib2
43
44# pylint: disable=import-error
45from oauth2client import client as oauth2_client
46from oauth2client import service_account as oauth2_service_account
47from oauth2client.contrib import multistore_file
48from oauth2client import tools as oauth2_tools
49
50from acloud import errors
51
52
53logger = logging.getLogger(__name__)
54HOME_FOLDER = os.path.expanduser("~")
55_WEB_SERVER_DEFAULT_PORT = 8080
56# If there is no specific scope use case, we will always use this default full
57# scopes to run CreateCredentials func and user will only go oauth2 flow once
58# after login with this full scopes credentials.
59_ALL_SCOPES = " ".join(["https://www.googleapis.com/auth/compute",
60                        "https://www.googleapis.com/auth/logging.write",
61                        "https://www.googleapis.com/auth/androidbuild.internal",
62                        "https://www.googleapis.com/auth/devstorage.read_write",
63                        "https://www.googleapis.com/auth/userinfo.email"])
64
65
66def _CreateOauthServiceAccountCreds(email, private_key_path, scopes):
67    """Create credentials with a normal service account.
68
69    Args:
70        email: email address as the account.
71        private_key_path: Path to the service account P12 key.
72        scopes: string, multiple scopes should be saperated by space.
73                        Api scopes to request for the oauth token.
74
75    Returns:
76        An oauth2client.OAuth2Credentials instance.
77
78    Raises:
79        errors.AuthenticationError: if failed to authenticate.
80    """
81    try:
82        credentials = oauth2_service_account.ServiceAccountCredentials.from_p12_keyfile(
83            email, private_key_path, scopes=scopes)
84    except EnvironmentError as e:
85        raise errors.AuthenticationError(
86            f"Could not authenticate using private key file ({private_key_path}) "
87            f" error message: {str(e)}")
88    return credentials
89
90
91# pylint: disable=invalid-name
92def _CreateOauthServiceAccountCredsWithJsonKey(json_private_key_path, scopes,
93                                               creds_cache_file, user_agent):
94    """Create credentials with a normal service account from json key file.
95
96    Args:
97        json_private_key_path: Path to the service account json key file.
98        scopes: string, multiple scopes should be saperated by space.
99                        Api scopes to request for the oauth token.
100        creds_cache_file: String, file name for the credential cache.
101                          e.g. .acloud_oauth2.dat
102                          Will be created at home folder.
103        user_agent: String, the user agent for the credential, e.g. "acloud"
104
105    Returns:
106        An oauth2client.OAuth2Credentials instance.
107
108    Raises:
109        errors.AuthenticationError: if failed to authenticate.
110    """
111    try:
112        credentials = oauth2_service_account.ServiceAccountCredentials.from_json_keyfile_name(
113            json_private_key_path, scopes=scopes)
114        storage = multistore_file.get_credential_storage(
115            filename=os.path.abspath(creds_cache_file),
116            client_id=credentials.client_id,
117            user_agent=user_agent,
118            scope=scopes)
119        credentials.set_store(storage)
120    except EnvironmentError as e:
121        raise errors.AuthenticationError(
122            f"Could not authenticate using json private key file ({json_private_key_path}) "
123            f"error message: {str(e)}")
124
125    return credentials
126
127
128class RunFlowFlags():
129    """Flags for oauth2client.tools.run_flow."""
130
131    def __init__(self, browser_auth):
132        self.auth_host_port = [8080, 8090]
133        self.auth_host_name = "localhost"
134        self.logging_level = "ERROR"
135        self.noauth_local_webserver = not browser_auth
136
137
138def _RunAuthFlow(storage, client_id, client_secret, user_agent, scopes):
139    """Get user oauth2 credentials.
140
141    Using the loopback IP address flow for desktop clients.
142
143    Args:
144        client_id: String, client id from the cloud project.
145        client_secret: String, client secret for the client_id.
146        user_agent: The user agent for the credential, e.g. "acloud"
147        scopes: String, scopes separated by space.
148
149    Returns:
150        An oauth2client.OAuth2Credentials instance.
151    """
152    flags = RunFlowFlags(browser_auth=True)
153    flow = oauth2_client.OAuth2WebServerFlow(
154        client_id=client_id,
155        client_secret=client_secret,
156        scope=scopes,
157        user_agent=user_agent,
158        redirect_uri=f"http://localhost:{_WEB_SERVER_DEFAULT_PORT}")
159    credentials = oauth2_tools.run_flow(
160        flow=flow, storage=storage, flags=flags)
161    return credentials
162
163
164def _CreateOauthUserCreds(creds_cache_file, client_id, client_secret,
165                          user_agent, scopes):
166    """Get user oauth2 credentials.
167
168    Args:
169        creds_cache_file: String, file name for the credential cache.
170                                            e.g. .acloud_oauth2.dat
171                                            Will be created at home folder.
172        client_id: String, client id from the cloud project.
173        client_secret: String, client secret for the client_id.
174        user_agent: The user agent for the credential, e.g. "acloud"
175        scopes: String, scopes separated by space.
176
177    Returns:
178        An oauth2client.OAuth2Credentials instance.
179    """
180    if not client_id or not client_secret:
181        raise errors.AuthenticationError(
182            "Could not authenticate using Oauth2 flow, please set client_id "
183            "and client_secret in your config file. Contact the cloud project's "
184            "admin if you don't have the client_id and client_secret.")
185    storage = multistore_file.get_credential_storage(
186        filename=os.path.abspath(creds_cache_file),
187        client_id=client_id,
188        user_agent=user_agent,
189        scope=scopes)
190    credentials = storage.get()
191    if credentials is not None:
192        if not credentials.access_token_expired and not credentials.invalid:
193            return credentials
194        try:
195            credentials.refresh(httplib2.Http())
196        except oauth2_client.AccessTokenRefreshError:
197            pass
198        if not credentials.invalid:
199            return credentials
200    return _RunAuthFlow(storage, client_id, client_secret, user_agent, scopes)
201
202
203def CreateCredentials(acloud_config, scopes=_ALL_SCOPES):
204    """Create credentials.
205
206    If no specific scope provided, we create a full scopes credentials for
207    authenticating and user will only go oauth2 flow once after login with
208    full scopes credentials.
209
210    Args:
211        acloud_config: An AcloudConfig object.
212        scopes: A string representing for scopes, separted by space,
213            like "SCOPE_1 SCOPE_2 SCOPE_3"
214
215    Returns:
216        An oauth2client.OAuth2Credentials instance.
217    """
218    if os.path.isabs(acloud_config.creds_cache_file):
219        creds_cache_file = acloud_config.creds_cache_file
220    else:
221        creds_cache_file = os.path.join(HOME_FOLDER,
222                                        acloud_config.creds_cache_file)
223
224    if acloud_config.service_account_json_private_key_path:
225        return _CreateOauthServiceAccountCredsWithJsonKey(
226            acloud_config.service_account_json_private_key_path,
227            scopes=scopes,
228            creds_cache_file=creds_cache_file,
229            user_agent=acloud_config.user_agent)
230    if acloud_config.service_account_private_key_path:
231        return _CreateOauthServiceAccountCreds(
232            acloud_config.service_account_name,
233            acloud_config.service_account_private_key_path,
234            scopes=scopes)
235
236    return _CreateOauthUserCreds(
237        creds_cache_file=creds_cache_file,
238        client_id=acloud_config.client_id,
239        client_secret=acloud_config.client_secret,
240        user_agent=acloud_config.user_agent,
241        scopes=scopes)
242