#!/usr/bin/env python
#
# Copyright 2016 - The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Module for handling Authentication.
Possible cases of authentication are noted below.
--------------------------------------------------------
account | authentcation
--------------------------------------------------------
google account (e.g. gmail)* | normal oauth2
service account* | oauth2 + private key
--------------------------------------------------------
* For now, non-google employees (i.e. non @google.com account) or
non-google-owned service account can not access Android Build API.
Only local build artifact can be used.
* Google-owned service account, if used, needs to be whitelisted by
Android Build team so that acloud can access build api.
"""
import logging
import os
import httplib2
# pylint: disable=import-error
from oauth2client import client as oauth2_client
from oauth2client import service_account as oauth2_service_account
from oauth2client.contrib import multistore_file
from oauth2client import tools as oauth2_tools
from acloud import errors
logger = logging.getLogger(__name__)
HOME_FOLDER = os.path.expanduser("~")
# If there is no specific scope use case, we will always use this default full
# scopes to run CreateCredentials func and user will only go oauth2 flow once
# after login with this full scopes credentials.
_ALL_SCOPES = " ".join(["https://www.googleapis.com/auth/compute",
"https://www.googleapis.com/auth/logging.write",
"https://www.googleapis.com/auth/androidbuild.internal",
"https://www.googleapis.com/auth/devstorage.read_write",
"https://www.googleapis.com/auth/userinfo.email"])
def _CreateOauthServiceAccountCreds(email, private_key_path, scopes):
"""Create credentials with a normal service account.
Args:
email: email address as the account.
private_key_path: Path to the service account P12 key.
scopes: string, multiple scopes should be saperated by space.
Api scopes to request for the oauth token.
Returns:
An oauth2client.OAuth2Credentials instance.
Raises:
errors.AuthenticationError: if failed to authenticate.
"""
try:
credentials = oauth2_service_account.ServiceAccountCredentials.from_p12_keyfile(
email, private_key_path, scopes=scopes)
except EnvironmentError as e:
raise errors.AuthenticationError(
"Could not authenticate using private key file (%s) "
" error message: %s" % (private_key_path, str(e)))
return credentials
# pylint: disable=invalid-name
def _CreateOauthServiceAccountCredsWithJsonKey(json_private_key_path, scopes):
"""Create credentials with a normal service account from json key file.
Args:
json_private_key_path: Path to the service account json key file.
scopes: string, multiple scopes should be saperated by space.
Api scopes to request for the oauth token.
Returns:
An oauth2client.OAuth2Credentials instance.
Raises:
errors.AuthenticationError: if failed to authenticate.
"""
try:
return (
oauth2_service_account.ServiceAccountCredentials
.from_json_keyfile_name(
json_private_key_path, scopes=scopes))
except EnvironmentError as e:
raise errors.AuthenticationError(
"Could not authenticate using json private key file (%s) "
" error message: %s" % (json_private_key_path, str(e)))
class RunFlowFlags(object):
"""Flags for oauth2client.tools.run_flow."""
def __init__(self, browser_auth):
self.auth_host_port = [8080, 8090]
self.auth_host_name = "localhost"
self.logging_level = "ERROR"
self.noauth_local_webserver = not browser_auth
def _RunAuthFlow(storage, client_id, client_secret, user_agent, scopes):
"""Get user oauth2 credentials.
Args:
client_id: String, client id from the cloud project.
client_secret: String, client secret for the client_id.
user_agent: The user agent for the credential, e.g. "acloud"
scopes: String, scopes separated by space.
Returns:
An oauth2client.OAuth2Credentials instance.
"""
flags = RunFlowFlags(browser_auth=False)
flow = oauth2_client.OAuth2WebServerFlow(
client_id=client_id,
client_secret=client_secret,
scope=scopes,
user_agent=user_agent)
credentials = oauth2_tools.run_flow(
flow=flow, storage=storage, flags=flags)
return credentials
def _CreateOauthUserCreds(creds_cache_file, client_id, client_secret,
user_agent, scopes):
"""Get user oauth2 credentials.
Args:
creds_cache_file: String, file name for the credential cache.
e.g. .acloud_oauth2.dat
Will be created at home folder.
client_id: String, client id from the cloud project.
client_secret: String, client secret for the client_id.
user_agent: The user agent for the credential, e.g. "acloud"
scopes: String, scopes separated by space.
Returns:
An oauth2client.OAuth2Credentials instance.
"""
if not client_id or not client_secret:
raise errors.AuthenticationError(
"Could not authenticate using Oauth2 flow, please set client_id "
"and client_secret in your config file. Contact the cloud project's "
"admin if you don't have the client_id and client_secret.")
storage = multistore_file.get_credential_storage(
filename=os.path.abspath(creds_cache_file),
client_id=client_id,
user_agent=user_agent,
scope=scopes)
credentials = storage.get()
if credentials is not None:
try:
credentials.refresh(httplib2.Http())
except oauth2_client.AccessTokenRefreshError:
pass
if not credentials.invalid:
return credentials
return _RunAuthFlow(storage, client_id, client_secret, user_agent, scopes)
def CreateCredentials(acloud_config, scopes=_ALL_SCOPES):
"""Create credentials.
If no specific scope provided, we create a full scopes credentials for
authenticating and user will only go oauth2 flow once after login with
full scopes credentials.
Args:
acloud_config: An AcloudConfig object.
scopes: A string representing for scopes, separted by space,
like "SCOPE_1 SCOPE_2 SCOPE_3"
Returns:
An oauth2client.OAuth2Credentials instance.
"""
if acloud_config.service_account_json_private_key_path:
return _CreateOauthServiceAccountCredsWithJsonKey(
acloud_config.service_account_json_private_key_path,
scopes=scopes)
elif acloud_config.service_account_private_key_path:
return _CreateOauthServiceAccountCreds(
acloud_config.service_account_name,
acloud_config.service_account_private_key_path,
scopes=scopes)
creds_cache_file = os.path.join(HOME_FOLDER,
acloud_config.creds_cache_file)
return _CreateOauthUserCreds(
creds_cache_file=creds_cache_file,
client_id=acloud_config.client_id,
client_secret=acloud_config.client_secret,
user_agent=acloud_config.user_agent,
scopes=scopes)