-
Notifications
You must be signed in to change notification settings - Fork 71
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Migrating to Google Auth Library #180
Changes from 16 commits
0236bf9
0b67559
799b005
2a0a767
9230969
2a688e2
a3bc162
c9a0f90
012f9ed
015a84f
51af33c
0b1c17b
9ea5409
d8c1571
35e9ae3
badbd4f
08d067c
2477cbe
91a4f63
9a9af28
46ba0c1
710eee1
6b3e901
82a8960
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
_OLD_CLIENT_CONFIG_KEYS = frozenset( | ||
( | ||
"client_id", | ||
"client_secret", | ||
"auth_uri", | ||
"token_uri", | ||
"revoke_uri", | ||
"redirect_uri", | ||
) | ||
) | ||
|
||
_CLIENT_CONFIG_KEYS = frozenset( | ||
( | ||
"client_id", | ||
"client_secret", | ||
"auth_uri", | ||
"token_uri", | ||
"redirect_uris", | ||
) | ||
) | ||
|
||
|
||
def verify_client_config(client_config, with_oauth_type=True): | ||
"""Verifies that format of the client config | ||
loaded from a Google-format client secrets file. | ||
""" | ||
|
||
oauth_type = None | ||
config = client_config | ||
|
||
if with_oauth_type: | ||
if "web" in client_config: | ||
oauth_type = "web" | ||
config = config["web"] | ||
|
||
elif "installed" in client_config: | ||
oauth_type = "installed" | ||
config = config["installed"] | ||
else: | ||
raise ValueError( | ||
"Client secrets must be for a web or installed app" | ||
) | ||
|
||
# This is the older format of client config | ||
if _OLD_CLIENT_CONFIG_KEYS.issubset(config.keys()): | ||
config["redirect_uris"] = [config["redirect_uri"]] | ||
|
||
# by default, the redirect uri is the first in the list | ||
if "redirect_uri" not in config: | ||
config["redirect_uri"] = config["redirect_uris"][0] | ||
|
||
if "revoke_uri" not in config: | ||
config["revoke_uri"] = "https://oauth2.googleapis.com/revoke" | ||
|
||
if not _CLIENT_CONFIG_KEYS.issubset(config.keys()): | ||
raise ValueError("Client secrets is not in the correct format.") | ||
|
||
return oauth_type, config |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
import os | ||
import json | ||
import warnings | ||
from filelock import FileLock | ||
|
||
|
||
_SYM_LINK_MESSAGE = "File: {0}: Is a symbolic link." | ||
_IS_DIR_MESSAGE = "{0}: Is a directory" | ||
_MISSING_FILE_MESSAGE = "Cannot access {0}: No such file or directory" | ||
|
||
|
||
def validate_file(filename): | ||
if os.path.islink(filename): | ||
raise IOError(_SYM_LINK_MESSAGE.format(filename)) | ||
elif os.path.isdir(filename): | ||
raise IOError(_IS_DIR_MESSAGE.format(filename)) | ||
elif not os.path.isfile(filename): | ||
warnings.warn(_MISSING_FILE_MESSAGE.format(filename)) | ||
|
||
|
||
class CredentialBackend(object): | ||
"""Adapter that provides a consistent interface to read and write credential files""" | ||
|
||
def _read_credentials(self, rpath): | ||
"""Specific implementation of how the storage object should retrieve a file.""" | ||
return NotImplementedError | ||
|
||
def _store_credentials(self, credential, rpath): | ||
"""Specific implementation of how the storage object should write a file""" | ||
return NotImplementedError | ||
|
||
def _delete_credentials(self, rpath): | ||
"""Specific implementation of how the storage object should delete a file.""" | ||
return NotImplementedError | ||
|
||
def read_credentials(self, rpath): | ||
"""Reads a credential config file and returns the config as a dictionary | ||
:param fname: host name of the local web server. | ||
:type host_name: str.` | ||
:return: A credential file | ||
""" | ||
return self._read_credentials(rpath) | ||
|
||
def store_credentials(self, credential, rpath): | ||
"""Write a credential to | ||
The Storage lock must be held when this is called. | ||
Args: | ||
credentials: Credentials, the credentials to store. | ||
""" | ||
self._store_credentials(credential, rpath) | ||
|
||
def delete_credentials(self, rpath): | ||
"""Delete credential. | ||
Frees any resources associated with storing the credential. | ||
The Storage lock must *not* be held when this is called. | ||
|
||
Returns: | ||
None | ||
""" | ||
self._delete_credentials(rpath) | ||
|
||
|
||
class FileBackend(CredentialBackend): | ||
# https://stackoverflow.com/questions/37084682/is-oauth-thread-safe | ||
"""Read and write credentials to a file backend with File Locking""" | ||
|
||
def __init__(self): | ||
self._locks = {} | ||
|
||
def createLock(self, rpath): | ||
self._locks[rpath] = FileLock("{}.lock".format(rpath)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The previous implementation was Semaphore / Mutex or something like this-based. File based locks are problematic. Primarily because of something happens you would have to go and delete them manually. Let's please stick for now with what we had in the code before. For multi-threaded safety it's enough to use an in-memory lock. When we write into a file (new credentials), we can guarantee multi-process safety with writing into a temp file first and then doing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm slightly confused by the purpose of any sort of thread locks for the Credentials object. I'm under the impression that there should only be one GoogleAuth object with Some sources that suggest that credentials should be synchronously refreshed: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I will update this to the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I think that's why those locks exist, since in the current implementation multiple threads could try to refresh (and write it back to a file) them simultaneously. With the current implementation I'm not sure we are writing the credentials on refresh. It would be another difference in the implementation that we need to double check and see if there are some important implications.
thanks. |
||
|
||
def getLock(self, rpath): | ||
if rpath not in self._locks: | ||
self.createLock(rpath) | ||
return self._locks[rpath] | ||
|
||
def _create_file_if_needed(self, rpath): | ||
"""Create an empty file if necessary. | ||
This method will not initialize the file. Instead it implements a | ||
simple version of "touch" to ensure the file has been created. | ||
""" | ||
if not os.path.exists(rpath): | ||
old_umask = os.umask(0o177) | ||
try: | ||
open(rpath, "a+b").close() | ||
finally: | ||
os.umask(old_umask) | ||
|
||
def _read_credentials(self, rpath): | ||
"""Reads a local json file and parses the information into a info dictionary. | ||
Returns: | ||
Raises: | ||
""" | ||
with self.getLock(rpath): | ||
with open(rpath, "r") as json_file: | ||
return json.load(json_file) | ||
|
||
def _store_credentials(self, credentials, rpath): | ||
"""Writes current credentials to a local json file. | ||
Args: | ||
Raises: | ||
""" | ||
with self.getLock(rpath): | ||
self._create_file_if_needed(rpath) | ||
validate_file(rpath) | ||
|
||
with open(rpath, "w") as json_file: | ||
json_file.write(credentials.to_json()) | ||
|
||
def _delete_credentials(self, rpath): | ||
"""Delete Credentials file. | ||
Args: | ||
credentials: Credentials, the credentials to store. | ||
""" | ||
with self.getLock(rpath): | ||
os.unlink(rpath) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
client_config_backend: file | ||
client_config_file: tmp/pydrive2/user.json | ||
|
||
save_credentials: True | ||
save_credentials_backend: file | ||
save_credentials_file: credentials/default_user.dat | ||
|
||
oauth_scope: | ||
- https://www.googleapis.com/auth/drive | ||
|
||
get_refresh_token: True |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
client_config_backend: file | ||
client_config_file: tmp/pydrive2/user.json | ||
|
||
save_credentials: True | ||
save_credentials_backend: file | ||
save_credentials_file: credentials/default_user_no_refresh.dat | ||
|
||
oauth_scope: | ||
- https://www.googleapis.com/auth/drive |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
client_config_backend: service | ||
service_config: | ||
use_default: True | ||
oauth_scope: | ||
- https://www.googleapis.com/auth/drive |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we skip for the sake of this PR the default credentials auth? I think it should be done outside of the ServiceAuth + probably we should be using a different setting for this (don't know which one yet)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll shift this into a subsequent PR once we've migrated to new google auth library