Tri Vo | a3141f5 | 2016-09-30 17:32:04 -0700 | [diff] [blame] | 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 whitelisted by |
| 36 | Android Build team so that acloud can access build api. |
| 37 | """ |
| 38 | |
| 39 | import logging |
| 40 | import os |
Tri Vo | a3141f5 | 2016-09-30 17:32:04 -0700 | [diff] [blame] | 41 | |
| 42 | import httplib2 |
| 43 | |
Kevin Cheng | a9dac95 | 2018-07-11 10:42:02 -0700 | [diff] [blame] | 44 | # pylint: disable=import-error |
Tri Vo | a3141f5 | 2016-09-30 17:32:04 -0700 | [diff] [blame] | 45 | from oauth2client import client as oauth2_client |
Kevin Cheng | a9dac95 | 2018-07-11 10:42:02 -0700 | [diff] [blame] | 46 | from oauth2client import service_account as oauth2_service_account |
Tri Vo | a3141f5 | 2016-09-30 17:32:04 -0700 | [diff] [blame] | 47 | from oauth2client.contrib import multistore_file |
| 48 | from oauth2client import tools as oauth2_tools |
| 49 | |
Sam Chiu | 7de3b23 | 2018-12-06 19:45:52 +0800 | [diff] [blame^] | 50 | from acloud import errors |
Tri Vo | a3141f5 | 2016-09-30 17:32:04 -0700 | [diff] [blame] | 51 | |
| 52 | logger = logging.getLogger(__name__) |
| 53 | HOME_FOLDER = os.path.expanduser("~") |
Sam Chiu | 4d9bb4b | 2018-10-26 11:38:23 +0800 | [diff] [blame] | 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"]) |
Tri Vo | a3141f5 | 2016-09-30 17:32:04 -0700 | [diff] [blame] | 62 | |
| 63 | |
| 64 | def _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: |
xingdai | 8a00d46 | 2018-07-30 14:24:48 -0700 | [diff] [blame] | 77 | errors.AuthenticationError: if failed to authenticate. |
Tri Vo | a3141f5 | 2016-09-30 17:32:04 -0700 | [diff] [blame] | 78 | """ |
| 79 | try: |
Kevin Cheng | a9dac95 | 2018-07-11 10:42:02 -0700 | [diff] [blame] | 80 | credentials = oauth2_service_account.ServiceAccountCredentials.from_p12_keyfile( |
| 81 | email, private_key_path, scopes=scopes) |
Tri Vo | a3141f5 | 2016-09-30 17:32:04 -0700 | [diff] [blame] | 82 | except EnvironmentError as e: |
xingdai | 8a00d46 | 2018-07-30 14:24:48 -0700 | [diff] [blame] | 83 | raise errors.AuthenticationError( |
Kevin Cheng | a9dac95 | 2018-07-11 10:42:02 -0700 | [diff] [blame] | 84 | "Could not authenticate using private key file (%s) " |
| 85 | " error message: %s" % (private_key_path, str(e))) |
Tri Vo | a3141f5 | 2016-09-30 17:32:04 -0700 | [diff] [blame] | 86 | return credentials |
| 87 | |
Sam Chiu | 4d9bb4b | 2018-10-26 11:38:23 +0800 | [diff] [blame] | 88 | # pylint: disable=invalid-name |
xingdai | 8a00d46 | 2018-07-30 14:24:48 -0700 | [diff] [blame] | 89 | def _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 | |
Tri Vo | a3141f5 | 2016-09-30 17:32:04 -0700 | [diff] [blame] | 114 | class 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 | |
| 124 | def _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 | |
| 147 | def _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: |
xingdai | 8a00d46 | 2018-07-30 14:24:48 -0700 | [diff] [blame] | 164 | raise errors.AuthenticationError( |
Tri Vo | a3141f5 | 2016-09-30 17:32:04 -0700 | [diff] [blame] | 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 | |
Sam Chiu | 4d9bb4b | 2018-10-26 11:38:23 +0800 | [diff] [blame] | 184 | def CreateCredentials(acloud_config, scopes=_ALL_SCOPES): |
Tri Vo | a3141f5 | 2016-09-30 17:32:04 -0700 | [diff] [blame] | 185 | """Create credentials. |
| 186 | |
Sam Chiu | 4d9bb4b | 2018-10-26 11:38:23 +0800 | [diff] [blame] | 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 | |
Tri Vo | a3141f5 | 2016-09-30 17:32:04 -0700 | [diff] [blame] | 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 | """ |
xingdai | 8a00d46 | 2018-07-30 14:24:48 -0700 | [diff] [blame] | 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: |
Tri Vo | a3141f5 | 2016-09-30 17:32:04 -0700 | [diff] [blame] | 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) |