core.py
1764 lines
| 52.0 KiB
| text/x-python
|
PythonLexer
r3912 | # -*- coding: utf-8 -*- | |||
import collections | ||||
import copy | ||||
import datetime | ||||
import hashlib | ||||
import hmac | ||||
import json | ||||
import logging | ||||
try: | ||||
import cPickle as pickle | ||||
except ImportError: | ||||
import pickle | ||||
import sys | ||||
import threading | ||||
import time | ||||
from xml.etree import ElementTree | ||||
from authomatic.exceptions import ( | ||||
ConfigError, | ||||
CredentialsError, | ||||
ImportStringError, | ||||
RequestElementsError, | ||||
SessionError, | ||||
) | ||||
from authomatic import six | ||||
from authomatic.six.moves import urllib_parse as parse | ||||
# ========================================================================= | ||||
# Global variables !!! | ||||
# ========================================================================= | ||||
_logger = logging.getLogger(__name__) | ||||
_logger.addHandler(logging.StreamHandler(sys.stdout)) | ||||
_counter = None | ||||
def normalize_dict(dict_): | ||||
""" | ||||
Replaces all values that are single-item iterables with the value of its | ||||
index 0. | ||||
:param dict dict_: | ||||
Dictionary to normalize. | ||||
:returns: | ||||
Normalized dictionary. | ||||
""" | ||||
return dict([(k, v[0] if not isinstance(v, str) and len(v) == 1 else v) | ||||
for k, v in list(dict_.items())]) | ||||
def items_to_dict(items): | ||||
""" | ||||
Converts list of tuples to dictionary with duplicate keys converted to | ||||
lists. | ||||
:param list items: | ||||
List of tuples. | ||||
:returns: | ||||
:class:`dict` | ||||
""" | ||||
res = collections.defaultdict(list) | ||||
for k, v in items: | ||||
res[k].append(v) | ||||
return normalize_dict(dict(res)) | ||||
class Counter(object): | ||||
""" | ||||
A simple counter to be used in the config to generate unique `id` values. | ||||
""" | ||||
def __init__(self, start=0): | ||||
self._count = start | ||||
def count(self): | ||||
self._count += 1 | ||||
return self._count | ||||
_counter = Counter() | ||||
def provider_id(): | ||||
""" | ||||
A simple counter to be used in the config to generate unique `IDs`. | ||||
:returns: | ||||
:class:`int`. | ||||
Use it in the :doc:`config` like this: | ||||
:: | ||||
import authomatic | ||||
CONFIG = { | ||||
'facebook': { | ||||
'class_': authomatic.providers.oauth2.Facebook, | ||||
'id': authomatic.provider_id(), # returns 1 | ||||
'consumer_key': '##########', | ||||
'consumer_secret': '##########', | ||||
'scope': ['user_about_me', 'email'] | ||||
}, | ||||
'google': { | ||||
'class_': 'authomatic.providers.oauth2.Google', | ||||
'id': authomatic.provider_id(), # returns 2 | ||||
'consumer_key': '##########', | ||||
'consumer_secret': '##########', | ||||
'scope': ['https://www.googleapis.com/auth/userinfo.profile', | ||||
'https://www.googleapis.com/auth/userinfo.email'] | ||||
}, | ||||
'windows_live': { | ||||
'class_': 'oauth2.WindowsLive', | ||||
'id': authomatic.provider_id(), # returns 3 | ||||
'consumer_key': '##########', | ||||
'consumer_secret': '##########', | ||||
'scope': ['wl.basic', 'wl.emails', 'wl.photos'] | ||||
}, | ||||
} | ||||
""" | ||||
return _counter.count() | ||||
def escape(s): | ||||
""" | ||||
Escape a URL including any /. | ||||
""" | ||||
return parse.quote(s.encode('utf-8'), safe='~') | ||||
def json_qs_parser(body): | ||||
""" | ||||
Parses response body from JSON, XML or query string. | ||||
:param body: | ||||
string | ||||
:returns: | ||||
:class:`dict`, :class:`list` if input is JSON or query string, | ||||
:class:`xml.etree.ElementTree.Element` if XML. | ||||
""" | ||||
try: | ||||
# Try JSON first. | ||||
return json.loads(body) | ||||
except (OverflowError, TypeError, ValueError): | ||||
pass | ||||
try: | ||||
# Then XML. | ||||
return ElementTree.fromstring(body) | ||||
except (ElementTree.ParseError, TypeError, ValueError): | ||||
pass | ||||
# Finally query string. | ||||
return dict(parse.parse_qsl(body)) | ||||
def import_string(import_name, silent=False): | ||||
""" | ||||
Imports an object by string in dotted notation. | ||||
taken `from webapp2.import_string() <http://webapp- | ||||
improved.appspot.com/api/webapp2.html#webapp2.import_string>`_ | ||||
""" | ||||
try: | ||||
if '.' in import_name: | ||||
module, obj = import_name.rsplit('.', 1) | ||||
return getattr(__import__(module, None, None, [obj]), obj) | ||||
else: | ||||
return __import__(import_name) | ||||
except (ImportError, AttributeError) as e: | ||||
if not silent: | ||||
raise ImportStringError('Import from string failed for path {0}' | ||||
.format(import_name), str(e)) | ||||
def resolve_provider_class(class_): | ||||
""" | ||||
Returns a provider class. | ||||
:param class_name: :class:`string` or | ||||
:class:`authomatic.providers.BaseProvider` subclass. | ||||
""" | ||||
if isinstance(class_, str): | ||||
# prepare path for authomatic.providers package | ||||
path = '.'.join([__package__, 'providers', class_]) | ||||
# try to import class by string from providers module or by fully | ||||
# qualified path | ||||
return import_string(class_, True) or import_string(path) | ||||
else: | ||||
return class_ | ||||
def id_to_name(config, short_name): | ||||
""" | ||||
Returns the provider :doc:`config` key based on it's ``id`` value. | ||||
:param dict config: | ||||
:doc:`config`. | ||||
:param id: | ||||
Value of the id parameter in the :ref:`config` to search for. | ||||
""" | ||||
for k, v in list(config.items()): | ||||
if v.get('id') == short_name: | ||||
return k | ||||
raise Exception( | ||||
'No provider with id={0} found in the config!'.format(short_name)) | ||||
class ReprMixin(object): | ||||
""" | ||||
Provides __repr__() method with output *ClassName(arg1=value, arg2=value)*. | ||||
Ignored are attributes | ||||
* which values are considered false. | ||||
* with leading underscore. | ||||
* listed in _repr_ignore. | ||||
Values of attributes listed in _repr_sensitive will be replaced by *###*. | ||||
Values which repr() string is longer than _repr_length_limit will be | ||||
represented as *ClassName(...)* | ||||
""" | ||||
#: Iterable of attributes to be ignored. | ||||
_repr_ignore = [] | ||||
#: Iterable of attributes which value should not be visible. | ||||
_repr_sensitive = [] | ||||
#: `int` Values longer than this will be truncated to *ClassName(...)*. | ||||
_repr_length_limit = 20 | ||||
def __repr__(self): | ||||
# get class name | ||||
name = self.__class__.__name__ | ||||
# construct keyword arguments | ||||
args = [] | ||||
for k, v in list(self.__dict__.items()): | ||||
# ignore attributes with leading underscores and those listed in | ||||
# _repr_ignore | ||||
if v and not k.startswith('_') and k not in self._repr_ignore: | ||||
# replace sensitive values | ||||
if k in self._repr_sensitive: | ||||
v = '###' | ||||
# if repr is too long | ||||
if len(repr(v)) > self._repr_length_limit: | ||||
# Truncate to ClassName(...) | ||||
v = '{0}(...)'.format(v.__class__.__name__) | ||||
else: | ||||
v = repr(v) | ||||
args.append('{0}={1}'.format(k, v)) | ||||
return '{0}({1})'.format(name, ', '.join(args)) | ||||
class Future(threading.Thread): | ||||
""" | ||||
Represents an activity run in a separate thread. Subclasses the standard | ||||
library :class:`threading.Thread` and adds :attr:`.get_result` method. | ||||
.. warning:: | ||||
|async| | ||||
""" | ||||
def __init__(self, func, *args, **kwargs): | ||||
""" | ||||
:param callable func: | ||||
The function to be run in separate thread. | ||||
Calls :data:`func` in separate thread and returns immediately. | ||||
Accepts arbitrary positional and keyword arguments which will be | ||||
passed to :data:`func`. | ||||
""" | ||||
super(Future, self).__init__() | ||||
self._func = func | ||||
self._args = args | ||||
self._kwargs = kwargs | ||||
self._result = None | ||||
self.start() | ||||
def run(self): | ||||
self._result = self._func(*self._args, **self._kwargs) | ||||
def get_result(self, timeout=None): | ||||
""" | ||||
Waits for the wrapped :data:`func` to finish and returns its result. | ||||
.. note:: | ||||
This will block the **calling thread** until the :data:`func` | ||||
returns. | ||||
:param timeout: | ||||
:class:`float` or ``None`` A timeout for the :data:`func` to | ||||
return in seconds. | ||||
:returns: | ||||
The result of the wrapped :data:`func`. | ||||
""" | ||||
self.join(timeout) | ||||
return self._result | ||||
class Session(object): | ||||
""" | ||||
A dictionary-like secure cookie session implementation. | ||||
""" | ||||
def __init__(self, adapter, secret, name='authomatic', max_age=600, | ||||
secure=False): | ||||
""" | ||||
:param str secret: | ||||
Session secret used to sign the session cookie. | ||||
:param str name: | ||||
Session cookie name. | ||||
:param int max_age: | ||||
Maximum allowed age of session cookie nonce in seconds. | ||||
:param bool secure: | ||||
If ``True`` the session cookie will be saved with ``Secure`` | ||||
attribute. | ||||
""" | ||||
self.adapter = adapter | ||||
self.name = name | ||||
self.secret = secret | ||||
self.max_age = max_age | ||||
self.secure = secure | ||||
self._data = {} | ||||
def create_cookie(self, delete=None): | ||||
""" | ||||
Creates the value for ``Set-Cookie`` HTTP header. | ||||
:param bool delete: | ||||
If ``True`` the cookie value will be ``deleted`` and the | ||||
Expires value will be ``Thu, 01-Jan-1970 00:00:01 GMT``. | ||||
""" | ||||
value = 'deleted' if delete else self._serialize(self.data) | ||||
split_url = parse.urlsplit(self.adapter.url) | ||||
domain = split_url.netloc.split(':')[0] | ||||
# Work-around for issue #11, failure of WebKit-based browsers to accept | ||||
# cookies set as part of a redirect response in some circumstances. | ||||
if '.' not in domain: | ||||
template = '{name}={value}; Path={path}; HttpOnly{secure}{expires}' | ||||
else: | ||||
template = ('{name}={value}; Domain={domain}; Path={path}; ' | ||||
'HttpOnly{secure}{expires}') | ||||
return template.format( | ||||
name=self.name, | ||||
value=value, | ||||
domain=domain, | ||||
path=split_url.path, | ||||
secure='; Secure' if self.secure else '', | ||||
expires='; Expires=Thu, 01-Jan-1970 00:00:01 GMT' if delete else '' | ||||
) | ||||
def save(self): | ||||
""" | ||||
Adds the session cookie to headers. | ||||
""" | ||||
if self.data: | ||||
cookie = self.create_cookie() | ||||
cookie_len = len(cookie) | ||||
if cookie_len > 4093: | ||||
raise SessionError('Cookie too long! The cookie size {0} ' | ||||
'is more than 4093 bytes.' | ||||
.format(cookie_len)) | ||||
self.adapter.set_header('Set-Cookie', cookie) | ||||
# Reset data | ||||
self._data = {} | ||||
def delete(self): | ||||
self.adapter.set_header('Set-Cookie', self.create_cookie(delete=True)) | ||||
def _get_data(self): | ||||
""" | ||||
Extracts the session data from cookie. | ||||
""" | ||||
cookie = self.adapter.cookies.get(self.name) | ||||
return self._deserialize(cookie) if cookie else {} | ||||
@property | ||||
def data(self): | ||||
""" | ||||
Gets session data lazily. | ||||
""" | ||||
if not self._data: | ||||
self._data = self._get_data() | ||||
# Always return a dict, even if deserialization returned nothing | ||||
if self._data is None: | ||||
self._data = {} | ||||
return self._data | ||||
def _signature(self, *parts): | ||||
""" | ||||
Creates signature for the session. | ||||
""" | ||||
signature = hmac.new(six.b(self.secret), digestmod=hashlib.sha1) | ||||
signature.update(six.b('|'.join(parts))) | ||||
return signature.hexdigest() | ||||
def _serialize(self, value): | ||||
""" | ||||
Converts the value to a signed string with timestamp. | ||||
:param value: | ||||
Object to be serialized. | ||||
:returns: | ||||
Serialized value. | ||||
""" | ||||
# data = copy.deepcopy(value) | ||||
data = value | ||||
# 1. Serialize | ||||
serialized = pickle.dumps(data).decode('latin-1') | ||||
# 2. Encode | ||||
# Percent encoding produces smaller result then urlsafe base64. | ||||
encoded = parse.quote(serialized, '') | ||||
# 3. Concatenate | ||||
timestamp = str(int(time.time())) | ||||
signature = self._signature(self.name, encoded, timestamp) | ||||
concatenated = '|'.join([encoded, timestamp, signature]) | ||||
return concatenated | ||||
def _deserialize(self, value): | ||||
""" | ||||
Deserializes and verifies the value created by :meth:`._serialize`. | ||||
:param str value: | ||||
The serialized value. | ||||
:returns: | ||||
Deserialized object. | ||||
""" | ||||
# 3. Split | ||||
encoded, timestamp, signature = value.split('|') | ||||
# Verify signature | ||||
if not signature == self._signature(self.name, encoded, timestamp): | ||||
raise SessionError('Invalid signature "{0}"!'.format(signature)) | ||||
# Verify timestamp | ||||
if int(timestamp) < int(time.time()) - self.max_age: | ||||
return None | ||||
# 2. Decode | ||||
decoded = parse.unquote(encoded) | ||||
# 1. Deserialize | ||||
deserialized = pickle.loads(decoded.encode('latin-1')) | ||||
return deserialized | ||||
def __setitem__(self, key, value): | ||||
self._data[key] = value | ||||
def __getitem__(self, key): | ||||
return self.data.__getitem__(key) | ||||
def __delitem__(self, key): | ||||
return self._data.__delitem__(key) | ||||
def get(self, key, default=None): | ||||
return self.data.get(key, default) | ||||
class User(ReprMixin): | ||||
""" | ||||
Provides unified interface to selected **user** info returned by different | ||||
**providers**. | ||||
.. note:: The value format may vary across providers. | ||||
""" | ||||
def __init__(self, provider, **kwargs): | ||||
#: A :doc:`provider <providers>` instance. | ||||
self.provider = provider | ||||
#: An :class:`.Credentials` instance. | ||||
self.credentials = kwargs.get('credentials') | ||||
#: A :class:`dict` containing all the **user** information returned | ||||
#: by the **provider**. | ||||
#: The structure differs across **providers**. | ||||
self.data = kwargs.get('data') | ||||
#: The :attr:`.Response.content` of the request made to update | ||||
#: the user. | ||||
self.content = kwargs.get('content') | ||||
#: :class:`str` ID assigned to the **user** by the **provider**. | ||||
self.id = kwargs.get('id') | ||||
#: :class:`str` User name e.g. *andrewpipkin*. | ||||
self.username = kwargs.get('username') | ||||
#: :class:`str` Name e.g. *Andrew Pipkin*. | ||||
self.name = kwargs.get('name') | ||||
#: :class:`str` First name e.g. *Andrew*. | ||||
self.first_name = kwargs.get('first_name') | ||||
#: :class:`str` Last name e.g. *Pipkin*. | ||||
self.last_name = kwargs.get('last_name') | ||||
#: :class:`str` Nickname e.g. *Andy*. | ||||
self.nickname = kwargs.get('nickname') | ||||
#: :class:`str` Link URL. | ||||
self.link = kwargs.get('link') | ||||
#: :class:`str` Gender. | ||||
self.gender = kwargs.get('gender') | ||||
#: :class:`str` Timezone. | ||||
self.timezone = kwargs.get('timezone') | ||||
#: :class:`str` Locale. | ||||
self.locale = kwargs.get('locale') | ||||
#: :class:`str` E-mail. | ||||
self.email = kwargs.get('email') | ||||
#: :class:`str` phone. | ||||
self.phone = kwargs.get('phone') | ||||
#: :class:`str` Picture URL. | ||||
self.picture = kwargs.get('picture') | ||||
#: Birth date as :class:`datetime.datetime()` or :class:`str` | ||||
# if parsing failed or ``None``. | ||||
self.birth_date = kwargs.get('birth_date') | ||||
#: :class:`str` Country. | ||||
self.country = kwargs.get('country') | ||||
#: :class:`str` City. | ||||
self.city = kwargs.get('city') | ||||
#: :class:`str` Geographical location. | ||||
self.location = kwargs.get('location') | ||||
#: :class:`str` Postal code. | ||||
self.postal_code = kwargs.get('postal_code') | ||||
#: Instance of the Google App Engine Users API | ||||
#: `User <https://developers.google.com/appengine/docs/python/users/userclass>`_ class. | ||||
#: Only present when using the :class:`authomatic.providers.gaeopenid.GAEOpenID` provider. | ||||
self.gae_user = kwargs.get('gae_user') | ||||
def update(self): | ||||
""" | ||||
Updates the user info by fetching the **provider's** user info URL. | ||||
:returns: | ||||
Updated instance of this class. | ||||
""" | ||||
return self.provider.update_user() | ||||
def async_update(self): | ||||
""" | ||||
Same as :meth:`.update` but runs asynchronously in a separate thread. | ||||
.. warning:: | ||||
|async| | ||||
:returns: | ||||
:class:`.Future` instance representing the separate thread. | ||||
""" | ||||
return Future(self.update) | ||||
def to_dict(self): | ||||
""" | ||||
Converts the :class:`.User` instance to a :class:`dict`. | ||||
:returns: | ||||
:class:`dict` | ||||
""" | ||||
# copy the dictionary | ||||
d = copy.copy(self.__dict__) | ||||
# Keep only the provider name to avoid circular reference | ||||
d['provider'] = self.provider.name | ||||
d['credentials'] = self.credentials.serialize( | ||||
) if self.credentials else None | ||||
d['birth_date'] = str(d['birth_date']) | ||||
# Remove content | ||||
d.pop('content') | ||||
if isinstance(self.data, ElementTree.Element): | ||||
d['data'] = None | ||||
return d | ||||
SupportedUserAttributesNT = collections.namedtuple( | ||||
typename='SupportedUserAttributesNT', | ||||
field_names=['birth_date', 'city', 'country', 'email', 'first_name', | ||||
'gender', 'id', 'last_name', 'link', 'locale', 'location', | ||||
'name', 'nickname', 'phone', 'picture', 'postal_code', | ||||
'timezone', 'username', ] | ||||
) | ||||
class SupportedUserAttributes(SupportedUserAttributesNT): | ||||
def __new__(cls, **kwargs): | ||||
defaults = dict((i, False) for i in SupportedUserAttributes._fields) # pylint:disable=no-member | ||||
defaults.update(**kwargs) | ||||
return super(SupportedUserAttributes, cls).__new__(cls, **defaults) | ||||
class Credentials(ReprMixin): | ||||
""" | ||||
Contains all necessary information to fetch **user's protected resources**. | ||||
""" | ||||
_repr_sensitive = ('token', 'refresh_token', 'token_secret', | ||||
'consumer_key', 'consumer_secret') | ||||
def __init__(self, config, **kwargs): | ||||
#: :class:`dict` :doc:`config`. | ||||
self.config = config | ||||
#: :class:`str` User **access token**. | ||||
self.token = kwargs.get('token', '') | ||||
#: :class:`str` Access token type. | ||||
self.token_type = kwargs.get('token_type', '') | ||||
#: :class:`str` Refresh token. | ||||
self.refresh_token = kwargs.get('refresh_token', '') | ||||
#: :class:`str` Access token secret. | ||||
self.token_secret = kwargs.get('token_secret', '') | ||||
#: :class:`int` Expiration date as UNIX timestamp. | ||||
self.expiration_time = int(kwargs.get('expiration_time', 0)) | ||||
#: A :doc:`Provider <providers>` instance**. | ||||
provider = kwargs.get('provider') | ||||
self.expire_in = int(kwargs.get('expire_in', 0)) | ||||
if provider: | ||||
#: :class:`str` Provider name specified in the :doc:`config`. | ||||
self.provider_name = provider.name | ||||
#: :class:`str` Provider type e.g. | ||||
# ``"authomatic.providers.oauth2.OAuth2"``. | ||||
self.provider_type = provider.get_type() | ||||
#: :class:`str` Provider type e.g. | ||||
# ``"authomatic.providers.oauth2.OAuth2"``. | ||||
self.provider_type_id = provider.type_id | ||||
#: :class:`str` Provider short name specified in the :doc:`config`. | ||||
self.provider_id = int(provider.id) if provider.id else None | ||||
#: :class:`class` Provider class. | ||||
self.provider_class = provider.__class__ | ||||
#: :class:`str` Consumer key specified in the :doc:`config`. | ||||
self.consumer_key = provider.consumer_key | ||||
#: :class:`str` Consumer secret specified in the :doc:`config`. | ||||
self.consumer_secret = provider.consumer_secret | ||||
else: | ||||
self.provider_name = kwargs.get('provider_name', '') | ||||
self.provider_type = kwargs.get('provider_type', '') | ||||
self.provider_type_id = kwargs.get('provider_type_id') | ||||
self.provider_id = kwargs.get('provider_id') | ||||
self.provider_class = kwargs.get('provider_class') | ||||
self.consumer_key = kwargs.get('consumer_key', '') | ||||
self.consumer_secret = kwargs.get('consumer_secret', '') | ||||
@property | ||||
def expire_in(self): | ||||
""" | ||||
""" | ||||
return self._expire_in | ||||
@expire_in.setter | ||||
def expire_in(self, value): | ||||
""" | ||||
Computes :attr:`.expiration_time` when the value is set. | ||||
""" | ||||
# pylint:disable=attribute-defined-outside-init | ||||
if value: | ||||
self._expiration_time = int(time.time()) + int(value) | ||||
self._expire_in = value | ||||
@property | ||||
def expiration_time(self): | ||||
return self._expiration_time | ||||
@expiration_time.setter | ||||
def expiration_time(self, value): | ||||
# pylint:disable=attribute-defined-outside-init | ||||
self._expiration_time = int(value) | ||||
self._expire_in = self._expiration_time - int(time.time()) | ||||
@property | ||||
def expiration_date(self): | ||||
""" | ||||
Expiration date as :class:`datetime.datetime` or ``None`` if | ||||
credentials never expire. | ||||
""" | ||||
if self.expire_in < 0: | ||||
return None | ||||
else: | ||||
return datetime.datetime.fromtimestamp(self.expiration_time) | ||||
@property | ||||
def valid(self): | ||||
""" | ||||
``True`` if credentials are valid, ``False`` if expired. | ||||
""" | ||||
if self.expiration_time: | ||||
return self.expiration_time > int(time.time()) | ||||
else: | ||||
return True | ||||
def expire_soon(self, seconds): | ||||
""" | ||||
Returns ``True`` if credentials expire sooner than specified. | ||||
:param int seconds: | ||||
Number of seconds. | ||||
:returns: | ||||
``True`` if credentials expire sooner than specified, | ||||
else ``False``. | ||||
""" | ||||
if self.expiration_time: | ||||
return self.expiration_time < int(time.time()) + int(seconds) | ||||
else: | ||||
return False | ||||
def refresh(self, force=False, soon=86400): | ||||
""" | ||||
Refreshes the credentials only if the **provider** supports it and if | ||||
it will expire in less than one day. It does nothing in other cases. | ||||
.. note:: | ||||
The credentials will be refreshed only if it gives sense | ||||
i.e. only |oauth2|_ has the notion of credentials | ||||
*refreshment/extension*. | ||||
And there are also differences across providers e.g. Google | ||||
supports refreshment only if there is a ``refresh_token`` in | ||||
the credentials and that in turn is present only if the | ||||
``access_type`` parameter was set to ``offline`` in the | ||||
**user authorization request**. | ||||
:param bool force: | ||||
If ``True`` the credentials will be refreshed even if they | ||||
won't expire soon. | ||||
:param int soon: | ||||
Number of seconds specifying what means *soon*. | ||||
""" | ||||
if hasattr(self.provider_class, 'refresh_credentials'): | ||||
if force or self.expire_soon(soon): | ||||
logging.info('PROVIDER NAME: {0}'.format(self.provider_name)) | ||||
return self.provider_class( | ||||
self, None, self.provider_name).refresh_credentials(self) | ||||
def async_refresh(self, *args, **kwargs): | ||||
""" | ||||
Same as :meth:`.refresh` but runs asynchronously in a separate thread. | ||||
.. warning:: | ||||
|async| | ||||
:returns: | ||||
:class:`.Future` instance representing the separate thread. | ||||
""" | ||||
return Future(self.refresh, *args, **kwargs) | ||||
def provider_type_class(self): | ||||
""" | ||||
Returns the :doc:`provider <providers>` class specified in the | ||||
:doc:`config`. | ||||
:returns: | ||||
:class:`authomatic.providers.BaseProvider` subclass. | ||||
""" | ||||
return resolve_provider_class(self.provider_type) | ||||
def serialize(self): | ||||
""" | ||||
Converts the credentials to a percent encoded string to be stored for | ||||
later use. | ||||
:returns: | ||||
:class:`string` | ||||
""" | ||||
if self.provider_id is None: | ||||
raise ConfigError( | ||||
'To serialize credentials you need to specify a ' | ||||
'unique integer under the "id" key in the config ' | ||||
'for each provider!') | ||||
# Get the provider type specific items. | ||||
rest = self.provider_type_class().to_tuple(self) | ||||
# Provider ID and provider type ID are always the first two items. | ||||
result = (self.provider_id, self.provider_type_id) + rest | ||||
# Make sure that all items are strings. | ||||
stringified = [str(i) for i in result] | ||||
# Concatenate by newline. | ||||
concatenated = '\n'.join(stringified) | ||||
# Percent encode. | ||||
return parse.quote(concatenated, '') | ||||
@classmethod | ||||
def deserialize(cls, config, credentials): | ||||
""" | ||||
A *class method* which reconstructs credentials created by | ||||
:meth:`serialize`. You can also pass it a :class:`.Credentials` | ||||
instance. | ||||
:param dict config: | ||||
The same :doc:`config` used in the :func:`.login` to get the | ||||
credentials. | ||||
:param str credentials: | ||||
:class:`string` The serialized credentials or | ||||
:class:`.Credentials` instance. | ||||
:returns: | ||||
:class:`.Credentials` | ||||
""" | ||||
# Accept both serialized and normal. | ||||
if isinstance(credentials, Credentials): | ||||
return credentials | ||||
decoded = parse.unquote(credentials) | ||||
split = decoded.split('\n') | ||||
# We need the provider ID to move forward. | ||||
if split[0] is None: | ||||
raise CredentialsError( | ||||
'To deserialize credentials you need to specify a unique ' | ||||
'integer under the "id" key in the config for each provider!') | ||||
# Get provider config by short name. | ||||
provider_name = id_to_name(config, int(split[0])) | ||||
cfg = config.get(provider_name) | ||||
# Get the provider class. | ||||
ProviderClass = resolve_provider_class(cfg.get('class_')) | ||||
deserialized = Credentials(config) | ||||
deserialized.provider_id = provider_id | ||||
deserialized.provider_type = ProviderClass.get_type() | ||||
deserialized.provider_type_id = split[1] | ||||
deserialized.provider_class = ProviderClass | ||||
deserialized.provider_name = provider_name | ||||
deserialized.provider_class = ProviderClass | ||||
# Add provider type specific properties. | ||||
return ProviderClass.reconstruct(split[2:], deserialized, cfg) | ||||
class LoginResult(ReprMixin): | ||||
""" | ||||
Result of the :func:`authomatic.login` function. | ||||
""" | ||||
def __init__(self, provider): | ||||
#: A :doc:`provider <providers>` instance. | ||||
self.provider = provider | ||||
#: An instance of the :exc:`authomatic.exceptions.BaseError` subclass. | ||||
self.error = None | ||||
def popup_js(self, callback_name=None, indent=None, | ||||
custom=None, stay_open=False): | ||||
""" | ||||
Returns JavaScript that: | ||||
#. Triggers the ``options.onLoginComplete(result, closer)`` | ||||
handler set with the :ref:`authomatic.setup() <js_setup>` | ||||
function of :ref:`javascript.js <js>`. | ||||
#. Calls the JavasScript callback specified by :data:`callback_name` | ||||
on the opener of the *login handler popup* and passes it the | ||||
*login result* JSON object as first argument and the `closer` | ||||
function which you should call in your callback to close the popup. | ||||
:param str callback_name: | ||||
The name of the javascript callback e.g ``foo.bar.loginCallback`` | ||||
will result in ``window.opener.foo.bar.loginCallback(result);`` | ||||
in the HTML. | ||||
:param int indent: | ||||
The number of spaces to indent the JSON result object. | ||||
If ``0`` or negative, only newlines are added. | ||||
If ``None``, no newlines are added. | ||||
:param custom: | ||||
Any JSON serializable object that will be passed to the | ||||
``result.custom`` attribute. | ||||
:param str stay_open: | ||||
If ``True``, the popup will stay open. | ||||
:returns: | ||||
:class:`str` with JavaScript. | ||||
""" | ||||
custom_callback = """ | ||||
try {{ window.opener.{cb}(result, closer); }} catch(e) {{}} | ||||
""".format(cb=callback_name) if callback_name else '' | ||||
# TODO: Move the window.close() to the opener | ||||
return """ | ||||
(function(){{ | ||||
closer = function(){{ | ||||
window.close(); | ||||
}}; | ||||
var result = {result}; | ||||
result.custom = {custom}; | ||||
{custom_callback} | ||||
try {{ | ||||
window.opener.authomatic.loginComplete(result, closer); | ||||
}} catch(e) {{}} | ||||
}})(); | ||||
""".format(result=self.to_json(indent), | ||||
custom=json.dumps(custom), | ||||
custom_callback=custom_callback, | ||||
stay_open='// ' if stay_open else '') | ||||
def popup_html(self, callback_name=None, indent=None, | ||||
title='Login | {0}', custom=None, stay_open=False): | ||||
""" | ||||
Returns a HTML with JavaScript that: | ||||
#. Triggers the ``options.onLoginComplete(result, closer)`` handler | ||||
set with the :ref:`authomatic.setup() <js_setup>` function of | ||||
:ref:`javascript.js <js>`. | ||||
#. Calls the JavasScript callback specified by :data:`callback_name` | ||||
on the opener of the *login handler popup* and passes it the | ||||
*login result* JSON object as first argument and the `closer` | ||||
function which you should call in your callback to close the popup. | ||||
:param str callback_name: | ||||
The name of the javascript callback e.g ``foo.bar.loginCallback`` | ||||
will result in ``window.opener.foo.bar.loginCallback(result);`` | ||||
in the HTML. | ||||
:param int indent: | ||||
The number of spaces to indent the JSON result object. | ||||
If ``0`` or negative, only newlines are added. | ||||
If ``None``, no newlines are added. | ||||
:param str title: | ||||
The text of the HTML title. You can use ``{0}`` tag inside, | ||||
which will be replaced by the provider name. | ||||
:param custom: | ||||
Any JSON serializable object that will be passed to the | ||||
``result.custom`` attribute. | ||||
:param str stay_open: | ||||
If ``True``, the popup will stay open. | ||||
:returns: | ||||
:class:`str` with HTML. | ||||
""" | ||||
return """ | ||||
<!DOCTYPE html> | ||||
<html> | ||||
<head><title>{title}</title></head> | ||||
<body> | ||||
<script type="text/javascript"> | ||||
{js} | ||||
</script> | ||||
</body> | ||||
</html> | ||||
""".format( | ||||
title=title.format(self.provider.name if self.provider else ''), | ||||
js=self.popup_js(callback_name, indent, custom, stay_open) | ||||
) | ||||
@property | ||||
def user(self): | ||||
""" | ||||
A :class:`.User` instance. | ||||
""" | ||||
return self.provider.user if self.provider else None | ||||
def to_dict(self): | ||||
return dict(provider=self.provider, user=self.user, error=self.error) | ||||
def to_json(self, indent=4): | ||||
return json.dumps(self, default=lambda obj: obj.to_dict( | ||||
) if hasattr(obj, 'to_dict') else '', indent=indent) | ||||
class Response(ReprMixin): | ||||
""" | ||||
Wraps :class:`httplib.HTTPResponse` and adds. | ||||
:attr:`.content` and :attr:`.data` attributes. | ||||
""" | ||||
def __init__(self, httplib_response, content_parser=None): | ||||
""" | ||||
:param httplib_response: | ||||
The wrapped :class:`httplib.HTTPResponse` instance. | ||||
:param function content_parser: | ||||
Callable which accepts :attr:`.content` as argument, | ||||
parses it and returns the parsed data as :class:`dict`. | ||||
""" | ||||
self.httplib_response = httplib_response | ||||
self.content_parser = content_parser or json_qs_parser | ||||
self._data = None | ||||
self._content = None | ||||
#: Same as :attr:`httplib.HTTPResponse.msg`. | ||||
self.msg = httplib_response.msg | ||||
#: Same as :attr:`httplib.HTTPResponse.version`. | ||||
self.version = httplib_response.version | ||||
#: Same as :attr:`httplib.HTTPResponse.status`. | ||||
self.status = httplib_response.status | ||||
#: Same as :attr:`httplib.HTTPResponse.reason`. | ||||
self.reason = httplib_response.reason | ||||
def read(self, amt=None): | ||||
""" | ||||
Same as :meth:`httplib.HTTPResponse.read`. | ||||
:param amt: | ||||
""" | ||||
return self.httplib_response.read(amt) | ||||
def getheader(self, name, default=None): | ||||
""" | ||||
Same as :meth:`httplib.HTTPResponse.getheader`. | ||||
:param name: | ||||
:param default: | ||||
""" | ||||
return self.httplib_response.getheader(name, default) | ||||
def fileno(self): | ||||
""" | ||||
Same as :meth:`httplib.HTTPResponse.fileno`. | ||||
""" | ||||
return self.httplib_response.fileno() | ||||
def getheaders(self): | ||||
""" | ||||
Same as :meth:`httplib.HTTPResponse.getheaders`. | ||||
""" | ||||
return self.httplib_response.getheaders() | ||||
@staticmethod | ||||
def is_binary_string(content): | ||||
""" | ||||
Return true if string is binary data. | ||||
""" | ||||
textchars = (bytearray([7, 8, 9, 10, 12, 13, 27]) + | ||||
bytearray(range(0x20, 0x100))) | ||||
return bool(content.translate(None, textchars)) | ||||
@property | ||||
def content(self): | ||||
""" | ||||
The whole response content. | ||||
""" | ||||
if not self._content: | ||||
content = self.httplib_response.read() | ||||
if self.is_binary_string(content): | ||||
self._content = content | ||||
else: | ||||
self._content = content.decode('utf-8') | ||||
return self._content | ||||
@property | ||||
def data(self): | ||||
""" | ||||
A :class:`dict` of data parsed from :attr:`.content`. | ||||
""" | ||||
if not self._data: | ||||
self._data = self.content_parser(self.content) | ||||
return self._data | ||||
class UserInfoResponse(Response): | ||||
""" | ||||
Inherits from :class:`.Response`, adds :attr:`~UserInfoResponse.user` | ||||
attribute. | ||||
""" | ||||
def __init__(self, user, *args, **kwargs): | ||||
super(UserInfoResponse, self).__init__(*args, **kwargs) | ||||
#: :class:`.User` instance. | ||||
self.user = user | ||||
class RequestElements(tuple): | ||||
""" | ||||
A tuple of ``(url, method, params, headers, body)`` request elements. | ||||
With some additional properties. | ||||
""" | ||||
def __new__(cls, url, method, params, headers, body): | ||||
return tuple.__new__(cls, (url, method, params, headers, body)) | ||||
@property | ||||
def url(self): | ||||
""" | ||||
Request URL. | ||||
""" | ||||
return self[0] | ||||
@property | ||||
def method(self): | ||||
""" | ||||
HTTP method of the request. | ||||
""" | ||||
return self[1] | ||||
@property | ||||
def params(self): | ||||
""" | ||||
Dictionary of request parameters. | ||||
""" | ||||
return self[2] | ||||
@property | ||||
def headers(self): | ||||
""" | ||||
Dictionary of request headers. | ||||
""" | ||||
return self[3] | ||||
@property | ||||
def body(self): | ||||
""" | ||||
:class:`str` Body of ``POST``, ``PUT`` and ``PATCH`` requests. | ||||
""" | ||||
return self[4] | ||||
@property | ||||
def query_string(self): | ||||
""" | ||||
Query string of the request. | ||||
""" | ||||
return parse.urlencode(self.params) | ||||
@property | ||||
def full_url(self): | ||||
""" | ||||
URL with query string. | ||||
""" | ||||
return self.url + '?' + self.query_string | ||||
def to_json(self): | ||||
return json.dumps(dict(url=self.url, | ||||
method=self.method, | ||||
params=self.params, | ||||
headers=self.headers, | ||||
body=self.body)) | ||||
class Authomatic(object): | ||||
def __init__( | ||||
self, config, secret, session_max_age=600, secure_cookie=False, | ||||
session=None, session_save_method=None, report_errors=True, | ||||
debug=False, logging_level=logging.INFO, prefix='authomatic', | ||||
logger=None | ||||
): | ||||
""" | ||||
Encapsulates all the functionality of this package. | ||||
:param dict config: | ||||
:doc:`config` | ||||
:param str secret: | ||||
A secret string that will be used as the key for signing | ||||
:class:`.Session` cookie and as a salt by *CSRF* token generation. | ||||
:param session_max_age: | ||||
Maximum allowed age of :class:`.Session` cookie nonce in seconds. | ||||
:param bool secure_cookie: | ||||
If ``True`` the :class:`.Session` cookie will be saved wit | ||||
``Secure`` attribute. | ||||
:param session: | ||||
Custom dictionary-like session implementation. | ||||
:param callable session_save_method: | ||||
A method of the supplied session or any mechanism that saves the | ||||
session data and cookie. | ||||
:param bool report_errors: | ||||
If ``True`` exceptions encountered during the **login procedure** | ||||
will be caught and reported in the :attr:`.LoginResult.error` | ||||
attribute. | ||||
Default is ``True``. | ||||
:param bool debug: | ||||
If ``True`` traceback of exceptions will be written to response. | ||||
Default is ``False``. | ||||
:param int logging_level: | ||||
The logging level threshold for the default logger as specified in | ||||
the standard Python | ||||
`logging library <http://docs.python.org/2/library/logging.html>`_. | ||||
This setting is ignored when :data:`logger` is set. | ||||
Default is ``logging.INFO``. | ||||
:param str prefix: | ||||
Prefix used as the :class:`.Session` cookie name. | ||||
:param logger: | ||||
A :class:`logging.logger` instance. | ||||
""" | ||||
self.config = config | ||||
self.secret = secret | ||||
self.session_max_age = session_max_age | ||||
self.secure_cookie = secure_cookie | ||||
self.session = session | ||||
self.session_save_method = session_save_method | ||||
self.report_errors = report_errors | ||||
self.debug = debug | ||||
self.logging_level = logging_level | ||||
self.prefix = prefix | ||||
self._logger = logger or logging.getLogger(str(id(self))) | ||||
# Set logging level. | ||||
if logger is None: | ||||
self._logger.setLevel(logging_level) | ||||
def login(self, adapter, provider_name, callback=None, | ||||
session=None, session_saver=None, **kwargs): | ||||
""" | ||||
If :data:`provider_name` specified, launches the login procedure for | ||||
corresponding :doc:`provider </reference/providers>` and returns | ||||
:class:`.LoginResult`. | ||||
If :data:`provider_name` is empty, acts like | ||||
:meth:`.Authomatic.backend`. | ||||
.. warning:: | ||||
The method redirects the **user** to the **provider** which in | ||||
turn redirects **him/her** back to the *request handler* where | ||||
it has been called. | ||||
:param str provider_name: | ||||
Name of the provider as specified in the keys of the :doc:`config`. | ||||
:param callable callback: | ||||
If specified the method will call the callback with | ||||
:class:`.LoginResult` passed as argument and will return nothing. | ||||
:param bool report_errors: | ||||
.. note:: | ||||
Accepts additional keyword arguments that will be passed to | ||||
:doc:`provider <providers>` constructor. | ||||
:returns: | ||||
:class:`.LoginResult` | ||||
""" | ||||
if provider_name: | ||||
# retrieve required settings for current provider and raise | ||||
# exceptions if missing | ||||
provider_settings = self.config.get(provider_name) | ||||
if not provider_settings: | ||||
raise ConfigError('Provider name "{0}" not specified!' | ||||
.format(provider_name)) | ||||
if not (session is None or session_saver is None): | ||||
session = session | ||||
session_saver = session_saver | ||||
else: | ||||
session = Session(adapter=adapter, | ||||
secret=self.secret, | ||||
max_age=self.session_max_age, | ||||
name=self.prefix, | ||||
secure=self.secure_cookie) | ||||
session_saver = session.save | ||||
# Resolve provider class. | ||||
class_ = provider_settings.get('class_') | ||||
if not class_: | ||||
raise ConfigError( | ||||
'The "class_" key not specified in the config' | ||||
' for provider {0}!'.format(provider_name)) | ||||
ProviderClass = resolve_provider_class(class_) | ||||
# FIXME: Find a nicer solution | ||||
ProviderClass._logger = self._logger | ||||
# instantiate provider class | ||||
provider = ProviderClass(self, | ||||
adapter=adapter, | ||||
provider_name=provider_name, | ||||
callback=callback, | ||||
session=session, | ||||
session_saver=session_saver, | ||||
**kwargs) | ||||
# return login result | ||||
return provider.login() | ||||
else: | ||||
# Act like backend. | ||||
self.backend(adapter) | ||||
def credentials(self, credentials): | ||||
""" | ||||
Deserializes credentials. | ||||
:param credentials: | ||||
Credentials serialized with :meth:`.Credentials.serialize` or | ||||
:class:`.Credentials` instance. | ||||
:returns: | ||||
:class:`.Credentials` | ||||
""" | ||||
return Credentials.deserialize(self.config, credentials) | ||||
def access(self, credentials, url, params=None, method='GET', | ||||
headers=None, body='', max_redirects=5, content_parser=None): | ||||
""" | ||||
Accesses **protected resource** on behalf of the **user**. | ||||
:param credentials: | ||||
The **user's** :class:`.Credentials` (serialized or normal). | ||||
:param str url: | ||||
The **protected resource** URL. | ||||
:param str method: | ||||
HTTP method of the request. | ||||
:param dict headers: | ||||
HTTP headers of the request. | ||||
:param str body: | ||||
Body of ``POST``, ``PUT`` and ``PATCH`` requests. | ||||
:param int max_redirects: | ||||
Maximum number of HTTP redirects to follow. | ||||
:param function content_parser: | ||||
A function to be used to parse the :attr:`.Response.data` | ||||
from :attr:`.Response.content`. | ||||
:returns: | ||||
:class:`.Response` | ||||
""" | ||||
# Deserialize credentials. | ||||
credentials = Credentials.deserialize(self.config, credentials) | ||||
# Resolve provider class. | ||||
ProviderClass = credentials.provider_class | ||||
logging.info('ACCESS HEADERS: {0}'.format(headers)) | ||||
# Access resource and return response. | ||||
provider = ProviderClass( | ||||
self, adapter=None, provider_name=credentials.provider_name) | ||||
provider.credentials = credentials | ||||
return provider.access(url=url, | ||||
params=params, | ||||
method=method, | ||||
headers=headers, | ||||
body=body, | ||||
max_redirects=max_redirects, | ||||
content_parser=content_parser) | ||||
def async_access(self, *args, **kwargs): | ||||
""" | ||||
Same as :meth:`.Authomatic.access` but runs asynchronously in a | ||||
separate thread. | ||||
.. warning:: | ||||
|async| | ||||
:returns: | ||||
:class:`.Future` instance representing the separate thread. | ||||
""" | ||||
return Future(self.access, *args, **kwargs) | ||||
def request_elements( | ||||
self, credentials=None, url=None, method='GET', params=None, | ||||
headers=None, body='', json_input=None, return_json=False | ||||
): | ||||
""" | ||||
Creates request elements for accessing **protected resource of a | ||||
user**. Required arguments are :data:`credentials` and :data:`url`. You | ||||
can pass :data:`credentials`, :data:`url`, :data:`method`, and | ||||
:data:`params` as a JSON object. | ||||
:param credentials: | ||||
The **user's** credentials (can be serialized). | ||||
:param str url: | ||||
The url of the protected resource. | ||||
:param str method: | ||||
The HTTP method of the request. | ||||
:param dict params: | ||||
Dictionary of request parameters. | ||||
:param dict headers: | ||||
Dictionary of request headers. | ||||
:param str body: | ||||
Body of ``POST``, ``PUT`` and ``PATCH`` requests. | ||||
:param str json_input: | ||||
you can pass :data:`credentials`, :data:`url`, :data:`method`, | ||||
:data:`params` and :data:`headers` in a JSON object. | ||||
Values from arguments will be used for missing properties. | ||||
:: | ||||
{ | ||||
"credentials": "###", | ||||
"url": "https://example.com/api", | ||||
"method": "POST", | ||||
"params": { | ||||
"foo": "bar" | ||||
}, | ||||
"headers": { | ||||
"baz": "bing", | ||||
"Authorization": "Bearer ###" | ||||
}, | ||||
"body": "Foo bar baz bing." | ||||
} | ||||
:param bool return_json: | ||||
if ``True`` the function returns a json object. | ||||
:: | ||||
{ | ||||
"url": "https://example.com/api", | ||||
"method": "POST", | ||||
"params": { | ||||
"access_token": "###", | ||||
"foo": "bar" | ||||
}, | ||||
"headers": { | ||||
"baz": "bing", | ||||
"Authorization": "Bearer ###" | ||||
}, | ||||
"body": "Foo bar baz bing." | ||||
} | ||||
:returns: | ||||
:class:`.RequestElements` or JSON string. | ||||
""" | ||||
# Parse values from JSON | ||||
if json_input: | ||||
parsed_input = json.loads(json_input) | ||||
credentials = parsed_input.get('credentials', credentials) | ||||
url = parsed_input.get('url', url) | ||||
method = parsed_input.get('method', method) | ||||
params = parsed_input.get('params', params) | ||||
headers = parsed_input.get('headers', headers) | ||||
body = parsed_input.get('body', body) | ||||
if not credentials and url: | ||||
raise RequestElementsError( | ||||
'To create request elements, you must provide credentials ' | ||||
'and URL either as keyword arguments or in the JSON object!') | ||||
# Get the provider class | ||||
credentials = Credentials.deserialize(self.config, credentials) | ||||
ProviderClass = credentials.provider_class | ||||
# Create request elements | ||||
request_elements = ProviderClass.create_request_elements( | ||||
ProviderClass.PROTECTED_RESOURCE_REQUEST_TYPE, | ||||
credentials=credentials, | ||||
url=url, | ||||
method=method, | ||||
params=params, | ||||
headers=headers, | ||||
body=body) | ||||
if return_json: | ||||
return request_elements.to_json() | ||||
else: | ||||
return request_elements | ||||
def backend(self, adapter): | ||||
""" | ||||
Converts a *request handler* to a JSON backend which you can use with | ||||
:ref:`authomatic.js <js>`. | ||||
Just call it inside a *request handler* like this: | ||||
:: | ||||
class JSONHandler(webapp2.RequestHandler): | ||||
def get(self): | ||||
authomatic.backend(Webapp2Adapter(self)) | ||||
:param adapter: | ||||
The only argument is an :doc:`adapter <adapters>`. | ||||
The *request handler* will now accept these request parameters: | ||||
:param str type: | ||||
Type of the request. Either ``auto``, ``fetch`` or ``elements``. | ||||
Default is ``auto``. | ||||
:param str credentials: | ||||
Serialized :class:`.Credentials`. | ||||
:param str url: | ||||
URL of the **protected resource** request. | ||||
:param str method: | ||||
HTTP method of the **protected resource** request. | ||||
:param str body: | ||||
HTTP body of the **protected resource** request. | ||||
:param JSON params: | ||||
HTTP params of the **protected resource** request as a JSON object. | ||||
:param JSON headers: | ||||
HTTP headers of the **protected resource** request as a | ||||
JSON object. | ||||
:param JSON json: | ||||
You can pass all of the aforementioned params except ``type`` | ||||
in a JSON object. | ||||
.. code-block:: javascript | ||||
{ | ||||
"credentials": "######", | ||||
"url": "https://example.com", | ||||
"method": "POST", | ||||
"params": {"foo": "bar"}, | ||||
"headers": {"baz": "bing"}, | ||||
"body": "the body of the request" | ||||
} | ||||
Depending on the ``type`` param, the handler will either write | ||||
a JSON object with *request elements* to the response, | ||||
and add an ``Authomatic-Response-To: elements`` response header, ... | ||||
.. code-block:: javascript | ||||
{ | ||||
"url": "https://example.com/api", | ||||
"method": "POST", | ||||
"params": { | ||||
"access_token": "###", | ||||
"foo": "bar" | ||||
}, | ||||
"headers": { | ||||
"baz": "bing", | ||||
"Authorization": "Bearer ###" | ||||
} | ||||
} | ||||
... or make a fetch to the **protected resource** and forward | ||||
it's response content, status and headers with an additional | ||||
``Authomatic-Response-To: fetch`` header to the response. | ||||
.. warning:: | ||||
The backend will not work if you write anything to the | ||||
response in the handler! | ||||
""" | ||||
AUTHOMATIC_HEADER = 'Authomatic-Response-To' | ||||
# Collect request params | ||||
request_type = adapter.params.get('type', 'auto') | ||||
json_input = adapter.params.get('json') | ||||
credentials = adapter.params.get('credentials') | ||||
url = adapter.params.get('url') | ||||
method = adapter.params.get('method', 'GET') | ||||
body = adapter.params.get('body', '') | ||||
params = adapter.params.get('params') | ||||
params = json.loads(params) if params else {} | ||||
headers = adapter.params.get('headers') | ||||
headers = json.loads(headers) if headers else {} | ||||
ProviderClass = Credentials.deserialize( | ||||
self.config, credentials).provider_class | ||||
if request_type == 'auto': | ||||
# If there is a "callback" param, it's a JSONP request. | ||||
jsonp = params.get('callback') | ||||
# JSONP is possible only with GET method. | ||||
if ProviderClass.supports_jsonp and method is 'GET': | ||||
request_type = 'elements' | ||||
else: | ||||
# Remove the JSONP callback | ||||
if jsonp: | ||||
params.pop('callback') | ||||
request_type = 'fetch' | ||||
if request_type == 'fetch': | ||||
# Access protected resource | ||||
response = self.access( | ||||
credentials, url, params, method, headers, body) | ||||
result = response.content | ||||
# Forward status | ||||
adapter.status = str(response.status) + ' ' + str(response.reason) | ||||
# Forward headers | ||||
for k, v in response.getheaders(): | ||||
logging.info(' {0}: {1}'.format(k, v)) | ||||
adapter.set_header(k, v) | ||||
elif request_type == 'elements': | ||||
# Create request elements | ||||
if json_input: | ||||
result = self.request_elements( | ||||
json_input=json_input, return_json=True) | ||||
else: | ||||
result = self.request_elements(credentials=credentials, | ||||
url=url, | ||||
method=method, | ||||
params=params, | ||||
headers=headers, | ||||
body=body, | ||||
return_json=True) | ||||
adapter.set_header('Content-Type', 'application/json') | ||||
else: | ||||
result = '{"error": "Bad Request!"}' | ||||
# Add the authomatic header | ||||
adapter.set_header(AUTHOMATIC_HEADER, request_type) | ||||
# Write result to response | ||||
adapter.write(result) | ||||