forked from pool/python-pydrive2
1794 lines
63 KiB
Diff
1794 lines
63 KiB
Diff
|
From 0236bf917d3103e95eafdf5c839d652e2a75c790 Mon Sep 17 00:00:00 2001
|
||
|
From: junpeng-jp <junpeng.ong@gmail.com>
|
||
|
Date: Sun, 29 May 2022 00:12:34 +0800
|
||
|
Subject: [PATCH 1/9] Update setup.py with new google-auth dependencies
|
||
|
|
||
|
---
|
||
|
setup.py | 4 ++++
|
||
|
1 file changed, 4 insertions(+)
|
||
|
|
||
|
diff --git a/setup.py b/setup.py
|
||
|
index 06982d0..2b2d952 100644
|
||
|
--- a/setup.py
|
||
|
+++ b/setup.py
|
||
|
@@ -33,6 +33,10 @@
|
||
|
"google-api-python-client >= 1.12.5",
|
||
|
"six >= 1.13.0",
|
||
|
"oauth2client >= 4.0.0",
|
||
|
+ "google-auth >= 2.6.6",
|
||
|
+ "google-auth-httplib2 >= 0.1.0",
|
||
|
+ "google-auth-oauthlib >= 0.5.1",
|
||
|
+ "filelock >= 3.7.0",
|
||
|
"PyYAML >= 3.0",
|
||
|
"pyOpenSSL >= 19.1.0",
|
||
|
],
|
||
|
|
||
|
From 0b675594b81092191a8b52ab7606b73df7773ed0 Mon Sep 17 00:00:00 2001
|
||
|
From: junpeng-jp <junpeng.ong@gmail.com>
|
||
|
Date: Sun, 29 May 2022 00:26:36 +0800
|
||
|
Subject: [PATCH 2/9] Update threadsafe code following google recommendations
|
||
|
|
||
|
---
|
||
|
pydrive2/auth.py | 52 +++++++++++++++--------------------------------
|
||
|
pydrive2/drive.py | 6 +++++-
|
||
|
pydrive2/files.py | 35 ++++++++++++++++++-------------
|
||
|
3 files changed, 42 insertions(+), 51 deletions(-)
|
||
|
|
||
|
diff --git a/pydrive2/auth.py b/pydrive2/auth.py
|
||
|
index a1b8b2f..08625e8 100644
|
||
|
--- a/pydrive2/auth.py
|
||
|
+++ b/pydrive2/auth.py
|
||
|
@@ -21,6 +21,7 @@
|
||
|
from .settings import SettingsError
|
||
|
from .settings import InvalidConfigError
|
||
|
|
||
|
+from google_auth_httplib2 import AuthorizedHttp
|
||
|
|
||
|
class AuthError(Exception):
|
||
|
"""Base error for authentication/authorization errors."""
|
||
|
@@ -61,23 +62,7 @@ def _decorated(self, *args, **kwargs):
|
||
|
if self.auth.service is None:
|
||
|
self.auth.Authorize()
|
||
|
|
||
|
- # Ensure that a thread-safe HTTP object is provided.
|
||
|
- if (
|
||
|
- kwargs is not None
|
||
|
- and "param" in kwargs
|
||
|
- and kwargs["param"] is not None
|
||
|
- and "http" in kwargs["param"]
|
||
|
- and kwargs["param"]["http"] is not None
|
||
|
- ):
|
||
|
- self.http = kwargs["param"]["http"]
|
||
|
- del kwargs["param"]["http"]
|
||
|
-
|
||
|
- else:
|
||
|
- # If HTTP object not specified, create or resuse an HTTP
|
||
|
- # object from the thread local storage.
|
||
|
- if not getattr(self.auth.thread_local, "http", None):
|
||
|
- self.auth.thread_local.http = self.auth.Get_Http_Object()
|
||
|
- self.http = self.auth.thread_local.http
|
||
|
+
|
||
|
|
||
|
return decoratee(self, *args, **kwargs)
|
||
|
|
||
|
@@ -173,15 +158,14 @@ class GoogleAuth(ApiAttributeMixin):
|
||
|
def __init__(self, settings_file="settings.yaml", http_timeout=None):
|
||
|
"""Create an instance of GoogleAuth.
|
||
|
|
||
|
- This constructor just sets the path of settings file.
|
||
|
- It does not actually read the file.
|
||
|
+ This constructor parses just he yaml settings file.
|
||
|
+ All other settings are lazy
|
||
|
|
||
|
:param settings_file: path of settings file. 'settings.yaml' by default.
|
||
|
:type settings_file: str.
|
||
|
"""
|
||
|
self.http_timeout = http_timeout
|
||
|
ApiAttributeMixin.__init__(self)
|
||
|
- self.thread_local = threading.local()
|
||
|
self.client_config = {}
|
||
|
try:
|
||
|
self.settings = LoadSettingsFile(settings_file)
|
||
|
@@ -196,6 +180,15 @@ def __init__(self, settings_file="settings.yaml", http_timeout=None):
|
||
|
# Only one (`file`) backend is supported now
|
||
|
self._default_storage = self._storages["file"]
|
||
|
|
||
|
+ self._service = None
|
||
|
+
|
||
|
+ # Lazy loading, read-only properties
|
||
|
+ @property
|
||
|
+ def service(self):
|
||
|
+ if not self._service:
|
||
|
+ self._service = build("drive", "v2", cache_discovery=False)
|
||
|
+ return self._service
|
||
|
+
|
||
|
@property
|
||
|
def access_token_expired(self):
|
||
|
"""Checks if access token doesn't exist or is expired.
|
||
|
@@ -597,12 +590,8 @@ def Refresh(self):
|
||
|
"No refresh_token found."
|
||
|
"Please set access_type of OAuth to offline."
|
||
|
)
|
||
|
- if self.http is None:
|
||
|
- self.http = self._build_http()
|
||
|
- try:
|
||
|
- self.credentials.refresh(self.http)
|
||
|
- except AccessTokenRefreshError as error:
|
||
|
- raise RefreshError("Access token refresh failed: %s" % error)
|
||
|
+
|
||
|
+
|
||
|
|
||
|
def GetAuthUrl(self):
|
||
|
"""Creates authentication url where user visits to grant access.
|
||
|
@@ -663,19 +652,10 @@ def Authorize(self):
|
||
|
"No valid credentials provided to authorize"
|
||
|
)
|
||
|
|
||
|
- if self.http is None:
|
||
|
- self.http = self._build_http()
|
||
|
- self.http = self.credentials.authorize(self.http)
|
||
|
- self.service = build(
|
||
|
- "drive", "v2", http=self.http, cache_discovery=False
|
||
|
- )
|
||
|
-
|
||
|
def Get_Http_Object(self):
|
||
|
"""Create and authorize an httplib2.Http object. Necessary for
|
||
|
thread-safety.
|
||
|
:return: The http object to be used in each call.
|
||
|
:rtype: httplib2.Http
|
||
|
"""
|
||
|
- http = self._build_http()
|
||
|
- http = self.credentials.authorize(http)
|
||
|
- return http
|
||
|
+ return AuthorizedHttp(self.credentials, http=self._build_http())
|
||
|
diff --git a/pydrive2/drive.py b/pydrive2/drive.py
|
||
|
index 8189ab1..b70462f 100644
|
||
|
--- a/pydrive2/drive.py
|
||
|
+++ b/pydrive2/drive.py
|
||
|
@@ -46,4 +46,8 @@ def GetAbout(self):
|
||
|
|
||
|
:returns: A dictionary of Google Drive information like user, usage, quota etc.
|
||
|
"""
|
||
|
- return self.auth.service.about().get().execute(http=self.http)
|
||
|
+ return (
|
||
|
+ self.auth.service.about()
|
||
|
+ .get()
|
||
|
+ .execute(http=self.auth.Get_Http_Object())
|
||
|
+ )
|
||
|
diff --git a/pydrive2/files.py b/pydrive2/files.py
|
||
|
index 4716c7d..abf4724 100644
|
||
|
--- a/pydrive2/files.py
|
||
|
+++ b/pydrive2/files.py
|
||
|
@@ -82,7 +82,7 @@ def _GetList(self):
|
||
|
self.metadata = (
|
||
|
self.auth.service.files()
|
||
|
.list(**dict(self))
|
||
|
- .execute(http=self.http)
|
||
|
+ .execute(http=self.auth.Get_Http_Object())
|
||
|
)
|
||
|
except errors.HttpError as error:
|
||
|
raise ApiRequestError(error)
|
||
|
@@ -440,7 +440,7 @@ def FetchMetadata(self, fields=None, fetch_all=False):
|
||
|
# Teamdrive support
|
||
|
supportsAllDrives=True,
|
||
|
)
|
||
|
- .execute(http=self.http)
|
||
|
+ .execute(http=self.auth.Get_Http_Object())
|
||
|
)
|
||
|
except errors.HttpError as error:
|
||
|
raise ApiRequestError(error)
|
||
|
@@ -543,7 +543,7 @@ def InsertPermission(self, new_permission, param=None):
|
||
|
permission = (
|
||
|
self.auth.service.permissions()
|
||
|
.insert(**param)
|
||
|
- .execute(http=self.http)
|
||
|
+ .execute(http=self.auth.Get_Http_Object())
|
||
|
)
|
||
|
except errors.HttpError as error:
|
||
|
raise ApiRequestError(error)
|
||
|
@@ -574,7 +574,7 @@ def GetPermissions(self):
|
||
|
# Teamdrive support
|
||
|
supportsAllDrives=True,
|
||
|
)
|
||
|
- .execute(http=self.http)
|
||
|
+ .execute(http=self.auth.Get_Http_Object())
|
||
|
).get("items")
|
||
|
|
||
|
if permissions:
|
||
|
@@ -594,13 +594,13 @@ def DeletePermission(self, permission_id):
|
||
|
return self._DeletePermission(permission_id)
|
||
|
|
||
|
def _WrapRequest(self, request):
|
||
|
- """Replaces request.http with self.http.
|
||
|
+ """Replaces request.http with self.auth.Get_Http_Object().
|
||
|
|
||
|
Ensures thread safety. Similar to other places where we call
|
||
|
`.execute(http=self.http)` to pass a client from the thread local storage.
|
||
|
"""
|
||
|
- if self.http:
|
||
|
- request.http = self.http
|
||
|
+ if self.auth:
|
||
|
+ request.http = self.auth.Get_Http_Object()
|
||
|
return request
|
||
|
|
||
|
@LoadAuth
|
||
|
@@ -624,7 +624,7 @@ def _FilesInsert(self, param=None):
|
||
|
metadata = (
|
||
|
self.auth.service.files()
|
||
|
.insert(**param)
|
||
|
- .execute(http=self.http)
|
||
|
+ .execute(http=self.auth.Get_Http_Object())
|
||
|
)
|
||
|
except errors.HttpError as error:
|
||
|
raise ApiRequestError(error)
|
||
|
@@ -648,7 +648,9 @@ def _FilesUnTrash(self, param=None):
|
||
|
param["supportsAllDrives"] = True
|
||
|
|
||
|
try:
|
||
|
- self.auth.service.files().untrash(**param).execute(http=self.http)
|
||
|
+ self.auth.service.files().untrash(**param).execute(
|
||
|
+ http=self.auth.Get_Http_Object()
|
||
|
+ )
|
||
|
except errors.HttpError as error:
|
||
|
raise ApiRequestError(error)
|
||
|
else:
|
||
|
@@ -672,7 +674,9 @@ def _FilesTrash(self, param=None):
|
||
|
param["supportsAllDrives"] = True
|
||
|
|
||
|
try:
|
||
|
- self.auth.service.files().trash(**param).execute(http=self.http)
|
||
|
+ self.auth.service.files().trash(**param).execute(
|
||
|
+ http=self.auth.Get_Http_Object()
|
||
|
+ )
|
||
|
except errors.HttpError as error:
|
||
|
raise ApiRequestError(error)
|
||
|
else:
|
||
|
@@ -697,7 +701,9 @@ def _FilesDelete(self, param=None):
|
||
|
param["supportsAllDrives"] = True
|
||
|
|
||
|
try:
|
||
|
- self.auth.service.files().delete(**param).execute(http=self.http)
|
||
|
+ self.auth.service.files().delete(**param).execute(
|
||
|
+ http=self.auth.Get_Http_Object()
|
||
|
+ )
|
||
|
except errors.HttpError as error:
|
||
|
raise ApiRequestError(error)
|
||
|
else:
|
||
|
@@ -726,7 +732,7 @@ def _FilesUpdate(self, param=None):
|
||
|
metadata = (
|
||
|
self.auth.service.files()
|
||
|
.update(**param)
|
||
|
- .execute(http=self.http)
|
||
|
+ .execute(http=self.auth.Get_Http_Object())
|
||
|
)
|
||
|
except errors.HttpError as error:
|
||
|
raise ApiRequestError(error)
|
||
|
@@ -756,7 +762,7 @@ def _FilesPatch(self, param=None):
|
||
|
metadata = (
|
||
|
self.auth.service.files()
|
||
|
.patch(**param)
|
||
|
- .execute(http=self.http)
|
||
|
+ .execute(http=self.auth.Get_Http_Object())
|
||
|
)
|
||
|
except errors.HttpError as error:
|
||
|
raise ApiRequestError(error)
|
||
|
@@ -785,7 +791,8 @@ def _DownloadFromUrl(self, url):
|
||
|
:returns: str -- content of downloaded file in string.
|
||
|
:raises: ApiRequestError
|
||
|
"""
|
||
|
- resp, content = self.http.request(url)
|
||
|
+ http = self.auth.Get_Http_Object()
|
||
|
+ resp, content = http.request(url)
|
||
|
if resp.status != 200:
|
||
|
raise ApiRequestError(errors.HttpError(resp, content, uri=url))
|
||
|
return content
|
||
|
|
||
|
From 799b00562c376f0b3a4b4cf6d518bd7d25df13da Mon Sep 17 00:00:00 2001
|
||
|
From: junpeng-jp <junpeng.ong@gmail.com>
|
||
|
Date: Sun, 29 May 2022 00:48:52 +0800
|
||
|
Subject: [PATCH 3/9] Migration to google-auth library (#89)
|
||
|
|
||
|
---
|
||
|
pydrive2/auth.py | 489 +++++++++++++++++----------------------
|
||
|
pydrive2/auth_helpers.py | 58 +++++
|
||
|
pydrive2/drive.py | 2 -
|
||
|
pydrive2/files.py | 14 --
|
||
|
pydrive2/settings.py | 5 +
|
||
|
5 files changed, 275 insertions(+), 293 deletions(-)
|
||
|
create mode 100644 pydrive2/auth_helpers.py
|
||
|
|
||
|
diff --git a/pydrive2/auth.py b/pydrive2/auth.py
|
||
|
index 08625e8..d70841e 100644
|
||
|
--- a/pydrive2/auth.py
|
||
|
+++ b/pydrive2/auth.py
|
||
|
@@ -1,9 +1,11 @@
|
||
|
-import webbrowser
|
||
|
import httplib2
|
||
|
+import json
|
||
|
import oauth2client.clientsecrets as clientsecrets
|
||
|
-import threading
|
||
|
+import google.oauth2.credentials
|
||
|
+import google.oauth2.service_account
|
||
|
|
||
|
from googleapiclient.discovery import build
|
||
|
+
|
||
|
from functools import wraps
|
||
|
from oauth2client.service_account import ServiceAccountCredentials
|
||
|
from oauth2client.client import FlowExchangeError
|
||
|
@@ -21,7 +23,30 @@
|
||
|
from .settings import SettingsError
|
||
|
from .settings import InvalidConfigError
|
||
|
|
||
|
+from .auth_helpers import verify_client_config
|
||
|
+from oauthlib.oauth2.rfc6749.errors import OAuth2Error, MissingCodeError
|
||
|
+from google_auth_oauthlib.flow import InstalledAppFlow
|
||
|
from google_auth_httplib2 import AuthorizedHttp
|
||
|
+from warnings import warn
|
||
|
+
|
||
|
+
|
||
|
+DEFAULT_SETTINGS = {
|
||
|
+ "client_config_backend": "file",
|
||
|
+ "client_config_file": "client_secrets.json",
|
||
|
+ "save_credentials": False,
|
||
|
+ "oauth_scope": ["https://www.googleapis.com/auth/drive"],
|
||
|
+}
|
||
|
+
|
||
|
+_CLIENT_AUTH_PROMPT_MESSAGE = "Please visit this URL:\n{url}\n"
|
||
|
+
|
||
|
+
|
||
|
+DEFAULT_SETTINGS = {
|
||
|
+ "client_config_backend": "file",
|
||
|
+ "client_config_file": "client_secrets.json",
|
||
|
+ "save_credentials": False,
|
||
|
+ "oauth_scope": ["https://www.googleapis.com/auth/drive"],
|
||
|
+}
|
||
|
+
|
||
|
|
||
|
class AuthError(Exception):
|
||
|
"""Base error for authentication/authorization errors."""
|
||
|
@@ -43,87 +68,6 @@ class RefreshError(AuthError):
|
||
|
"""Access token refresh error."""
|
||
|
|
||
|
|
||
|
-def LoadAuth(decoratee):
|
||
|
- """Decorator to check if the auth is valid and loads auth if not."""
|
||
|
-
|
||
|
- @wraps(decoratee)
|
||
|
- def _decorated(self, *args, **kwargs):
|
||
|
- # Initialize auth if needed.
|
||
|
- if self.auth is None:
|
||
|
- self.auth = GoogleAuth()
|
||
|
- # Re-create access token if it expired.
|
||
|
- if self.auth.access_token_expired:
|
||
|
- if getattr(self.auth, "auth_method", False) == "service":
|
||
|
- self.auth.ServiceAuth()
|
||
|
- else:
|
||
|
- self.auth.LocalWebserverAuth()
|
||
|
-
|
||
|
- # Initialise service if not built yet.
|
||
|
- if self.auth.service is None:
|
||
|
- self.auth.Authorize()
|
||
|
-
|
||
|
-
|
||
|
-
|
||
|
- return decoratee(self, *args, **kwargs)
|
||
|
-
|
||
|
- return _decorated
|
||
|
-
|
||
|
-
|
||
|
-def CheckServiceAuth(decoratee):
|
||
|
- """Decorator to authorize service account."""
|
||
|
-
|
||
|
- @wraps(decoratee)
|
||
|
- def _decorated(self, *args, **kwargs):
|
||
|
- self.auth_method = "service"
|
||
|
- dirty = False
|
||
|
- save_credentials = self.settings.get("save_credentials")
|
||
|
- if self.credentials is None and save_credentials:
|
||
|
- self.LoadCredentials()
|
||
|
- if self.credentials is None:
|
||
|
- decoratee(self, *args, **kwargs)
|
||
|
- self.Authorize()
|
||
|
- dirty = True
|
||
|
- elif self.access_token_expired:
|
||
|
- self.Refresh()
|
||
|
- dirty = True
|
||
|
- self.credentials.set_store(self._default_storage)
|
||
|
- if dirty and save_credentials:
|
||
|
- self.SaveCredentials()
|
||
|
-
|
||
|
- return _decorated
|
||
|
-
|
||
|
-
|
||
|
-def CheckAuth(decoratee):
|
||
|
- """Decorator to check if it requires OAuth2 flow request."""
|
||
|
-
|
||
|
- @wraps(decoratee)
|
||
|
- def _decorated(self, *args, **kwargs):
|
||
|
- dirty = False
|
||
|
- code = None
|
||
|
- save_credentials = self.settings.get("save_credentials")
|
||
|
- if self.credentials is None and save_credentials:
|
||
|
- self.LoadCredentials()
|
||
|
- if self.flow is None:
|
||
|
- self.GetFlow()
|
||
|
- if self.credentials is None:
|
||
|
- code = decoratee(self, *args, **kwargs)
|
||
|
- dirty = True
|
||
|
- else:
|
||
|
- if self.access_token_expired:
|
||
|
- if self.credentials.refresh_token is not None:
|
||
|
- self.Refresh()
|
||
|
- else:
|
||
|
- code = decoratee(self, *args, **kwargs)
|
||
|
- dirty = True
|
||
|
- if code is not None:
|
||
|
- self.Auth(code)
|
||
|
- self.credentials.set_store(self._default_storage)
|
||
|
- if dirty and save_credentials:
|
||
|
- self.SaveCredentials()
|
||
|
-
|
||
|
- return _decorated
|
||
|
-
|
||
|
-
|
||
|
class GoogleAuth(ApiAttributeMixin):
|
||
|
"""Wrapper class for oauth2client library in google-api-python-client.
|
||
|
|
||
|
@@ -132,20 +76,6 @@ class GoogleAuth(ApiAttributeMixin):
|
||
|
and authorization.
|
||
|
"""
|
||
|
|
||
|
- DEFAULT_SETTINGS = {
|
||
|
- "client_config_backend": "file",
|
||
|
- "client_config_file": "client_secrets.json",
|
||
|
- "save_credentials": False,
|
||
|
- "oauth_scope": ["https://www.googleapis.com/auth/drive"],
|
||
|
- }
|
||
|
- CLIENT_CONFIGS_LIST = [
|
||
|
- "client_id",
|
||
|
- "client_secret",
|
||
|
- "auth_uri",
|
||
|
- "token_uri",
|
||
|
- "revoke_uri",
|
||
|
- "redirect_uri",
|
||
|
- ]
|
||
|
SERVICE_CONFIGS_LIST = ["client_user_email"]
|
||
|
settings = ApiAttribute("settings")
|
||
|
client_config = ApiAttribute("client_config")
|
||
|
@@ -166,21 +96,23 @@ def __init__(self, settings_file="settings.yaml", http_timeout=None):
|
||
|
"""
|
||
|
self.http_timeout = http_timeout
|
||
|
ApiAttributeMixin.__init__(self)
|
||
|
- self.client_config = {}
|
||
|
+
|
||
|
try:
|
||
|
self.settings = LoadSettingsFile(settings_file)
|
||
|
except SettingsError:
|
||
|
- self.settings = self.DEFAULT_SETTINGS
|
||
|
+ self.settings = DEFAULT_SETTINGS
|
||
|
else:
|
||
|
- if self.settings is None:
|
||
|
- self.settings = self.DEFAULT_SETTINGS
|
||
|
- else:
|
||
|
- ValidateSettings(self.settings)
|
||
|
+ # if no exceptions
|
||
|
+ ValidateSettings(self.settings)
|
||
|
self._storages = self._InitializeStoragesFromSettings()
|
||
|
# Only one (`file`) backend is supported now
|
||
|
self._default_storage = self._storages["file"]
|
||
|
|
||
|
self._service = None
|
||
|
+ self._credentials = None
|
||
|
+ self._client_config = None
|
||
|
+ self._oauth_type = None
|
||
|
+ self._flow = None
|
||
|
|
||
|
# Lazy loading, read-only properties
|
||
|
@property
|
||
|
@@ -189,17 +121,51 @@ def service(self):
|
||
|
self._service = build("drive", "v2", cache_discovery=False)
|
||
|
return self._service
|
||
|
|
||
|
+ @property
|
||
|
+ def client_config(self):
|
||
|
+ if not self._client_config:
|
||
|
+ self.LoadClientConfig()
|
||
|
+ return self._client_config
|
||
|
+
|
||
|
+ @property
|
||
|
+ def oauth_type(self):
|
||
|
+ if not self._oauth_type:
|
||
|
+ self.LoadClientConfig()
|
||
|
+ return self._oauth_type
|
||
|
+
|
||
|
+ @property
|
||
|
+ def flow(self):
|
||
|
+ if not self._flow:
|
||
|
+ self.GetFlow()
|
||
|
+ return self._flow
|
||
|
+
|
||
|
+ @property
|
||
|
+ def credentials(self):
|
||
|
+ if not self._credentials:
|
||
|
+ if self.oauth_type in ("web", "installed"):
|
||
|
+ self.LocalWebserverAuth()
|
||
|
+
|
||
|
+ elif self.oauth_type == "service":
|
||
|
+ self.ServiceAuth()
|
||
|
+ else:
|
||
|
+ raise InvalidConfigError(
|
||
|
+ "Only web, installed, service oauth is supported"
|
||
|
+ )
|
||
|
+
|
||
|
+ return self._credentials
|
||
|
+
|
||
|
+ # Other properties
|
||
|
@property
|
||
|
def access_token_expired(self):
|
||
|
"""Checks if access token doesn't exist or is expired.
|
||
|
|
||
|
:returns: bool -- True if access token doesn't exist or is expired.
|
||
|
"""
|
||
|
- if self.credentials is None:
|
||
|
+ if not self.credentials:
|
||
|
return True
|
||
|
- return self.credentials.access_token_expired
|
||
|
|
||
|
- @CheckAuth
|
||
|
+ return not self.credentials.valid
|
||
|
+
|
||
|
def LocalWebserverAuth(
|
||
|
self, host_name="localhost", port_numbers=None, launch_browser=True
|
||
|
):
|
||
|
@@ -219,27 +185,53 @@ def LocalWebserverAuth(
|
||
|
:raises: AuthenticationRejected, AuthenticationError
|
||
|
"""
|
||
|
if port_numbers is None:
|
||
|
- port_numbers = [
|
||
|
- 8080,
|
||
|
- 8090,
|
||
|
- ] # Mutable objects should not be default
|
||
|
- # values, as each call's changes are global.
|
||
|
- success = False
|
||
|
- port_number = 0
|
||
|
- for port in port_numbers:
|
||
|
- port_number = port
|
||
|
- try:
|
||
|
- httpd = ClientRedirectServer(
|
||
|
- (host_name, port), ClientRedirectHandler
|
||
|
+ port_numbers = [8080, 8090]
|
||
|
+
|
||
|
+ additional_config = {}
|
||
|
+ # offline token request needed to obtain refresh token
|
||
|
+ # make sure that consent is requested
|
||
|
+ if self.settings.get("get_refresh_token"):
|
||
|
+ additional_config["access_type"] = "offline"
|
||
|
+ additional_config["prompt"] = "select_account"
|
||
|
+
|
||
|
+ try:
|
||
|
+ for port in port_numbers:
|
||
|
+ self._credentials = self.flow.run_local_server(
|
||
|
+ host=host_name,
|
||
|
+ port=port,
|
||
|
+ authorization_prompt_message=_CLIENT_AUTH_PROMPT_MESSAGE,
|
||
|
+ open_browser=launch_browser,
|
||
|
+ **additional_config,
|
||
|
)
|
||
|
- except OSError:
|
||
|
- pass
|
||
|
- else:
|
||
|
- success = True
|
||
|
+ # if any port results in successful auth, we're done
|
||
|
break
|
||
|
- if success:
|
||
|
- oauth_callback = f"http://{host_name}:{port_number}/"
|
||
|
- else:
|
||
|
+
|
||
|
+ except OSError as e:
|
||
|
+ # OSError: [WinError 10048] ...
|
||
|
+ # When WSGIServer.allow_reuse_address = False,
|
||
|
+ # raise OSError when binding to a used port
|
||
|
+
|
||
|
+ # If some other error besides the socket address reuse error
|
||
|
+ if e.errno != 10048:
|
||
|
+ raise
|
||
|
+
|
||
|
+ print("Port {} is in use. Trying a different port".format(port))
|
||
|
+
|
||
|
+ except MissingCodeError as e:
|
||
|
+ # if code is not found in the redirect uri's query parameters
|
||
|
+ print(
|
||
|
+ "Failed to find 'code' in the query parameters of the redirect."
|
||
|
+ )
|
||
|
+ print("Please check that your redirect uri is correct.")
|
||
|
+ raise AuthenticationError("No code found in redirect")
|
||
|
+
|
||
|
+ except OAuth2Error as e:
|
||
|
+ # catch oauth 2 errors
|
||
|
+ print("Authentication request was rejected")
|
||
|
+ raise AuthenticationRejected("User rejected authentication")
|
||
|
+
|
||
|
+ # If we have tried all ports and could not find a port
|
||
|
+ if not self._credentials:
|
||
|
print(
|
||
|
"Failed to start a local web server. Please check your firewall"
|
||
|
)
|
||
|
@@ -248,30 +240,7 @@ def LocalWebserverAuth(
|
||
|
)
|
||
|
print("using configured ports. Default ports are 8080 and 8090.")
|
||
|
raise AuthenticationError()
|
||
|
- self.flow.redirect_uri = oauth_callback
|
||
|
- authorize_url = self.GetAuthUrl()
|
||
|
- if launch_browser:
|
||
|
- webbrowser.open(authorize_url, new=1, autoraise=True)
|
||
|
- print("Your browser has been opened to visit:")
|
||
|
- else:
|
||
|
- print("Open your browser to visit:")
|
||
|
- print()
|
||
|
- print(" " + authorize_url)
|
||
|
- print()
|
||
|
- httpd.handle_request()
|
||
|
- if "error" in httpd.query_params:
|
||
|
- print("Authentication request was rejected")
|
||
|
- raise AuthenticationRejected("User rejected authentication")
|
||
|
- if "code" in httpd.query_params:
|
||
|
- return httpd.query_params["code"]
|
||
|
- else:
|
||
|
- print(
|
||
|
- 'Failed to find "code" in the query parameters of the redirect.'
|
||
|
- )
|
||
|
- print("Try command-line authentication")
|
||
|
- raise AuthenticationError("No code found in redirect")
|
||
|
|
||
|
- @CheckAuth
|
||
|
def CommandLineAuth(self):
|
||
|
"""Authenticate and authorize from user by printing authentication url
|
||
|
retrieving authentication code from command-line.
|
||
|
@@ -286,36 +255,31 @@ def CommandLineAuth(self):
|
||
|
print()
|
||
|
return input("Enter verification code: ").strip()
|
||
|
|
||
|
- @CheckServiceAuth
|
||
|
def ServiceAuth(self):
|
||
|
"""Authenticate and authorize using P12 private key, client id
|
||
|
and client email for a Service account.
|
||
|
:raises: AuthError, InvalidConfigError
|
||
|
"""
|
||
|
- if set(self.SERVICE_CONFIGS_LIST) - set(self.client_config):
|
||
|
- self.LoadServiceConfigSettings()
|
||
|
- scopes = scopes_to_string(self.settings["oauth_scope"])
|
||
|
client_service_json = self.client_config.get("client_json_file_path")
|
||
|
+
|
||
|
if client_service_json:
|
||
|
- self.credentials = (
|
||
|
- ServiceAccountCredentials.from_json_keyfile_name(
|
||
|
- filename=client_service_json, scopes=scopes
|
||
|
- )
|
||
|
+ additional_config = {}
|
||
|
+ additional_config["subject"] = self.client_config.get(
|
||
|
+ "client_user_email"
|
||
|
)
|
||
|
- else:
|
||
|
- service_email = self.client_config["client_service_email"]
|
||
|
- file_path = self.client_config["client_pkcs12_file_path"]
|
||
|
- self.credentials = ServiceAccountCredentials.from_p12_keyfile(
|
||
|
- service_account_email=service_email,
|
||
|
- filename=file_path,
|
||
|
- scopes=scopes,
|
||
|
+ additional_config["scopes"] = self.settings["oauth_scope"]
|
||
|
+
|
||
|
+ self._credentials = google.oauth2.service_account.Credentials.from_service_account_file(
|
||
|
+ client_service_json, **additional_config
|
||
|
)
|
||
|
|
||
|
- user_email = self.client_config.get("client_user_email")
|
||
|
- if user_email:
|
||
|
- self.credentials = self.credentials.create_delegated(
|
||
|
- sub=user_email
|
||
|
+ elif self.client_config.get("use_default"):
|
||
|
+ # if no service credential file in yaml settings
|
||
|
+ # default to checking env var `GOOGLE_APPLICATION_CREDENTIALS`
|
||
|
+ credentials, _ = google.auth.default(
|
||
|
+ scopes=self.settings["oauth_scope"]
|
||
|
)
|
||
|
+ self._credentials = credentials
|
||
|
|
||
|
def _InitializeStoragesFromSettings(self):
|
||
|
result = {"file": None}
|
||
|
@@ -463,144 +427,102 @@ def LoadClientConfigFile(self, client_config_file=None):
|
||
|
"""
|
||
|
if client_config_file is None:
|
||
|
client_config_file = self.settings["client_config_file"]
|
||
|
- try:
|
||
|
- client_type, client_info = clientsecrets.loadfile(
|
||
|
- client_config_file
|
||
|
- )
|
||
|
- except clientsecrets.InvalidClientSecretsError as error:
|
||
|
- raise InvalidConfigError("Invalid client secrets file %s" % error)
|
||
|
- if client_type not in (
|
||
|
- clientsecrets.TYPE_WEB,
|
||
|
- clientsecrets.TYPE_INSTALLED,
|
||
|
- ):
|
||
|
- raise InvalidConfigError(
|
||
|
- "Unknown client_type of client config file"
|
||
|
- )
|
||
|
|
||
|
- # General settings.
|
||
|
- try:
|
||
|
- config_index = [
|
||
|
- "client_id",
|
||
|
- "client_secret",
|
||
|
- "auth_uri",
|
||
|
- "token_uri",
|
||
|
- ]
|
||
|
- for config in config_index:
|
||
|
- self.client_config[config] = client_info[config]
|
||
|
-
|
||
|
- self.client_config["revoke_uri"] = client_info.get("revoke_uri")
|
||
|
- self.client_config["redirect_uri"] = client_info["redirect_uris"][
|
||
|
- 0
|
||
|
- ]
|
||
|
- except KeyError:
|
||
|
- raise InvalidConfigError("Insufficient client config in file")
|
||
|
-
|
||
|
- # Service auth related fields.
|
||
|
- service_auth_config = ["client_email"]
|
||
|
+ with open(client_config_file, "r") as json_file:
|
||
|
+ client_config = json.load(json_file)
|
||
|
+
|
||
|
try:
|
||
|
- for config in service_auth_config:
|
||
|
- self.client_config[config] = client_info[config]
|
||
|
- except KeyError:
|
||
|
- pass # The service auth fields are not present, handling code can go here.
|
||
|
+ # check the format of the loaded client config
|
||
|
+ client_type, checked_config = verify_client_config(client_config)
|
||
|
+ except ValueError as e:
|
||
|
+ raise InvalidConfigError("Invalid client secrets file: %s" % e)
|
||
|
+
|
||
|
+ self._client_config = checked_config
|
||
|
+ self._oauth_type = client_type
|
||
|
|
||
|
def LoadServiceConfigSettings(self):
|
||
|
"""Loads client configuration from settings file.
|
||
|
:raises: InvalidConfigError
|
||
|
"""
|
||
|
- for file_format in ["json", "pkcs12"]:
|
||
|
- config = f"client_{file_format}_file_path"
|
||
|
- value = self.settings["service_config"].get(config)
|
||
|
- if value:
|
||
|
- self.client_config[config] = value
|
||
|
- break
|
||
|
- else:
|
||
|
- raise InvalidConfigError(
|
||
|
- "Either json or pkcs12 file path required "
|
||
|
- "for service authentication"
|
||
|
+ service_config = self.settings["service_config"]
|
||
|
+
|
||
|
+ # see https://github.com/googleapis/google-auth-library-python/issues/288
|
||
|
+ if "client_pkcs12_file_path" in service_config:
|
||
|
+ raise DeprecationWarning(
|
||
|
+ "PKCS#12 files are no longer supported in the new google.auth library. "
|
||
|
+ "Please download a new json service credential file from google cloud console. "
|
||
|
+ "For more info, visit https://github.com/googleapis/google-auth-library-python/issues/288"
|
||
|
)
|
||
|
|
||
|
- if file_format == "pkcs12":
|
||
|
- self.SERVICE_CONFIGS_LIST.append("client_service_email")
|
||
|
-
|
||
|
- for config in self.SERVICE_CONFIGS_LIST:
|
||
|
- try:
|
||
|
- self.client_config[config] = self.settings["service_config"][
|
||
|
- config
|
||
|
- ]
|
||
|
- except KeyError:
|
||
|
- err = "Insufficient service config in settings"
|
||
|
- err += f"\n\nMissing: {config} key."
|
||
|
- raise InvalidConfigError(err)
|
||
|
+ self._client_config = service_config
|
||
|
+ self._oauth_type = "service"
|
||
|
|
||
|
def LoadClientConfigSettings(self):
|
||
|
"""Loads client configuration from settings file.
|
||
|
|
||
|
:raises: InvalidConfigError
|
||
|
"""
|
||
|
- for config in self.CLIENT_CONFIGS_LIST:
|
||
|
- try:
|
||
|
- self.client_config[config] = self.settings["client_config"][
|
||
|
- config
|
||
|
- ]
|
||
|
- except KeyError:
|
||
|
- raise InvalidConfigError(
|
||
|
- "Insufficient client config in settings"
|
||
|
- )
|
||
|
+
|
||
|
+ try:
|
||
|
+ client_config = self.settings["client_config"]
|
||
|
+ except KeyError as e:
|
||
|
+ raise InvalidConfigError(
|
||
|
+ "Settings does not contain 'client_config'"
|
||
|
+ )
|
||
|
+
|
||
|
+ try:
|
||
|
+ _, checked_config = verify_client_config(
|
||
|
+ client_config, with_oauth_type=False
|
||
|
+ )
|
||
|
+ except ValueError as e:
|
||
|
+ raise InvalidConfigError("Invalid client secrets file: %s" % e)
|
||
|
+
|
||
|
+ # assumed to be Installed App Flow as the Local Server Auth is appropriate for this type of device
|
||
|
+ self._client_config = checked_config
|
||
|
+ self._oauth_type = "installed"
|
||
|
|
||
|
def GetFlow(self):
|
||
|
"""Gets Flow object from client configuration.
|
||
|
|
||
|
:raises: InvalidConfigError
|
||
|
"""
|
||
|
- if not all(
|
||
|
- config in self.client_config for config in self.CLIENT_CONFIGS_LIST
|
||
|
- ):
|
||
|
- self.LoadClientConfig()
|
||
|
- constructor_kwargs = {
|
||
|
- "redirect_uri": self.client_config["redirect_uri"],
|
||
|
- "auth_uri": self.client_config["auth_uri"],
|
||
|
- "token_uri": self.client_config["token_uri"],
|
||
|
- "access_type": "online",
|
||
|
- }
|
||
|
- if self.client_config["revoke_uri"] is not None:
|
||
|
- constructor_kwargs["revoke_uri"] = self.client_config["revoke_uri"]
|
||
|
- self.flow = OAuth2WebServerFlow(
|
||
|
- self.client_config["client_id"],
|
||
|
- self.client_config["client_secret"],
|
||
|
- scopes_to_string(self.settings["oauth_scope"]),
|
||
|
- **constructor_kwargs,
|
||
|
- )
|
||
|
- if self.settings.get("get_refresh_token"):
|
||
|
- self.flow.params.update(
|
||
|
- {"access_type": "offline", "approval_prompt": "force"}
|
||
|
+
|
||
|
+ additional_config = {}
|
||
|
+ scopes = self.settings.get("oauth_scope")
|
||
|
+
|
||
|
+ if self.oauth_type in ("web", "installed"):
|
||
|
+ self._flow = InstalledAppFlow.from_client_config(
|
||
|
+ {self.oauth_type: self.client_config},
|
||
|
+ scopes,
|
||
|
+ **additional_config,
|
||
|
)
|
||
|
|
||
|
+ if self.oauth_type == "service":
|
||
|
+ # In a service oauth2 flow,
|
||
|
+ # the oauth subject does not have to provide any consent via the client
|
||
|
+ pass
|
||
|
+
|
||
|
def Refresh(self):
|
||
|
"""Refreshes the access_token.
|
||
|
|
||
|
:raises: RefreshError
|
||
|
"""
|
||
|
- if self.credentials is None:
|
||
|
- raise RefreshError("No credential to refresh.")
|
||
|
- if (
|
||
|
- self.credentials.refresh_token is None
|
||
|
- and self.auth_method != "service"
|
||
|
- ):
|
||
|
- raise RefreshError(
|
||
|
- "No refresh_token found."
|
||
|
- "Please set access_type of OAuth to offline."
|
||
|
- )
|
||
|
-
|
||
|
-
|
||
|
+ raise DeprecationWarning(
|
||
|
+ "Refresh is now handled automatically within the new google.auth Credential objects. "
|
||
|
+ "There's no need to manually refresh your credentials now."
|
||
|
+ )
|
||
|
|
||
|
def GetAuthUrl(self):
|
||
|
"""Creates authentication url where user visits to grant access.
|
||
|
|
||
|
:returns: str -- Authentication url.
|
||
|
"""
|
||
|
- if self.flow is None:
|
||
|
- self.GetFlow()
|
||
|
- return self.flow.step1_get_authorize_url()
|
||
|
+ if self.oauth_type == "service":
|
||
|
+ raise AuthenticationError(
|
||
|
+ "Authentication is not required for service client type."
|
||
|
+ )
|
||
|
+
|
||
|
+ return self.flow.authorization_url()
|
||
|
|
||
|
def Auth(self, code):
|
||
|
"""Authenticate, authorize, and build service.
|
||
|
@@ -619,13 +541,26 @@ def Authenticate(self, code):
|
||
|
:type code: str.
|
||
|
:raises: AuthenticationError
|
||
|
"""
|
||
|
- if self.flow is None:
|
||
|
- self.GetFlow()
|
||
|
+ if self.oauth_type == "service":
|
||
|
+ raise AuthenticationError(
|
||
|
+ "Authentication is not required for service client type."
|
||
|
+ )
|
||
|
+
|
||
|
try:
|
||
|
- self.credentials = self.flow.step2_exchange(code)
|
||
|
- except FlowExchangeError as e:
|
||
|
- raise AuthenticationError("OAuth2 code exchange failed: %s" % e)
|
||
|
- print("Authentication successful.")
|
||
|
+ self.flow.fetch_token(code=code)
|
||
|
+
|
||
|
+ except MissingCodeError as e:
|
||
|
+ # if code is not found in the redirect uri's query parameters
|
||
|
+ print(
|
||
|
+ "Failed to find 'code' in the query parameters of the redirect."
|
||
|
+ )
|
||
|
+ print("Please check that your redirect uri is correct.")
|
||
|
+ raise AuthenticationError("No code found in redirect")
|
||
|
+
|
||
|
+ except OAuth2Error as e:
|
||
|
+ # catch oauth 2 errors
|
||
|
+ print("Authentication request was rejected")
|
||
|
+ raise AuthenticationRejected("User rejected authentication")
|
||
|
|
||
|
def _build_http(self):
|
||
|
http = httplib2.Http(timeout=self.http_timeout)
|
||
|
diff --git a/pydrive2/auth_helpers.py b/pydrive2/auth_helpers.py
|
||
|
new file mode 100644
|
||
|
index 0000000..04e5f67
|
||
|
--- /dev/null
|
||
|
+++ b/pydrive2/auth_helpers.py
|
||
|
@@ -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
|
||
|
diff --git a/pydrive2/drive.py b/pydrive2/drive.py
|
||
|
index b70462f..83ad391 100644
|
||
|
--- a/pydrive2/drive.py
|
||
|
+++ b/pydrive2/drive.py
|
||
|
@@ -1,7 +1,6 @@
|
||
|
from .apiattr import ApiAttributeMixin
|
||
|
from .files import GoogleDriveFile
|
||
|
from .files import GoogleDriveFileList
|
||
|
-from .auth import LoadAuth
|
||
|
|
||
|
|
||
|
class GoogleDrive(ApiAttributeMixin):
|
||
|
@@ -40,7 +39,6 @@ def ListFile(self, param=None):
|
||
|
"""
|
||
|
return GoogleDriveFileList(auth=self.auth, param=param)
|
||
|
|
||
|
- @LoadAuth
|
||
|
def GetAbout(self):
|
||
|
"""Return information about the Google Drive of the auth instance.
|
||
|
|
||
|
diff --git a/pydrive2/files.py b/pydrive2/files.py
|
||
|
index abf4724..ab26c72 100644
|
||
|
--- a/pydrive2/files.py
|
||
|
+++ b/pydrive2/files.py
|
||
|
@@ -12,7 +12,6 @@
|
||
|
from .apiattr import ApiAttributeMixin
|
||
|
from .apiattr import ApiResource
|
||
|
from .apiattr import ApiResourceList
|
||
|
-from .auth import LoadAuth
|
||
|
|
||
|
BLOCK_SIZE = 1024
|
||
|
# Usage: MIME_TYPE_TO_BOM['<Google Drive mime type>']['<download mimetype>'].
|
||
|
@@ -68,7 +67,6 @@ def __init__(self, auth=None, param=None):
|
||
|
"""Create an instance of GoogleDriveFileList."""
|
||
|
super().__init__(auth=auth, metadata=param)
|
||
|
|
||
|
- @LoadAuth
|
||
|
def _GetList(self):
|
||
|
"""Overwritten method which actually makes API call to list files.
|
||
|
|
||
|
@@ -287,7 +285,6 @@ def GetContentString(
|
||
|
self.FetchContent(mimetype, remove_bom)
|
||
|
return self.content.getvalue().decode(encoding)
|
||
|
|
||
|
- @LoadAuth
|
||
|
def GetContentFile(
|
||
|
self,
|
||
|
filename,
|
||
|
@@ -355,7 +352,6 @@ def download(fd, request):
|
||
|
if bom:
|
||
|
self._RemovePrefix(fd, bom)
|
||
|
|
||
|
- @LoadAuth
|
||
|
def GetContentIOBuffer(
|
||
|
self,
|
||
|
mimetype=None,
|
||
|
@@ -412,7 +408,6 @@ def GetContentIOBuffer(
|
||
|
chunksize=chunksize,
|
||
|
)
|
||
|
|
||
|
- @LoadAuth
|
||
|
def FetchMetadata(self, fields=None, fetch_all=False):
|
||
|
"""Download file's metadata from id using Files.get().
|
||
|
|
||
|
@@ -552,7 +547,6 @@ def InsertPermission(self, new_permission, param=None):
|
||
|
|
||
|
return permission
|
||
|
|
||
|
- @LoadAuth
|
||
|
def GetPermissions(self):
|
||
|
"""Get file's or shared drive's permissions.
|
||
|
|
||
|
@@ -603,7 +597,6 @@ def _WrapRequest(self, request):
|
||
|
request.http = self.auth.Get_Http_Object()
|
||
|
return request
|
||
|
|
||
|
- @LoadAuth
|
||
|
def _FilesInsert(self, param=None):
|
||
|
"""Upload a new file using Files.insert().
|
||
|
|
||
|
@@ -633,7 +626,6 @@ def _FilesInsert(self, param=None):
|
||
|
self.dirty["content"] = False
|
||
|
self.UpdateMetadata(metadata)
|
||
|
|
||
|
- @LoadAuth
|
||
|
def _FilesUnTrash(self, param=None):
|
||
|
"""Un-delete (Trash) a file using Files.UnTrash().
|
||
|
:param param: additional parameter to file.
|
||
|
@@ -658,7 +650,6 @@ def _FilesUnTrash(self, param=None):
|
||
|
self.metadata["labels"]["trashed"] = False
|
||
|
return True
|
||
|
|
||
|
- @LoadAuth
|
||
|
def _FilesTrash(self, param=None):
|
||
|
"""Soft-delete (Trash) a file using Files.Trash().
|
||
|
|
||
|
@@ -684,7 +675,6 @@ def _FilesTrash(self, param=None):
|
||
|
self.metadata["labels"]["trashed"] = True
|
||
|
return True
|
||
|
|
||
|
- @LoadAuth
|
||
|
def _FilesDelete(self, param=None):
|
||
|
"""Delete a file using Files.Delete()
|
||
|
(WARNING: deleting permanently deletes the file!)
|
||
|
@@ -709,7 +699,6 @@ def _FilesDelete(self, param=None):
|
||
|
else:
|
||
|
return True
|
||
|
|
||
|
- @LoadAuth
|
||
|
@LoadMetadata
|
||
|
def _FilesUpdate(self, param=None):
|
||
|
"""Update metadata and/or content using Files.Update().
|
||
|
@@ -741,7 +730,6 @@ def _FilesUpdate(self, param=None):
|
||
|
self.dirty["content"] = False
|
||
|
self.UpdateMetadata(metadata)
|
||
|
|
||
|
- @LoadAuth
|
||
|
@LoadMetadata
|
||
|
def _FilesPatch(self, param=None):
|
||
|
"""Update metadata using Files.Patch().
|
||
|
@@ -782,7 +770,6 @@ def _BuildMediaBody(self):
|
||
|
self.content, self["mimeType"], resumable=True
|
||
|
)
|
||
|
|
||
|
- @LoadAuth
|
||
|
def _DownloadFromUrl(self, url):
|
||
|
"""Download file from url using provided credential.
|
||
|
|
||
|
@@ -797,7 +784,6 @@ def _DownloadFromUrl(self, url):
|
||
|
raise ApiRequestError(errors.HttpError(resp, content, uri=url))
|
||
|
return content
|
||
|
|
||
|
- @LoadAuth
|
||
|
def _DeletePermission(self, permission_id):
|
||
|
"""Deletes the permission remotely, and from the file object itself.
|
||
|
|
||
|
diff --git a/pydrive2/settings.py b/pydrive2/settings.py
|
||
|
index f5e84ab..e3a46a6 100644
|
||
|
--- a/pydrive2/settings.py
|
||
|
+++ b/pydrive2/settings.py
|
||
|
@@ -75,6 +75,7 @@
|
||
|
"client_service_email": {"type": str, "required": False},
|
||
|
"client_pkcs12_file_path": {"type": str, "required": False},
|
||
|
"client_json_file_path": {"type": str, "required": False},
|
||
|
+ "use_default": {"type": bool, "required": False},
|
||
|
},
|
||
|
},
|
||
|
"oauth_scope": {
|
||
|
@@ -107,6 +108,10 @@ def LoadSettingsFile(filename=SETTINGS_FILE):
|
||
|
data = load(stream, Loader=Loader)
|
||
|
except (YAMLError, OSError) as e:
|
||
|
raise SettingsError(e)
|
||
|
+
|
||
|
+ if not data:
|
||
|
+ raise SettingsError("{} is an empty settings file.".format(filename))
|
||
|
+
|
||
|
return data
|
||
|
|
||
|
|
||
|
|
||
|
From 2a0a76709a33a6098bc9095896d3aaabc7b63e10 Mon Sep 17 00:00:00 2001
|
||
|
From: junpeng-jp <junpeng.ong@gmail.com>
|
||
|
Date: Sun, 29 May 2022 00:57:00 +0800
|
||
|
Subject: [PATCH 4/9] Implement lockfile storage (#89)
|
||
|
|
||
|
---
|
||
|
pydrive2/auth.py | 106 ++++++++++++++++++++++++++-------------
|
||
|
pydrive2/storage.py | 117 ++++++++++++++++++++++++++++++++++++++++++++
|
||
|
2 files changed, 188 insertions(+), 35 deletions(-)
|
||
|
create mode 100644 pydrive2/storage.py
|
||
|
|
||
|
diff --git a/pydrive2/auth.py b/pydrive2/auth.py
|
||
|
index d70841e..32c1c18 100644
|
||
|
--- a/pydrive2/auth.py
|
||
|
+++ b/pydrive2/auth.py
|
||
|
@@ -104,10 +104,8 @@ def __init__(self, settings_file="settings.yaml", http_timeout=None):
|
||
|
else:
|
||
|
# if no exceptions
|
||
|
ValidateSettings(self.settings)
|
||
|
- self._storages = self._InitializeStoragesFromSettings()
|
||
|
- # Only one (`file`) backend is supported now
|
||
|
- self._default_storage = self._storages["file"]
|
||
|
|
||
|
+ self._storage = None
|
||
|
self._service = None
|
||
|
self._credentials = None
|
||
|
self._client_config = None
|
||
|
@@ -139,10 +137,28 @@ def flow(self):
|
||
|
self.GetFlow()
|
||
|
return self._flow
|
||
|
|
||
|
+ @property
|
||
|
+ def storage(self):
|
||
|
+ if not self.settings.get("save_credentials"):
|
||
|
+ return None
|
||
|
+
|
||
|
+ if not self._storage:
|
||
|
+ self._InitializeStoragesFromSettings()
|
||
|
+ return self._storage
|
||
|
+
|
||
|
@property
|
||
|
def credentials(self):
|
||
|
if not self._credentials:
|
||
|
if self.oauth_type in ("web", "installed"):
|
||
|
+ # try to load from backend if available
|
||
|
+ # credentials would auto-refresh if expired
|
||
|
+ if self.storage:
|
||
|
+ try:
|
||
|
+ self.LoadCredentials()
|
||
|
+ return self._credentials
|
||
|
+ except FileNotFoundError:
|
||
|
+ pass
|
||
|
+
|
||
|
self.LocalWebserverAuth()
|
||
|
|
||
|
elif self.oauth_type == "service":
|
||
|
@@ -282,21 +298,14 @@ def ServiceAuth(self):
|
||
|
self._credentials = credentials
|
||
|
|
||
|
def _InitializeStoragesFromSettings(self):
|
||
|
- result = {"file": None}
|
||
|
- backend = self.settings.get("save_credentials_backend")
|
||
|
- save_credentials = self.settings.get("save_credentials")
|
||
|
- if backend == "file":
|
||
|
- credentials_file = self.settings.get("save_credentials_file")
|
||
|
- if credentials_file is None:
|
||
|
+ if self.settings.get("save_credentials"):
|
||
|
+ backend = self.settings.get("save_credentials_backend")
|
||
|
+ if backend != "file":
|
||
|
raise InvalidConfigError(
|
||
|
- "Please specify credentials file to read"
|
||
|
+ "Unknown save_credentials_backend: %s" % backend
|
||
|
)
|
||
|
- result[backend] = Storage(credentials_file)
|
||
|
- elif save_credentials:
|
||
|
- raise InvalidConfigError(
|
||
|
- "Unknown save_credentials_backend: %s" % backend
|
||
|
- )
|
||
|
- return result
|
||
|
+
|
||
|
+ self._storage = FileBackend()
|
||
|
|
||
|
def LoadCredentials(self, backend=None):
|
||
|
"""Loads credentials or create empty credentials if it doesn't exist.
|
||
|
@@ -324,25 +333,55 @@ def LoadCredentialsFile(self, credentials_file=None):
|
||
|
:raises: InvalidConfigError, InvalidCredentialsError
|
||
|
"""
|
||
|
if credentials_file is None:
|
||
|
- self._default_storage = self._storages["file"]
|
||
|
- if self._default_storage is None:
|
||
|
+ credentials_file = self.settings.get("save_credentials_file")
|
||
|
+ if credentials_file is None:
|
||
|
raise InvalidConfigError(
|
||
|
- "Backend `file` is not configured, specify "
|
||
|
- "credentials file to read in the settings "
|
||
|
- "file or pass an explicit value"
|
||
|
+ "Please specify credentials file to read"
|
||
|
)
|
||
|
- else:
|
||
|
- self._default_storage = Storage(credentials_file)
|
||
|
|
||
|
try:
|
||
|
- self.credentials = self._default_storage.get()
|
||
|
+ auth_info = self.storage.read_credentials(credentials_file)
|
||
|
+ except FileNotFoundError:
|
||
|
+ # if credential found was not found, raise the error for handling
|
||
|
+ raise
|
||
|
except OSError:
|
||
|
+ # catch other errors
|
||
|
raise InvalidCredentialsError(
|
||
|
"Credentials file cannot be symbolic link"
|
||
|
)
|
||
|
|
||
|
- if self.credentials:
|
||
|
- self.credentials.set_store(self._default_storage)
|
||
|
+ try:
|
||
|
+ self._credentials = google.oauth2.credentials.Credentials.from_authorized_user_info(
|
||
|
+ auth_info, scopes=auth_info["scopes"]
|
||
|
+ )
|
||
|
+ except ValueError:
|
||
|
+ # if saved credentials lack a refresh token
|
||
|
+ # handled for backwards compatibility
|
||
|
+ warn(
|
||
|
+ "Loading authorized user credentials without a refresh token is "
|
||
|
+ "not officially supported by google auth library. We recommend that "
|
||
|
+ "you only store refreshable credentials moving forward."
|
||
|
+ )
|
||
|
+
|
||
|
+ self._credentials = google.oauth2.credentials.Credentials(
|
||
|
+ token=auth_info.get("token"),
|
||
|
+ token_uri="https://oauth2.googleapis.com/token", # always overrides
|
||
|
+ scopes=auth_info.get("scopes"),
|
||
|
+ client_id=auth_info.get("client_id"),
|
||
|
+ client_secret=auth_info.get("client_secret"),
|
||
|
+ )
|
||
|
+
|
||
|
+ # in-case reauth / consent required
|
||
|
+ # create a flow object so that reauth flow can be triggered
|
||
|
+ additional_config = {}
|
||
|
+ if self.credentials.refresh_token:
|
||
|
+ additional_config["access_type"] = "offline"
|
||
|
+
|
||
|
+ self._flow = InstalledAppFlow.from_client_config(
|
||
|
+ {self.oauth_type: self.client_config},
|
||
|
+ self.credentials.scopes,
|
||
|
+ **additional_config,
|
||
|
+ )
|
||
|
|
||
|
def SaveCredentials(self, backend=None):
|
||
|
"""Saves credentials according to specified backend.
|
||
|
@@ -370,22 +409,19 @@ def SaveCredentialsFile(self, credentials_file=None):
|
||
|
:type credentials_file: str.
|
||
|
:raises: InvalidConfigError, InvalidCredentialsError
|
||
|
"""
|
||
|
- if self.credentials is None:
|
||
|
+ if not self.credentials:
|
||
|
raise InvalidCredentialsError("No credentials to save")
|
||
|
|
||
|
if credentials_file is None:
|
||
|
- storage = self._storages["file"]
|
||
|
- if storage is None:
|
||
|
+ credentials_file = self.settings.get("save_credentials_file")
|
||
|
+ if credentials_file is None:
|
||
|
raise InvalidConfigError(
|
||
|
- "Backend `file` is not configured, specify "
|
||
|
- "credentials file to read in the settings "
|
||
|
- "file or pass an explicit value"
|
||
|
+ "Please specify credentials file to read"
|
||
|
)
|
||
|
- else:
|
||
|
- storage = Storage(credentials_file)
|
||
|
|
||
|
try:
|
||
|
- storage.put(self.credentials)
|
||
|
+ self.storage.store_credentials(self.credentials, credentials_file)
|
||
|
+
|
||
|
except OSError:
|
||
|
raise InvalidCredentialsError(
|
||
|
"Credentials file cannot be symbolic link"
|
||
|
diff --git a/pydrive2/storage.py b/pydrive2/storage.py
|
||
|
new file mode 100644
|
||
|
index 0000000..ad9f772
|
||
|
--- /dev/null
|
||
|
+++ b/pydrive2/storage.py
|
||
|
@@ -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))
|
||
|
+
|
||
|
+ 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)
|
||
|
|
||
|
From 9230969d92714251d9bed2dd4d5cd80a96e7d70d Mon Sep 17 00:00:00 2001
|
||
|
From: junpeng-jp <junpeng.ong@gmail.com>
|
||
|
Date: Sun, 29 May 2022 00:51:00 +0800
|
||
|
Subject: [PATCH 5/9] Cleanup of oauth tests & remove service config read/write
|
||
|
checks
|
||
|
|
||
|
---
|
||
|
pydrive2/test/settings/default.yaml | 2 +-
|
||
|
pydrive2/test/settings/test_oauth_test_07.yaml | 2 +-
|
||
|
pydrive2/test/settings/test_oauth_test_08.yaml | 2 +-
|
||
|
pydrive2/test/settings/test_oauth_test_09.yaml | 2 +-
|
||
|
pydrive2/test/test_oauth.py | 13 +++++++------
|
||
|
5 files changed, 11 insertions(+), 10 deletions(-)
|
||
|
|
||
|
diff --git a/pydrive2/test/settings/default.yaml b/pydrive2/test/settings/default.yaml
|
||
|
index ef6e654..b31b91f 100644
|
||
|
--- a/pydrive2/test/settings/default.yaml
|
||
|
+++ b/pydrive2/test/settings/default.yaml
|
||
|
@@ -1,6 +1,6 @@
|
||
|
client_config_backend: service
|
||
|
service_config:
|
||
|
- client_json_file_path: /tmp/pydrive2/credentials.json
|
||
|
+ client_json_file_path: tmp/pydrive2/credentials.json
|
||
|
|
||
|
save_credentials: True
|
||
|
save_credentials_backend: file
|
||
|
diff --git a/pydrive2/test/settings/test_oauth_test_07.yaml b/pydrive2/test/settings/test_oauth_test_07.yaml
|
||
|
index a806515..673a6ec 100644
|
||
|
--- a/pydrive2/test/settings/test_oauth_test_07.yaml
|
||
|
+++ b/pydrive2/test/settings/test_oauth_test_07.yaml
|
||
|
@@ -1,6 +1,6 @@
|
||
|
client_config_backend: service
|
||
|
service_config:
|
||
|
- client_json_file_path: /tmp/pydrive2/credentials.json
|
||
|
+ client_json_file_path: tmp/pydrive2/credentials.json
|
||
|
|
||
|
save_credentials: True
|
||
|
save_credentials_backend: file
|
||
|
diff --git a/pydrive2/test/settings/test_oauth_test_08.yaml b/pydrive2/test/settings/test_oauth_test_08.yaml
|
||
|
index 1fcbc05..f927e53 100644
|
||
|
--- a/pydrive2/test/settings/test_oauth_test_08.yaml
|
||
|
+++ b/pydrive2/test/settings/test_oauth_test_08.yaml
|
||
|
@@ -1,6 +1,6 @@
|
||
|
client_config_backend: service
|
||
|
service_config:
|
||
|
- client_json_file_path: /tmp/pydrive2/credentials.json
|
||
|
+ client_json_file_path: tmp/pydrive2/credentials.json
|
||
|
|
||
|
save_credentials: False
|
||
|
|
||
|
diff --git a/pydrive2/test/settings/test_oauth_test_09.yaml b/pydrive2/test/settings/test_oauth_test_09.yaml
|
||
|
index 09eca82..b131ca1 100644
|
||
|
--- a/pydrive2/test/settings/test_oauth_test_09.yaml
|
||
|
+++ b/pydrive2/test/settings/test_oauth_test_09.yaml
|
||
|
@@ -1,6 +1,6 @@
|
||
|
client_config_backend: service
|
||
|
service_config:
|
||
|
- client_json_file_path: /tmp/pydrive2/credentials.json
|
||
|
+ client_json_file_path: tmp/pydrive2/credentials.json
|
||
|
|
||
|
save_credentials: True
|
||
|
save_credentials_backend: file
|
||
|
diff --git a/pydrive2/test/test_oauth.py b/pydrive2/test/test_oauth.py
|
||
|
index d876661..b57b2bb 100644
|
||
|
--- a/pydrive2/test/test_oauth.py
|
||
|
+++ b/pydrive2/test/test_oauth.py
|
||
|
@@ -22,6 +22,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")
|
||
|
@@ -35,6 +36,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")
|
||
|
@@ -48,6 +50,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)
|
||
|
|
||
|
@@ -59,6 +62,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")
|
||
|
@@ -70,6 +74,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)
|
||
|
|
||
|
@@ -79,6 +84,7 @@ def test_06_ServiceAuthFromSavedCredentialsP12File():
|
||
|
setup_credentials("credentials/6.dat")
|
||
|
ga = GoogleAuth(settings_file_path("test_oauth_test_06.yaml"))
|
||
|
ga.ServiceAuth()
|
||
|
+ assert ga.credentials
|
||
|
assert not ga.access_token_expired
|
||
|
time.sleep(1)
|
||
|
|
||
|
@@ -91,12 +97,9 @@ def test_07_ServiceAuthFromSavedCredentialsJsonFile():
|
||
|
delete_file(credentials_file)
|
||
|
assert not os.path.exists(credentials_file)
|
||
|
ga.ServiceAuth()
|
||
|
- assert os.path.exists(credentials_file)
|
||
|
- # Secondary auth should be made only using the previously saved
|
||
|
- # login info
|
||
|
ga = GoogleAuth(settings_file_path("test_oauth_test_07.yaml"))
|
||
|
ga.ServiceAuth()
|
||
|
- assert not ga.access_token_expired
|
||
|
+ assert ga.credentials
|
||
|
time.sleep(1)
|
||
|
|
||
|
|
||
|
@@ -120,8 +123,6 @@ def test_09_SaveLoadCredentialsUsesDefaultStorage(mocker):
|
||
|
assert not os.path.exists(credentials_file)
|
||
|
spy = mocker.spy(Storage, "__init__")
|
||
|
ga.ServiceAuth()
|
||
|
- ga.LoadCredentials()
|
||
|
- ga.SaveCredentials()
|
||
|
assert spy.call_count == 0
|
||
|
|
||
|
|
||
|
|
||
|
From 2a688e28b15cb310ea39faac4375029c62a574ef Mon Sep 17 00:00:00 2001
|
||
|
From: junpeng-jp <junpeng.ong@gmail.com>
|
||
|
Date: Sun, 29 May 2022 00:51:32 +0800
|
||
|
Subject: [PATCH 6/9] Add oauth test 10: google.auth.default auth
|
||
|
|
||
|
---
|
||
|
pydrive2/test/settings/test_oauth_test_10.yaml | 5 +++++
|
||
|
pydrive2/test/test_oauth.py | 10 ++++++++++
|
||
|
2 files changed, 15 insertions(+)
|
||
|
create mode 100644 pydrive2/test/settings/test_oauth_test_10.yaml
|
||
|
|
||
|
diff --git a/pydrive2/test/settings/test_oauth_test_10.yaml b/pydrive2/test/settings/test_oauth_test_10.yaml
|
||
|
new file mode 100644
|
||
|
index 0000000..2af85fc
|
||
|
--- /dev/null
|
||
|
+++ b/pydrive2/test/settings/test_oauth_test_10.yaml
|
||
|
@@ -0,0 +1,5 @@
|
||
|
+client_config_backend: service
|
||
|
+service_config:
|
||
|
+ use_default: True
|
||
|
+oauth_scope:
|
||
|
+ - https://www.googleapis.com/auth/drive
|
||
|
diff --git a/pydrive2/test/test_oauth.py b/pydrive2/test/test_oauth.py
|
||
|
index b57b2bb..40963c1 100644
|
||
|
--- a/pydrive2/test/test_oauth.py
|
||
|
+++ b/pydrive2/test/test_oauth.py
|
||
|
@@ -126,6 +126,16 @@ def test_09_SaveLoadCredentialsUsesDefaultStorage(mocker):
|
||
|
assert spy.call_count == 0
|
||
|
|
||
|
|
||
|
+def test_10_ServiceAuthFromEnvironmentDefault():
|
||
|
+ # Test fix for https://github.com/iterative/PyDrive2/issues/163
|
||
|
+ # Make sure that Load and Save credentials by default reuse the
|
||
|
+ # same Storage (since it defined lock which make it TS)
|
||
|
+ ga = GoogleAuth(settings_file_path("test_oauth_test_10.yaml"))
|
||
|
+ ga.ServiceAuth()
|
||
|
+ assert ga.credentials
|
||
|
+ time.sleep(1)
|
||
|
+
|
||
|
+
|
||
|
def CheckCredentialsFile(credentials, no_file=False):
|
||
|
ga = GoogleAuth(settings_file_path("test_oauth_default.yaml"))
|
||
|
ga.LoadCredentialsFile(credentials)
|
||
|
|
||
|
From a3bc162c59a5044df8983e953331a084a0bc9ffa Mon Sep 17 00:00:00 2001
|
||
|
From: junpeng-jp <junpeng.ong@gmail.com>
|
||
|
Date: Sun, 29 May 2022 00:52:54 +0800
|
||
|
Subject: [PATCH 7/9] Deprecate command line auth (#173)
|
||
|
|
||
|
---
|
||
|
pydrive2/auth.py | 18 +++++++++++-------
|
||
|
1 file changed, 11 insertions(+), 7 deletions(-)
|
||
|
|
||
|
diff --git a/pydrive2/auth.py b/pydrive2/auth.py
|
||
|
index 32c1c18..646268c 100644
|
||
|
--- a/pydrive2/auth.py
|
||
|
+++ b/pydrive2/auth.py
|
||
|
@@ -263,13 +263,17 @@ def CommandLineAuth(self):
|
||
|
|
||
|
:returns: str -- code returned from commandline.
|
||
|
"""
|
||
|
- self.flow.redirect_uri = OOB_CALLBACK_URN
|
||
|
- authorize_url = self.GetAuthUrl()
|
||
|
- print("Go to the following link in your browser:")
|
||
|
- print()
|
||
|
- print(" " + authorize_url)
|
||
|
- print()
|
||
|
- return input("Enter verification code: ").strip()
|
||
|
+
|
||
|
+ warn(
|
||
|
+ (
|
||
|
+ "The command line auth has been deprecated. "
|
||
|
+ "The recommended alternative is to use local webserver auth with a loopback address."
|
||
|
+ ),
|
||
|
+ DeprecationWarning,
|
||
|
+ )
|
||
|
+
|
||
|
+ self.LocalWebserverAuth(host_name="127.0.0.1")
|
||
|
+
|
||
|
|
||
|
def ServiceAuth(self):
|
||
|
"""Authenticate and authorize using P12 private key, client id
|
||
|
|
||
|
From c9a0f90759df11f3c544c57f69edfc8ce936816e Mon Sep 17 00:00:00 2001
|
||
|
From: junpeng-jp <junpeng.ong@gmail.com>
|
||
|
Date: Sun, 29 May 2022 00:58:10 +0800
|
||
|
Subject: [PATCH 8/9] Remove dependencies on oauth2client
|
||
|
|
||
|
---
|
||
|
pydrive2/auth.py | 18 ++++--------------
|
||
|
setup.py | 1 -
|
||
|
2 files changed, 4 insertions(+), 15 deletions(-)
|
||
|
|
||
|
diff --git a/pydrive2/auth.py b/pydrive2/auth.py
|
||
|
index 646268c..cf59a29 100644
|
||
|
--- a/pydrive2/auth.py
|
||
|
+++ b/pydrive2/auth.py
|
||
|
@@ -1,21 +1,10 @@
|
||
|
import httplib2
|
||
|
import json
|
||
|
-import oauth2client.clientsecrets as clientsecrets
|
||
|
import google.oauth2.credentials
|
||
|
import google.oauth2.service_account
|
||
|
|
||
|
from googleapiclient.discovery import build
|
||
|
-
|
||
|
-from functools import wraps
|
||
|
-from oauth2client.service_account import ServiceAccountCredentials
|
||
|
-from oauth2client.client import FlowExchangeError
|
||
|
-from oauth2client.client import AccessTokenRefreshError
|
||
|
-from oauth2client.client import OAuth2WebServerFlow
|
||
|
-from oauth2client.client import OOB_CALLBACK_URN
|
||
|
-from oauth2client.file import Storage
|
||
|
-from oauth2client.tools import ClientRedirectHandler
|
||
|
-from oauth2client.tools import ClientRedirectServer
|
||
|
-from oauth2client._helpers import scopes_to_string
|
||
|
+from pydrive2.storage import FileBackend
|
||
|
from .apiattr import ApiAttribute
|
||
|
from .apiattr import ApiAttributeMixin
|
||
|
from .settings import LoadSettingsFile
|
||
|
@@ -257,6 +246,9 @@ def LocalWebserverAuth(
|
||
|
print("using configured ports. Default ports are 8080 and 8090.")
|
||
|
raise AuthenticationError()
|
||
|
|
||
|
+ if self.storage:
|
||
|
+ self.SaveCredentials()
|
||
|
+
|
||
|
def CommandLineAuth(self):
|
||
|
"""Authenticate and authorize from user by printing authentication url
|
||
|
retrieving authentication code from command-line.
|
||
|
@@ -274,7 +266,6 @@ def CommandLineAuth(self):
|
||
|
|
||
|
self.LocalWebserverAuth(host_name="127.0.0.1")
|
||
|
|
||
|
-
|
||
|
def ServiceAuth(self):
|
||
|
"""Authenticate and authorize using P12 private key, client id
|
||
|
and client email for a Service account.
|
||
|
@@ -526,7 +517,6 @@ def GetFlow(self):
|
||
|
|
||
|
:raises: InvalidConfigError
|
||
|
"""
|
||
|
-
|
||
|
additional_config = {}
|
||
|
scopes = self.settings.get("oauth_scope")
|
||
|
|
||
|
diff --git a/setup.py b/setup.py
|
||
|
index 2b2d952..74e6d24 100644
|
||
|
--- a/setup.py
|
||
|
+++ b/setup.py
|
||
|
@@ -32,7 +32,6 @@
|
||
|
install_requires=[
|
||
|
"google-api-python-client >= 1.12.5",
|
||
|
"six >= 1.13.0",
|
||
|
- "oauth2client >= 4.0.0",
|
||
|
"google-auth >= 2.6.6",
|
||
|
"google-auth-httplib2 >= 0.1.0",
|
||
|
"google-auth-oauthlib >= 0.5.1",
|
||
|
|
||
|
From 012f9edfc8f7da06ff74542fbf8938b772e2483f Mon Sep 17 00:00:00 2001
|
||
|
From: junpeng-jp <junpeng.ong@gmail.com>
|
||
|
Date: Wed, 1 Jun 2022 23:09:53 +0800
|
||
|
Subject: [PATCH 9/9] Changed mocker.spy to track calls to FileBackend's
|
||
|
__init__
|
||
|
|
||
|
---
|
||
|
pydrive2/test/test_oauth.py | 4 ++--
|
||
|
1 file changed, 2 insertions(+), 2 deletions(-)
|
||
|
|
||
|
diff --git a/pydrive2/test/test_oauth.py b/pydrive2/test/test_oauth.py
|
||
|
index 40963c1..7e6df5d 100644
|
||
|
--- a/pydrive2/test/test_oauth.py
|
||
|
+++ b/pydrive2/test/test_oauth.py
|
||
|
@@ -8,7 +8,7 @@
|
||
|
delete_file,
|
||
|
settings_file_path,
|
||
|
)
|
||
|
-from oauth2client.file import Storage
|
||
|
+from ..storage import FileBackend
|
||
|
|
||
|
|
||
|
def setup_module(module):
|
||
|
@@ -121,7 +121,7 @@ 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()
|
||
|
assert spy.call_count == 0
|
||
|
|