Hello community,

here is the log from the commit of package python-oauth2client for 
openSUSE:Factory checked in at 2017-02-07 12:09:32
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-oauth2client (Old)
 and      /work/SRC/openSUSE:Factory/.python-oauth2client.new (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-oauth2client"

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-oauth2client/python-oauth2client.changes  
2016-10-18 10:41:42.000000000 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-oauth2client.new/python-oauth2client.changes 
    2017-02-07 12:09:33.349938019 +0100
@@ -1,0 +2,11 @@
+Mon Jan 30 20:33:27 UTC 2017 - rjsch...@suse.com
+
+- Add o2c_hide-deprecation-warning.patch
+- Add o2c_reauth.patch (bsc#1002895)
+
+-------------------------------------------------------------------
+Wed Oct 12 21:29:55 UTC 2016 - rjsch...@suse.com
+
+- Add missing dependency on python-fasteners
+
+-------------------------------------------------------------------

New:
----
  o2c_hide-deprecation-warning.patch
  o2c_reauth.patch

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-oauth2client.spec ++++++
--- /var/tmp/diff_new_pack.MfQ3SU/_old  2017-02-07 12:09:33.677891614 +0100
+++ /var/tmp/diff_new_pack.MfQ3SU/_new  2017-02-07 12:09:33.677891614 +0100
@@ -1,7 +1,7 @@
 #
 # spec file for package python-oauth2client
 #
-# Copyright (c) 2016 SUSE LINUX GmbH, Nuernberg, Germany.
+# Copyright (c) 2017 SUSE LINUX GmbH, Nuernberg, Germany.
 #
 # All modifications and additions to the file contributed by third parties
 # remain the property of their copyright owners, unless otherwise agreed
@@ -25,7 +25,10 @@
 Url:            https://github.com/google/oauth2client
 Source0:        oauth2client-%{version}.tar.gz
 Patch1:         oauth2client-init-django-settings.patch
+Patch2:         o2c_reauth.patch
+Patch3:         o2c_hide-deprecation-warning.patch
 Requires:       python
+Requires:       python-fasteners
 Requires:       python-httplib2        >= 0.9.1
 Requires:       python-keyring
 Requires:       python-pyasn1          >= 0.1.7
@@ -49,6 +52,7 @@
 BuildRequires:  python-rsa             >= 3.1.4
 BuildRequires:  python-setuptools
 BuildRequires:  python-six             >= 1.6.1
+BuildRequires:  python-tox
 BuildRequires:  python-unittest2
 Conflicts:      google-api-python-client  < 1.3.0
 # Don't ask, we obviously have problems working together
@@ -130,6 +134,8 @@
 rm -rf oauth2client/contrib/*django*
 %endif
 %patch1
+%patch2
+%patch3
 
 %build
 python setup.py build
@@ -150,6 +156,7 @@
 # hope for the best
 #%check
 #nosetests
+#tox
 
 %files
 %defattr(-,root,root,-)

++++++ o2c_hide-deprecation-warning.patch ++++++
--- oauth2client/contrib/multistore_file.py.orig
+++ oauth2client/contrib/multistore_file.py
@@ -58,10 +58,10 @@ __author__ = 'jb...@google.com (Joe Beda
 
 logger = logging.getLogger(__name__)
 
-logger.warning(
-    'The oauth2client.contrib.multistore_file module has been deprecated and '
-    'will be removed in the next release of oauth2client. Please migrate to '
-    'multiprocess_file_storage.')
+#logger.warning(
+#    'The oauth2client.contrib.multistore_file module has been deprecated and '
+#    'will be removed in the next release of oauth2client. Please migrate to '
+#    'multiprocess_file_storage.')
 
 # A dict from 'filename'->_MultiStore instances
 _multistores = {}
++++++ o2c_reauth.patch ++++++
--- /dev/null
+++ oauth2client/contrib/reauth.py
@@ -0,0 +1,244 @@
+# Copyright 2014 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.
+
+"""A module that provides functions for handling rapt authentication."""
+
+import base64
+import getpass
+import json
+import sys
+import urllib
+
+from pyu2f import errors as u2ferrors
+from pyu2f import model
+from pyu2f.convenience import authenticator
+
+from oauth2client.contrib import reauth_errors
+
+
+REAUTH_API = 'https://reauth.googleapis.com/v2/sessions'
+REAUTH_SCOPE = 'https://www.googleapis.com/auth/accounts.reauth'
+REAUTH_ORIGIN = 'https://accounts.google.com'
+
+
+def HandleErrors(msg):
+  if 'error' in msg:
+    raise reauth_errors.ReauthAPIError(msg['error']['message'])
+  return msg
+
+
+class ReauthChallenge(object):
+  """Base class for reauth challenges."""
+
+  def __init__(self, http_request, access_token):
+    self.http_request = http_request
+    self.access_token = access_token
+
+  def GetName(self):
+    """Returns the name of the challenge. Must match what the server 
expects."""
+    raise NotImplementedError()
+
+  def IsLocallyEligible(self):
+    """Returns true if a challenge is supported locally on this machine."""
+    raise NotImplementedError()
+
+  def Execute(self, metadata, session_id):
+    """Execute internal challenge logic and pass credentials to reauth API."""
+    client_input = self.InternalObtainCredentials(metadata)
+
+    if not client_input:
+      return None
+
+    body = {
+        'sessionId': session_id,
+        'challengeId': metadata['challengeId'],
+        'action': 'RESPOND',
+        'proposalResponse': client_input,
+    }
+    _, content = self.http_request(
+        '{0}/{1}:continue'.format(REAUTH_API, session_id),
+        method='POST',
+        body=json.dumps(body),
+        headers={'Authorization': 'Bearer ' + self.access_token}
+    )
+    response = json.loads(content)
+    HandleErrors(response)
+    return response
+
+  def InternalObtainCredentials(self, metadata):
+    """Performs logic required to obtain credentials and returns it."""
+    raise NotImplementedError()
+
+
+class PasswordChallenge(ReauthChallenge):
+  """Challenge that asks for user's password."""
+
+  def GetName(self):
+    return 'PASSWORD'
+
+  def IsLocallyEligible(self):
+    return True
+
+  def InternalObtainCredentials(self, unused_metadata):
+    passwd = getpass.getpass('Please enter your password:')
+    if not passwd:
+      passwd = ' '  # avoid the server crashing in case of no password :D
+    return {'credential': passwd}
+
+
+class SecurityKeyChallenge(ReauthChallenge):
+  """Challenge that asks for user's security key touch."""
+
+  def GetName(self):
+    return 'SECURITY_KEY'
+
+  def IsLocallyEligible(self):
+    return True
+
+  def InternalObtainCredentials(self, metadata):
+    sk = metadata['securityKey']
+    challenges = sk['challenges']
+    app_id = sk['applicationId']
+
+    challenge_data = []
+    for c in challenges:
+      kh = c['keyHandle'].encode('ascii')
+      key = model.RegisteredKey(bytearray(base64.urlsafe_b64decode(kh)))
+      challenge = c['challenge'].encode('ascii')
+      challenge = base64.urlsafe_b64decode(challenge)
+      challenge_data.append({'key': key, 'challenge': challenge})
+
+    try:
+      api = authenticator.CreateCompositeAuthenticator(REAUTH_ORIGIN)
+      response = api.Authenticate(app_id, challenge_data,
+                                  print_callback=sys.stderr.write)
+      return {'securityKey': response}
+    except u2ferrors.U2FError as e:
+      if e.code == u2ferrors.U2FError.DEVICE_INELIGIBLE:
+        sys.stderr.write('Ineligible security key.\n')
+      elif e.code == u2ferrors.U2FError.TIMEOUT:
+        sys.stderr.write('Timed out while waiting for security key touch.\n')
+      else:
+        raise e
+    except u2ferrors.NoDeviceFoundError:
+      sys.stderr.write('No security key found.\n')
+    return None
+
+
+class ReauthManager(object):
+  """Reauth manager class that handles reauth challenges."""
+
+  def __init__(self, http_request, access_token):
+    self.http_request = http_request
+    self.access_token = access_token
+    self.challenges = self.InternalBuildChallenges()
+
+  def InternalBuildChallenges(self):
+    out = {}
+    for c in [SecurityKeyChallenge(self.http_request, self.access_token),
+              PasswordChallenge(self.http_request, self.access_token)]:
+      if c.IsLocallyEligible():
+        out[c.GetName()] = c
+    return out
+
+  def InternalStart(self, requested_scopes):
+    """Does initial request to reauth API and initialize the challenges."""
+    body = {'supportedChallengeTypes': self.challenges.keys()}
+    if requested_scopes:
+      body['oauthScopesForDomainPolicyLookup'] = requested_scopes
+    _, content = self.http_request(
+        '{0}:start'.format(REAUTH_API),
+        method='POST',
+        body=json.dumps(body),
+        headers={'Authorization': 'Bearer ' + self.access_token}
+    )
+    response = json.loads(content)
+    HandleErrors(response)
+    return response
+
+  def DoOneRoundOfChallenges(self, msg):
+    next_msg = None
+    for challenge in msg['challenges']:
+      if challenge['status'] != 'READY':
+        # Skip non-activated challneges.
+        continue
+      c = self.challenges[challenge['challengeType']]
+      next_msg = c.Execute(challenge, msg['sessionId'])
+    return next_msg
+
+  def ObtainProofOfReauth(self, requested_scopes=None):
+    """Obtain proof of reauth (rapt token)."""
+    msg = None
+    max_challenge_count = 5
+
+    while max_challenge_count:
+      max_challenge_count -= 1
+
+      if not msg:
+        msg = self.InternalStart(requested_scopes)
+
+      if msg['status'] == 'AUTHENTICATED':
+        return msg['encodedProofOfReauthToken']
+
+      if not (msg['status'] == 'CHALLENGE_REQUIRED' or
+              msg['status'] == 'CHALLENGE_PENDING'):
+        raise reauth_errors.ReauthAPIError(
+            'Challenge status {0}'.format(msg['status']))
+
+      if not sys.stdin.isatty():
+        raise reauth_errors.ReauthUnattendedError()
+
+      msg = self.DoOneRoundOfChallenges(msg)
+
+    # If we got here it means we didn't get authenticated.
+    raise reauth_errors.ReauthFailError()
+
+
+def ObtainRapt(http_request, access_token, requested_scopes):
+  rm = ReauthManager(http_request, access_token)
+  rapt = rm.ObtainProofOfReauth(requested_scopes=requested_scopes)
+  return rapt
+
+
+def GetRaptToken(http_request, client_id, client_secret, refresh_token,
+                 token_uri, scopes=None):
+  """Given an http request method and refresh_token, get rapt token."""
+  sys.stderr.write('Reauthentication required.\n')
+
+  # Get access token for reauth.
+  query_params = {
+      'client_id': client_id,
+      'client_secret': client_secret,
+      'refresh_token': refresh_token,
+      'scope': REAUTH_SCOPE,
+      'grant_type': 'refresh_token',
+  }
+  _, content = http_request(
+      token_uri,
+      method='POST',
+      body=urllib.urlencode(query_params),
+      headers={'Content-Type': 'application/x-www-form-urlencoded'},
+  )
+  try:
+    reauth_access_token = json.loads(content)['access_token']
+  except (ValueError, KeyError) as _:
+    raise reauth_errors.ReauthAccessTokenRefreshError
+
+  # Get rapt token from reauth API.
+  rapt_token = ObtainRapt(
+      http_request,
+      reauth_access_token,
+      requested_scopes=scopes)
+
+  return rapt_token
--- /dev/null
+++ oauth2client/contrib/reauth_errors.py
@@ -0,0 +1,52 @@
+# Copyright 2017 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.
+"""A module that provides rapt authentication errors."""
+
+class ReauthError(Exception):
+  """Base exception for reauthentication."""
+  pass
+
+
+class ReauthUnattendedError(ReauthError):
+  """An exception for when reauth cannot be answered."""
+
+  def __init__(self):
+    super(ReauthUnattendedError, self).__init__(
+        'Reauthentication challenge could not be answered because you are not '
+        'in an interactive session.')
+
+
+class ReauthFailError(ReauthError):
+  """An exception for when reauth failed."""
+
+  def __init__(self):
+    super(ReauthFailError, self).__init__(
+        'Reauthentication challenge failed.')
+
+
+class ReauthAPIError(ReauthError):
+  """An exception for when reauth API returned something we can't handle."""
+
+  def __init__(self, api_error):
+    super(ReauthAPIError, self).__init__(
+        'Reauthentication challenge failed due to API error: {0}.'.format(
+            api_error))
+
+
+class ReauthAccessTokenRefreshError(ReauthError):
+  """An exception for when we can't get an access token for reauth."""
+
+  def __init__(self):
+    super(ReauthAccessTokenRefreshError, self).__init__(
+        'Failed to get an access token for reauthentication.')

Reply via email to