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