Skip to content

Migrating to Google Auth Library #221

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

Closed
wants to merge 15 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
642 changes: 314 additions & 328 deletions pydrive2/auth.py

Large diffs are not rendered by default.

58 changes: 58 additions & 0 deletions pydrive2/auth_helpers.py
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
116 changes: 116 additions & 0 deletions pydrive2/storage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import os
import json
import warnings
import threading


_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 credentials"""

def _read_credentials(self, **kwargs):
"""Specific implementation of how credentials are retrieved from backend"""
return NotImplementedError

def _store_credentials(self, credential, **kwargs):
"""Specific implementation of how credentials are written to backend"""
return NotImplementedError

def _delete_credentials(self, **kwargs):
"""Specific implementation of how credentials are deleted from backend"""
return NotImplementedError

def read_credentials(self, **kwargs):
"""Reads a credential config from the backend and
returns the config as a dictionary
:return: A dictionary of the credentials
"""
return self._read_credentials(**kwargs)

def store_credentials(self, credential, **kwargs):
"""Write a credential to the backend"""
self._store_credentials(credential, **kwargs)

def delete_credentials(self, **kwargs):
"""Delete credential.
Frees any resources associated with storing the credential
"""
self._delete_credentials(**kwargs)


class FileBackend(CredentialBackend):
"""Read and write credential to a specific file backend with Thread-locking"""

def __init__(self, filename):
self._filename = filename
self._thread_lock = threading.Lock()

def _create_file_if_needed(self, filename):
"""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(filename):
old_umask = os.umask(0o177)
try:
open(filename, "a+b").close()
finally:
os.umask(old_umask)

def _read_credentials(self, **kwargs):
"""Reads a local json file and parses the information into a info dictionary."""
with self._thread_lock:
validate_file(self._filename)
with open(self._filename, "r") as json_file:
return json.load(json_file)

def _store_credentials(self, credentials, **kwargs):
"""Writes current credentials to a local json file."""
with self._thread_lock:
# write new credentials to the temp file
dirname, filename = os.path.split(self._filename)
temp_path = os.path.join(dirname, "temp_{}".format(filename))
self._create_file_if_needed(temp_path)

with open(temp_path, "w") as json_file:
json_file.write(credentials.to_json())

# replace the existing credential file
os.replace(temp_path, self._filename)

def _delete_credentials(self, **kwargs):
"""Delete credentials file."""
with self._thread_lock:
os.unlink(self._filename)


class DictionaryBackend(CredentialBackend):
"""Read and write credentials to a dictionary backend"""

def __init__(self, dictionary):
self._dictionary = dictionary

def _read_credentials(self, key):
"""Reads a local json file and parses the information into a info dictionary."""
return self._dictionary.get(key)

def _store_credentials(self, credentials, key):
"""Writes current credentials to a local json file."""
self._dictionary[key] = credentials.to_json()

def _delete_credentials(self, key):
"""Delete Credentials file."""
self._dictionary.pop(key, None)
11 changes: 11 additions & 0 deletions pydrive2/test/settings/default_user.yaml
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
9 changes: 9 additions & 0 deletions pydrive2/test/settings/default_user_no_refresh.yaml
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
28 changes: 17 additions & 11 deletions pydrive2/test/test_oauth.py
Original file line number Diff line number Diff line change
@@ -10,7 +10,7 @@
settings_file_path,
GDRIVE_USER_CREDENTIALS_DATA,
)
from oauth2client.file import Storage
from ..storage import FileBackend


def setup_module(module):
@@ -24,6 +24,7 @@ def test_01_LocalWebserverAuthWithClientConfigFromFile():
# Test if authentication works with config read from file
ga = GoogleAuth(settings_file_path("test_oauth_test_01.yaml"))
ga.LocalWebserverAuth()
assert ga.credentials
assert not ga.access_token_expired
# Test if correct credentials file is created
CheckCredentialsFile("credentials/1.dat")
@@ -37,6 +38,7 @@ def test_02_LocalWebserverAuthWithClientConfigFromSettings():
# Test if authentication works with config read from settings
ga = GoogleAuth(settings_file_path("test_oauth_test_02.yaml"))
ga.LocalWebserverAuth()
assert ga.credentials
assert not ga.access_token_expired
# Test if correct credentials file is created
CheckCredentialsFile("credentials/2.dat")
@@ -50,6 +52,7 @@ def test_03_LocalWebServerAuthWithNoCredentialsSaving():
ga = GoogleAuth(settings_file_path("test_oauth_test_03.yaml"))
assert not ga.settings["save_credentials"]
ga.LocalWebserverAuth()
assert ga.credentials
assert not ga.access_token_expired
time.sleep(1)

@@ -61,6 +64,7 @@ def test_04_CommandLineAuthWithClientConfigFromFile():
# Test if authentication works with config read from file
ga = GoogleAuth(settings_file_path("test_oauth_test_04.yaml"))
ga.CommandLineAuth()
assert ga.credentials
assert not ga.access_token_expired
# Test if correct credentials file is created
CheckCredentialsFile("credentials/4.dat")
@@ -72,6 +76,7 @@ def test_05_ConfigFromSettingsWithoutOauthScope():
# Test if authentication works without oauth_scope
ga = GoogleAuth(settings_file_path("test_oauth_test_05.yaml"))
ga.LocalWebserverAuth()
assert ga.credentials
assert not ga.access_token_expired
time.sleep(1)

@@ -81,7 +86,7 @@ def test_06_ServiceAuthFromSavedCredentialsP12File():
setup_credentials("credentials/6.dat")
ga = GoogleAuth(settings_file_path("test_oauth_test_06.yaml"))
ga.ServiceAuth()
assert not ga.access_token_expired
assert ga.credentials
time.sleep(1)


@@ -92,13 +97,14 @@ def test_07_ServiceAuthFromSavedCredentialsJsonFile():
# Delete old credentials file
delete_file(credentials_file)
assert not os.path.exists(credentials_file)
# For Service Auth, credentials are created with no token (treated as expired)
# JWT token is not populated until the first request where auto-refresh happens
# assert not ga.access_token_expired
ga.ServiceAuth()
assert os.path.exists(credentials_file)
# Secondary auth should be made only using the previously saved
# login info
assert ga.credentials
ga = GoogleAuth(settings_file_path("test_oauth_test_07.yaml"))
ga.ServiceAuth()
assert not ga.access_token_expired
assert ga.credentials
time.sleep(1)


@@ -108,6 +114,7 @@ def test_08_ServiceAuthFromJsonFileNoCredentialsSaving():
ga = GoogleAuth(settings_file_path("test_oauth_test_08.yaml"))
assert not ga.settings["save_credentials"]
ga.ServiceAuth()
assert ga.credentials
time.sleep(1)


@@ -120,10 +127,9 @@ def test_09_SaveLoadCredentialsUsesDefaultStorage(mocker):
# Delete old credentials file
delete_file(credentials_file)
assert not os.path.exists(credentials_file)
spy = mocker.spy(Storage, "__init__")
spy = mocker.spy(FileBackend, "__init__")
ga.ServiceAuth()
ga.LoadCredentials()
ga.SaveCredentials()
assert ga.credentials
assert spy.call_count == 0


@@ -142,14 +148,14 @@ def test_10_ServiceAuthFromSavedCredentialsDictionary():
}
ga = GoogleAuth(settings=settings)
ga.ServiceAuth()
assert not ga.access_token_expired
assert ga.credentials
assert creds_dict
first_creds_dict = creds_dict.copy()
# Secondary auth should be made only using the previously saved
# login info
ga = GoogleAuth(settings=settings)
ga.ServiceAuth()
assert not ga.access_token_expired
assert ga.credentials
assert creds_dict == first_creds_dict
time.sleep(1)

42 changes: 42 additions & 0 deletions pydrive2/test/test_oauth_custom.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import pytest
import os
from pydrive2.auth import GoogleAuth
from pydrive2.drive import GoogleDrive
from pydrive2.test.test_util import (
settings_file_path,
setup_credentials,
delete_file,
)


@pytest.fixture
def googleauth_preauth():
setup_credentials()
# Delete old credentials file
delete_file("credentials/default_user.dat")
ga = GoogleAuth(settings_file_path("default_user.yaml"))

return ga


@pytest.mark.manual
def test_01_CustomAuthWithSavingOfCredentials(googleauth_preauth):

credentials_file = googleauth_preauth.settings["save_credentials_file"]

assert not os.path.exists(credentials_file)

auth_url, state = googleauth_preauth.GetAuthUrl()
print("please visit this url: {}".format(auth_url))

googleauth_preauth.Authenticate(input("Please enter the auth code: "))

# credentials have been loaded
assert googleauth_preauth.credentials
# check that credentials file has been saved
assert os.path.exists(credentials_file)

gdrive = GoogleDrive(googleauth_preauth)

about_object = gdrive.GetAbout()
assert about_object is not None
Loading