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