mirror of
https://github.com/frappe/frappe_docker.git
synced 2026-06-20 07:05:08 +00:00
674 lines
26 KiB
Python
674 lines
26 KiB
Python
# -*- coding: utf-8 -*-
|
|
'''
|
|
rauth.service
|
|
-------------
|
|
|
|
Provides OAuth 1.0/a, 2.0 and Ofly service containers.
|
|
'''
|
|
|
|
from rauth.compat import urlencode
|
|
from rauth.session import OAuth1Session, OAuth2Session, OflySession
|
|
from rauth.utils import ENTITY_METHODS, parse_utf8_qsl
|
|
|
|
PROCESS_TOKEN_ERROR = ('Decoder failed to handle {key} with data as returned '
|
|
'by provider. A different decoder may be needed. '
|
|
'Provider returned: {raw}')
|
|
|
|
|
|
def process_token_request(r, decoder, *args):
|
|
try:
|
|
data = decoder(r.content)
|
|
return tuple(data[key] for key in args)
|
|
except KeyError as e: # pragma: no cover
|
|
bad_key = e.args[0]
|
|
raise KeyError(PROCESS_TOKEN_ERROR.format(key=bad_key, raw=r.content))
|
|
|
|
|
|
class Service(object):
|
|
__attrs__ = ['name', 'base_url', 'authorize_url']
|
|
|
|
def __init__(self, name, base_url, authorize_url):
|
|
#: The service name, e.g. 'twitter'.
|
|
self.name = name
|
|
|
|
#: The base URL used for making API requests.
|
|
self.base_url = base_url
|
|
|
|
#: The authorization URL.
|
|
self.authorize_url = authorize_url
|
|
|
|
def __getstate__(self):
|
|
return dict((attr, getattr(self, attr, None)) for
|
|
attr in self.__attrs__)
|
|
|
|
def __setstate__(self, state):
|
|
for attr, value in state.items():
|
|
setattr(self, attr, value)
|
|
|
|
|
|
class OAuth1Service(Service):
|
|
'''
|
|
An OAuth 1.0/a Service container.
|
|
|
|
This class provides a wrapper around a specialized Requests'
|
|
:class:`~requests.sessions.Session` object. Primarily this wrapper is used
|
|
to produce authenticated session objects. These may be used to make
|
|
requests against OAuth 1.0/a endpoints.
|
|
|
|
You might intialize :class:`OAuth1Service` something like
|
|
this::
|
|
|
|
service = OAuth1Service(
|
|
name='example',
|
|
consumer_key='123',
|
|
consumer_secret='456',
|
|
request_token_url='http://example.com/request_token',
|
|
access_token_url='http://example.com/access_token',
|
|
authorize_url='http://example.com/authorize',
|
|
base_url='http://example.com/api')
|
|
|
|
Now the request token should be retrieved::
|
|
|
|
request_token, request_token_secret = service.get_request_token()
|
|
|
|
.. admonition:: Differing Request Token Formats
|
|
|
|
Some services provide different formatting when returning tokens. For
|
|
this reason the service wrapper provides a special method
|
|
:meth:`get_raw_request_token`. This will return the unparsed response.
|
|
At this point it's up to you to extract the necessary data.
|
|
|
|
It's time to access the authorize URI and direct the client to authorize
|
|
requests on their behalf. This URI is retrieved as follows::
|
|
|
|
authorize_url = service.get_authorize_url(request_token)
|
|
|
|
Once the client has authorized the request it is now possible to retrieve
|
|
an access token. Do so as follows::
|
|
|
|
session = service.get_auth_session(request_token, request_token_secret)
|
|
|
|
.. admonition:: Differing Access Token Formats
|
|
|
|
Some services provide different formatting when returning tokens. For
|
|
this reason the service wrapper provides a special method
|
|
:meth:`get_raw_access_token`. This will return the unparsed response.
|
|
At this point it's up to you to extract the necessary data.
|
|
|
|
Finally we have an authenticated session and are ready to make requests
|
|
against OAuth 1.0/a endpoints. Because Rauth is a wrapper around
|
|
Requests, the same API you would use with Requests is exposed and
|
|
expected::
|
|
|
|
r = session.get('some/resource/', params={'format': 'json'})
|
|
print r.json()
|
|
|
|
:param consumer_key: Client consumer key, required for signing.
|
|
:type consumer_key: str
|
|
:param consumer_secret: Client consumer secret, required for signing.
|
|
:type consumer_secret: str
|
|
:param name: The service name, defaults to `None`.
|
|
:type name: str
|
|
:param request_token_url: Request token endpoint, defaults to `None`.
|
|
:type request_token_url: str
|
|
:param access_token_url: Access token endpoint, defaults to `None`.
|
|
:type access_token_url: str
|
|
:param authorize_url: Authorize endpoint, defaults to `None`.
|
|
:type authorize_url: str
|
|
:param base_url: A base URL from which to construct requests, defaults to
|
|
`None`.
|
|
:type base_url: str
|
|
:param session_obj: Object used to construct sessions with, defaults to
|
|
:class:`rauth.OAuth1Session <OAuth1Session>`
|
|
:type session_obj: :class:`Session`
|
|
:param signature_obj: Object used to construct signatures with, defaults
|
|
to :class:`rauth.oauth.HmacSha1Signature <HmacSha1Signature>`
|
|
:type signature_obj: :class:`SignatureMethod`
|
|
'''
|
|
__attrs__ = Service.__attrs__ + ['consumer_key',
|
|
'consumer_secret',
|
|
'request_token_url',
|
|
'access_token_url',
|
|
'session_obj']
|
|
|
|
def __init__(self,
|
|
consumer_key,
|
|
consumer_secret,
|
|
name=None,
|
|
request_token_url=None,
|
|
access_token_url=None,
|
|
authorize_url=None,
|
|
base_url=None,
|
|
session_obj=None,
|
|
signature_obj=None):
|
|
|
|
#: Client credentials.
|
|
self.consumer_key = consumer_key
|
|
self.consumer_secret = consumer_secret
|
|
|
|
#: Authorization endpoints.
|
|
self.request_token_url = request_token_url
|
|
self.access_token_url = access_token_url
|
|
|
|
#: Object used to construct sessions with.
|
|
self.session_obj = session_obj or OAuth1Session
|
|
|
|
#: Object used to construct signatures with.
|
|
self.signature_obj = signature_obj
|
|
|
|
#: Request and access token responses.
|
|
self.request_token_response = None
|
|
self.access_token_response = None
|
|
|
|
super(OAuth1Service, self).__init__(name,
|
|
base_url,
|
|
authorize_url)
|
|
|
|
def get_session(self, token=None, signature=None):
|
|
'''
|
|
If provided a `token` parameter, tries to retrieve a stored
|
|
`rauth.OAuth1Session` instance. Otherwise generates a new session
|
|
instance with the :class:`rauth.OAuth1Service.consumer_key` and
|
|
:class:`rauth.OAuth1Service.consumer_secret` stored on the
|
|
`rauth.OAuth1Service` instance.
|
|
|
|
:param token: A tuple of strings with which to memoize the session
|
|
object instance.
|
|
:type token: tuple
|
|
'''
|
|
if token is not None:
|
|
access_token, access_token_secret = token
|
|
session = self.session_obj(self.consumer_key,
|
|
self.consumer_secret,
|
|
access_token,
|
|
access_token_secret,
|
|
signature or self.signature_obj,
|
|
service=self)
|
|
else: # pragma: no cover
|
|
signature = signature or self.signature_obj
|
|
session = self.session_obj(self.consumer_key,
|
|
self.consumer_secret,
|
|
signature=signature,
|
|
service=self)
|
|
return session
|
|
|
|
def get_raw_request_token(self, method='GET', **kwargs):
|
|
'''
|
|
Returns a Requests' response over the
|
|
:attr:`rauth.OAuth1Service.request_token_url`.
|
|
|
|
Use this if your endpoint if you need the full `Response` object.
|
|
|
|
:param method: A string representation of the HTTP method to be used,
|
|
defaults to `GET`.
|
|
:type method: str
|
|
:param \*\*kwargs: Optional arguments. Same as Requests.
|
|
:type \*\*kwargs: dict
|
|
'''
|
|
# ensure we've set the request_token_url
|
|
if self.request_token_url is None:
|
|
raise TypeError('request_token_url must not be None')
|
|
|
|
session = self.get_session()
|
|
self.request_token_response = session.request(method,
|
|
self.request_token_url,
|
|
**kwargs)
|
|
return self.request_token_response
|
|
|
|
def get_request_token(self,
|
|
method='GET',
|
|
decoder=parse_utf8_qsl,
|
|
key_token='oauth_token',
|
|
key_token_secret='oauth_token_secret',
|
|
**kwargs):
|
|
'''
|
|
Return a request token pair.
|
|
|
|
:param method: A string representation of the HTTP method to be used,
|
|
defaults to `GET`.
|
|
:type method: str
|
|
:param decoder: A function used to parse the Response content. Should
|
|
return a dictionary.
|
|
:type decoder: func
|
|
:param key_token: The key the access token will be decoded by, defaults
|
|
to 'oauth_token'.
|
|
:type string:
|
|
:param key_token_secret: The key the access token will be decoded by,
|
|
defaults to 'oauth_token_secret'.
|
|
:type string:
|
|
:param \*\*kwargs: Optional arguments. Same as Requests.
|
|
:type \*\*kwargs: dict
|
|
'''
|
|
r = self.get_raw_request_token(method=method, **kwargs)
|
|
request_token, request_token_secret = \
|
|
process_token_request(r, decoder, key_token, key_token_secret)
|
|
return request_token, request_token_secret
|
|
|
|
def get_authorize_url(self, request_token, **params):
|
|
'''
|
|
Returns a formatted authorize URL.
|
|
|
|
:param request_token: The request token as returned by
|
|
:class:`get_request_token`.
|
|
:type request_token: str
|
|
:param \*\*params: Additional keyworded arguments to be added to the
|
|
request querystring.
|
|
:type \*\*params: dict
|
|
'''
|
|
params.update({'oauth_token': request_token})
|
|
return self.authorize_url + '?' + urlencode(params)
|
|
|
|
def get_raw_access_token(self,
|
|
request_token,
|
|
request_token_secret,
|
|
method='GET',
|
|
**kwargs):
|
|
'''
|
|
Returns a Requests' response over the
|
|
:attr:`rauth.OAuth1Service.access_token_url`.
|
|
|
|
Use this if your endpoint if you need the full `Response` object.
|
|
|
|
:param request_token: The request token as returned by
|
|
:meth:`get_request_token`.
|
|
:type request_token: str
|
|
:param request_token_secret: The request token secret as returned by
|
|
:meth:`get_request_token`.
|
|
:type request_token_secret: str
|
|
:param method: A string representation of the HTTP method to be
|
|
used, defaults to `GET`.
|
|
:type method: str
|
|
:param \*\*kwargs: Optional arguments. Same as Requests.
|
|
:type \*\*kwargs: dict
|
|
'''
|
|
# ensure we've set the access_token_url
|
|
if self.access_token_url is None:
|
|
raise TypeError('access_token_url must not be None')
|
|
|
|
session = self.get_session((request_token, request_token_secret))
|
|
self.access_token_response = session.request(method,
|
|
self.access_token_url,
|
|
**kwargs)
|
|
return self.access_token_response
|
|
|
|
def get_access_token(self,
|
|
request_token,
|
|
request_token_secret,
|
|
method='GET',
|
|
decoder=parse_utf8_qsl,
|
|
key_token='oauth_token',
|
|
key_token_secret='oauth_token_secret',
|
|
**kwargs):
|
|
'''
|
|
Returns an access token pair.
|
|
|
|
:param request_token: The request token as returned by
|
|
:meth:`get_request_token`.
|
|
:type request_token: str
|
|
:param request_token_secret: The request token secret as returned by
|
|
:meth:`get_request_token`.
|
|
:type request_token_secret: str
|
|
:param method: A string representation of the HTTP method to be
|
|
used, defaults to `GET`.
|
|
:type method: str
|
|
:param decoder: A function used to parse the Response content. Should
|
|
return a dictionary.
|
|
:type decoder: func
|
|
:param key_token: The key the access token will be decoded by, defaults
|
|
to 'oauth_token'.
|
|
:type string:
|
|
:param key_token_secret: The key the access token will be decoded by,
|
|
defaults to 'oauth_token_secret'.
|
|
:type string:
|
|
:param \*\*kwargs: Optional arguments. Same as Requests.
|
|
:type \*\*kwargs: dict
|
|
'''
|
|
r = self.get_raw_access_token(request_token,
|
|
request_token_secret,
|
|
method=method,
|
|
**kwargs)
|
|
|
|
access_token, access_token_secret = \
|
|
process_token_request(r, decoder, key_token, key_token_secret)
|
|
return access_token, access_token_secret
|
|
|
|
def get_auth_session(self,
|
|
request_token,
|
|
request_token_secret,
|
|
method='GET',
|
|
**kwargs):
|
|
'''
|
|
Gets an access token, intializes a new authenticated session with the
|
|
access token. Returns an instance of :attr:`session_obj`.
|
|
|
|
:param request_token: The request token as returned by
|
|
:meth:`get_request_token`.
|
|
:type request_token: str
|
|
:param request_token_secret: The request token secret as returned by
|
|
:meth:`get_request_token`.
|
|
:type request_token_secret: str
|
|
:param method: A string representation of the HTTP method to be
|
|
used, defaults to `GET`.
|
|
:type method: str
|
|
:param \*\*kwargs: Optional arguments. Same as Requests.
|
|
:type \*\*kwargs: dict
|
|
'''
|
|
token = self.get_access_token(request_token,
|
|
request_token_secret,
|
|
method=method,
|
|
**kwargs)
|
|
session = self.get_session(token)
|
|
|
|
if self.request_token_response:
|
|
session.request_token_response = self.request_token_response
|
|
if self.access_token_response:
|
|
session.access_token_response = self.access_token_response
|
|
|
|
return session
|
|
|
|
|
|
class OAuth2Service(Service):
|
|
'''
|
|
An OAuth 2.0 Service container.
|
|
|
|
This class provides a wrapper around a specialized Requests'
|
|
:class:`~requests.session.Session` object. Primarily this wrapper is used
|
|
for producing authenticated session objects which are used to make requests
|
|
against OAuth 2.0 endpoints.
|
|
|
|
You might intialize :class:`OAuth2Service` something like this::
|
|
|
|
service = OAuth2Service(
|
|
name='example',
|
|
client_id='123',
|
|
client_secret='456',
|
|
access_token_url='https://example.com/token',
|
|
authorize_url='https://example.com/authorize',
|
|
base_url='https://example.com/api/')
|
|
|
|
Given the simplicity of OAuth 2.0 now this object `service` can be used to
|
|
retrieve an authenticated session in two simple steps::
|
|
|
|
# the return URL is used to validate the request
|
|
params = {'redirect_uri': 'http://example.com/',
|
|
'response_type': 'code'}
|
|
url = service.get_authorize_url(**params)
|
|
|
|
# once the above URL is consumed by a client we can ask for an access
|
|
# token. note that the code is retrieved from the redirect URL above,
|
|
# as set by the provider
|
|
data = {'code': 'foobar',
|
|
'grant_type': 'authorization_code',
|
|
'redirect_uri': 'http://example.com/'}
|
|
|
|
session = service.get_auth_session(data=data)
|
|
|
|
Now that we have retrieved a session, we may make requests against the
|
|
OAuth 2.0 provider's endpoints. As much as possible the Requests' API
|
|
is preserved and you may make requests using the same parameters you would
|
|
using Requests::
|
|
|
|
r = session.get('foo', params={'format': 'json'})
|
|
print r.json()
|
|
|
|
:param client_id: Client id.
|
|
:type client_id: str
|
|
:param client_secret: Client secret.
|
|
:type client_secret: str
|
|
:param name: The service name, defaults to `None`.
|
|
:type name: str
|
|
:param access_token_url: Access token endpoint, defaults to `None`.
|
|
:type access_token_url: str
|
|
:param authorize_url: Authorize endpoint, defaults to `None`.
|
|
:type authorize_url: str
|
|
:param base_url: A base URL from which to construct requests, defaults to
|
|
`None`.
|
|
:type base_url: str
|
|
:param session_obj: Object used to construct sessions with, defaults to
|
|
:class:`OAuth2Session`
|
|
:type session_obj: :class:`rauth.Session`
|
|
'''
|
|
__attrs__ = Service.__attrs__ + ['client_id',
|
|
'client_secret',
|
|
'access_token_url',
|
|
'session_obj']
|
|
|
|
def __init__(self,
|
|
client_id,
|
|
client_secret,
|
|
name=None,
|
|
access_token_url=None,
|
|
authorize_url=None,
|
|
base_url=None,
|
|
session_obj=None):
|
|
|
|
#: Client credentials.
|
|
self.client_id = client_id
|
|
self.client_secret = client_secret
|
|
|
|
#: The provider's access token URL.
|
|
self.access_token_url = access_token_url
|
|
|
|
#: Object used to construct sessions with.
|
|
self.session_obj = session_obj or OAuth2Session
|
|
|
|
#: Access token response.
|
|
self.access_token_response = None
|
|
|
|
super(OAuth2Service, self).__init__(name,
|
|
base_url,
|
|
authorize_url)
|
|
|
|
def get_session(self, token=None):
|
|
'''
|
|
If provided, the `token` parameter is used to initialize an
|
|
authenticated session, otherwise an unauthenticated session object is
|
|
generated. Returns an instance of :attr:`session_obj`..
|
|
|
|
:param token: A token with which to initilize the session.
|
|
:type token: str
|
|
'''
|
|
if token is not None:
|
|
session = self.session_obj(self.client_id,
|
|
self.client_secret,
|
|
token,
|
|
service=self)
|
|
else: # pragma: no cover
|
|
session = self.session_obj(self.client_id,
|
|
self.client_secret,
|
|
service=self)
|
|
return session
|
|
|
|
def get_authorize_url(self, **params):
|
|
'''
|
|
Returns a formatted authorize URL.
|
|
|
|
:param \*\*params: Additional keyworded arguments to be added to the
|
|
URL querystring.
|
|
:type \*\*params: dict
|
|
'''
|
|
|
|
params.update({'client_id': self.client_id})
|
|
return self.authorize_url + '?' + urlencode(params)
|
|
|
|
def get_raw_access_token(self, method='POST', **kwargs):
|
|
'''
|
|
Returns a Requests' response over the
|
|
:attr:`OAuth2Service.access_token_url`.
|
|
|
|
Use this if your endpoint if you need the full `Response` object.
|
|
|
|
:param method: A string representation of the HTTP method to be used,
|
|
defaults to `POST`.
|
|
:type method: str
|
|
:param \*\*kwargs: Optional arguments. Same as Requests.
|
|
:type \*\*kwargs: dict
|
|
'''
|
|
key = 'params'
|
|
if method in ENTITY_METHODS:
|
|
key = 'data'
|
|
|
|
kwargs.setdefault(key, {})
|
|
kwargs[key].update({'client_id': self.client_id,
|
|
'client_secret': self.client_secret})
|
|
|
|
session = self.get_session()
|
|
self.access_token_response = session.request(method,
|
|
self.access_token_url,
|
|
**kwargs)
|
|
return self.access_token_response
|
|
|
|
def get_access_token(self,
|
|
method='POST',
|
|
decoder=parse_utf8_qsl,
|
|
key='access_token',
|
|
**kwargs):
|
|
'''
|
|
Returns an access token.
|
|
|
|
:param method: A string representation of the HTTP method to be used,
|
|
defaults to `POST`.
|
|
:type method: str
|
|
:param decoder: A function used to parse the Response content. Should
|
|
return a dictionary.
|
|
:type decoder: func
|
|
:param key: The key the access token will be decoded by, defaults to
|
|
'access_token'.
|
|
:type string:
|
|
:param \*\*kwargs: Optional arguments. Same as Requests.
|
|
:type \*\*kwargs: dict
|
|
'''
|
|
r = self.get_raw_access_token(method, **kwargs)
|
|
access_token, = process_token_request(r, decoder, key)
|
|
return access_token
|
|
|
|
def get_auth_session(self, method='POST', **kwargs):
|
|
'''
|
|
Gets an access token, intializes a new authenticated session with the
|
|
access token. Returns an instance of :attr:`session_obj`.
|
|
|
|
:param method: A string representation of the HTTP method to be used,
|
|
defaults to `POST`.
|
|
:type method: str
|
|
:param \*\*kwargs: Optional arguments. Same as Requests.
|
|
:type \*\*kwargs: dict
|
|
'''
|
|
session = self.get_session(self.get_access_token(method, **kwargs))
|
|
|
|
if self.access_token_response:
|
|
session.access_token_response = self.access_token_response
|
|
|
|
return session
|
|
|
|
|
|
class OflyService(Service):
|
|
'''
|
|
An Ofly Service container.
|
|
|
|
This class wraps an Ofly service i.e., Shutterfly. The process
|
|
is similar to that of OAuth 1.0 but simplified.
|
|
|
|
You might intialize :class:`OflyService` something like this::
|
|
|
|
service = OflyService(name='example',
|
|
app_id='123',
|
|
app_secret='456',
|
|
authorize_url='http://example.com/authorize')
|
|
|
|
A signed authorize URL is then produced via calling
|
|
`service.get_authorize_url`. Once this has been visited by the client and
|
|
assuming the client authorizes the request.
|
|
|
|
Normal API calls can now be made using a session instance. Retrieve the
|
|
authenticated session like so::
|
|
|
|
session = service.get_auth_session('foo')
|
|
|
|
# now we can make regular Requests' calls
|
|
r = session.get('bar')
|
|
|
|
:param app_id: The oFlyAppId, i.e. "application ID".
|
|
:type app_id: str
|
|
:param app_secret: The oFlyAppSecret, i.e. "shared secret".
|
|
:type app_secret: str
|
|
:param name: The service name, defaults to `None`.
|
|
:type name: str
|
|
:param authorize_url: Authorize endpoint, defaults to `None`.
|
|
:type authorize_url: str
|
|
:param base_url: A base URL from which to construct requests, defaults to
|
|
`None`.
|
|
:type base_url: str
|
|
:param user_id: The oflyUserid, defaults to `None`. Note: this is required
|
|
for Ofly requests, retrieved via authorize URL.
|
|
:type user_id: str
|
|
:param session_obj: Object used to construct sessions with, defaults to
|
|
`rauth.OflySession`
|
|
:type session_obj: :class:`rauth.Session`
|
|
'''
|
|
__attrs__ = Service.__attrs__ + ['app_id',
|
|
'app_secret',
|
|
'user_id',
|
|
'session_obj']
|
|
|
|
def __init__(self,
|
|
app_id,
|
|
app_secret,
|
|
name=None,
|
|
authorize_url=None,
|
|
base_url=None,
|
|
user_id=None,
|
|
session_obj=None):
|
|
#: Client credentials.
|
|
self.app_id = app_id
|
|
self.app_secret = app_secret
|
|
|
|
#: The oflyUserid.
|
|
self.user_id = user_id
|
|
|
|
#: Object used to construct sessions with.
|
|
self.session_obj = session_obj or OflySession
|
|
|
|
super(OflyService, self).__init__(name,
|
|
base_url,
|
|
authorize_url)
|
|
|
|
def get_session(self, token):
|
|
'''
|
|
The token parameter should be `oFlyUserid`. This is used to initialize
|
|
an authenticated session instance. Returns an instance of
|
|
:attr:`session_obj`.
|
|
|
|
:param token: A token with which to initialize the session with, e.g.
|
|
:attr:`OflyService.user_id`.
|
|
:type token: str
|
|
'''
|
|
return self.session_obj(self.app_id,
|
|
self.app_secret,
|
|
token,
|
|
service=self)
|
|
|
|
def get_authorize_url(self, **params):
|
|
'''
|
|
Returns a formatted authorize URL.
|
|
|
|
:param \*\*params: Additional keyworded arguments to be added to the
|
|
request querystring.
|
|
:type \*\*params: dict
|
|
'''
|
|
params = self.session_obj.sign(self.authorize_url,
|
|
self.app_id,
|
|
self.app_secret,
|
|
**params)
|
|
return self.authorize_url + '?' + params
|
|
|
|
def get_auth_session(self, user_id, **kwargs):
|
|
'''
|
|
Intializes a new authenticated session with `user_id` as oFlyUserid.
|
|
Returns an instance of :attr:`session_obj`.
|
|
|
|
:param user_id: The oflyUserid, defaults to `None`.
|
|
:type user_id: str
|
|
:param \*\*kwargs: Optional arguments. Same as Requests.
|
|
:type \*\*kwargs: dict
|
|
'''
|
|
return self.get_session(user_id)
|