|
66 | 66 | # Google Data client libraries may need to set this to [401, 403].
|
67 | 67 | REFRESH_STATUS_CODES = [401]
|
68 | 68 |
|
| 69 | +# The value representing user credentials. |
| 70 | +AUTHORIZED_USER = 'authorized_user' |
| 71 | + |
| 72 | +# The value representing service account credentials. |
| 73 | +SERVICE_ACCOUNT = 'service_account' |
| 74 | + |
| 75 | +# The environment variable pointing the file with local Default Credentials. |
| 76 | +GOOGLE_CREDENTIALS_DEFAULT = 'GOOGLE_CREDENTIALS_DEFAULT' |
69 | 77 |
|
70 | 78 | class Error(Exception):
|
71 | 79 | """Base error for this module."""
|
@@ -99,6 +107,10 @@ class NonAsciiHeaderError(Error):
|
99 | 107 | """Header names and values must be ASCII strings."""
|
100 | 108 |
|
101 | 109 |
|
| 110 | +class DefaultCredentialsError(Error): |
| 111 | + """Error retrieving the Default Credentials.""" |
| 112 | + |
| 113 | + |
102 | 114 | def _abstract():
|
103 | 115 | raise NotImplementedError('You need to override this function')
|
104 | 116 |
|
@@ -126,7 +138,7 @@ class Credentials(object):
|
126 | 138 | an HTTP transport.
|
127 | 139 |
|
128 | 140 | Subclasses must also specify a classmethod named 'from_json' that takes a JSON
|
129 |
| - string as input and returns an instaniated Credentials object. |
| 141 | + string as input and returns an instantiated Credentials object. |
130 | 142 | """
|
131 | 143 |
|
132 | 144 | NON_SERIALIZED_MEMBERS = ['store']
|
@@ -375,7 +387,7 @@ def _update_query_params(uri, params):
|
375 | 387 | The same URI but with the new query parameters added.
|
376 | 388 | """
|
377 | 389 | parts = list(urlparse.urlparse(uri))
|
378 |
| - query_params = dict(parse_qsl(parts[4])) # 4 is the index of the query part |
| 390 | + query_params = dict(parse_qsl(parts[4])) # 4 is the index of the query part |
379 | 391 | query_params.update(params)
|
380 | 392 | parts[4] = urllib.urlencode(query_params)
|
381 | 393 | return urlparse.urlunparse(parts)
|
@@ -587,6 +599,20 @@ def access_token_expired(self):
|
587 | 599 | return True
|
588 | 600 | return False
|
589 | 601 |
|
| 602 | + def get_access_token(self, http=None): |
| 603 | + """Return the access token. |
| 604 | +
|
| 605 | + If the token does not exist, get one. |
| 606 | + If the token expired, refresh it. |
| 607 | + """ |
| 608 | + if self.access_token and not self.access_token_expired: |
| 609 | + return self.access_token |
| 610 | + else: |
| 611 | + if not http: |
| 612 | + http = httplib2.Http() |
| 613 | + self.refresh(http) |
| 614 | + return self.access_token |
| 615 | + |
590 | 616 | def set_store(self, store):
|
591 | 617 | """Set the Storage for the credential.
|
592 | 618 |
|
@@ -820,7 +846,303 @@ def _revoke(self, http_request):
|
820 | 846 | self._do_revoke(http_request, self.access_token)
|
821 | 847 |
|
822 | 848 |
|
823 |
| -class AssertionCredentials(OAuth2Credentials): |
| 849 | +_env_name = None |
| 850 | + |
| 851 | + |
| 852 | +def _get_environment(urllib2_urlopen=None): |
| 853 | + """Detect the environment the code is being run on.""" |
| 854 | + |
| 855 | + global _env_name |
| 856 | + |
| 857 | + if _env_name: |
| 858 | + return _env_name |
| 859 | + |
| 860 | + server_software = os.environ.get('SERVER_SOFTWARE', '') |
| 861 | + if server_software.startswith('Google App Engine/'): |
| 862 | + _env_name = 'GAE_PRODUCTION' |
| 863 | + elif server_software.startswith('Development/'): |
| 864 | + _env_name = 'GAE_LOCAL' |
| 865 | + else: |
| 866 | + import urllib2 |
| 867 | + try: |
| 868 | + if urllib2_urlopen is None: |
| 869 | + urllib2_urlopen = urllib2.urlopen |
| 870 | + response = urllib2_urlopen('http://metadata.google.internal') |
| 871 | + if any('Metadata-Flavor: Google' in h for h in response.info().headers): |
| 872 | + _env_name = 'GCE_PRODUCTION' |
| 873 | + else: |
| 874 | + _env_name = 'UNKNOWN' |
| 875 | + except urllib2.URLError: |
| 876 | + _env_name = 'UNKNOWN' |
| 877 | + |
| 878 | + return _env_name |
| 879 | + |
| 880 | + |
| 881 | +class GoogleCredentials(OAuth2Credentials): |
| 882 | + """Default credentials for use in calling Google APIs. |
| 883 | + |
| 884 | + The Default Credentials are being constructed as a function of the environment |
| 885 | + where the code is being run. More details can be found on this page: |
| 886 | + https://developers.google.com/accounts/docs/default-credentials |
| 887 | +
|
| 888 | + Here is an example of how to use the Default Credentials for a service that |
| 889 | + requires authentication: |
| 890 | +
|
| 891 | + <code> |
| 892 | + from googleapiclient.discovery import build |
| 893 | + from oauth2client.client import GoogleCredentials |
| 894 | +
|
| 895 | + PROJECT = 'bamboo-machine-422' # replace this with one of your projects |
| 896 | + ZONE = 'us-central1-a' # replace this with the zone you care about |
| 897 | +
|
| 898 | + service = build('compute', 'v1', credentials=GoogleCredentials.get_default()) |
| 899 | +
|
| 900 | + request = service.instances().list(project=PROJECT, zone=ZONE) |
| 901 | + response = request.execute() |
| 902 | +
|
| 903 | + print response |
| 904 | + </code> |
| 905 | +
|
| 906 | + A service that does not require authentication does not need credentials |
| 907 | + to be passed in: |
| 908 | +
|
| 909 | + <code> |
| 910 | + from googleapiclient.discovery import build |
| 911 | +
|
| 912 | + service = build('discovery', 'v1') |
| 913 | +
|
| 914 | + request = service.apis().list() |
| 915 | + response = request.execute() |
| 916 | +
|
| 917 | + print response |
| 918 | + </code> |
| 919 | + """ |
| 920 | + |
| 921 | + def __init__(self, access_token, client_id, client_secret, refresh_token, |
| 922 | + token_expiry, token_uri, user_agent, |
| 923 | + revoke_uri=GOOGLE_REVOKE_URI): |
| 924 | + """Create an instance of GoogleCredentials. |
| 925 | +
|
| 926 | + This constructor is not usually called by the user, instead |
| 927 | + GoogleCredentials objects are instantiated by |
| 928 | + GoogleCredentials.from_stream() or GoogleCredentials.get_default(). |
| 929 | +
|
| 930 | + Args: |
| 931 | + access_token: string, access token. |
| 932 | + client_id: string, client identifier. |
| 933 | + client_secret: string, client secret. |
| 934 | + refresh_token: string, refresh token. |
| 935 | + token_expiry: datetime, when the access_token expires. |
| 936 | + token_uri: string, URI of token endpoint. |
| 937 | + user_agent: string, The HTTP User-Agent to provide for this application. |
| 938 | + revoke_uri: string, URI for revoke endpoint. |
| 939 | + Defaults to GOOGLE_REVOKE_URI; a token can't be revoked if this is None. |
| 940 | + """ |
| 941 | + super(GoogleCredentials, self).__init__( |
| 942 | + access_token, client_id, client_secret, refresh_token, token_expiry, |
| 943 | + token_uri, user_agent, revoke_uri=revoke_uri) |
| 944 | + |
| 945 | + def create_scoped_required(self): |
| 946 | + """Whether this Credentials object is scopeless. |
| 947 | + |
| 948 | + create_scoped(scopes) method needs to be called in order to create |
| 949 | + a Credentials object for API calls. |
| 950 | + """ |
| 951 | + return False |
| 952 | + |
| 953 | + def create_scoped(self, scopes): |
| 954 | + """Create a Credentials object for the given scopes. |
| 955 | + |
| 956 | + The Credentials type is preserved. |
| 957 | + """ |
| 958 | + return self |
| 959 | + |
| 960 | + @staticmethod |
| 961 | + def get_default(): |
| 962 | + """Get the Default Credentials for the current environment. |
| 963 | +
|
| 964 | + Exceptions: |
| 965 | + DefaultCredentialsError: raised when the credentials fail to be retrieved. |
| 966 | + """ |
| 967 | + |
| 968 | + _env_name = _get_environment() |
| 969 | + |
| 970 | + if _env_name in ('GAE_PRODUCTION', 'GAE_LOCAL'): |
| 971 | + # if we are running inside Google App Engine |
| 972 | + # there is no need to look for credentials in local files |
| 973 | + default_credential_filename = None |
| 974 | + well_known_file = None |
| 975 | + else: |
| 976 | + default_credential_filename = _get_environment_variable_file() |
| 977 | + well_known_file = _get_well_known_file() |
| 978 | + |
| 979 | + if default_credential_filename: |
| 980 | + try: |
| 981 | + return _get_default_credential_from_file(default_credential_filename) |
| 982 | + except (DefaultCredentialsError, ValueError) as error: |
| 983 | + extra_help = (' (pointed to by ' + GOOGLE_CREDENTIALS_DEFAULT + |
| 984 | + ' environment variable)') |
| 985 | + _raise_exception_for_reading_json(default_credential_filename, |
| 986 | + extra_help, error) |
| 987 | + elif well_known_file: |
| 988 | + try: |
| 989 | + return _get_default_credential_from_file(well_known_file) |
| 990 | + except (DefaultCredentialsError, ValueError) as error: |
| 991 | + extra_help = (' (produced automatically when running' |
| 992 | + ' "gcloud auth login" command)') |
| 993 | + _raise_exception_for_reading_json(well_known_file, extra_help, error) |
| 994 | + elif _env_name in ('GAE_PRODUCTION', 'GAE_LOCAL'): |
| 995 | + return _get_default_credential_GAE() |
| 996 | + elif _env_name == 'GCE_PRODUCTION': |
| 997 | + return _get_default_credential_GCE() |
| 998 | + else: |
| 999 | + raise DefaultCredentialsError( |
| 1000 | + "The Default Credentials are not available. They are available if " |
| 1001 | + "running in Google App Engine or Google Compute Engine. They are " |
| 1002 | + "also available if using the Google Cloud SDK and running 'gcloud " |
| 1003 | + "auth login'. Otherwise, the environment variable " + |
| 1004 | + GOOGLE_CREDENTIALS_DEFAULT + " must be defined pointing to a file " |
| 1005 | + "defining the credentials. " |
| 1006 | + "See https://developers.google.com/accounts/docs/default-credentials " |
| 1007 | + "for details.") |
| 1008 | + |
| 1009 | + @staticmethod |
| 1010 | + def from_stream(credential_filename): |
| 1011 | + """Create a Credentials object by reading the information from a given file. |
| 1012 | + |
| 1013 | + It returns an object of type GoogleCredentials. |
| 1014 | + |
| 1015 | + Args: |
| 1016 | + credential_filename: the path to the file from where the credentials |
| 1017 | + are to be read |
| 1018 | +
|
| 1019 | + Exceptions: |
| 1020 | + DefaultCredentialsError: raised when the credentials fail to be retrieved. |
| 1021 | + """ |
| 1022 | + |
| 1023 | + if credential_filename and os.path.isfile(credential_filename): |
| 1024 | + try: |
| 1025 | + return _get_default_credential_from_file(credential_filename) |
| 1026 | + except (DefaultCredentialsError, ValueError) as error: |
| 1027 | + extra_help = ' (provided as parameter to the from_stream() method)' |
| 1028 | + _raise_exception_for_reading_json(credential_filename, |
| 1029 | + extra_help, |
| 1030 | + error) |
| 1031 | + else: |
| 1032 | + raise DefaultCredentialsError('The parameter passed to the from_stream()' |
| 1033 | + ' method should point to a file.') |
| 1034 | + |
| 1035 | + |
| 1036 | +def _get_environment_variable_file(): |
| 1037 | + default_credential_filename = os.environ.get(GOOGLE_CREDENTIALS_DEFAULT, |
| 1038 | + None) |
| 1039 | + |
| 1040 | + if default_credential_filename: |
| 1041 | + if os.path.isfile(default_credential_filename): |
| 1042 | + return default_credential_filename |
| 1043 | + else: |
| 1044 | + raise DefaultCredentialsError( |
| 1045 | + 'File ' + default_credential_filename + ' (pointed by ' + |
| 1046 | + GOOGLE_CREDENTIALS_DEFAULT + ' environment variable) does not exist!') |
| 1047 | + |
| 1048 | + |
| 1049 | +def _get_well_known_file(): |
| 1050 | + """Get the well known file produced by command 'gcloud auth login'.""" |
| 1051 | + # TODO(orestica): Revisit this method once gcloud provides a better way |
| 1052 | + # of pinpointing the exact location of the file. |
| 1053 | + |
| 1054 | + WELL_KNOWN_CREDENTIALS_FILE = 'credentials_default.json' |
| 1055 | + CLOUDSDK_CONFIG_DIRECTORY = 'gcloud' |
| 1056 | + |
| 1057 | + if os.name == 'nt': |
| 1058 | + try: |
| 1059 | + default_config_path = os.path.join(os.environ['APPDATA'], |
| 1060 | + CLOUDSDK_CONFIG_DIRECTORY) |
| 1061 | + except KeyError: |
| 1062 | + # This should never happen unless someone is really messing with things. |
| 1063 | + drive = os.environ.get('SystemDrive', 'C:') |
| 1064 | + default_config_path = os.path.join(drive, '\\', CLOUDSDK_CONFIG_DIRECTORY) |
| 1065 | + else: |
| 1066 | + default_config_path = os.path.join(os.path.expanduser('~'), |
| 1067 | + '.config', |
| 1068 | + CLOUDSDK_CONFIG_DIRECTORY) |
| 1069 | + |
| 1070 | + default_config_path = os.path.join(default_config_path, |
| 1071 | + WELL_KNOWN_CREDENTIALS_FILE) |
| 1072 | + |
| 1073 | + if os.path.isfile(default_config_path): |
| 1074 | + return default_config_path |
| 1075 | + |
| 1076 | + |
| 1077 | +def _get_default_credential_from_file(default_credential_filename): |
| 1078 | + """Build the Default Credentials from file.""" |
| 1079 | + |
| 1080 | + import service_account |
| 1081 | + |
| 1082 | + # read the credentials from the file |
| 1083 | + with open(default_credential_filename) as default_credential: |
| 1084 | + client_credentials = service_account.simplejson.load(default_credential) |
| 1085 | + |
| 1086 | + credentials_type = client_credentials.get('type') |
| 1087 | + if credentials_type == AUTHORIZED_USER: |
| 1088 | + required_fields = set(['client_id', 'client_secret', 'refresh_token']) |
| 1089 | + elif credentials_type == SERVICE_ACCOUNT: |
| 1090 | + required_fields = set(['client_id', 'client_email', 'private_key_id', |
| 1091 | + 'private_key']) |
| 1092 | + else: |
| 1093 | + raise DefaultCredentialsError("'type' field should be defined " |
| 1094 | + "(and have one of the '" + AUTHORIZED_USER + |
| 1095 | + "' or '" + SERVICE_ACCOUNT + "' values)") |
| 1096 | + |
| 1097 | + missing_fields = required_fields.difference(client_credentials.keys()) |
| 1098 | + |
| 1099 | + if missing_fields: |
| 1100 | + _raise_exception_for_missing_fields(missing_fields) |
| 1101 | + |
| 1102 | + if client_credentials['type'] == AUTHORIZED_USER: |
| 1103 | + return GoogleCredentials( |
| 1104 | + access_token=None, |
| 1105 | + client_id=client_credentials['client_id'], |
| 1106 | + client_secret=client_credentials['client_secret'], |
| 1107 | + refresh_token=client_credentials['refresh_token'], |
| 1108 | + token_expiry=None, |
| 1109 | + token_uri=GOOGLE_TOKEN_URI, |
| 1110 | + user_agent='Python client library') |
| 1111 | + else: # client_credentials['type'] == SERVICE_ACCOUNT |
| 1112 | + return service_account._ServiceAccountCredentials( |
| 1113 | + service_account_id=client_credentials['client_id'], |
| 1114 | + service_account_email=client_credentials['client_email'], |
| 1115 | + private_key_id=client_credentials['private_key_id'], |
| 1116 | + private_key_pkcs8_text=client_credentials['private_key'], |
| 1117 | + scopes=[]) |
| 1118 | + |
| 1119 | + |
| 1120 | +def _raise_exception_for_missing_fields(missing_fields): |
| 1121 | + raise DefaultCredentialsError('The following field(s): ' + |
| 1122 | + ', '.join(missing_fields) + ' must be defined.') |
| 1123 | + |
| 1124 | + |
| 1125 | +def _raise_exception_for_reading_json(credential_file, |
| 1126 | + extra_help, |
| 1127 | + error): |
| 1128 | + raise DefaultCredentialsError('An error was encountered while reading ' |
| 1129 | + 'json file: '+ credential_file + extra_help + |
| 1130 | + ': ' + str(error)) |
| 1131 | + |
| 1132 | + |
| 1133 | +def _get_default_credential_GAE(): |
| 1134 | + from oauth2client.appengine import AppAssertionCredentials |
| 1135 | + |
| 1136 | + return AppAssertionCredentials([]) |
| 1137 | + |
| 1138 | + |
| 1139 | +def _get_default_credential_GCE(): |
| 1140 | + from oauth2client.gce import AppAssertionCredentials |
| 1141 | + |
| 1142 | + return AppAssertionCredentials([]) |
| 1143 | + |
| 1144 | + |
| 1145 | +class AssertionCredentials(GoogleCredentials): |
824 | 1146 | """Abstract Credentials object used for OAuth 2.0 assertion grants.
|
825 | 1147 |
|
826 | 1148 | This credential does not require a flow to instantiate because it
|
@@ -899,7 +1221,7 @@ class SignedJwtAssertionCredentials(AssertionCredentials):
|
899 | 1221 | later. For App Engine you may also consider using AppAssertionCredentials.
|
900 | 1222 | """
|
901 | 1223 |
|
902 |
| - MAX_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds |
| 1224 | + MAX_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds |
903 | 1225 |
|
904 | 1226 | @util.positional(4)
|
905 | 1227 | def __init__(self,
|
|
0 commit comments