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