This repository has been archived by the owner on Nov 5, 2019. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 432
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adds support for Google Developer Shell session credentials
- Loading branch information
Showing
2 changed files
with
265 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
# Copyright 2015 Google Inc. All Rights Reserved. | ||
# | ||
# 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. | ||
|
||
"""OAuth 2.0 utitilies for Google Developer Shell environment.""" | ||
|
||
import json | ||
import os | ||
|
||
from oauth2client import client | ||
|
||
|
||
DEVSHELL_ENV = 'DEVSHELL_CLIENT_PORT' | ||
|
||
|
||
class Error(Exception): | ||
"""Errors for this module.""" | ||
pass | ||
|
||
|
||
class CommunicationError(Error): | ||
"""Errors for communication with the Developer Shell server.""" | ||
|
||
|
||
class NoDevshellServer(Error): | ||
"""Error when no Developer Shell server can be contacted.""" | ||
|
||
|
||
# The request for credential information to the Developer Shell client socket is | ||
# always an empty PBLite-formatted JSON object, so just define it as a constant. | ||
CREDENTIAL_INFO_REQUEST_JSON = '[]' | ||
|
||
|
||
class CredentialInfoResponse(object): | ||
"""Credential information response from Developer Shell server. | ||
The credential information response from Developer Shell socket is a | ||
PBLite-formatted JSON array with fields encoded by their index in the array: | ||
* Index 0 - user email | ||
* Index 1 - default project ID. None if the project context is not known. | ||
* Index 2 - OAuth2 access token. None if there is no valid auth context. | ||
""" | ||
|
||
def __init__(self, json_string): | ||
"""Initialize the response data from JSON PBLite array.""" | ||
pbl = json.loads(json_string) | ||
if not isinstance(pbl, list): | ||
raise ValueError('Not a list: ' + str(pbl)) | ||
self.user_email = pbl[0] if len(pbl) else None | ||
self.project_id = pbl[1] if len(pbl) > 1 else None | ||
self.access_token = pbl[2] if len(pbl) > 2 else None | ||
|
||
|
||
def _SendRecv(): | ||
"""Communicate with the Developer Shell server socket.""" | ||
|
||
port = int(os.getenv(DEVSHELL_ENV, 0)) | ||
if port == 0: | ||
raise NoDevshellServer() | ||
|
||
# pylint:disable=g-import-not-at-top, Delay for performance. | ||
import socket | ||
|
||
s = socket.socket() | ||
s.connect(('localhost', port)) | ||
|
||
data = CREDENTIAL_INFO_REQUEST_JSON | ||
msg = '%s\n%s' % (len(data), data) | ||
s.sendall(msg.encode()) | ||
|
||
resp_buffer = '' | ||
resp_1 = s.recv(6).decode() | ||
if '\n' not in resp_1: | ||
raise CommunicationError('saw no newline in the first 6 bytes') | ||
nstr, extra = resp_1.split('\n', 1) | ||
resp_buffer = extra | ||
n = int(nstr) | ||
to_read = n-len(extra) | ||
if to_read > 0: | ||
resp_buffer += s.recv(to_read, socket.MSG_WAITALL).decode() | ||
|
||
return CredentialInfoResponse(resp_buffer) | ||
|
||
|
||
class DevshellCredentials(client.GoogleCredentials): | ||
"""Credentials object for Google Developer Shell environment. | ||
This object will allow a Google Developer Shell session to identify its user | ||
to Google and other OAuth 2.0 servers that can verify assertions. It can be | ||
used for the purpose of accessing data stored under the user account. | ||
This credential does not require a flow to instantiate because it represents | ||
a two legged flow, and therefore has all of the required information to | ||
generate and refresh its own access tokens. | ||
""" | ||
|
||
def __init__(self, user_agent=None): | ||
super(DevshellCredentials, self).__init__( | ||
None, # access_token, initialized below | ||
None, # client_id | ||
None, # client_secret | ||
None, # refresh_token | ||
None, # token_expiry | ||
None, # token_uri | ||
user_agent) | ||
self._refresh(None) | ||
|
||
def _refresh(self, http): | ||
self.devshell_response = _SendRecv() | ||
self.access_token = self.devshell_response.access_token | ||
|
||
@property | ||
def user_email(self): | ||
return self.devshell_response.user_email | ||
|
||
@property | ||
def project_id(self): | ||
return self.devshell_response.project_id | ||
|
||
@classmethod | ||
def from_json(cls, json_data): | ||
raise NotImplementedError( | ||
'Cannot load Developer Shell credentials from JSON.') | ||
|
||
@property | ||
def serialization_data(self): | ||
raise NotImplementedError( | ||
'Cannot serialize Developer Shell credentials.') | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
# Copyright 2015 Google Inc. All Rights Reserved. | ||
# | ||
# 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. | ||
|
||
|
||
"""Tests for oauth2client.devshell.""" | ||
|
||
import os | ||
import socket | ||
import threading | ||
import unittest | ||
|
||
from oauth2client.client import save_to_well_known_file | ||
from oauth2client.devshell import _SendRecv | ||
from oauth2client.devshell import CREDENTIAL_INFO_REQUEST_JSON | ||
from oauth2client.devshell import DEVSHELL_ENV | ||
from oauth2client.devshell import DevshellCredentials | ||
from oauth2client.devshell import NoDevshellServer | ||
|
||
|
||
class _AuthReferenceServer(threading.Thread): | ||
|
||
def __init__(self, response=None): | ||
super(_AuthReferenceServer, self).__init__(None) | ||
self.response = (response or | ||
'["joe@example.com", "fooproj", "sometoken"]') | ||
|
||
def __enter__(self): | ||
self.start_server() | ||
|
||
def start_server(self): | ||
self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | ||
self._socket.bind(('localhost', 0)) | ||
port = self._socket.getsockname()[1] | ||
os.environ[DEVSHELL_ENV] = str(port) | ||
self._socket.listen(0) | ||
self.start() | ||
return self | ||
|
||
def __exit__(self, e_type, value, traceback): | ||
self.stop_server() | ||
|
||
def stop_server(self): | ||
del os.environ[DEVSHELL_ENV] | ||
self._socket.close() | ||
|
||
def run(self): | ||
s = None | ||
try: | ||
self._socket.settimeout(15) | ||
s, unused_addr = self._socket.accept() | ||
resp_buffer = '' | ||
resp_1 = s.recv(6).decode() | ||
if '\n' not in resp_1: | ||
raise Exception('invalid request data') | ||
nstr, extra = resp_1.split('\n', 1) | ||
resp_buffer = extra | ||
n = int(nstr) | ||
to_read = n-len(extra) | ||
if to_read > 0: | ||
resp_buffer += s.recv(to_read, socket.MSG_WAITALL) | ||
if resp_buffer != CREDENTIAL_INFO_REQUEST_JSON: | ||
raise Exception('bad request') | ||
l = len(self.response) | ||
s.sendall(('%d\n%s' % (l, self.response)).encode()) | ||
finally: | ||
if s: | ||
s.close() | ||
|
||
|
||
class DevshellCredentialsTests(unittest.TestCase): | ||
|
||
def test_signals_no_server(self): | ||
with self.assertRaises(NoDevshellServer): | ||
DevshellCredentials() | ||
|
||
def test_request_response(self): | ||
with _AuthReferenceServer(): | ||
response = _SendRecv() | ||
self.assertEqual(response.user_email, 'joe@example.com') | ||
self.assertEqual(response.project_id, 'fooproj') | ||
self.assertEqual(response.access_token, 'sometoken') | ||
|
||
def test_no_refresh_token(self): | ||
with _AuthReferenceServer(): | ||
creds = DevshellCredentials() | ||
self.assertIsNone(creds.refresh_token) | ||
|
||
def test_reads_credentials(self): | ||
with _AuthReferenceServer(): | ||
creds = DevshellCredentials() | ||
self.assertEqual('joe@example.com', creds.user_email) | ||
self.assertEqual('fooproj', creds.project_id) | ||
self.assertEqual('sometoken', creds.access_token) | ||
|
||
def test_handles_skipped_fields(self): | ||
with _AuthReferenceServer('["joe@example.com"]'): | ||
creds = DevshellCredentials() | ||
self.assertEqual('joe@example.com', creds.user_email) | ||
self.assertEqual(None, creds.project_id) | ||
self.assertEqual(None, creds.access_token) | ||
|
||
def test_handles_ignores_extra_fields(self): | ||
with _AuthReferenceServer( | ||
'["joe@example.com", "fooproj", "sometoken", "extra"]'): | ||
creds = DevshellCredentials() | ||
self.assertEqual('joe@example.com', creds.user_email) | ||
self.assertEqual('fooproj', creds.project_id) | ||
self.assertEqual('sometoken', creds.access_token) | ||
|
||
def test_refuses_to_save_to_well_known_file(self): | ||
with _AuthReferenceServer(): | ||
creds = DevshellCredentials() | ||
with self.assertRaises(NotImplementedError): | ||
save_to_well_known_file(creds) | ||
|