• 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 
18 Possible cases of authentication are noted below.
19 
20 --------------------------------------------------------
21      account                   | authentcation
22 --------------------------------------------------------
23 
24 google account (e.g. gmail)*   | normal oauth2
25 
26 
27 service 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 
39 import logging
40 import os
41 
42 import httplib2
43 
44 # pylint: disable=import-error
45 from oauth2client import client as oauth2_client
46 from oauth2client import service_account as oauth2_service_account
47 from oauth2client.contrib import multistore_file
48 from oauth2client import tools as oauth2_tools
49 
50 from acloud import errors
51 
52 
53 logger = logging.getLogger(__name__)
54 HOME_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 
66 def _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
92 def _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 
128 class 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 
138 def _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 
164 def _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 
203 def 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