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# If there is no specific scope use case, we will always use this default full 56# scopes to run CreateCredentials func and user will only go oauth2 flow once 57# after login with this full scopes credentials. 58_ALL_SCOPES = " ".join(["https://www.googleapis.com/auth/compute", 59 "https://www.googleapis.com/auth/logging.write", 60 "https://www.googleapis.com/auth/androidbuild.internal", 61 "https://www.googleapis.com/auth/devstorage.read_write", 62 "https://www.googleapis.com/auth/userinfo.email"]) 63 64 65def _CreateOauthServiceAccountCreds(email, private_key_path, scopes): 66 """Create credentials with a normal service account. 67 68 Args: 69 email: email address as the account. 70 private_key_path: Path to the service account P12 key. 71 scopes: string, multiple scopes should be saperated by space. 72 Api scopes to request for the oauth token. 73 74 Returns: 75 An oauth2client.OAuth2Credentials instance. 76 77 Raises: 78 errors.AuthenticationError: if failed to authenticate. 79 """ 80 try: 81 credentials = oauth2_service_account.ServiceAccountCredentials.from_p12_keyfile( 82 email, private_key_path, scopes=scopes) 83 except EnvironmentError as e: 84 raise errors.AuthenticationError( 85 "Could not authenticate using private key file (%s) " 86 " error message: %s" % (private_key_path, str(e))) 87 return credentials 88 89# pylint: disable=invalid-name 90def _CreateOauthServiceAccountCredsWithJsonKey(json_private_key_path, scopes): 91 """Create credentials with a normal service account from json key file. 92 93 Args: 94 json_private_key_path: Path to the service account json key file. 95 scopes: string, multiple scopes should be saperated by space. 96 Api scopes to request for the oauth token. 97 98 Returns: 99 An oauth2client.OAuth2Credentials instance. 100 101 Raises: 102 errors.AuthenticationError: if failed to authenticate. 103 """ 104 try: 105 return ( 106 oauth2_service_account.ServiceAccountCredentials 107 .from_json_keyfile_name( 108 json_private_key_path, scopes=scopes)) 109 except EnvironmentError as e: 110 raise errors.AuthenticationError( 111 "Could not authenticate using json private key file (%s) " 112 " error message: %s" % (json_private_key_path, str(e))) 113 114# pylint: disable=old-style-class 115class RunFlowFlags(): 116 """Flags for oauth2client.tools.run_flow.""" 117 118 def __init__(self, browser_auth): 119 self.auth_host_port = [8080, 8090] 120 self.auth_host_name = "localhost" 121 self.logging_level = "ERROR" 122 self.noauth_local_webserver = not browser_auth 123 124 125def _RunAuthFlow(storage, client_id, client_secret, user_agent, scopes): 126 """Get user oauth2 credentials. 127 128 Args: 129 client_id: String, client id from the cloud project. 130 client_secret: String, client secret for the client_id. 131 user_agent: The user agent for the credential, e.g. "acloud" 132 scopes: String, scopes separated by space. 133 134 Returns: 135 An oauth2client.OAuth2Credentials instance. 136 """ 137 flags = RunFlowFlags(browser_auth=False) 138 flow = oauth2_client.OAuth2WebServerFlow( 139 client_id=client_id, 140 client_secret=client_secret, 141 scope=scopes, 142 user_agent=user_agent) 143 credentials = oauth2_tools.run_flow( 144 flow=flow, storage=storage, flags=flags) 145 return credentials 146 147 148def _CreateOauthUserCreds(creds_cache_file, client_id, client_secret, 149 user_agent, scopes): 150 """Get user oauth2 credentials. 151 152 Args: 153 creds_cache_file: String, file name for the credential cache. 154 e.g. .acloud_oauth2.dat 155 Will be created at home folder. 156 client_id: String, client id from the cloud project. 157 client_secret: String, client secret for the client_id. 158 user_agent: The user agent for the credential, e.g. "acloud" 159 scopes: String, scopes separated by space. 160 161 Returns: 162 An oauth2client.OAuth2Credentials instance. 163 """ 164 if not client_id or not client_secret: 165 raise errors.AuthenticationError( 166 "Could not authenticate using Oauth2 flow, please set client_id " 167 "and client_secret in your config file. Contact the cloud project's " 168 "admin if you don't have the client_id and client_secret.") 169 storage = multistore_file.get_credential_storage( 170 filename=os.path.abspath(creds_cache_file), 171 client_id=client_id, 172 user_agent=user_agent, 173 scope=scopes) 174 credentials = storage.get() 175 if credentials is not None: 176 if not credentials.access_token_expired and not credentials.invalid: 177 return credentials 178 try: 179 credentials.refresh(httplib2.Http()) 180 except oauth2_client.AccessTokenRefreshError: 181 pass 182 if not credentials.invalid: 183 return credentials 184 return _RunAuthFlow(storage, client_id, client_secret, user_agent, scopes) 185 186 187def CreateCredentials(acloud_config, scopes=_ALL_SCOPES): 188 """Create credentials. 189 190 If no specific scope provided, we create a full scopes credentials for 191 authenticating and user will only go oauth2 flow once after login with 192 full scopes credentials. 193 194 Args: 195 acloud_config: An AcloudConfig object. 196 scopes: A string representing for scopes, separted by space, 197 like "SCOPE_1 SCOPE_2 SCOPE_3" 198 199 Returns: 200 An oauth2client.OAuth2Credentials instance. 201 """ 202 if acloud_config.service_account_json_private_key_path: 203 return _CreateOauthServiceAccountCredsWithJsonKey( 204 acloud_config.service_account_json_private_key_path, 205 scopes=scopes) 206 if acloud_config.service_account_private_key_path: 207 return _CreateOauthServiceAccountCreds( 208 acloud_config.service_account_name, 209 acloud_config.service_account_private_key_path, 210 scopes=scopes) 211 212 if os.path.isabs(acloud_config.creds_cache_file): 213 creds_cache_file = acloud_config.creds_cache_file 214 else: 215 creds_cache_file = os.path.join(HOME_FOLDER, 216 acloud_config.creds_cache_file) 217 return _CreateOauthUserCreds( 218 creds_cache_file=creds_cache_file, 219 client_id=acloud_config.client_id, 220 client_secret=acloud_config.client_secret, 221 user_agent=acloud_config.user_agent, 222 scopes=scopes) 223