diff --git a/pkgs/python-packages.nix b/pkgs/python-packages.nix
--- a/pkgs/python-packages.nix
+++ b/pkgs/python-packages.nix
@@ -84,17 +84,6 @@ self: super: {
license = [ pkgs.lib.licenses.mit ];
};
};
- "authomatic" = super.buildPythonPackage {
- name = "authomatic-0.1.0.post1";
- doCheck = false;
- src = fetchurl {
- url = "https://code.rhodecode.com/upstream/authomatic/artifacts/download/0-4fe9c041-a567-4f84-be4c-7efa2a606d3c.tar.gz?md5=f6bdc3c769688212db68233e8d2b0383";
- sha256 = "0pc716mva0ym6xd8jwzjbjp8dqxy9069wwwv2aqwb8lyhl4757ab";
- };
- meta = {
- license = [ pkgs.lib.licenses.mit ];
- };
- };
"babel" = super.buildPythonPackage {
name = "babel-1.3";
doCheck = false;
@@ -920,11 +909,11 @@ self: super: {
};
};
"meld3" = super.buildPythonPackage {
- name = "meld3-1.0.2";
+ name = "meld3-2.0.0";
doCheck = false;
src = fetchurl {
- url = "https://files.pythonhosted.org/packages/45/a0/317c6422b26c12fe0161e936fc35f36552069ba8e6f7ecbd99bbffe32a5f/meld3-1.0.2.tar.gz";
- sha256 = "0n4mkwlpsqnmn0dm0wm5hn9nkda0nafl0jdy5sdl5977znh59dzp";
+ url = "https://files.pythonhosted.org/packages/00/3b/023446ddc1bf0b519c369cbe88269c30c6a64bd10af4817c73f560c302f7/meld3-2.0.0.tar.gz";
+ sha256 = "1fbyafwi0d54394hkmp65nf6vk0qm4kipf5z60pdp4244rvadz8y";
};
meta = {
license = [ { fullName = "BSD-derived (http://www.repoze.org/LICENSE.txt)"; } ];
@@ -1271,11 +1260,11 @@ self: super: {
};
};
"pyasn1" = super.buildPythonPackage {
- name = "pyasn1-0.4.6";
+ name = "pyasn1-0.4.7";
doCheck = false;
src = fetchurl {
- url = "https://files.pythonhosted.org/packages/e3/12/dfffc84b783e280e942409d6b651fe4a5a746433c34589da7362db2c99c6/pyasn1-0.4.6.tar.gz";
- sha256 = "11mwdsvrbwvjmny40cxa76h81bbc8jfr1prvw6hw7yvg374xawxp";
+ url = "https://files.pythonhosted.org/packages/ca/f8/2a60a2c88a97558bdd289b6dc9eb75b00bd90ff34155d681ba6dbbcb46b2/pyasn1-0.4.7.tar.gz";
+ sha256 = "0146ryp4g09ycy8p3l2vigmgfg42n4gb8whgg8cysrhxr9b56jd9";
};
meta = {
license = [ pkgs.lib.licenses.bsdOriginal ];
@@ -1738,7 +1727,6 @@ self: super: {
doCheck = true;
propagatedBuildInputs = [
self."amqp"
- self."authomatic"
self."babel"
self."beaker"
self."bleach"
@@ -1916,11 +1904,11 @@ self: super: {
};
};
"setuptools" = super.buildPythonPackage {
- name = "setuptools-41.1.0";
+ name = "setuptools-41.2.0";
doCheck = false;
src = fetchurl {
- url = "https://files.pythonhosted.org/packages/68/0c/e470db6866aedbff3c4c88faf7f81b90343d8ff32cd68b62db1b65037fb4/setuptools-41.1.0.zip";
- sha256 = "1a246z6cikg42adqmpswzjp59hkqwr7xxqs7xyags4cr556bh6f5";
+ url = "https://files.pythonhosted.org/packages/d9/ca/7279974e489e8b65003fe618a1a741d6350227fa2bf48d16be76c7422423/setuptools-41.2.0.zip";
+ sha256 = "04k0dp9msmlv3g3zx7f5p8wdjr6hdf5c0bgmczlc4yncwyx6pf36";
};
meta = {
license = [ pkgs.lib.licenses.mit ];
diff --git a/requirements.txt b/requirements.txt
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,8 +1,6 @@
## dependencies
amqp==2.3.1
-# not released authomatic that has updated some oauth providers
-https://code.rhodecode.com/upstream/authomatic/artifacts/download/0-4fe9c041-a567-4f84-be4c-7efa2a606d3c.tar.gz?md5=f6bdc3c769688212db68233e8d2b0383#egg=authomatic==0.1.0.post1
babel==1.3
beaker==1.9.1
diff --git a/rhodecode/lib/_vendor/__init__.py b/rhodecode/lib/_vendor/__init__.py
new file mode 100644
--- /dev/null
+++ b/rhodecode/lib/_vendor/__init__.py
@@ -0,0 +1,29 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (C) 2012-2019 RhodeCode GmbH
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License, version 3
+# (only), as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+# This program is dual-licensed. If you wish to learn more about the
+# RhodeCode Enterprise Edition, including its added features, Support services,
+# and proprietary license terms, please see https://rhodecode.com/licenses/
+
+# This package contains non rhodecode licensed packages that are
+# vendored for various reasons
+
+import os
+import sys
+
+vendor_dir = os.path.abspath(os.path.dirname(__file__))
+
+sys.path.append(vendor_dir)
diff --git a/rhodecode/lib/_vendor/authomatic/__init__.py b/rhodecode/lib/_vendor/authomatic/__init__.py
new file mode 100755
--- /dev/null
+++ b/rhodecode/lib/_vendor/authomatic/__init__.py
@@ -0,0 +1,14 @@
+# -*- coding: utf-8 -*-
+"""
+Helper functions for use with :class:`Authomatic`.
+
+.. autosummary::
+ :nosignatures:
+
+ authomatic.provider_id
+
+"""
+
+from . import six
+from .core import Authomatic
+from .core import provider_id
diff --git a/rhodecode/lib/_vendor/authomatic/adapters.py b/rhodecode/lib/_vendor/authomatic/adapters.py
new file mode 100755
--- /dev/null
+++ b/rhodecode/lib/_vendor/authomatic/adapters.py
@@ -0,0 +1,282 @@
+# -*- coding: utf-8 -*-
+"""
+Adapters
+--------
+
+.. contents::
+ :backlinks: none
+
+The :func:`authomatic.login` function needs access to functionality like
+getting the **URL** of the handler where it is being called, getting the
+**request params** and **cookies** and **writing the body**, **headers**
+and **status** to the response.
+
+Since implementation of these features varies across Python web frameworks,
+the Authomatic library uses **adapters** to unify these differences into a
+single interface.
+
+Available Adapters
+^^^^^^^^^^^^^^^^^^
+
+If you are missing an adapter for the framework of your choice, please
+open an `enhancement issue `_
+or consider a contribution to this module by
+:ref:`implementing ` one by yourself.
+Its very easy and shouldn't take you more than a few minutes.
+
+.. autoclass:: DjangoAdapter
+ :members:
+
+.. autoclass:: Webapp2Adapter
+ :members:
+
+.. autoclass:: WebObAdapter
+ :members:
+
+.. autoclass:: WerkzeugAdapter
+ :members:
+
+.. _implement_adapters:
+
+Implementing an Adapter
+^^^^^^^^^^^^^^^^^^^^^^^
+
+Implementing an adapter for a Python web framework is pretty easy.
+
+Do it by subclassing the :class:`.BaseAdapter` abstract class.
+There are only **six** members that you need to implement.
+
+Moreover if your framework is based on the |webob|_ or |werkzeug|_ package
+you can subclass the :class:`.WebObAdapter` or :class:`.WerkzeugAdapter`
+respectively.
+
+.. autoclass:: BaseAdapter
+ :members:
+
+"""
+
+import abc
+from authomatic.core import Response
+
+
+class BaseAdapter(object):
+ """
+ Base class for platform adapters.
+
+ Defines common interface for WSGI framework specific functionality.
+
+ """
+
+ __metaclass__ = abc.ABCMeta
+
+ @abc.abstractproperty
+ def params(self):
+ """
+ Must return a :class:`dict` of all request parameters of any HTTP
+ method.
+
+ :returns:
+ :class:`dict`
+
+ """
+
+ @abc.abstractproperty
+ def url(self):
+ """
+ Must return the url of the actual request including path but without
+ query and fragment.
+
+ :returns:
+ :class:`str`
+
+ """
+
+ @abc.abstractproperty
+ def cookies(self):
+ """
+ Must return cookies as a :class:`dict`.
+
+ :returns:
+ :class:`dict`
+
+ """
+
+ @abc.abstractmethod
+ def write(self, value):
+ """
+ Must write specified value to response.
+
+ :param str value:
+ String to be written to response.
+
+ """
+
+ @abc.abstractmethod
+ def set_header(self, key, value):
+ """
+ Must set response headers to ``Key: value``.
+
+ :param str key:
+ Header name.
+
+ :param str value:
+ Header value.
+
+ """
+
+ @abc.abstractmethod
+ def set_status(self, status):
+ """
+ Must set the response status e.g. ``'302 Found'``.
+
+ :param str status:
+ The HTTP response status.
+
+ """
+
+
+class DjangoAdapter(BaseAdapter):
+ """
+ Adapter for the |django|_ framework.
+ """
+
+ def __init__(self, request, response):
+ """
+ :param request:
+ An instance of the :class:`django.http.HttpRequest` class.
+
+ :param response:
+ An instance of the :class:`django.http.HttpResponse` class.
+ """
+ self.request = request
+ self.response = response
+
+ @property
+ def params(self):
+ params = {}
+ params.update(self.request.GET.dict())
+ params.update(self.request.POST.dict())
+ return params
+
+ @property
+ def url(self):
+ return self.request.build_absolute_uri(self.request.path)
+
+ @property
+ def cookies(self):
+ return dict(self.request.COOKIES)
+
+ def write(self, value):
+ self.response.write(value)
+
+ def set_header(self, key, value):
+ self.response[key] = value
+
+ def set_status(self, status):
+ status_code, reason = status.split(' ', 1)
+ self.response.status_code = int(status_code)
+
+
+class WebObAdapter(BaseAdapter):
+ """
+ Adapter for the |webob|_ package.
+ """
+
+ def __init__(self, request, response):
+ """
+ :param request:
+ A |webob|_ :class:`Request` instance.
+
+ :param response:
+ A |webob|_ :class:`Response` instance.
+ """
+ self.request = request
+ self.response = response
+
+ # =========================================================================
+ # Request
+ # =========================================================================
+
+ @property
+ def url(self):
+ return self.request.path_url
+
+ @property
+ def params(self):
+ return dict(self.request.params)
+
+ @property
+ def cookies(self):
+ return dict(self.request.cookies)
+
+ # =========================================================================
+ # Response
+ # =========================================================================
+
+ def write(self, value):
+ self.response.write(value)
+
+ def set_header(self, key, value):
+ self.response.headers[key] = str(value)
+
+ def set_status(self, status):
+ self.response.status = status
+
+
+class Webapp2Adapter(WebObAdapter):
+ """
+ Adapter for the |webapp2|_ framework.
+
+ Inherits from the :class:`.WebObAdapter`.
+
+ """
+
+ def __init__(self, handler):
+ """
+ :param handler:
+ A :class:`webapp2.RequestHandler` instance.
+ """
+ self.request = handler.request
+ self.response = handler.response
+
+
+class WerkzeugAdapter(BaseAdapter):
+ """
+ Adapter for |flask|_ and other |werkzeug|_ based frameworks.
+
+ Thanks to `Mark Steve Samson `_.
+
+ """
+
+ @property
+ def params(self):
+ return self.request.args
+
+ @property
+ def url(self):
+ return self.request.base_url
+
+ @property
+ def cookies(self):
+ return self.request.cookies
+
+ def __init__(self, request, response):
+ """
+ :param request:
+ Instance of the :class:`werkzeug.wrappers.Request` class.
+
+ :param response:
+ Instance of the :class:`werkzeug.wrappers.Response` class.
+ """
+
+ self.request = request
+ self.response = response
+
+ def write(self, value):
+ self.response.data = self.response.data.decode('utf-8') + value
+
+ def set_header(self, key, value):
+ self.response.headers[key] = value
+
+ def set_status(self, status):
+ self.response.status = status
diff --git a/rhodecode/lib/_vendor/authomatic/core.py b/rhodecode/lib/_vendor/authomatic/core.py
new file mode 100755
--- /dev/null
+++ b/rhodecode/lib/_vendor/authomatic/core.py
@@ -0,0 +1,1764 @@
+# -*- 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() `_
+
+ """
+
+ 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 ` 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 `_ 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 ` 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 ` 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 ` 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() `
+ function of :ref:`javascript.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() ` function of
+ :ref:`javascript.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 """
+
+
+ {title}
+
+
+
+
+ """.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 `_.
+ 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 ` 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 ` 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 `.
+
+ 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 `.
+
+ 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)
diff --git a/rhodecode/lib/_vendor/authomatic/exceptions.py b/rhodecode/lib/_vendor/authomatic/exceptions.py
new file mode 100755
--- /dev/null
+++ b/rhodecode/lib/_vendor/authomatic/exceptions.py
@@ -0,0 +1,84 @@
+# -*- coding: utf-8 -*-
+"""
+Provides various exception types for the library.
+"""
+
+
+class BaseError(Exception):
+ """
+ Base error for all errors.
+ """
+
+ def __init__(self, message, original_message='', url='', status=None):
+ super(BaseError, self).__init__(message)
+
+ #: Error message.
+ self.message = message
+
+ #: Original message.
+ self.original_message = original_message
+
+ #: URL related with the error.
+ self.url = url
+
+ #: HTTP status code related with the error.
+ self.status = status
+
+ def to_dict(self):
+ return self.__dict__
+
+
+class ConfigError(BaseError):
+ pass
+
+
+class SessionError(BaseError):
+ pass
+
+
+class CredentialsError(BaseError):
+ pass
+
+
+class HTTPError(BaseError):
+ pass
+
+
+class CSRFError(BaseError):
+ pass
+
+
+class ImportStringError(BaseError):
+ pass
+
+
+class AuthenticationError(BaseError):
+ pass
+
+
+class OAuth1Error(BaseError):
+ pass
+
+
+class OAuth2Error(BaseError):
+ pass
+
+
+class OpenIDError(BaseError):
+ pass
+
+
+class CancellationError(BaseError):
+ pass
+
+
+class FailureError(BaseError):
+ pass
+
+
+class FetchError(BaseError):
+ pass
+
+
+class RequestElementsError(BaseError):
+ pass
diff --git a/rhodecode/lib/_vendor/authomatic/extras/__init__.py b/rhodecode/lib/_vendor/authomatic/extras/__init__.py
new file mode 100755
diff --git a/rhodecode/lib/_vendor/authomatic/extras/flask.py b/rhodecode/lib/_vendor/authomatic/extras/flask.py
new file mode 100755
--- /dev/null
+++ b/rhodecode/lib/_vendor/authomatic/extras/flask.py
@@ -0,0 +1,47 @@
+# -*- coding: utf-8 -*-
+"""
+|flask| Extras
+--------------
+
+Utilities you can use when using this library with the |flask|_ framework.
+
+Thanks to `Mark Steve Samson `_.
+"""
+
+from __future__ import absolute_import
+from functools import wraps
+
+from authomatic.adapters import WerkzeugAdapter
+from authomatic import Authomatic
+from flask import make_response, request, session
+
+
+class FlaskAuthomatic(Authomatic):
+ """
+ Flask Plugin for authomatic support.
+ """
+
+ result = None
+
+ def login(self, *login_args, **login_kwargs):
+ """
+ Decorator for Flask view functions.
+ """
+
+ def decorator(f):
+ @wraps(f)
+ def decorated(*args, **kwargs):
+ self.response = make_response()
+ adapter = WerkzeugAdapter(request, self.response)
+ login_kwargs.setdefault('session', session)
+ login_kwargs.setdefault('session_saver', self.session_saver)
+ self.result = super(FlaskAuthomatic, self).login(
+ adapter,
+ *login_args,
+ **login_kwargs)
+ return f(*args, **kwargs)
+ return decorated
+ return decorator
+
+ def session_saver(self):
+ session.modified = True
diff --git a/rhodecode/lib/_vendor/authomatic/extras/gae/__init__.py b/rhodecode/lib/_vendor/authomatic/extras/gae/__init__.py
new file mode 100755
--- /dev/null
+++ b/rhodecode/lib/_vendor/authomatic/extras/gae/__init__.py
@@ -0,0 +1,241 @@
+# -*- coding: utf-8 -*-
+"""
+|gae| Extras
+------------
+
+Utilities you can use when using this library on |gae|_.
+"""
+
+from google.appengine.ext import ndb
+from webapp2_extras import sessions
+
+from authomatic import exceptions
+from authomatic.extras import interfaces
+from authomatic.extras.gae.openid import NDBOpenIDStore
+
+
+__all__ = ['ndb_config', 'Webapp2Session']
+
+
+class GAEError(exceptions.BaseError):
+ pass
+
+
+class Webapp2Session(interfaces.BaseSession):
+ """
+ A simple wrapper for |webapp2|_ sessions. If you provide a session it wraps
+ it and adds the :meth:`.save` method.
+
+ If you don't provide a session it creates a new one but you must provide
+ the :data:`.secret`.
+
+ For more about |webapp2| sessions see:
+ http://webapp-improved.appspot.com/api/webapp2_extras/sessions.html.
+
+ """
+
+ def __init__(self, handler, session=None, secret=None,
+ cookie_name='webapp2authomatic', backend='memcache',
+ config=None):
+ """
+ .. warning::
+
+ Do not use the ``'securecookie'`` backend with
+ :class:`.providers.OpenID` provider. The
+ `python-openid`_ library saves **non json serializable** objects
+ to session which the ``'securecookie'`` backend cannot cope with.
+
+ :param handler:
+ A :class:`webapp2.RequestHandler` instance.
+
+ :param session:
+ A :class:`webapp2_extras.session.SessionDict` instance.
+
+ :param str secret:
+ The session secret.
+
+ :param str cookie_name:
+ The name of the session cookie.
+
+ :param backend:
+ The session backend. One of ``'memcache'`` or ``'datastore'``.
+
+ :param config:
+ The session config.
+
+ """
+
+ self.handler = handler
+
+ if session is None:
+ if not secret:
+ raise GAEError('Either session or secret must be specified!')
+ else:
+ # Create new session.
+ cfg = config or dict(
+ secret_key=secret, cookie_name=cookie_name)
+ session_store = sessions.SessionStore(handler.request, cfg)
+ self.session_dict = session_store.get_session(backend=backend)
+ else:
+ # Use supplied session.
+ self.session_dict = session
+
+ def save(self):
+ return self.session_dict.container.save_session(self.handler.response)
+
+ def __setitem__(self, key, value):
+ return self.session_dict.__setitem__(key, value)
+
+ def __getitem__(self, key):
+ return self.session_dict.__getitem__(key)
+
+ def __delitem__(self, key):
+ return self.session_dict.__delitem__(key)
+
+ def get(self, key):
+ return self.session_dict.get(key)
+
+
+class NDBConfig(ndb.Model):
+ """
+ |gae| `NDB `_
+ based :doc:`config`.
+
+ .. note::
+
+ By :class:`.OpenID` provider uses :class:`.NDBOpenIDStore`
+ as default :attr:`.OpenID.store`.
+
+ """
+
+ # General properties
+ provider_name = ndb.StringProperty()
+ class_ = ndb.StringProperty()
+
+ # AuthorizationProvider properties
+ provider_id = ndb.IntegerProperty()
+ consumer_key = ndb.StringProperty()
+ consumer_secret = ndb.StringProperty()
+
+ # OAuth2 properties
+ scope = ndb.StringProperty()
+ offline = ndb.BooleanProperty()
+
+ # AuthenticationProvider properties
+ identifier_param = ndb.StringProperty()
+
+ @classmethod
+ def get(cls, key, default=None):
+ """
+ Resembles the :meth:`dict.get` method.
+
+ :returns:
+ A configuration dictionary for specified provider.
+
+ """
+
+ # Query datastore.
+ result = cls.query(cls.provider_name == key).get()
+
+ if result:
+ result_dict = result.to_dict()
+
+ # Use NDBOpenIDStore by default
+ result_dict['store'] = NDBOpenIDStore
+
+ # Convert coma-separated values to list. Currently only scope is
+ # csv.
+ for i in ('scope', ):
+ prop = result_dict.get(i)
+ if prop:
+ result_dict[i] = [s.strip() for s in prop.split(',')]
+ else:
+ result_dict[i] = None
+
+ return result_dict
+ else:
+ return default
+
+ @classmethod
+ def values(cls):
+ """
+ Resembles the :meth:`dict.values` method.
+ """
+
+ # get all items
+ results = cls.query().fetch()
+ # return list of dictionaries
+ return [result.to_dict() for result in results]
+
+ @classmethod
+ def initialize(cls):
+ """
+ Creates an **"Example"** entity of kind **"NDBConfig"** in the
+ datastore if the model is empty and raises and error to inform you that
+ you should populate the model with data.
+
+ .. note::
+
+ The *Datastore Viewer* in the ``_ah/admin/`` won't let you add
+ properties to a model if there is not an entity with that
+ property already. Therefore it is a good idea to keep the
+ **"Example"** entity (which has all possible properties set) in
+ the datastore.
+
+ """
+
+ if not len(cls.query().fetch()):
+
+ example = cls.get_or_insert('Example')
+
+ example.class_ = 'Provider class e.g. ' + \
+ '"authomatic.providers.oauth2.Facebook".'
+ example.provider_name = 'Your custom provider name e.g. "fb".'
+
+ # AuthorizationProvider
+ example.consumer_key = 'Consumer key.'
+ example.consumer_secret = 'Consumer secret'
+ example.provider_id = 1
+
+ # OAuth2
+ example.scope = 'coma, separated, list, of, scopes'
+
+ # AuthenticationProvider
+ example.identifier_param = 'Querystring parameter for claimed ' + \
+ 'id. default is "id"'
+
+ # Save the example
+ example.put()
+
+ # Raise an information error.
+ raise GAEError(
+ 'A NDBConfig data model was created! Go to Datastore Viewer '
+ 'in your dashboard and populate it with data!')
+
+
+def ndb_config():
+ """
+ Allows you to have a **datastore** :doc:`config` instead of a hardcoded
+ one.
+
+ This function creates an **"Example"** entity of kind **"NDBConfig"** in
+ the datastore if the model is empty and raises and error to inform you
+ that you should populate the model with data.
+
+ .. note::
+
+ The *Datastore Viewer* of the |gae|_ admin won't let you add
+ properties to a model if there is not an entity with that property
+ already. Therefore it is a good idea to keep the **"Example"**
+ entity (which has all properties set) in the datastore.
+
+ :raises:
+ :exc:`.GAEError`
+
+ :returns:
+ :class:`.NDBConfig`
+
+ """
+
+ NDBConfig.initialize()
+ return NDBConfig
diff --git a/rhodecode/lib/_vendor/authomatic/extras/gae/openid.py b/rhodecode/lib/_vendor/authomatic/extras/gae/openid.py
new file mode 100755
--- /dev/null
+++ b/rhodecode/lib/_vendor/authomatic/extras/gae/openid.py
@@ -0,0 +1,156 @@
+# -*- coding: utf-8 -*-
+
+# We need absolute import to import from openid library which has the same
+# name as this module
+from __future__ import absolute_import
+import logging
+import datetime
+
+from google.appengine.ext import ndb
+import openid.store.interface
+
+
+class NDBOpenIDStore(ndb.Expando, openid.store.interface.OpenIDStore):
+ """
+ |gae| `NDB `_
+ based implementation of the :class:`openid.store.interface.OpenIDStore`
+ interface of the `python-openid`_ library.
+ """
+
+ serialized = ndb.StringProperty()
+ expiration_date = ndb.DateTimeProperty()
+ # we need issued to sort by most recently issued
+ issued = ndb.IntegerProperty()
+
+ @staticmethod
+ def _log(*args, **kwargs):
+ pass
+
+ @classmethod
+ def storeAssociation(cls, server_url, association):
+ # store an entity with key = server_url
+
+ issued = datetime.datetime.fromtimestamp(association.issued)
+ lifetime = datetime.timedelta(0, association.lifetime)
+
+ expiration_date = issued + lifetime
+ entity = cls.get_or_insert(
+ association.handle, parent=ndb.Key(
+ 'ServerUrl', server_url))
+
+ entity.serialized = association.serialize()
+ entity.expiration_date = expiration_date
+ entity.issued = association.issued
+
+ cls._log(
+ logging.DEBUG,
+ u'NDBOpenIDStore: Putting OpenID association to datastore.')
+
+ entity.put()
+
+ @classmethod
+ def cleanupAssociations(cls):
+ # query for all expired
+ cls._log(
+ logging.DEBUG,
+ u'NDBOpenIDStore: Querying datastore for OpenID associations.')
+ query = cls.query(cls.expiration_date <= datetime.datetime.now())
+
+ # fetch keys only
+ expired = query.fetch(keys_only=True)
+
+ # delete all expired
+ cls._log(
+ logging.DEBUG,
+ u'NDBOpenIDStore: Deleting expired OpenID associations from datastore.')
+ ndb.delete_multi(expired)
+
+ return len(expired)
+
+ @classmethod
+ def getAssociation(cls, server_url, handle=None):
+ cls.cleanupAssociations()
+
+ if handle:
+ key = ndb.Key('ServerUrl', server_url, cls, handle)
+ cls._log(
+ logging.DEBUG,
+ u'NDBOpenIDStore: Getting OpenID association from datastore by key.')
+ entity = key.get()
+ else:
+ # return most recently issued association
+ cls._log(
+ logging.DEBUG,
+ u'NDBOpenIDStore: Querying datastore for OpenID associations by ancestor.')
+ entity = cls.query(ancestor=ndb.Key(
+ 'ServerUrl', server_url)).order(-cls.issued).get()
+
+ if entity and entity.serialized:
+ return openid.association.Association.deserialize(
+ entity.serialized)
+
+ @classmethod
+ def removeAssociation(cls, server_url, handle):
+ key = ndb.Key('ServerUrl', server_url, cls, handle)
+ cls._log(
+ logging.DEBUG,
+ u'NDBOpenIDStore: Getting OpenID association from datastore by key.')
+ if key.get():
+ cls._log(
+ logging.DEBUG,
+ u'NDBOpenIDStore: Deleting OpenID association from datastore.')
+ key.delete()
+ return True
+
+ @classmethod
+ def useNonce(cls, server_url, timestamp, salt):
+
+ # check whether there is already an entity with the same ancestor path
+ # in the datastore
+ key = ndb.Key(
+ 'ServerUrl',
+ str(server_url) or 'x',
+ 'TimeStamp',
+ str(timestamp),
+ cls,
+ str(salt))
+
+ cls._log(
+ logging.DEBUG,
+ u'NDBOpenIDStore: Getting OpenID nonce from datastore by key.')
+ result = key.get()
+
+ if result:
+ # if so, the nonce is not valid so return False
+ cls._log(
+ logging.WARNING,
+ u'NDBOpenIDStore: Nonce was already used!')
+ return False
+ else:
+ # if not, store the key to datastore and return True
+ nonce = cls(key=key)
+ nonce.expiration_date = datetime.datetime.fromtimestamp(
+ timestamp) + datetime.timedelta(0, openid.store.nonce.SKEW)
+ cls._log(
+ logging.DEBUG,
+ u'NDBOpenIDStore: Putting new nonce to datastore.')
+ nonce.put()
+ return True
+
+ @classmethod
+ def cleanupNonces(cls):
+ # get all expired nonces
+ cls._log(
+ logging.DEBUG,
+ u'NDBOpenIDStore: Querying datastore for OpenID nonces ordered by expiration date.')
+ expired = cls.query().filter(
+ cls.expiration_date <= datetime.datetime.now()).fetch(
+ keys_only=True)
+
+ # delete all expired
+ cls._log(
+ logging.DEBUG,
+ u'NDBOpenIDStore: Deleting expired OpenID nonces from datastore.')
+ ndb.delete_multi(expired)
+
+ return len(expired)
diff --git a/rhodecode/lib/_vendor/authomatic/extras/interfaces.py b/rhodecode/lib/_vendor/authomatic/extras/interfaces.py
new file mode 100755
--- /dev/null
+++ b/rhodecode/lib/_vendor/authomatic/extras/interfaces.py
@@ -0,0 +1,73 @@
+# -*- coding: utf-8 -*-
+"""
+Interfaces
+^^^^^^^^^^
+
+If you want to implement framework specific extras, use these abstract
+classes as bases:
+
+"""
+
+import abc
+
+
+class BaseSession(object):
+ """
+ Abstract class for custom session implementations.
+ """
+
+ __metaclass__ = abc.ABCMeta
+
+ @abc.abstractmethod
+ def save(self):
+ """
+ Called only once per request.
+
+ Should implement a mechanism for setting the the session
+ **cookie** and saving the session **data** to storage.
+
+ """
+
+ @abc.abstractmethod
+ def __setitem__(self, key, value):
+ """
+ Same as :meth:`dict.__setitem__`.
+ """
+
+ @abc.abstractmethod
+ def __getitem__(self, key):
+ """
+ Same as :meth:`dict.__getitem__`.
+ """
+
+ @abc.abstractmethod
+ def __delitem__(self, key):
+ """
+ Same as :meth:`dict.__delitem__`.
+ """
+
+ @abc.abstractmethod
+ def get(self, key):
+ """
+ Same as :meth:`dict.get`.
+ """
+
+
+class BaseConfig(object):
+ """
+ Abstract class for :doc:`config` implementations.
+ """
+
+ __metaclass__ = abc.ABCMeta
+
+ @abc.abstractmethod
+ def get(self, key):
+ """
+ Same as :attr:`dict.get`.
+ """
+
+ @abc.abstractmethod
+ def values(self):
+ """
+ Same as :meth:`dict.values`.
+ """
diff --git a/rhodecode/lib/_vendor/authomatic/providers/__init__.py b/rhodecode/lib/_vendor/authomatic/providers/__init__.py
new file mode 100755
--- /dev/null
+++ b/rhodecode/lib/_vendor/authomatic/providers/__init__.py
@@ -0,0 +1,1012 @@
+# -*- coding: utf-8 -*-
+"""
+Abstract Classes for Providers
+------------------------------
+
+Abstract base classes for implementation of protocol specific providers.
+
+.. note::
+
+ Attributes prefixed with ``_x_`` serve the purpose of unification
+ of differences across providers.
+
+.. autosummary::
+
+ login_decorator
+ BaseProvider
+ AuthorizationProvider
+ AuthenticationProvider
+
+"""
+
+import abc
+import base64
+import hashlib
+import logging
+import random
+import sys
+import traceback
+import uuid
+
+import authomatic.core
+from authomatic.exceptions import (
+ ConfigError,
+ FetchError,
+ CredentialsError,
+)
+from authomatic import six
+from authomatic.six.moves import urllib_parse as parse
+from authomatic.six.moves import http_client
+from authomatic.exceptions import CancellationError
+
+__all__ = [
+ 'BaseProvider',
+ 'AuthorizationProvider',
+ 'AuthenticationProvider',
+ 'login_decorator']
+
+
+def _error_traceback_html(exc_info, traceback_):
+ """
+ Generates error traceback HTML.
+
+ :param tuple exc_info:
+ Output of :func:`sys.exc_info` function.
+
+ :param traceback:
+ Output of :func:`traceback.format_exc` function.
+
+ """
+
+ html = """
+
+
+ ERROR: {error}
+
+
+ The Authomatic library encountered an error!
+ {error}
+ {traceback}
+
+
+ """
+
+ return html.format(error=exc_info[1], traceback=traceback_)
+
+
+def login_decorator(func):
+ """
+ Decorate the :meth:`.BaseProvider.login` implementations with this
+ decorator.
+
+ Provides mechanism for error reporting and returning result which
+ makes the :meth:`.BaseProvider.login` implementation cleaner.
+
+ """
+
+ def wrap(provider, *args, **kwargs):
+ error = None
+ result = authomatic.core.LoginResult(provider)
+
+ try:
+ func(provider, *args, **kwargs)
+ except Exception as e: # pylint:disable=broad-except
+ if provider.settings.report_errors:
+ error = e
+ if not isinstance(error, CancellationError):
+ provider._log(
+ logging.ERROR,
+ u'Reported suppressed exception: {0}!'.format(
+ repr(error)),
+ exc_info=1)
+ else:
+ if provider.settings.debug:
+ # TODO: Check whether it actually works without middleware
+ provider.write(
+ _error_traceback_html(
+ sys.exc_info(),
+ traceback.format_exc()))
+ raise
+
+ # If there is user or error the login procedure has finished
+ if provider.user or error:
+ result = authomatic.core.LoginResult(provider)
+ # Add error to result
+ result.error = error
+
+ # delete session cookie
+ if isinstance(provider.session, authomatic.core.Session):
+ provider.session.delete()
+
+ provider._log(logging.INFO, u'Procedure finished.')
+
+ if provider.callback:
+ provider.callback(result)
+ return result
+ else:
+ # Save session
+ provider.save_session()
+
+ return wrap
+
+
+class BaseProvider(object):
+ """
+ Abstract base class for all providers.
+ """
+
+ PROVIDER_TYPE_ID = 0
+
+ _repr_ignore = ('user',)
+
+ __metaclass__ = abc.ABCMeta
+
+ supported_user_attributes = authomatic.core.SupportedUserAttributes()
+
+ def __init__(self, settings, adapter, provider_name, session=None,
+ session_saver=None, callback=None, js_callback=None,
+ prefix='authomatic', **kwargs):
+
+ self.settings = settings
+ self.adapter = adapter
+
+ self.session = session
+ self.save_session = session_saver
+
+ #: :class:`str` The provider name as specified in the :doc:`config`.
+ self.name = provider_name
+
+ #: :class:`callable` An optional callback called when the login
+ #: procedure is finished with :class:`.core.LoginResult` passed as
+ #: argument.
+ self.callback = callback
+
+ #: :class:`str` Name of an optional javascript callback.
+ self.js_callback = js_callback
+
+ #: :class:`.core.User`.
+ self.user = None
+
+ #: :class:`bool` If ``True``, the
+ #: :attr:`.BaseProvider.user_authorization_url` will be displayed
+ #: in a *popup mode*, if the **provider** supports it.
+ self.popup = self._kwarg(kwargs, 'popup')
+
+ @property
+ def url(self):
+ return self.adapter.url
+
+ @property
+ def params(self):
+ return self.adapter.params
+
+ def write(self, value):
+ self.adapter.write(value)
+
+ def set_header(self, key, value):
+ self.adapter.set_header(key, value)
+
+ def set_status(self, status):
+ self.adapter.set_status(status)
+
+ def redirect(self, url):
+ self.set_status('302 Found')
+ self.set_header('Location', url)
+
+ # ========================================================================
+ # Abstract methods
+ # ========================================================================
+
+ @abc.abstractmethod
+ def login(self):
+ """
+ Launches the *login procedure* to get **user's credentials** from
+ **provider**.
+
+ Should be decorated with :func:`.login_decorator`. The *login
+ procedure* is considered finished when the :attr:`.user`
+ attribute is not empty when the method runs out of it's flow or
+ when there are errors.
+
+ """
+
+ # ========================================================================
+ # Exposed methods
+ # ========================================================================
+
+ def to_dict(self):
+ """
+ Converts the provider instance to a :class:`dict`.
+
+ :returns:
+ :class:`dict`
+
+ """
+
+ return dict(name=self.name,
+ id=getattr(self, 'id', None),
+ type_id=self.type_id,
+ type=self.get_type(),
+ scope=getattr(self, 'scope', None),
+ user=self.user.id if self.user else None)
+
+ @classmethod
+ def get_type(cls):
+ """
+ Returns the provider type.
+
+ :returns:
+ :class:`str` The full dotted path to base class e.g.
+ :literal:`"authomatic.providers.oauth2.OAuth2"`.
+
+ """
+
+ return cls.__module__ + '.' + cls.__bases__[0].__name__
+
+ def update_user(self):
+ """
+ Updates and returns :attr:`.user`.
+
+ :returns:
+ :class:`.User`
+
+ """
+
+ # ========================================================================
+ # Internal methods
+ # ========================================================================
+
+ @property
+ def type_id(self):
+ pass
+
+ def _kwarg(self, kwargs, kwname, default=None):
+ """
+ Resolves keyword arguments from constructor or :doc:`config`.
+
+ .. note::
+
+ The keyword arguments take this order of precedence:
+
+ 1. Arguments passed to constructor through the
+ :func:`authomatic.login`.
+ 2. Provider specific arguments from :doc:`config`.
+ 3. Arguments from :doc:`config` set in the ``__defaults__`` key.
+ 2. The value from :data:`default` argument.
+
+ :param dict kwargs:
+ Keyword arguments dictionary.
+ :param str kwname:
+ Name of the desired keyword argument.
+
+ """
+
+ return kwargs.get(kwname) or \
+ self.settings.config.get(self.name, {}).get(kwname) or \
+ self.settings.config.get('__defaults__', {}).get(kwname) or \
+ default
+
+ def _session_key(self, key):
+ """
+ Generates session key string.
+
+ :param str key:
+ e.g. ``"authomatic:facebook:key"``
+
+ """
+
+ return '{0}:{1}:{2}'.format(self.settings.prefix, self.name, key)
+
+ def _session_set(self, key, value):
+ """
+ Saves a value to session.
+ """
+
+ self.session[self._session_key(key)] = value
+
+ def _session_get(self, key):
+ """
+ Retrieves a value from session.
+ """
+
+ return self.session.get(self._session_key(key))
+
+ @staticmethod
+ def csrf_generator(secret):
+ """
+ Generates CSRF token.
+
+ Inspired by this article:
+ http://blog.ptsecurity.com/2012/10/random-number-security-in-python.html
+
+ :returns:
+ :class:`str` Random unguessable string.
+
+ """
+
+ # Create hash from random string plus salt.
+ hashed = hashlib.md5(uuid.uuid4().bytes + six.b(secret)).hexdigest()
+
+ # Each time return random portion of the hash.
+ span = 5
+ shift = random.randint(0, span)
+ return hashed[shift:shift - span - 1]
+
+ @classmethod
+ def _log(cls, level, msg, **kwargs):
+ """
+ Logs a message with pre-formatted prefix.
+
+ :param int level:
+ Logging level as specified in the
+ `login module `_ of
+ Python standard library.
+
+ :param str msg:
+ The actual message.
+
+ """
+
+ logger = getattr(cls, '_logger', None) or authomatic.core._logger
+ logger.log(
+ level, ': '.join(
+ ('authomatic', cls.__name__, msg)), **kwargs)
+
+ def _fetch(self, url, method='GET', params=None, headers=None,
+ body='', max_redirects=5, content_parser=None):
+ """
+ Fetches a URL.
+
+ :param str url:
+ The URL to fetch.
+
+ :param str method:
+ HTTP method of the request.
+
+ :param dict params:
+ Dictionary of request parameters.
+
+ :param dict headers:
+ HTTP headers of the request.
+
+ :param str body:
+ Body of ``POST``, ``PUT`` and ``PATCH`` requests.
+
+ :param int max_redirects:
+ Number of maximum HTTP redirects to follow.
+
+ :param function content_parser:
+ A callable to be used to parse the :attr:`.Response.data`
+ from :attr:`.Response.content`.
+
+ """
+ # 'magic' using _kwarg method
+ # pylint:disable=no-member
+ params = params or {}
+ params.update(self.access_params)
+
+ headers = headers or {}
+ headers.update(self.access_headers)
+
+ scheme, host, path, query, fragment = parse.urlsplit(url)
+ query = parse.urlencode(params)
+
+ if method in ('POST', 'PUT', 'PATCH'):
+ if not body:
+ # Put querystring to body
+ body = query
+ query = ''
+ headers.update(
+ {'Content-Type': 'application/x-www-form-urlencoded'})
+ request_path = parse.urlunsplit(('', '', path or '', query or '', ''))
+
+ self._log(logging.DEBUG, u' \u251C\u2500 host: {0}'.format(host))
+ self._log(
+ logging.DEBUG,
+ u' \u251C\u2500 path: {0}'.format(request_path))
+ self._log(logging.DEBUG, u' \u251C\u2500 method: {0}'.format(method))
+ self._log(logging.DEBUG, u' \u251C\u2500 body: {0}'.format(body))
+ self._log(logging.DEBUG, u' \u251C\u2500 params: {0}'.format(params))
+ self._log(logging.DEBUG, u' \u2514\u2500 headers: {0}'.format(headers))
+
+ # Connect
+ if scheme.lower() == 'https':
+ connection = http_client.HTTPSConnection(host)
+ else:
+ connection = http_client.HTTPConnection(host)
+
+ try:
+ connection.request(method, request_path, body, headers)
+ except Exception as e:
+ raise FetchError('Fetching URL failed',
+ original_message=str(e),
+ url=request_path)
+
+ response = connection.getresponse()
+ location = response.getheader('Location')
+
+ if response.status in (300, 301, 302, 303, 307) and location:
+ if location == url:
+ raise FetchError('Url redirects to itself!',
+ url=location,
+ status=response.status)
+
+ elif max_redirects > 0:
+ remaining_redirects = max_redirects - 1
+
+ self._log(logging.DEBUG, u'Redirecting to {0}'.format(url))
+ self._log(logging.DEBUG, u'Remaining redirects: {0}'
+ .format(remaining_redirects))
+
+ # Call this method again.
+ response = self._fetch(url=location,
+ params=params,
+ method=method,
+ headers=headers,
+ max_redirects=remaining_redirects)
+
+ else:
+ raise FetchError('Max redirects reached!',
+ url=location,
+ status=response.status)
+ else:
+ self._log(logging.DEBUG, u'Got response:')
+ self._log(logging.DEBUG, u' \u251C\u2500 url: {0}'.format(url))
+ self._log(
+ logging.DEBUG,
+ u' \u251C\u2500 status: {0}'.format(
+ response.status))
+ self._log(
+ logging.DEBUG,
+ u' \u2514\u2500 headers: {0}'.format(
+ response.getheaders()))
+
+ return authomatic.core.Response(response, content_parser)
+
+ def _update_or_create_user(self, data, credentials=None, content=None):
+ """
+ Updates or creates :attr:`.user`.
+
+ :returns:
+ :class:`.User`
+
+ """
+
+ if not self.user:
+ self.user = authomatic.core.User(self, credentials=credentials)
+
+ self.user.content = content
+ self.user.data = data
+
+ # Update.
+ for key in self.user.__dict__:
+ # Exclude data.
+ if key not in ('data', 'content'):
+ # Extract every data item whose key matches the user
+ # property name, but only if it has a value.
+ value = data.get(key)
+ if value:
+ setattr(self.user, key, value)
+
+ # Handle different structure of data by different providers.
+ self.user = self._x_user_parser(self.user, data)
+
+ if self.user.id:
+ self.user.id = str(self.user.id)
+
+ # TODO: Move to User
+ # If there is no user.name,
+ if not self.user.name:
+ if self.user.first_name and self.user.last_name:
+ # Create it from first name and last name if available.
+ self.user.name = ' '.join((self.user.first_name,
+ self.user.last_name))
+ else:
+ # Or use one of these.
+ self.user.name = (self.user.username or self.user.nickname or
+ self.user.first_name or self.user.last_name)
+
+ if not self.user.location:
+ if self.user.city and self.user.country:
+ self.user.location = '{0}, {1}'.format(self.user.city,
+ self.user.country)
+ else:
+ self.user.location = self.user.city or self.user.country
+
+ return self.user
+
+ @staticmethod
+ def _x_user_parser(user, data):
+ """
+ Handles different structure of user info data by different providers.
+
+ :param user:
+ :class:`.User`
+ :param dict data:
+ User info data returned by provider.
+
+ """
+
+ return user
+
+ @staticmethod
+ def _http_status_in_category(status, category):
+ """
+ Checks whether a HTTP status code is in the category denoted by the
+ hundreds digit.
+ """
+
+ assert category < 10, 'HTTP status category must be a one-digit int!'
+ cat = category * 100
+ return status >= cat and status < cat + 100
+
+
+class AuthorizationProvider(BaseProvider):
+ """
+ Base provider for *authorization protocols* i.e. protocols which allow a
+ **provider** to authorize a **consumer** to access **protected resources**
+ of a **user**.
+
+ e.g. `OAuth 2.0 `_ or `OAuth 1.0a
+ `_.
+
+ """
+
+ USER_AUTHORIZATION_REQUEST_TYPE = 2
+ ACCESS_TOKEN_REQUEST_TYPE = 3
+ PROTECTED_RESOURCE_REQUEST_TYPE = 4
+ REFRESH_TOKEN_REQUEST_TYPE = 5
+
+ BEARER = 'Bearer'
+
+ _x_term_dict = {}
+
+ #: If ``True`` the provider doesn't support Cross-site HTTP requests.
+ same_origin = True
+
+ #: :class:`bool` Whether the provider supports JSONP requests.
+ supports_jsonp = False
+
+ # Whether to use the HTTP Authorization header.
+ _x_use_authorization_header = True
+
+ def __init__(self, *args, **kwargs):
+ """
+ Accepts additional keyword arguments:
+
+ :arg str consumer_key:
+ The *key* assigned to our application (**consumer**) by the
+ **provider**.
+
+ :arg str consumer_secret:
+ The *secret* assigned to our application (**consumer**) by the
+ **provider**.
+
+ :arg int id:
+ A unique numeric ID used to serialize :class:`.Credentials`.
+
+ :arg dict user_authorization_params:
+ A dictionary of additional request parameters for
+ **user authorization request**.
+
+ :arg dict access_token_params:
+ A dictionary of additional request parameters for
+ **access_with_credentials token request**.
+
+ :arg dict access_headers:
+ A dictionary of default HTTP headers that will be used when
+ accessing **user's** protected resources.
+ Applied by :meth:`.access()`, :meth:`.update_user()` and
+ :meth:`.User.update()`
+
+ :arg dict access_params:
+ A dictionary of default query string parameters that will be used
+ when accessing **user's** protected resources.
+ Applied by :meth:`.access()`, :meth:`.update_user()` and
+ :meth:`.User.update()`
+
+ """
+
+ super(AuthorizationProvider, self).__init__(*args, **kwargs)
+
+ self.consumer_key = self._kwarg(kwargs, 'consumer_key')
+ self.consumer_secret = self._kwarg(kwargs, 'consumer_secret')
+
+ self.user_authorization_params = self._kwarg(
+ kwargs, 'user_authorization_params', {})
+
+ self.access_token_headers = self._kwarg(
+ kwargs, 'user_authorization_headers', {})
+ self.access_token_params = self._kwarg(
+ kwargs, 'access_token_params', {})
+
+ self.id = self._kwarg(kwargs, 'id')
+
+ self.access_headers = self._kwarg(kwargs, 'access_headers', {})
+ self.access_params = self._kwarg(kwargs, 'access_params', {})
+
+ #: :class:`.Credentials` to access **user's protected resources**.
+ self.credentials = authomatic.core.Credentials(
+ self.settings.config, provider=self)
+
+ #: Response of the *access token request*.
+ self.access_token_response = None
+
+ # ========================================================================
+ # Abstract properties
+ # ========================================================================
+
+ @abc.abstractproperty
+ def user_authorization_url(self):
+ """
+ :class:`str` URL to which we redirect the **user** to grant our app
+ i.e. the **consumer** an **authorization** to access his
+ **protected resources**. See
+ http://tools.ietf.org/html/rfc6749#section-4.1.1 and
+ http://oauth.net/core/1.0a/#auth_step2.
+ """
+
+ @abc.abstractproperty
+ def access_token_url(self):
+ """
+ :class:`str` URL where we can get the *access token* to access
+ **protected resources** of a **user**. See
+ http://tools.ietf.org/html/rfc6749#section-4.1.3 and
+ http://oauth.net/core/1.0a/#auth_step3.
+ """
+
+ @abc.abstractproperty
+ def user_info_url(self):
+ """
+ :class:`str` URL where we can get the **user** info.
+ see http://tools.ietf.org/html/rfc6749#section-7 and
+ http://oauth.net/core/1.0a/#anchor12.
+ """
+
+ # ========================================================================
+ # Abstract methods
+ # ========================================================================
+
+ @abc.abstractmethod
+ def to_tuple(self, credentials):
+ """
+ Must convert :data:`credentials` to a :class:`tuple` to be used by
+ :meth:`.Credentials.serialize`.
+
+ .. warning::
+
+ |classmethod|
+
+ :param credentials:
+ :class:`.Credentials`
+
+ :returns:
+ :class:`tuple`
+
+ """
+
+ @abc.abstractmethod
+ def reconstruct(self, deserialized_tuple, credentials, cfg):
+ """
+ Must convert the :data:`deserialized_tuple` back to
+ :class:`.Credentials`.
+
+ .. warning::
+
+ |classmethod|
+
+ :param tuple deserialized_tuple:
+ A tuple whose first index is the :attr:`.id` and the rest
+ are all the items of the :class:`tuple` created by
+ :meth:`.to_tuple`.
+
+ :param credentials:
+ A :class:`.Credentials` instance.
+
+ :param dict cfg:
+ Provider configuration from :doc:`config`.
+
+ """
+
+ @abc.abstractmethod
+ def create_request_elements(self, request_type, credentials,
+ url, method='GET', params=None, headers=None,
+ body=''):
+ """
+ Must return :class:`.RequestElements`.
+
+ .. warning::
+
+ |classmethod|
+
+ :param int request_type:
+ Type of the request specified by one of the class's constants.
+
+ :param credentials:
+ :class:`.Credentials` of the **user** whose
+ **protected resource** we want to access.
+
+ :param str url:
+ URL of the request.
+
+ :param str method:
+ 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.
+
+ :returns:
+ :class:`.RequestElements`
+
+ """
+
+ # ========================================================================
+ # Exposed methods
+ # ========================================================================
+
+ @property
+ def type_id(self):
+ """
+ A short string representing the provider implementation id used for
+ serialization of :class:`.Credentials` and to identify the type of
+ provider in JavaScript.
+
+ The part before hyphen denotes the type of the provider, the part
+ after hyphen denotes the class id e.g.
+ ``oauth2.Facebook.type_id = '2-5'``,
+ ``oauth1.Twitter.type_id = '1-5'``.
+
+ """
+
+ cls = self.__class__
+ mod = sys.modules.get(cls.__module__)
+
+ return str(self.PROVIDER_TYPE_ID) + '-' + \
+ str(mod.PROVIDER_ID_MAP.index(cls))
+
+ def access(self, url, params=None, method='GET', headers=None,
+ body='', max_redirects=5, content_parser=None):
+ """
+ Fetches the **protected resource** of an authenticated **user**.
+
+ :param credentials:
+ The **user's** :class:`.Credentials` (serialized or normal).
+
+ :param str url:
+ The URL of the **protected resource**.
+
+ :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`
+
+ """
+
+ if not self.user and not self.credentials:
+ raise CredentialsError(u'There is no authenticated user!')
+
+ headers = headers or {}
+
+ self._log(
+ logging.INFO,
+ u'Accessing protected resource {0}.'.format(url))
+
+ request_elements = self.create_request_elements(
+ request_type=self.PROTECTED_RESOURCE_REQUEST_TYPE,
+ credentials=self.credentials,
+ url=url,
+ body=body,
+ params=params,
+ headers=headers,
+ method=method
+ )
+
+ response = self._fetch(*request_elements,
+ max_redirects=max_redirects,
+ content_parser=content_parser)
+
+ self._log(
+ logging.INFO,
+ u'Got response. HTTP status = {0}.'.format(
+ response.status))
+ return response
+
+ def async_access(self, *args, **kwargs):
+ """
+ Same as :meth:`.access` but runs asynchronously in a separate thread.
+
+ .. warning::
+
+ |async|
+
+ :returns:
+ :class:`.Future` instance representing the separate thread.
+
+ """
+
+ return authomatic.core.Future(self.access, *args, **kwargs)
+
+ def update_user(self):
+ """
+ Updates the :attr:`.BaseProvider.user`.
+
+ .. warning::
+ Fetches the :attr:`.user_info_url`!
+
+ :returns:
+ :class:`.UserInfoResponse`
+
+ """
+ if self.user_info_url:
+ response = self._access_user_info()
+ self.user = self._update_or_create_user(response.data,
+ content=response.content)
+ return authomatic.core.UserInfoResponse(self.user,
+ response.httplib_response)
+
+ # ========================================================================
+ # Internal methods
+ # ========================================================================
+
+ @classmethod
+ def _authorization_header(cls, credentials):
+ """
+ Creates authorization headers if the provider supports it. See:
+ http://en.wikipedia.org/wiki/Basic_access_authentication.
+
+ :param credentials:
+ :class:`.Credentials`
+
+ :returns:
+ Headers as :class:`dict`.
+
+ """
+
+ if cls._x_use_authorization_header:
+ res = ':'.join(
+ (credentials.consumer_key,
+ credentials.consumer_secret))
+ res = base64.b64encode(six.b(res)).decode()
+ return {'Authorization': 'Basic {0}'.format(res)}
+ else:
+ return {}
+
+ def _check_consumer(self):
+ """
+ Validates the :attr:`.consumer`.
+ """
+
+ # 'magic' using _kwarg method
+ # pylint:disable=no-member
+ if not self.consumer.key:
+ raise ConfigError(
+ 'Consumer key not specified for provider {0}!'.format(
+ self.name))
+
+ if not self.consumer.secret:
+ raise ConfigError(
+ 'Consumer secret not specified for provider {0}!'.format(
+ self.name))
+
+ @staticmethod
+ def _split_url(url):
+ """
+ Splits given url to url base and params converted to list of tuples.
+ """
+
+ split = parse.urlsplit(url)
+ base = parse.urlunsplit((split.scheme, split.netloc, split.path, 0, 0))
+ params = parse.parse_qsl(split.query, True)
+
+ return base, params
+
+ @classmethod
+ def _x_request_elements_filter(
+ cls, request_type, request_elements, credentials):
+ """
+ Override this to handle special request requirements of zealous
+ providers.
+
+ .. warning::
+
+ |classmethod|
+
+ :param int request_type:
+ Type of request.
+
+ :param request_elements:
+ :class:`.RequestElements`
+
+ :param credentials:
+ :class:`.Credentials`
+
+ :returns:
+ :class:`.RequestElements`
+
+ """
+
+ return request_elements
+
+ @staticmethod
+ def _x_credentials_parser(credentials, data):
+ """
+ Override this to handle differences in naming conventions across
+ providers.
+
+ :param credentials:
+ :class:`.Credentials`
+
+ :param dict data:
+ Response data dictionary.
+
+ :returns:
+ :class:`.Credentials`
+
+ """
+ return credentials
+
+ def _access_user_info(self):
+ """
+ Accesses the :attr:`.user_info_url`.
+
+ :returns:
+ :class:`.UserInfoResponse`
+
+ """
+ url = self.user_info_url.format(**self.user.__dict__)
+ return self.access(url)
+
+
+class AuthenticationProvider(BaseProvider):
+ """
+ Base provider for *authentication protocols* i.e. protocols which allow a
+ **provider** to authenticate a *claimed identity* of a **user**.
+
+ e.g. `OpenID `_.
+
+ """
+
+ #: Indicates whether the **provider** supports access_with_credentials to
+ #: **user's** protected resources.
+ # TODO: Useless
+ has_protected_resources = False
+
+ def __init__(self, *args, **kwargs):
+ super(AuthenticationProvider, self).__init__(*args, **kwargs)
+
+ # Lookup default identifier, if available in provider
+ default_identifier = getattr(self, 'identifier', None)
+
+ # Allow for custom name for the "id" querystring parameter.
+ self.identifier_param = kwargs.get('identifier_param', 'id')
+
+ # Get the identifier from request params, or use default as fallback.
+ self.identifier = self.params.get(
+ self.identifier_param, default_identifier)
+
+
+PROVIDER_ID_MAP = [
+ AuthenticationProvider,
+ AuthorizationProvider,
+ BaseProvider,
+]
diff --git a/rhodecode/lib/_vendor/authomatic/providers/gaeopenid.py b/rhodecode/lib/_vendor/authomatic/providers/gaeopenid.py
new file mode 100755
--- /dev/null
+++ b/rhodecode/lib/_vendor/authomatic/providers/gaeopenid.py
@@ -0,0 +1,112 @@
+# -*- coding: utf-8 -*-
+"""
+Google App Engine OpenID Providers
+----------------------------------
+
+|openid|_ provider implementations based on the |gae_users_api|_.
+
+.. note::
+
+ When using the :class:`GAEOpenID` provider, the :class:`.User` object
+ will always have only the
+ :attr:`.User.user_id`,
+ :attr:`.User.email`,
+ :attr:`.User.gae_user`
+ attributes populated with data.
+ Moreover the :attr:`.User.user_id` will always be empty on the
+ `GAE Development Server
+ `_.
+
+.. autosummary::
+
+ GAEOpenID
+ Yahoo
+ Google
+
+"""
+
+import logging
+
+from google.appengine.api import users
+
+import authomatic.core as core
+from authomatic import providers
+from authomatic.exceptions import FailureError
+
+
+__all__ = ['GAEOpenID', 'Yahoo', 'Google']
+
+
+class GAEOpenID(providers.AuthenticationProvider):
+ """
+ |openid|_ provider based on the |gae_users_api|_.
+
+ Accepts additional keyword arguments inherited from
+ :class:`.AuthenticationProvider`.
+
+ """
+
+ @providers.login_decorator
+ def login(self):
+ """
+ Launches the OpenID authentication procedure.
+ """
+
+ if self.params.get(self.identifier_param):
+ # =================================================================
+ # Phase 1 before redirect.
+ # =================================================================
+ self._log(
+ logging.INFO,
+ u'Starting OpenID authentication procedure.')
+
+ url = users.create_login_url(
+ dest_url=self.url, federated_identity=self.identifier)
+
+ self._log(logging.INFO, u'Redirecting user to {0}.'.format(url))
+
+ self.redirect(url)
+ else:
+ # =================================================================
+ # Phase 2 after redirect.
+ # =================================================================
+
+ self._log(
+ logging.INFO,
+ u'Continuing OpenID authentication procedure after redirect.')
+
+ user = users.get_current_user()
+
+ if user:
+ self._log(logging.INFO, u'Authentication successful.')
+ self._log(logging.INFO, u'Creating user.')
+ self.user = core.User(self,
+ id=user.federated_identity(),
+ email=user.email(),
+ gae_user=user)
+
+ # =============================================================
+ # We're done
+ # =============================================================
+ else:
+ raise FailureError(
+ 'Unable to authenticate identifier "{0}"!'.format(
+ self.identifier))
+
+
+class Yahoo(GAEOpenID):
+ """
+ :class:`.GAEOpenID` provider with the :attr:`.identifier` set to
+ ``"me.yahoo.com"``.
+ """
+
+ identifier = 'me.yahoo.com'
+
+
+class Google(GAEOpenID):
+ """
+ :class:`.GAEOpenID` provider with the :attr:`.identifier` set to
+ ``"https://www.google.com/accounts/o8/id"``.
+ """
+
+ identifier = 'https://www.google.com/accounts/o8/id'
diff --git a/rhodecode/lib/_vendor/authomatic/providers/oauth1.py b/rhodecode/lib/_vendor/authomatic/providers/oauth1.py
new file mode 100755
--- /dev/null
+++ b/rhodecode/lib/_vendor/authomatic/providers/oauth1.py
@@ -0,0 +1,1377 @@
+# -*- coding: utf-8 -*-
+"""
+|oauth1| Providers
+--------------------
+
+Providers which implement the |oauth1|_ protocol.
+
+.. autosummary::
+
+ OAuth1
+ Bitbucket
+ Flickr
+ Meetup
+ Plurk
+ Twitter
+ Tumblr
+ UbuntuOne
+ Vimeo
+ Xero
+ Xing
+ Yahoo
+
+"""
+
+import abc
+import binascii
+import datetime
+import hashlib
+import hmac
+import logging
+import time
+import uuid
+
+import authomatic.core as core
+from authomatic import providers
+from authomatic.exceptions import (
+ CancellationError,
+ FailureError,
+ OAuth1Error,
+)
+from authomatic import six
+from authomatic.six.moves import urllib_parse as parse
+
+
+__all__ = [
+ 'OAuth1',
+ 'Bitbucket',
+ 'Flickr',
+ 'Meetup',
+ 'Plurk',
+ 'Twitter',
+ 'Tumblr',
+ 'UbuntuOne',
+ 'Vimeo',
+ 'Xero',
+ 'Xing',
+ 'Yahoo'
+]
+
+
+def _normalize_params(params):
+ """
+ Returns a normalized query string sorted first by key, then by value
+ excluding the ``realm`` and ``oauth_signature`` parameters as specified
+ here: http://oauth.net/core/1.0a/#rfc.section.9.1.1.
+
+ :param params:
+ :class:`dict` or :class:`list` of tuples.
+
+ """
+
+ if isinstance(params, dict):
+ params = list(params.items())
+
+ # remove "realm" and "oauth_signature"
+ params = sorted([
+ (k, v) for k, v in params
+ if k not in ('oauth_signature', 'realm')
+ ])
+ # sort
+ # convert to query string
+ qs = parse.urlencode(params)
+ # replace "+" to "%20"
+ qs = qs.replace('+', '%20')
+ # replace "%7E" to "%20"
+ qs = qs.replace('%7E', '~')
+
+ return qs
+
+
+def _join_by_ampersand(*args):
+ return '&'.join([core.escape(i) for i in args])
+
+
+def _create_base_string(method, base, params):
+ """
+ Returns base string for HMAC-SHA1 signature as specified in:
+ http://oauth.net/core/1.0a/#rfc.section.9.1.3.
+ """
+
+ normalized_qs = _normalize_params(params)
+ return _join_by_ampersand(method, base, normalized_qs)
+
+
+class BaseSignatureGenerator(object):
+ """
+ Abstract base class for all signature generators.
+ """
+
+ __metaclass__ = abc.ABCMeta
+
+ #: :class:`str` The name of the signature method.
+ method = ''
+
+ @abc.abstractmethod
+ def create_signature(self, method, base, params,
+ consumer_secret, token_secret=''):
+ """
+ Must create signature based on the parameters as specified in
+ http://oauth.net/core/1.0a/#signing_process.
+
+ .. warning::
+
+ |classmethod|
+
+ :param str method:
+ HTTP method of the request to be signed.
+
+ :param str base:
+ Base URL of the request without query string an fragment.
+
+ :param dict params:
+ Dictionary or list of tuples of the request parameters.
+
+ :param str consumer_secret:
+ :attr:`.core.Consumer.secret`
+
+ :param str token_secret:
+ Access token secret as specified in
+ http://oauth.net/core/1.0a/#anchor3.
+
+ :returns:
+ The signature string.
+
+ """
+
+
+class HMACSHA1SignatureGenerator(BaseSignatureGenerator):
+ """
+ HMAC-SHA1 signature generator.
+
+ See: http://oauth.net/core/1.0a/#anchor15
+
+ """
+
+ method = 'HMAC-SHA1'
+
+ @classmethod
+ def _create_key(cls, consumer_secret, token_secret=''):
+ """
+ Returns a key for HMAC-SHA1 signature as specified at:
+ http://oauth.net/core/1.0a/#rfc.section.9.2.
+
+ :param str consumer_secret:
+ :attr:`.core.Consumer.secret`
+
+ :param str token_secret:
+ Access token secret as specified in
+ http://oauth.net/core/1.0a/#anchor3.
+
+ :returns:
+ Key to sign the request with.
+
+ """
+
+ return _join_by_ampersand(consumer_secret, token_secret or '')
+
+ @classmethod
+ def create_signature(cls, method, base, params,
+ consumer_secret, token_secret=''):
+ """
+ Returns HMAC-SHA1 signature as specified at:
+ http://oauth.net/core/1.0a/#rfc.section.9.2.
+
+ :param str method:
+ HTTP method of the request to be signed.
+
+ :param str base:
+ Base URL of the request without query string an fragment.
+
+ :param dict params:
+ Dictionary or list of tuples of the request parameters.
+
+ :param str consumer_secret:
+ :attr:`.core.Consumer.secret`
+
+ :param str token_secret:
+ Access token secret as specified in
+ http://oauth.net/core/1.0a/#anchor3.
+
+ :returns:
+ The signature string.
+
+ """
+
+ base_string = _create_base_string(method, base, params)
+ key = cls._create_key(consumer_secret, token_secret)
+
+ hashed = hmac.new(
+ six.b(key),
+ base_string.encode('utf-8'),
+ hashlib.sha1)
+
+ base64_encoded = binascii.b2a_base64(hashed.digest())[:-1]
+
+ return base64_encoded
+
+
+class PLAINTEXTSignatureGenerator(BaseSignatureGenerator):
+ """
+ PLAINTEXT signature generator.
+
+ See: http://oauth.net/core/1.0a/#anchor21
+
+ """
+
+ method = 'PLAINTEXT'
+
+ @classmethod
+ def create_signature(cls, method, base, params,
+ consumer_secret, token_secret=''):
+
+ consumer_secret = parse.quote(consumer_secret, '')
+ token_secret = parse.quote(token_secret, '')
+
+ return parse.quote('&'.join((consumer_secret, token_secret)), '')
+
+
+class OAuth1(providers.AuthorizationProvider):
+ """
+ Base class for |oauth1|_ providers.
+ """
+
+ _signature_generator = HMACSHA1SignatureGenerator
+
+ PROVIDER_TYPE_ID = 1
+ REQUEST_TOKEN_REQUEST_TYPE = 1
+
+ def __init__(self, *args, **kwargs):
+ """
+ Accepts additional keyword arguments:
+
+ :param str consumer_key:
+ The *key* assigned to our application (**consumer**) by
+ the **provider**.
+
+ :param str consumer_secret:
+ The *secret* assigned to our application (**consumer**) by
+ the **provider**.
+
+ :param id:
+ A unique short name used to serialize :class:`.Credentials`.
+
+ :param dict user_authorization_params:
+ A dictionary of additional request parameters for
+ **user authorization request**.
+
+ :param dict access_token_params:
+ A dictionary of additional request parameters for
+ **access token request**.
+
+ :param dict request_token_params:
+ A dictionary of additional request parameters for
+ **request token request**.
+
+ """
+
+ super(OAuth1, self).__init__(*args, **kwargs)
+
+ self.request_token_params = self._kwarg(
+ kwargs, 'request_token_params', {})
+
+ # ========================================================================
+ # Abstract properties
+ # ========================================================================
+
+ @abc.abstractproperty
+ def request_token_url(self):
+ """
+ :class:`str` URL where we can get the |oauth1| request token.
+ see http://oauth.net/core/1.0a/#auth_step1.
+ """
+
+ # ========================================================================
+ # Internal methods
+ # ========================================================================
+
+ @classmethod
+ def create_request_elements(
+ cls, request_type, credentials, url, params=None, headers=None,
+ body='', method='GET', verifier='', callback=''
+ ):
+ """
+ Creates |oauth1| request elements.
+ """
+
+ params = params or {}
+ headers = headers or {}
+
+ consumer_key = credentials.consumer_key or ''
+ consumer_secret = credentials.consumer_secret or ''
+ token = credentials.token or ''
+ token_secret = credentials.token_secret or ''
+
+ # separate url base and query parameters
+ url, base_params = cls._split_url(url)
+
+ # add extracted params to future params
+ params.update(dict(base_params))
+
+ if request_type == cls.USER_AUTHORIZATION_REQUEST_TYPE:
+ # no need for signature
+ if token:
+ params['oauth_token'] = token
+ else:
+ raise OAuth1Error(
+ 'Credentials with valid token are required to create '
+ 'User Authorization URL!')
+ else:
+ # signature needed
+ if request_type == cls.REQUEST_TOKEN_REQUEST_TYPE:
+ # Request Token URL
+ if consumer_key and consumer_secret and callback:
+ params['oauth_consumer_key'] = consumer_key
+ params['oauth_callback'] = callback
+ else:
+ raise OAuth1Error(
+ 'Credentials with valid consumer_key, consumer_secret '
+ 'and callback are required to create Request Token '
+ 'URL!')
+
+ elif request_type == cls.ACCESS_TOKEN_REQUEST_TYPE:
+ # Access Token URL
+ if consumer_key and consumer_secret and token and verifier:
+ params['oauth_token'] = token
+ params['oauth_consumer_key'] = consumer_key
+ params['oauth_verifier'] = verifier
+ else:
+ raise OAuth1Error(
+ 'Credentials with valid consumer_key, '
+ 'consumer_secret, token and argument verifier'
+ ' are required to create Access Token URL!')
+
+ elif request_type == cls.PROTECTED_RESOURCE_REQUEST_TYPE:
+ # Protected Resources URL
+ if consumer_key and consumer_secret and token and token_secret:
+ params['oauth_token'] = token
+ params['oauth_consumer_key'] = consumer_key
+ else:
+ raise OAuth1Error(
+ 'Credentials with valid consumer_key, ' +
+ 'consumer_secret, token and token_secret are required '
+ 'to create Protected Resources URL!')
+
+ # Sign request.
+ # http://oauth.net/core/1.0a/#anchor13
+
+ # Prepare parameters for signature base string
+ # http://oauth.net/core/1.0a/#rfc.section.9.1
+ params['oauth_signature_method'] = cls._signature_generator.method
+ params['oauth_timestamp'] = str(int(time.time()))
+ params['oauth_nonce'] = cls.csrf_generator(str(uuid.uuid4()))
+ params['oauth_version'] = '1.0'
+
+ # add signature to params
+ params['oauth_signature'] = cls._signature_generator.create_signature( # noqa
+ method, url, params, consumer_secret, token_secret)
+
+ request_elements = core.RequestElements(
+ url, method, params, headers, body)
+
+ return cls._x_request_elements_filter(
+ request_type, request_elements, credentials)
+
+ # ========================================================================
+ # Exposed methods
+ # ========================================================================
+
+ @staticmethod
+ def to_tuple(credentials):
+ return (credentials.token, credentials.token_secret)
+
+ @classmethod
+ def reconstruct(cls, deserialized_tuple, credentials, cfg):
+
+ token, token_secret = deserialized_tuple
+
+ credentials.token = token
+ credentials.token_secret = token_secret
+ credentials.consumer_key = cfg.get('consumer_key', '')
+ credentials.consumer_secret = cfg.get('consumer_secret', '')
+
+ return credentials
+
+ @providers.login_decorator
+ def login(self):
+ # get request parameters from which we can determine the login phase
+ denied = self.params.get('denied')
+ verifier = self.params.get('oauth_verifier', '')
+ request_token = self.params.get('oauth_token', '')
+
+ if request_token and verifier:
+ # Phase 2 after redirect with success
+ self._log(
+ logging.INFO,
+ u'Continuing OAuth 1.0a authorization procedure after '
+ u'redirect.')
+ token_secret = self._session_get('token_secret')
+ if not token_secret:
+ raise FailureError(
+ u'Unable to retrieve token secret from storage!')
+
+ # Get Access Token
+ self._log(
+ logging.INFO,
+ u'Fetching for access token from {0}.'.format(
+ self.access_token_url))
+
+ self.credentials.token = request_token
+ self.credentials.token_secret = token_secret
+
+ request_elements = self.create_request_elements(
+ request_type=self.ACCESS_TOKEN_REQUEST_TYPE,
+ url=self.access_token_url,
+ credentials=self.credentials,
+ verifier=verifier,
+ params=self.access_token_params
+ )
+
+ response = self._fetch(*request_elements)
+ self.access_token_response = response
+
+ if not self._http_status_in_category(response.status, 2):
+ raise FailureError(
+ 'Failed to obtain OAuth 1.0a oauth_token from {0}! '
+ 'HTTP status code: {1}.'
+ .format(self.access_token_url, response.status),
+ original_message=response.content,
+ status=response.status,
+ url=self.access_token_url
+ )
+
+ self._log(logging.INFO, u'Got access token.')
+ self.credentials.token = response.data.get('oauth_token', '')
+ self.credentials.token_secret = response.data.get(
+ 'oauth_token_secret', ''
+ )
+
+ self.credentials = self._x_credentials_parser(self.credentials,
+ response.data)
+ self._update_or_create_user(response.data, self.credentials)
+
+ # =================================================================
+ # We're done!
+ # =================================================================
+
+ elif denied:
+ # Phase 2 after redirect denied
+ raise CancellationError(
+ 'User denied the request token {0} during a redirect'
+ 'to {1}!'.format(denied, self.user_authorization_url),
+ original_message=denied,
+ url=self.user_authorization_url)
+ else:
+ # Phase 1 before redirect
+ self._log(
+ logging.INFO,
+ u'Starting OAuth 1.0a authorization procedure.')
+
+ # Fetch for request token
+ request_elements = self.create_request_elements(
+ request_type=self.REQUEST_TOKEN_REQUEST_TYPE,
+ credentials=self.credentials,
+ url=self.request_token_url,
+ callback=self.url,
+ params=self.request_token_params
+ )
+
+ self._log(
+ logging.INFO,
+ u'Fetching for request token and token secret.')
+ response = self._fetch(*request_elements)
+
+ # check if response status is OK
+ if not self._http_status_in_category(response.status, 2):
+ raise FailureError(
+ u'Failed to obtain request token from {0}! HTTP status '
+ u'code: {1} content: {2}'.format(
+ self.request_token_url,
+ response.status,
+ response.content
+ ),
+ original_message=response.content,
+ status=response.status,
+ url=self.request_token_url)
+
+ # extract request token
+ request_token = response.data.get('oauth_token')
+ if not request_token:
+ raise FailureError(
+ 'Response from {0} doesn\'t contain oauth_token '
+ 'parameter!'.format(self.request_token_url),
+ original_message=response.content,
+ url=self.request_token_url)
+
+ # we need request token for user authorization redirect
+ self.credentials.token = request_token
+
+ # extract token secret and save it to storage
+ token_secret = response.data.get('oauth_token_secret')
+ if token_secret:
+ # we need token secret after user authorization redirect to get
+ # access token
+ self._session_set('token_secret', token_secret)
+ else:
+ raise FailureError(
+ u'Failed to obtain token secret from {0}!'.format(
+ self.request_token_url),
+ original_message=response.content,
+ url=self.request_token_url)
+
+ self._log(logging.INFO, u'Got request token and token secret')
+
+ # Create User Authorization URL
+ request_elements = self.create_request_elements(
+ request_type=self.USER_AUTHORIZATION_REQUEST_TYPE,
+ credentials=self.credentials,
+ url=self.user_authorization_url,
+ params=self.user_authorization_params
+ )
+
+ self._log(
+ logging.INFO,
+ u'Redirecting user to {0}.'.format(
+ request_elements.full_url))
+
+ self.redirect(request_elements.full_url)
+
+
+class Bitbucket(OAuth1):
+ """
+ Bitbucket |oauth1| provider.
+
+ * Dashboard: https://bitbucket.org/account/user/peterhudec/api
+ * Docs: https://confluence.atlassian.com/display/BITBUCKET/oauth+Endpoint
+ * API reference:
+ https://confluence.atlassian.com/display/BITBUCKET/Using+the+Bitbucket+REST+APIs
+
+ Supported :class:`.User` properties:
+
+ * first_name
+ * id
+ * last_name
+ * link
+ * name
+ * picture
+ * username
+ * email
+
+ Unsupported :class:`.User` properties:
+
+ * birth_date
+ * city
+ * country
+ * gender
+ * locale
+ * location
+ * nickname
+ * phone
+ * postal_code
+ * timezone
+
+ .. note::
+
+ To get the full user info, you need to select both the *Account Read*
+ and the *Repositories Read* permission in the Bitbucket application
+ edit form.
+
+ """
+
+ supported_user_attributes = core.SupportedUserAttributes(
+ first_name=True,
+ id=True,
+ last_name=True,
+ link=True,
+ name=True,
+ picture=True,
+ username=True,
+ email=True
+ )
+
+ request_token_url = 'https://bitbucket.org/!api/1.0/oauth/request_token'
+ user_authorization_url = 'https://bitbucket.org/!api/1.0/oauth/' + \
+ 'authenticate'
+ access_token_url = 'https://bitbucket.org/!api/1.0/oauth/access_token'
+ user_info_url = 'https://api.bitbucket.org/1.0/user'
+ user_email_url = 'https://api.bitbucket.org/1.0/emails'
+
+ @staticmethod
+ def _x_user_parser(user, data):
+ _user = data.get('user', {})
+ user.username = user.id = _user.get('username')
+ user.name = _user.get('display_name')
+ user.first_name = _user.get('first_name')
+ user.last_name = _user.get('last_name')
+ user.picture = _user.get('avatar')
+ user.link = 'https://bitbucket.org/api{0}'\
+ .format(_user.get('resource_uri'))
+ return user
+
+ def _access_user_info(self):
+ """
+ Email is available in separate method so second request is needed.
+ """
+ response = super(Bitbucket, self)._access_user_info()
+
+ response.data.setdefault("email", None)
+
+ email_response = self.access(self.user_email_url)
+ if email_response.data:
+ for item in email_response.data:
+ if item.get("primary", False):
+ response.data.update(email=item.get("email", None))
+
+ return response
+
+
+class Flickr(OAuth1):
+ """
+ Flickr |oauth1| provider.
+
+ * Dashboard: https://www.flickr.com/services/apps/
+ * Docs: https://www.flickr.com/services/api/auth.oauth.html
+ * API reference: https://www.flickr.com/services/api/
+
+ Supported :class:`.User` properties:
+
+ * id
+ * name
+ * username
+
+ Unsupported :class:`.User` properties:
+
+ * birth_date
+ * city
+ * country
+ * email
+ * first_name
+ * gender
+ * last_name
+ * link
+ * locale
+ * location
+ * nickname
+ * phone
+ * picture
+ * postal_code
+ * timezone
+
+ .. note::
+
+ If you encounter the "Oops! Flickr doesn't recognise the
+ permission set." message, you need to add the ``perms=read`` or
+ ``perms=write`` parameter to the *user authorization request*.
+ You can do it by adding the ``user_authorization_params``
+ key to the :doc:`config`:
+
+ .. code-block:: python
+ :emphasize-lines: 6
+
+ CONFIG = {
+ 'flickr': {
+ 'class_': oauth1.Flickr,
+ 'consumer_key': '##########',
+ 'consumer_secret': '##########',
+ 'user_authorization_params': dict(perms='read'),
+ },
+ }
+
+ """
+
+ supported_user_attributes = core.SupportedUserAttributes(
+ id=True,
+ name=True,
+ username=True
+ )
+
+ request_token_url = 'http://www.flickr.com/services/oauth/request_token'
+ user_authorization_url = 'http://www.flickr.com/services/oauth/authorize'
+ access_token_url = 'http://www.flickr.com/services/oauth/access_token'
+ user_info_url = None
+
+ supports_jsonp = True
+
+ @staticmethod
+ def _x_user_parser(user, data):
+ _user = data.get('user', {})
+
+ user.name = data.get('fullname') or _user.get(
+ 'username', {}).get('_content')
+ user.id = data.get('user_nsid') or _user.get('id')
+
+ return user
+
+
+class Meetup(OAuth1):
+ """
+ Meetup |oauth1| provider.
+
+ .. note::
+
+ Meetup also supports |oauth2| but you need the **user ID** to update
+ the **user** info, which they don't provide in the |oauth2| access
+ token response.
+
+ * Dashboard: http://www.meetup.com/meetup_api/oauth_consumers/
+ * Docs: http://www.meetup.com/meetup_api/auth/#oauth
+ * API: http://www.meetup.com/meetup_api/docs/
+
+ Supported :class:`.User` properties:
+
+ * city
+ * country
+ * id
+ * link
+ * locale
+ * location
+ * name
+ * picture
+
+ Unsupported :class:`.User` properties:
+
+ * birth_date
+ * email
+ * first_name
+ * gender
+ * last_name
+ * nickname
+ * phone
+ * postal_code
+ * timezone
+ * username
+
+ """
+
+ supported_user_attributes = core.SupportedUserAttributes(
+ city=True,
+ country=True,
+ id=True,
+ link=True,
+ locale=True,
+ location=True,
+ name=True,
+ picture=True
+ )
+
+ request_token_url = 'https://api.meetup.com/oauth/request/'
+ user_authorization_url = 'http://www.meetup.com/authorize/'
+ access_token_url = 'https://api.meetup.com/oauth/access/'
+ user_info_url = 'https://api.meetup.com/2/member/{id}'
+
+ @staticmethod
+ def _x_user_parser(user, data):
+
+ user.id = data.get('id') or data.get('member_id')
+ user.locale = data.get('lang')
+ user.picture = data.get('photo', {}).get('photo_link')
+
+ return user
+
+
+class Plurk(OAuth1):
+ """
+ Plurk |oauth1| provider.
+
+ * Dashboard: http://www.plurk.com/PlurkApp/
+ * Docs:
+ * API: http://www.plurk.com/API
+ * API explorer: http://www.plurk.com/OAuth/test/
+
+ Supported :class:`.User` properties:
+
+ * birth_date
+ * city
+ * country
+ * email
+ * gender
+ * id
+ * link
+ * locale
+ * location
+ * name
+ * nickname
+ * picture
+ * timezone
+ * username
+
+ Unsupported :class:`.User` properties:
+
+ * first_name
+ * last_name
+ * phone
+ * postal_code
+
+ """
+
+ supported_user_attributes = core.SupportedUserAttributes(
+ birth_date=True,
+ city=True,
+ country=True,
+ email=True,
+ gender=True,
+ id=True,
+ link=True,
+ locale=True,
+ location=True,
+ name=True,
+ nickname=True,
+ picture=True,
+ timezone=True,
+ username=True
+ )
+
+ request_token_url = 'http://www.plurk.com/OAuth/request_token'
+ user_authorization_url = 'http://www.plurk.com/OAuth/authorize'
+ access_token_url = 'http://www.plurk.com/OAuth/access_token'
+ user_info_url = 'http://www.plurk.com/APP/Profile/getOwnProfile'
+
+ @staticmethod
+ def _x_user_parser(user, data):
+
+ _user = data.get('user_info', {})
+
+ user.email = _user.get('email')
+ user.gender = _user.get('gender')
+ user.id = _user.get('id') or _user.get('uid')
+ user.locale = _user.get('default_lang')
+ user.name = _user.get('full_name')
+ user.nickname = _user.get('nick_name')
+ user.picture = 'http://avatars.plurk.com/{0}-big2.jpg'.format(user.id)
+ user.timezone = _user.get('timezone')
+ user.username = _user.get('display_name')
+
+ user.link = 'http://www.plurk.com/{0}/'.format(user.username)
+
+ user.city, user.country = _user.get('location', ',').split(',')
+ user.city = user.city.strip()
+ user.country = user.country.strip()
+
+ _bd = _user.get('date_of_birth')
+ if _bd:
+ try:
+ user.birth_date = datetime.datetime.strptime(
+ _bd,
+ "%a, %d %b %Y %H:%M:%S %Z"
+ )
+ except ValueError:
+ pass
+
+ return user
+
+
+class Twitter(OAuth1):
+ """
+ Twitter |oauth1| provider.
+
+ * Dashboard: https://dev.twitter.com/apps
+ * Docs: https://dev.twitter.com/docs
+ * API reference: https://dev.twitter.com/docs/api
+
+ .. note:: To prevent multiple authorization attempts, you should enable
+ the option:
+ ``Allow this application to be used to Sign in with Twitter``
+ in the Twitter 'Application Management' page. (http://apps.twitter.com)
+
+ Supported :class:`.User` properties:
+
+ * email
+ * city
+ * country
+ * id
+ * link
+ * locale
+ * location
+ * name
+ * picture
+ * username
+
+ Unsupported :class:`.User` properties:
+
+ * birth_date
+ * email
+ * gender
+ * first_name
+ * last_name
+ * locale
+ * nickname
+ * phone
+ * postal_code
+ * timezone
+
+ """
+
+ supported_user_attributes = core.SupportedUserAttributes(
+ city=True,
+ country=True,
+ id=True,
+ email=False,
+ link=True,
+ locale=False,
+ location=True,
+ name=True,
+ picture=True,
+ username=True
+ )
+
+ request_token_url = 'https://api.twitter.com/oauth/request_token'
+ user_authorization_url = 'https://api.twitter.com/oauth/authenticate'
+ access_token_url = 'https://api.twitter.com/oauth/access_token'
+ user_info_url = (
+ 'https://api.twitter.com/1.1/account/verify_credentials.json?'
+ 'include_entities=true&include_email=true'
+ )
+ supports_jsonp = True
+
+ @staticmethod
+ def _x_user_parser(user, data):
+ user.username = data.get('screen_name')
+ user.id = data.get('id') or data.get('user_id')
+ user.picture = data.get('profile_image_url')
+ user.locale = data.get('lang')
+ user.link = data.get('url')
+ _location = data.get('location', '')
+ if _location:
+ user.location = _location.strip()
+ _split_location = _location.split(',')
+ if len(_split_location) > 1:
+ _city, _country = _split_location
+ user.country = _country.strip()
+ else:
+ _city = _split_location[0]
+ user.city = _city.strip()
+ return user
+
+
+class Tumblr(OAuth1):
+ """
+ Tumblr |oauth1| provider.
+
+ * Dashboard: http://www.tumblr.com/oauth/apps
+ * Docs: http://www.tumblr.com/docs/en/api/v2#auth
+ * API reference: http://www.tumblr.com/docs/en/api/v2
+
+ Supported :class:`.User` properties:
+
+ * id
+ * name
+ * username
+
+ Unsupported :class:`.User` properties:
+
+ * birth_date
+ * city
+ * country
+ * email
+ * gender
+ * first_name
+ * last_name
+ * link
+ * locale
+ * location
+ * nickname
+ * phone
+ * picture
+ * postal_code
+ * timezone
+
+ """
+
+ supported_user_attributes = core.SupportedUserAttributes(
+ id=True,
+ name=True,
+ username=True
+ )
+
+ request_token_url = 'http://www.tumblr.com/oauth/request_token'
+ user_authorization_url = 'http://www.tumblr.com/oauth/authorize'
+ access_token_url = 'http://www.tumblr.com/oauth/access_token'
+ user_info_url = 'http://api.tumblr.com/v2/user/info'
+
+ supports_jsonp = True
+
+ @staticmethod
+ def _x_user_parser(user, data):
+ _user = data.get('response', {}).get('user', {})
+ user.username = user.id = _user.get('name')
+ return user
+
+
+class UbuntuOne(OAuth1):
+ """
+ Ubuntu One |oauth1| provider.
+
+ .. note::
+
+ The UbuntuOne service
+ `has been shut down `__.
+
+ .. warning::
+
+ Uses the `PLAINTEXT `_
+ Signature method!
+
+ * Dashboard: https://one.ubuntu.com/developer/account_admin/auth/web
+ * Docs: https://one.ubuntu.com/developer/account_admin/auth/web
+ * API reference: https://one.ubuntu.com/developer/contents
+
+ """
+
+ _signature_generator = PLAINTEXTSignatureGenerator
+
+ request_token_url = 'https://one.ubuntu.com/oauth/request/'
+ user_authorization_url = 'https://one.ubuntu.com/oauth/authorize/'
+ access_token_url = 'https://one.ubuntu.com/oauth/access/'
+ user_info_url = 'https://one.ubuntu.com/api/account/'
+
+
+class Vimeo(OAuth1):
+ """
+ Vimeo |oauth1| provider.
+
+ .. warning::
+
+ Vimeo needs one more fetch to get rich user info!
+
+ * Dashboard: https://developer.vimeo.com/apps
+ * Docs: https://developer.vimeo.com/apis/advanced#oauth-endpoints
+ * API reference: https://developer.vimeo.com/apis
+
+ Supported :class:`.User` properties:
+
+ * id
+ * link
+ * location
+ * name
+ * picture
+
+ Unsupported :class:`.User` properties:
+
+ * birth_date
+ * city
+ * country
+ * email
+ * gender
+ * first_name
+ * last_name
+ * locale
+ * nickname
+ * phone
+ * postal_code
+ * timezone
+ * username
+
+ """
+
+ supported_user_attributes = core.SupportedUserAttributes(
+ id=True,
+ link=True,
+ location=True,
+ name=True,
+ picture=True
+ )
+
+ request_token_url = 'https://vimeo.com/oauth/request_token'
+ user_authorization_url = 'https://vimeo.com/oauth/authorize'
+ access_token_url = 'https://vimeo.com/oauth/access_token'
+ user_info_url = ('http://vimeo.com/api/rest/v2?'
+ 'format=json&method=vimeo.oauth.checkAccessToken')
+
+ def _access_user_info(self):
+ """
+ Vimeo requires the user ID to access the user info endpoint, so we need
+ to make two requests: one to get user ID and second to get user info.
+ """
+ response = super(Vimeo, self)._access_user_info()
+ uid = response.data.get('oauth', {}).get('user', {}).get('id')
+ if uid:
+ return self.access('http://vimeo.com/api/v2/{0}/info.json'
+ .format(uid))
+ return response
+
+ @staticmethod
+ def _x_user_parser(user, data):
+ user.name = data.get('display_name')
+ user.link = data.get('profile_url')
+ user.picture = data.get('portrait_huge')
+ return user
+
+
+class Xero(OAuth1):
+ """
+ Xero |oauth1| provider.
+
+ .. note::
+
+ API returns XML!
+
+ * Dashboard: https://api.xero.com/Application
+ * Docs: http://blog.xero.com/developer/api-overview/public-applications/
+ * API reference: http://blog.xero.com/developer/api/
+
+ Supported :class:`.User` properties:
+
+ * email
+ * first_name
+ * id
+ * last_name
+ * name
+
+ Unsupported :class:`.User` properties:
+
+ * birth_date
+ * city
+ * country
+ * gender
+ * link
+ * locale
+ * location
+ * nickname
+ * phone
+ * picture
+ * postal_code
+ * timezone
+ * username
+
+ """
+
+ supported_user_attributes = core.SupportedUserAttributes(
+ email=True,
+ first_name=True,
+ id=True,
+ last_name=True,
+ name=True
+ )
+
+ request_token_url = 'https://api.xero.com/oauth/RequestToken'
+ user_authorization_url = 'https://api.xero.com/oauth/Authorize'
+ access_token_url = 'https://api.xero.com/oauth/AccessToken'
+ user_info_url = 'https://api.xero.com/api.xro/2.0/Users'
+
+ @staticmethod
+ def _x_user_parser(user, data):
+ # Data is xml.etree.ElementTree.Element object.
+ if not isinstance(data, dict):
+ # But only on user.update()
+ _user = data.find('Users/User')
+ user.id = _user.find('UserID').text
+ user.first_name = _user.find('FirstName').text
+ user.last_name = _user.find('LastName').text
+ user.email = _user.find('EmailAddress').text
+
+ return user
+
+
+class Yahoo(OAuth1):
+ """
+ Yahoo |oauth1| provider.
+
+ * Dashboard: https://developer.apps.yahoo.com/dashboard/
+ * Docs: http://developer.yahoo.com/oauth/guide/oauth-auth-flow.html
+ * API: http://developer.yahoo.com/everything.html
+ * API explorer: http://developer.yahoo.com/yql/console/
+
+ Supported :class:`.User` properties:
+
+ * city
+ * country
+ * id
+ * link
+ * location
+ * name
+ * nickname
+ * picture
+
+ Unsupported :class:`.User` properties:
+
+ * birth_date
+ * gender
+ * locale
+ * phone
+ * postal_code
+ * timezone
+ * username
+
+ """
+
+ supported_user_attributes = core.SupportedUserAttributes(
+ city=True,
+ country=True,
+ id=True,
+ link=True,
+ location=True,
+ name=True,
+ nickname=True,
+ picture=True
+ )
+
+ request_token_url = 'https://api.login.yahoo.com/oauth/v2/' + \
+ 'get_request_token'
+ user_authorization_url = 'https://api.login.yahoo.com/oauth/v2/' + \
+ 'request_auth'
+ access_token_url = 'https://api.login.yahoo.com/oauth/v2/get_token'
+ user_info_url = (
+ 'https://query.yahooapis.com/v1/yql?q=select%20*%20from%20'
+ 'social.profile%20where%20guid%3Dme%3B&format=json'
+ )
+
+ same_origin = False
+ supports_jsonp = True
+
+ @staticmethod
+ def _x_user_parser(user, data):
+
+ _user = data.get('query', {}).get('results', {}).get('profile', {})
+
+ user.id = _user.get('guid')
+ user.gender = _user.get('gender')
+ user.nickname = _user.get('nickname')
+ user.link = _user.get('profileUrl')
+
+ emails = _user.get('emails')
+ if isinstance(emails, list):
+ for email in emails:
+ if 'primary' in list(email.keys()):
+ user.email = email.get('handle')
+ elif isinstance(emails, dict):
+ user.email = emails.get('handle')
+
+ user.picture = _user.get('image', {}).get('imageUrl')
+
+ try:
+ user.city, user.country = _user.get('location', ',').split(',')
+ user.city = user.city.strip()
+ user.country = user.country.strip()
+ except ValueError:
+ # probably user hasn't activated Yahoo Profile
+ user.city = None
+ user.country = None
+ return user
+
+
+class Xing(OAuth1):
+ """
+ Xing |oauth1| provider.
+
+ * Dashboard: https://dev.xing.com/applications
+ * Docs: https://dev.xing.com/docs/authentication
+ * API reference: https://dev.xing.com/docs/resources
+
+ Supported :class:`.User` properties:
+
+ * birth_date
+ * city
+ * country
+ * email
+ * first_name
+ * gender
+ * id
+ * last_name
+ * link
+ * locale
+ * location
+ * name
+ * phone
+ * picture
+ * postal_code
+ * timezone
+ * username
+
+ Unsupported :class:`.User` properties:
+
+ * nickname
+
+ """
+
+ request_token_url = 'https://api.xing.com/v1/request_token'
+ user_authorization_url = 'https://api.xing.com/v1/authorize'
+ access_token_url = 'https://api.xing.com/v1/access_token'
+ user_info_url = 'https://api.xing.com/v1/users/me'
+
+ supported_user_attributes = core.SupportedUserAttributes(
+ birth_date=True,
+ city=True,
+ country=True,
+ email=True,
+ first_name=True,
+ gender=True,
+ id=True,
+ last_name=True,
+ link=True,
+ locale=True,
+ location=True,
+ name=True,
+ phone=True,
+ picture=True,
+ postal_code=True,
+ timezone=True,
+ username=True,
+ )
+
+ @staticmethod
+ def _x_user_parser(user, data):
+ _users = data.get('users', [])
+ if _users and _users[0]:
+ _user = _users[0]
+ user.id = _user.get('id')
+ user.name = _user.get('display_name')
+ user.first_name = _user.get('first_name')
+ user.last_name = _user.get('last_name')
+ user.gender = _user.get('gender')
+ user.timezone = _user.get('time_zone', {}).get('name')
+ user.email = _user.get('active_email')
+ user.link = _user.get('permalink')
+ user.username = _user.get('page_name')
+ user.picture = _user.get('photo_urls', {}).get('large')
+
+ _address = _user.get('business_address', {})
+ if _address:
+ user.city = _address.get('city')
+ user.country = _address.get('country')
+ user.postal_code = _address.get('zip_code')
+ user.phone = (
+ _address.get('phone', '') or
+ _address.get('mobile_phone', '')).replace('|', '')
+
+ _languages = list(_user.get('languages', {}).keys())
+ if _languages and _languages[0]:
+ user.locale = _languages[0]
+
+ _birth_date = _user.get('birth_date', {})
+ _year = _birth_date.get('year')
+ _month = _birth_date.get('month')
+ _day = _birth_date.get('day')
+ if _year and _month and _day:
+ user.birth_date = datetime.datetime(_year, _month, _day)
+
+ return user
+
+
+# The provider type ID is generated from this list's indexes!
+# Always append new providers at the end so that ids of existing providers
+# don't change!
+PROVIDER_ID_MAP = [
+ Bitbucket,
+ Flickr,
+ Meetup,
+ OAuth1,
+ Plurk,
+ Tumblr,
+ Twitter,
+ UbuntuOne,
+ Vimeo,
+ Xero,
+ Xing,
+ Yahoo,
+]
diff --git a/rhodecode/lib/_vendor/authomatic/providers/oauth2.py b/rhodecode/lib/_vendor/authomatic/providers/oauth2.py
new file mode 100755
--- /dev/null
+++ b/rhodecode/lib/_vendor/authomatic/providers/oauth2.py
@@ -0,0 +1,2053 @@
+# -*- coding: utf-8 -*-
+"""
+|oauth2| Providers
+-------------------
+
+Providers which implement the |oauth2|_ protocol.
+
+.. autosummary::
+
+ OAuth2
+ Amazon
+ Behance
+ Bitly
+ Bitbucket
+ Cosm
+ DeviantART
+ Eventbrite
+ Facebook
+ Foursquare
+ GitHub
+ Google
+ LinkedIn
+ PayPal
+ Reddit
+ Viadeo
+ VK
+ WindowsLive
+ Yammer
+ Yandex
+
+"""
+
+import base64
+import datetime
+import json
+import logging
+
+from authomatic.six.moves.urllib.parse import unquote
+from authomatic import providers
+from authomatic.exceptions import CancellationError, FailureError, OAuth2Error
+import authomatic.core as core
+
+
+__all__ = [
+ 'OAuth2',
+ 'Amazon',
+ 'Behance',
+ 'Bitly',
+ 'Bitbucket',
+ 'Cosm',
+ 'DeviantART',
+ 'Eventbrite',
+ 'Facebook',
+ 'Foursquare',
+ 'GitHub',
+ 'Google',
+ 'LinkedIn',
+ 'PayPal',
+ 'Reddit',
+ 'Viadeo',
+ 'VK',
+ 'WindowsLive',
+ 'Yammer',
+ 'Yandex'
+]
+
+
+class OAuth2(providers.AuthorizationProvider):
+ """
+ Base class for |oauth2|_ providers.
+ """
+
+ PROVIDER_TYPE_ID = 2
+ TOKEN_TYPES = ['', 'Bearer']
+
+ #: A scope preset to get most of the **user** info.
+ #: Use it in the :doc:`config` like
+ #: ``{'scope': oauth2.Facebook.user_info_scope}``.
+ user_info_scope = []
+
+ #: :class:`bool` If ``False``, the provider doesn't support CSRF
+ #: protection.
+ supports_csrf_protection = True
+
+ #: :class:`bool` If ``False``, the provider doesn't support user_state.
+ supports_user_state = True
+
+ token_request_method = 'POST' # method for requesting an access token
+
+ def __init__(self, *args, **kwargs):
+ """
+ Accepts additional keyword arguments:
+
+ :param list scope:
+ List of strings specifying requested permissions as described
+ in the
+ `OAuth 2.0 spec `_.
+
+ :param bool offline:
+ If ``True`` the **provider** will be set up to request an
+ *offline access token*.
+ Default is ``False``.
+
+ As well as those inherited from :class:`.AuthorizationProvider`
+ constructor.
+
+ """
+
+ super(OAuth2, self).__init__(*args, **kwargs)
+
+ self.scope = self._kwarg(kwargs, 'scope', [])
+ self.offline = self._kwarg(kwargs, 'offline', False)
+
+ # ========================================================================
+ # Internal methods
+ # ========================================================================
+
+ def _x_scope_parser(self, scope):
+ """
+ Override this to handle differences between accepted format of scope
+ across providers.
+
+ :attr list scope:
+ List of scopes.
+
+ """
+
+ # pylint:disable=no-self-use
+
+ # Most providers accept csv scope.
+ return ','.join(scope) if scope else ''
+
+ @classmethod
+ def create_request_elements(
+ cls, request_type, credentials, url, method='GET', params=None,
+ headers=None, body='', secret=None, redirect_uri='', scope='',
+ csrf='', user_state=''
+ ):
+ """
+ Creates |oauth2| request elements.
+ """
+
+ headers = headers or {}
+ params = params or {}
+
+ consumer_key = credentials.consumer_key or ''
+ consumer_secret = credentials.consumer_secret or ''
+ token = credentials.token or ''
+ refresh_token = credentials.refresh_token or credentials.token or ''
+
+ # Separate url base and query parameters.
+ url, base_params = cls._split_url(url)
+
+ # Add params extracted from URL.
+ params.update(dict(base_params))
+
+ if request_type == cls.USER_AUTHORIZATION_REQUEST_TYPE:
+ # User authorization request.
+ # TODO: Raise error for specific message for each missing argument.
+ if consumer_key and redirect_uri and (
+ csrf or not cls.supports_csrf_protection):
+ params['client_id'] = consumer_key
+ params['redirect_uri'] = redirect_uri
+ params['scope'] = scope
+ if cls.supports_user_state:
+ params['state'] = base64.urlsafe_b64encode(
+ json.dumps(
+ {"csrf": csrf, "user_state": user_state}
+ ).encode('utf-8')
+ )
+ else:
+ params['state'] = csrf
+ params['response_type'] = 'code'
+
+ # Add authorization header
+ headers.update(cls._authorization_header(credentials))
+ else:
+ raise OAuth2Error(
+ 'Credentials with valid consumer_key and arguments '
+ 'redirect_uri, scope and state are required to create '
+ 'OAuth 2.0 user authorization request elements!')
+
+ elif request_type == cls.ACCESS_TOKEN_REQUEST_TYPE:
+ # Access token request.
+ if consumer_key and consumer_secret:
+ params['code'] = token
+ params['client_id'] = consumer_key
+ params['client_secret'] = consumer_secret
+ params['redirect_uri'] = redirect_uri
+ params['grant_type'] = 'authorization_code'
+
+ # TODO: Check whether all providers accept it
+ headers.update(cls._authorization_header(credentials))
+ else:
+ raise OAuth2Error(
+ 'Credentials with valid token, consumer_key, '
+ 'consumer_secret and argument redirect_uri are required '
+ 'to create OAuth 2.0 access token request elements!')
+
+ elif request_type == cls.REFRESH_TOKEN_REQUEST_TYPE:
+ # Refresh access token request.
+ if refresh_token and consumer_key and consumer_secret:
+ params['refresh_token'] = refresh_token
+ params['client_id'] = consumer_key
+ params['client_secret'] = consumer_secret
+ params['grant_type'] = 'refresh_token'
+ else:
+ raise OAuth2Error(
+ 'Credentials with valid refresh_token, consumer_key, '
+ 'consumer_secret are required to create OAuth 2.0 '
+ 'refresh token request elements!')
+
+ elif request_type == cls.PROTECTED_RESOURCE_REQUEST_TYPE:
+ # Protected resource request.
+
+ # Add Authorization header. See:
+ # http://tools.ietf.org/html/rfc6749#section-7.1
+ if credentials.token_type == cls.BEARER:
+ # http://tools.ietf.org/html/rfc6750#section-2.1
+ headers.update(
+ {'Authorization': 'Bearer {0}'.format(credentials.token)})
+
+ elif token:
+ params['access_token'] = token
+ else:
+ raise OAuth2Error(
+ 'Credentials with valid token are required to create '
+ 'OAuth 2.0 protected resources request elements!')
+
+ request_elements = core.RequestElements(
+ url, method, params, headers, body)
+
+ return cls._x_request_elements_filter(
+ request_type, request_elements, credentials)
+
+ @staticmethod
+ def _x_refresh_credentials_if(credentials):
+ """
+ Override this to specify conditions when it gives sense to refresh
+ credentials.
+
+ .. warning::
+
+ |classmethod|
+
+ :param credentials:
+ :class:`.Credentials`
+
+ :returns:
+ ``True`` or ``False``
+
+ """
+
+ if credentials.refresh_token:
+ return True
+
+ # ========================================================================
+ # Exposed methods
+ # ========================================================================
+
+ @classmethod
+ def to_tuple(cls, credentials):
+ return (credentials.token,
+ credentials.refresh_token,
+ credentials.expiration_time,
+ cls.TOKEN_TYPES.index(credentials.token_type))
+
+ @classmethod
+ def reconstruct(cls, deserialized_tuple, credentials, cfg):
+
+ token, refresh_token, expiration_time, token_type = deserialized_tuple
+
+ credentials.token = token
+ credentials.refresh_token = refresh_token
+ credentials.expiration_time = expiration_time
+ credentials.token_type = cls.TOKEN_TYPES[int(token_type)]
+
+ return credentials
+
+ @classmethod
+ def decode_state(cls, state, param='user_state'):
+ """
+ Decode state and return param.
+
+ :param str state:
+ state parameter passed through by provider
+
+ :param str param:
+ key to query from decoded state variable. Options include 'csrf'
+ and 'user_state'.
+
+ :returns:
+ string value from decoded state
+
+ """
+ if state and cls.supports_user_state:
+ # urlsafe_b64 may include = which the browser quotes so must
+ # unquote Cast to str to void b64decode translation error. Base64
+ # should be str compatible.
+ return json.loads(base64.urlsafe_b64decode(
+ unquote(str(state))).decode('utf-8'))[param]
+ else:
+ return state if param == 'csrf' else ''
+
+ def refresh_credentials(self, credentials):
+ """
+ Refreshes :class:`.Credentials` if it gives sense.
+
+ :param credentials:
+ :class:`.Credentials` to be refreshed.
+
+ :returns:
+ :class:`.Response`.
+
+ """
+
+ if not self._x_refresh_credentials_if(credentials):
+ return
+
+ # We need consumer key and secret to make this kind of request.
+ cfg = credentials.config.get(credentials.provider_name)
+ credentials.consumer_key = cfg.get('consumer_key')
+ credentials.consumer_secret = cfg.get('consumer_secret')
+
+ request_elements = self.create_request_elements(
+ request_type=self.REFRESH_TOKEN_REQUEST_TYPE,
+ credentials=credentials,
+ url=self.access_token_url,
+ method='POST'
+ )
+
+ self._log(logging.INFO, u'Refreshing credentials.')
+ response = self._fetch(*request_elements)
+
+ # We no longer need consumer info.
+ credentials.consumer_key = None
+ credentials.consumer_secret = None
+
+ # Extract the refreshed data.
+ access_token = response.data.get('access_token')
+ refresh_token = response.data.get('refresh_token')
+
+ # Update credentials only if there is access token.
+ if access_token:
+ credentials.token = access_token
+ credentials.expire_in = response.data.get('expires_in')
+
+ # Update refresh token only if there is a new one.
+ if refresh_token:
+ credentials.refresh_token = refresh_token
+
+ # Handle different naming conventions across providers.
+ credentials = self._x_credentials_parser(
+ credentials, response.data)
+
+ return response
+
+ @providers.login_decorator
+ def login(self):
+
+ # get request parameters from which we can determine the login phase
+ authorization_code = self.params.get('code')
+ error = self.params.get('error')
+ error_message = self.params.get('error_message')
+ state = self.params.get('state')
+ # optional user_state to be passed in oauth2 state
+ user_state = self.params.get('user_state', '')
+
+ if authorization_code or not self.user_authorization_url:
+
+ if authorization_code:
+ # =============================================================
+ # Phase 2 after redirect with success
+ # =============================================================
+
+ self._log(
+ logging.INFO,
+ u'Continuing OAuth 2.0 authorization procedure after '
+ u'redirect.')
+
+ # validate CSRF token
+ if self.supports_csrf_protection:
+ self._log(
+ logging.INFO,
+ u'Validating request by comparing request state with '
+ u'stored state.')
+ stored_csrf = self._session_get('csrf')
+
+ state_csrf = self.decode_state(state, 'csrf')
+ if not stored_csrf:
+ raise FailureError(u'Unable to retrieve stored state!')
+ elif stored_csrf != state_csrf:
+ raise FailureError(
+ u'The returned state csrf cookie "{0}" doesn\'t '
+ u'match with the stored state!'.format(
+ state_csrf
+ ),
+ url=self.user_authorization_url)
+ self._log(logging.INFO, u'Request is valid.')
+ else:
+ self._log(logging.WARN, u'Skipping CSRF validation!')
+
+ elif not self.user_authorization_url:
+ # =============================================================
+ # Phase 1 without user authorization redirect.
+ # =============================================================
+
+ self._log(
+ logging.INFO,
+ u'Starting OAuth 2.0 authorization procedure without '
+ u'user authorization redirect.')
+
+ # exchange authorization code for access token by the provider
+ self._log(
+ logging.INFO,
+ u'Fetching access token from {0}.'.format(
+ self.access_token_url))
+
+ self.credentials.token = authorization_code
+
+ request_elements = self.create_request_elements(
+ request_type=self.ACCESS_TOKEN_REQUEST_TYPE,
+ credentials=self.credentials,
+ url=self.access_token_url,
+ method=self.token_request_method,
+ redirect_uri=self.url,
+ params=self.access_token_params,
+ headers=self.access_token_headers
+ )
+
+ response = self._fetch(*request_elements)
+ self.access_token_response = response
+
+ access_token = response.data.get('access_token', '')
+ refresh_token = response.data.get('refresh_token', '')
+
+ if response.status != 200 or not access_token:
+ raise FailureError(
+ 'Failed to obtain OAuth 2.0 access token from {0}! '
+ 'HTTP status: {1}, message: {2}.'.format(
+ self.access_token_url,
+ response.status,
+ response.content
+ ),
+ original_message=response.content,
+ status=response.status,
+ url=self.access_token_url)
+
+ self._log(logging.INFO, u'Got access token.')
+
+ if refresh_token:
+ self._log(logging.INFO, u'Got refresh access token.')
+
+ # OAuth 2.0 credentials need access_token, refresh_token,
+ # token_type and expire_in.
+ self.credentials.token = access_token
+ self.credentials.refresh_token = refresh_token
+ self.credentials.expire_in = response.data.get('expires_in')
+ self.credentials.token_type = response.data.get('token_type', '')
+ # sWe don't need these two guys anymore.
+ self.credentials.consumer_key = ''
+ self.credentials.consumer_secret = ''
+
+ # update credentials
+ self.credentials = self._x_credentials_parser(
+ self.credentials, response.data)
+
+ # create user
+ self._update_or_create_user(response.data, self.credentials)
+
+ # =================================================================
+ # We're done!
+ # =================================================================
+
+ elif error or error_message:
+ # =================================================================
+ # Phase 2 after redirect with error
+ # =================================================================
+
+ error_reason = self.params.get('error_reason') or error
+ error_description = self.params.get('error_description') \
+ or error_message or error
+
+ if error_reason and 'denied' in error_reason:
+ raise CancellationError(error_description,
+ url=self.user_authorization_url)
+ else:
+ raise FailureError(
+ error_description,
+ url=self.user_authorization_url)
+
+ elif (
+ not self.params or
+ len(self.params) == 1 and
+ 'user_state' in self.params
+ ):
+ # =================================================================
+ # Phase 1 before redirect
+ # =================================================================
+
+ self._log(
+ logging.INFO,
+ u'Starting OAuth 2.0 authorization procedure.')
+
+ csrf = ''
+ if self.supports_csrf_protection:
+ # generate csfr
+ csrf = self.csrf_generator(self.settings.secret)
+ # and store it to session
+ self._session_set('csrf', csrf)
+ else:
+ self._log(
+ logging.WARN,
+ u'Provider doesn\'t support CSRF validation!')
+
+ request_elements = self.create_request_elements(
+ request_type=self.USER_AUTHORIZATION_REQUEST_TYPE,
+ credentials=self.credentials,
+ url=self.user_authorization_url,
+ redirect_uri=self.url,
+ scope=self._x_scope_parser(
+ self.scope),
+ csrf=csrf,
+ user_state=user_state,
+ params=self.user_authorization_params
+ )
+
+ self._log(
+ logging.INFO,
+ u'Redirecting user to {0}.'.format(
+ request_elements.full_url))
+
+ self.redirect(request_elements.full_url)
+
+
+class Amazon(OAuth2):
+ """
+ Amazon |oauth2| provider.
+
+ Thanks to `Ghufran Syed `__.
+
+ * Dashboard: https://developer.amazon.com/lwa/sp/overview.html
+ * Docs: https://developer.amazon.com/public/apis/engage/login-with-amazon/docs/conceptual_overview.html
+ * API reference: https://developer.amazon.com/public/apis
+
+ .. note::
+
+ Amazon only accepts **redirect_uri** with **https** schema,
+ Therefore the *login handler* must also be accessible through
+ **https**.
+
+ Supported :class:`.User` properties:
+
+ * email
+ * id
+ * name
+ * postal_code
+
+ Unsupported :class:`.User` properties:
+
+ * birth_date
+ * city
+ * country
+ * first_name
+ * gender
+ * last_name
+ * link
+ * locale
+ * nickname
+ * phone
+ * picture
+ * timezone
+ * username
+
+ """
+
+ user_authorization_url = 'https://www.amazon.com/ap/oa'
+ access_token_url = 'https://api.amazon.com/auth/o2/token'
+ user_info_url = 'https://api.amazon.com/user/profile'
+ user_info_scope = ['profile', 'postal_code']
+
+ supported_user_attributes = core.SupportedUserAttributes(
+ email=True,
+ id=True,
+ name=True,
+ postal_code=True
+ )
+
+ def _x_scope_parser(self, scope):
+ # Amazon has space-separated scopes
+ return ' '.join(scope)
+
+ @staticmethod
+ def _x_user_parser(user, data):
+ user.id = data.get('user_id')
+ return user
+
+ @classmethod
+ def _x_credentials_parser(cls, credentials, data):
+ if data.get('token_type') == 'bearer':
+ credentials.token_type = cls.BEARER
+ return credentials
+
+
+class Behance(OAuth2):
+ """
+ Behance |oauth2| provider.
+
+ .. note::
+
+ Behance doesn't support third party authorization anymore,
+ which renders this class pretty much useless.
+
+ * Dashboard: http://www.behance.net/dev/apps
+ * Docs: http://www.behance.net/dev/authentication
+ * API reference: http://www.behance.net/dev/api/endpoints/
+
+ """
+
+ user_authorization_url = 'https://www.behance.net/v2/oauth/authenticate'
+ access_token_url = 'https://www.behance.net/v2/oauth/token'
+ user_info_url = ''
+
+ user_info_scope = ['activity_read']
+
+ def _x_scope_parser(self, scope):
+ """
+ Behance has pipe-separated scopes.
+ """
+ return '|'.join(scope)
+
+ @staticmethod
+ def _x_user_parser(user, data):
+
+ _user = data.get('user', {})
+
+ user.id = _user.get('id')
+ user.first_name = _user.get('first_name')
+ user.last_name = _user.get('last_name')
+ user.username = _user.get('username')
+ user.city = _user.get('city')
+ user.country = _user.get('country')
+ user.link = _user.get('url')
+ user.name = _user.get('display_name')
+ user.picture = _user.get('images', {}).get('138')
+
+ return user
+
+
+class Bitly(OAuth2):
+ """
+ Bitly |oauth2| provider.
+
+ .. warning::
+
+ |no-csrf|
+
+ * Dashboard: https://bitly.com/a/oauth_apps
+ * Docs: http://dev.bitly.com/authentication.html
+ * API reference: http://dev.bitly.com/api.html
+
+ Supported :class:`.User` properties:
+
+ * id
+ * link
+ * name
+ * picture
+ * username
+
+ Unsupported :class:`.User` properties:
+
+ * birth_date
+ * city
+ * country
+ * email
+ * first_name
+ * gender
+ * last_name
+ * locale
+ * nickname
+ * phone
+ * postal_code
+ * timezone
+
+ """
+
+ supported_user_attributes = core.SupportedUserAttributes(
+ id=True,
+ link=True,
+ name=True,
+ picture=True,
+ username=True
+ )
+
+ supports_csrf_protection = False
+ _x_use_authorization_header = False
+
+ user_authorization_url = 'https://bitly.com/oauth/authorize'
+ access_token_url = 'https://api-ssl.bitly.com/oauth/access_token'
+ user_info_url = 'https://api-ssl.bitly.com/v3/user/info'
+
+ def __init__(self, *args, **kwargs):
+ super(Bitly, self).__init__(*args, **kwargs)
+
+ if self.offline:
+ if 'grant_type' not in self.access_token_params:
+ self.access_token_params['grant_type'] = 'refresh_token'
+
+ @staticmethod
+ def _x_user_parser(user, data):
+ info = data.get('data', {})
+
+ user.id = info.get('login')
+ user.name = info.get('full_name')
+ user.username = info.get('display_name')
+ user.picture = info.get('profile_image')
+ user.link = info.get('profile_url')
+
+ return user
+
+
+class Cosm(OAuth2):
+ """
+ Cosm |oauth2| provider.
+
+ .. note::
+
+ Cosm doesn't provide any *user info URL*.
+
+ * Dashboard: https://cosm.com/users/{your_username}/apps
+ * Docs: https://cosm.com/docs/
+ * API reference: https://cosm.com/docs/v2/
+
+ """
+
+ user_authorization_url = 'https://cosm.com/oauth/authenticate'
+ access_token_url = 'https://cosm.com/oauth/token'
+ user_info_url = ''
+
+ @staticmethod
+ def _x_user_parser(user, data):
+ user.id = user.username = data.get('user')
+ return user
+
+
+class DeviantART(OAuth2):
+ """
+ DeviantART |oauth2| provider.
+
+ * Dashboard: https://www.deviantart.com/settings/myapps
+ * Docs: https://www.deviantart.com/developers/authentication
+ * API reference: http://www.deviantart.com/developers/oauth2
+
+ .. note::
+
+ Although it is not documented anywhere, DeviantART requires the
+ *access token* request to contain a ``User-Agent`` header.
+ You can apply a default ``User-Agent`` header for all API calls in the
+ config like this:
+
+ .. code-block:: python
+ :emphasize-lines: 6
+
+ CONFIG = {
+ 'deviantart': {
+ 'class_': oauth2.DeviantART,
+ 'consumer_key': '#####',
+ 'consumer_secret': '#####',
+ 'access_headers': {'User-Agent': 'Some User Agent'},
+ }
+ }
+
+ Supported :class:`.User` properties:
+
+ * name
+ * picture
+ * username
+
+ Unsupported :class:`.User` properties:
+
+ * birth_date
+ * city
+ * country
+ * email
+ * first_name
+ * gender
+ * id
+ * last_name
+ * link
+ * locale
+ * nickname
+ * phone
+ * postal_code
+ * timezone
+
+ """
+
+ user_authorization_url = 'https://www.deviantart.com/oauth2/authorize'
+ access_token_url = 'https://www.deviantart.com/oauth2/token'
+ user_info_url = 'https://www.deviantart.com/api/oauth2/user/whoami'
+
+ user_info_scope = ['basic']
+
+ supported_user_attributes = core.SupportedUserAttributes(
+ name=True,
+ picture=True,
+ username=True
+ )
+
+ def __init__(self, *args, **kwargs):
+ super(DeviantART, self).__init__(*args, **kwargs)
+
+ if self.offline:
+ if 'grant_type' not in self.access_token_params:
+ self.access_token_params['grant_type'] = 'refresh_token'
+
+ @staticmethod
+ def _x_user_parser(user, data):
+ user.picture = data.get('usericonurl')
+ return user
+
+
+class Eventbrite(OAuth2):
+ """
+ Eventbrite |oauth2| provider.
+
+ Thanks to `Paul Brown `__.
+
+ * Dashboard: http://www.eventbrite.com/myaccount/apps/
+ * Docs: https://developer.eventbrite.com/docs/auth/
+ * API: http://developer.eventbrite.com/docs/
+
+ Supported :class:`.User` properties:
+
+ * email
+ * first_name
+ * id
+ * last_name
+ * name
+
+ Unsupported :class:`.User` properties:
+
+ * birth_date
+ * city
+ * country
+ * gender
+ * link
+ * locale
+ * nickname
+ * phone
+ * picture
+ * postal_code
+ * timezone
+ * username
+
+ """
+
+ user_authorization_url = 'https://www.eventbrite.com/oauth/authorize'
+ access_token_url = 'https://www.eventbrite.com/oauth/token'
+ user_info_url = 'https://www.eventbriteapi.com/v3/users/me'
+
+ supported_user_attributes = core.SupportedUserAttributes(
+ email=True,
+ first_name=True,
+ id=True,
+ last_name=True,
+ name=True,
+ )
+
+ @classmethod
+ def _x_credentials_parser(cls, credentials, data):
+ if data.get('token_type') == 'bearer':
+ credentials.token_type = cls.BEARER
+ return credentials
+
+ @staticmethod
+ def _x_user_parser(user, data):
+ for email in data.get('emails', []):
+ if email.get('primary'):
+ user.email = email.get('email')
+ break
+
+ return user
+
+
+class Facebook(OAuth2):
+ """
+ Facebook |oauth2| provider.
+
+ * Dashboard: https://developers.facebook.com/apps
+ * Docs: http://developers.facebook.com/docs/howtos/login/server-side-login/
+ * API reference: http://developers.facebook.com/docs/reference/api/
+ * API explorer: http://developers.facebook.com/tools/explorer
+
+ Supported :class:`.User` properties:
+
+ * birth_date
+ * email
+ * first_name
+ * id
+ * last_name
+ * name
+ * picture
+
+ Unsupported :class:`.User` properties:
+
+ * nickname
+ * phone
+ * postal_code
+ * username
+
+ """
+ user_authorization_url = 'https://www.facebook.com/dialog/oauth'
+ access_token_url = 'https://graph.facebook.com/oauth/access_token'
+ user_info_url = 'https://graph.facebook.com/v2.3/me'
+ user_info_scope = ['email', 'public_profile', 'user_birthday',
+ 'user_location']
+ same_origin = False
+
+ supported_user_attributes = core.SupportedUserAttributes(
+ birth_date=True,
+ city=False,
+ country=False,
+ email=True,
+ first_name=True,
+ gender=False,
+ id=True,
+ last_name=True,
+ link=False,
+ locale=False,
+ location=False,
+ name=True,
+ picture=True,
+ timezone=False,
+ username=False,
+ )
+
+ @classmethod
+ def _x_request_elements_filter(cls, request_type, request_elements,
+ credentials):
+
+ if request_type == cls.REFRESH_TOKEN_REQUEST_TYPE:
+ # As always, Facebook has it's original name for "refresh_token"!
+ url, method, params, headers, body = request_elements
+ params['fb_exchange_token'] = params.pop('refresh_token')
+ params['grant_type'] = 'fb_exchange_token'
+ request_elements = core.RequestElements(url, method, params,
+ headers, body)
+
+ return request_elements
+
+ def __init__(self, *args, **kwargs):
+ super(Facebook, self).__init__(*args, **kwargs)
+
+ # Handle special Facebook requirements to be able
+ # to refresh the access token.
+ if self.offline:
+ # Facebook needs an offline_access scope.
+ if 'offline_access' not in self.scope:
+ self.scope.append('offline_access')
+
+ if self.popup:
+ self.user_authorization_url += '?display=popup'
+
+ @staticmethod
+ def _x_user_parser(user, data):
+ _birth_date = data.get('birthday')
+ if _birth_date:
+ try:
+ user.birth_date = datetime.datetime.strptime(_birth_date,
+ '%m/%d/%Y')
+ except ValueError:
+ pass
+
+ user.picture = ('http://graph.facebook.com/{0}/picture?type=large'
+ .format(user.id))
+
+ user.location = data.get('location', {}).get('name')
+ if user.location:
+ split_location = user.location.split(', ')
+ user.city = split_location[0].strip()
+ if len(split_location) > 1:
+ user.country = split_location[1].strip()
+
+ return user
+
+ @staticmethod
+ def _x_credentials_parser(credentials, data):
+ """
+ We need to override this method to fix Facebooks naming deviation.
+ """
+
+ # Facebook returns "expires" instead of "expires_in".
+ credentials.expire_in = data.get('expires')
+
+ if data.get('token_type') == 'bearer':
+ # TODO: cls is not available here, hardcode for now.
+ credentials.token_type = 'Bearer'
+
+ return credentials
+
+ @staticmethod
+ def _x_refresh_credentials_if(credentials):
+ # Always refresh.
+ return True
+
+ def access(self, url, params=None, **kwargs):
+ if params is None:
+ params = {}
+ params['fields'] = 'id,first_name,last_name,picture,email,gender,' + \
+ 'timezone,location,birthday,locale'
+
+ return super(Facebook, self).access(url, params, **kwargs)
+
+
+class Foursquare(OAuth2):
+ """
+ Foursquare |oauth2| provider.
+
+ * Dashboard: https://foursquare.com/developers/apps
+ * Docs: https://developer.foursquare.com/overview/auth.html
+ * API reference: https://developer.foursquare.com/docs/
+
+ .. note::
+
+ Foursquare requires a *version* parameter in each request.
+ The default value is ``v=20140501``. You can override the version in
+ the ``params`` parameter of the :meth:`.Authomatic.access` method.
+ See https://developer.foursquare.com/overview/versioning
+
+ Supported :class:`.User` properties:
+
+ * city
+ * country
+ * email
+ * first_name
+ * gender
+ * id
+ * last_name
+ * location
+ * name
+ * phone
+ * picture
+
+ Unsupported :class:`.User` properties:
+
+ * birth_date
+ * link
+ * locale
+ * nickname
+ * postal_code
+ * timezone
+ * username
+
+ """
+
+ user_authorization_url = 'https://foursquare.com/oauth2/authenticate'
+ access_token_url = 'https://foursquare.com/oauth2/access_token'
+ user_info_url = 'https://api.foursquare.com/v2/users/self'
+
+ same_origin = False
+
+ supported_user_attributes = core.SupportedUserAttributes(
+ birth_date=True,
+ city=True,
+ country=True,
+ email=True,
+ first_name=True,
+ gender=True,
+ id=True,
+ last_name=True,
+ location=True,
+ name=True,
+ phone=True,
+ picture=True
+ )
+
+ @classmethod
+ def _x_request_elements_filter(cls, request_type, request_elements,
+ credentials):
+
+ if request_type == cls.PROTECTED_RESOURCE_REQUEST_TYPE:
+ # Foursquare uses OAuth 1.0 "oauth_token" for what should be
+ # "access_token" in OAuth 2.0!
+ url, method, params, headers, body = request_elements
+ params['oauth_token'] = params.pop('access_token')
+
+ # Foursquare needs the version "v" parameter in every request.
+ # https://developer.foursquare.com/overview/versioning
+ if not params.get('v'):
+ params['v'] = '20140501'
+
+ request_elements = core.RequestElements(url, method, params,
+ headers, body)
+
+ return request_elements
+
+ @staticmethod
+ def _x_user_parser(user, data):
+
+ _resp = data.get('response', {})
+ _user = _resp.get('user', {})
+
+ user.id = _user.get('id')
+ user.first_name = _user.get('firstName')
+ user.last_name = _user.get('lastName')
+ user.gender = _user.get('gender')
+
+ _birth_date = _user.get('birthday')
+ if _birth_date:
+ user.birth_date = datetime.datetime.fromtimestamp(_birth_date)
+
+ _photo = _user.get('photo', {})
+ if isinstance(_photo, dict):
+ _photo_prefix = _photo.get('prefix', '').strip('/')
+ _photo_suffix = _photo.get('suffix', '').strip('/')
+ user.picture = '/'.join([_photo_prefix, _photo_suffix])
+
+ if isinstance(_photo, str):
+ user.picture = _photo
+
+ user.location = _user.get('homeCity')
+ if user.location:
+ split_location = user.location.split(',')
+ user.city = split_location[0].strip()
+ if len(user.location) > 1:
+ user.country = split_location[1].strip()
+
+ _contact = _user.get('contact', {})
+ user.email = _contact.get('email')
+ user.phone = _contact.get('phone')
+
+ return user
+
+
+class Bitbucket(OAuth2):
+
+ user_authorization_url = 'https://bitbucket.org/site/oauth2/authorize'
+ access_token_url = 'https://bitbucket.org/site/oauth2/access_token'
+ user_info_url = 'https://bitbucket.org/api/2.0/user'
+ user_email_info_url = 'https://bitbucket.org/api/2.0/user/emails'
+
+ same_origin = False
+
+ supported_user_attributes = core.SupportedUserAttributes(
+ id=True,
+ first_name=True,
+ last_name=True,
+ link=True,
+ name=True,
+ picture=True,
+ username=True,
+ email=True
+ )
+
+ @staticmethod
+ def _x_user_parser(user, data):
+ user.username = user.id = data.get('username')
+ user.name = data.get('display_name')
+ user.first_name = data.get('first_name')
+ user.last_name = data.get('last_name')
+
+ return user
+
+ @classmethod
+ def _x_credentials_parser(cls, credentials, data):
+ if data.get('token_type') == 'bearer':
+ credentials.token_type = cls.BEARER
+ return credentials
+
+ def _access_user_info(self):
+ """
+ Email is available in separate method so second request is needed.
+ """
+ response = super(Bitbucket, self)._access_user_info()
+
+ response.data.setdefault("email", None)
+
+ email_response = self.access(self.user_email_info_url)
+ emails = email_response.data.get('values', [])
+ if emails:
+ for item in emails:
+ if item.get("is_primary", False):
+ response.data.update(email=item.get("email", None))
+
+ return response
+
+
+class GitHub(OAuth2):
+ """
+ GitHub |oauth2| provider.
+
+ * Dashboard: https://github.com/settings/developers
+ * Docs: http://developer.github.com/v3/#authentication
+ * API reference: http://developer.github.com/v3/
+
+ .. note::
+
+ GitHub API
+ `documentation `_
+ says:
+
+ all API requests MUST include a valid ``User-Agent`` header.
+
+ You can apply a default ``User-Agent`` header for all API calls in
+ the config like this:
+
+ .. code-block:: python
+ :emphasize-lines: 6
+
+ CONFIG = {
+ 'github': {
+ 'class_': oauth2.GitHub,
+ 'consumer_key': '#####',
+ 'consumer_secret': '#####',
+ 'access_headers': {'User-Agent': 'Awesome-Octocat-App'},
+ }
+ }
+
+ Supported :class:`.User` properties:
+
+ * email
+ * id
+ * link
+ * location
+ * name
+ * picture
+ * username
+
+ Unsupported :class:`.User` properties:
+
+ * birth_date
+ * city
+ * country
+ * first_name
+ * gender
+ * last_name
+ * locale
+ * nickname
+ * phone
+ * postal_code
+ * timezone
+
+ """
+
+ user_authorization_url = 'https://github.com/login/oauth/authorize'
+ access_token_url = 'https://github.com/login/oauth/access_token'
+ user_info_url = 'https://api.github.com/user'
+
+ same_origin = False
+
+ supported_user_attributes = core.SupportedUserAttributes(
+ email=True,
+ id=True,
+ link=True,
+ location=True,
+ name=True,
+ picture=True,
+ username=True
+ )
+
+ @staticmethod
+ def _x_user_parser(user, data):
+ user.username = data.get('login')
+ user.picture = data.get('avatar_url')
+ user.link = data.get('html_url')
+ return user
+
+ @classmethod
+ def _x_credentials_parser(cls, credentials, data):
+ if data.get('token_type') == 'bearer':
+ credentials.token_type = cls.BEARER
+ return credentials
+
+
+class Google(OAuth2):
+ """
+ Google |oauth2| provider.
+
+ * Dashboard: https://console.developers.google.com/project
+ * Docs: https://developers.google.com/accounts/docs/OAuth2
+ * API reference: https://developers.google.com/gdata/docs/directory
+ * API explorer: https://developers.google.com/oauthplayground/
+
+ Supported :class:`.User` properties:
+
+ * email
+ * first_name
+ * gender
+ * id
+ * last_name
+ * link
+ * locale
+ * name
+ * picture
+
+ Unsupported :class:`.User` properties:
+
+ * birth_date
+ * city
+ * country
+ * nickname
+ * phone
+ * postal_code
+ * timezone
+ * username
+
+ .. note::
+
+ To get the user info, you need to activate the **Google+ API**
+ in the **APIs & auth >> APIs** section of the`Google Developers Console
+ `__.
+
+ """
+
+ user_authorization_url = 'https://accounts.google.com/o/oauth2/auth'
+ access_token_url = 'https://accounts.google.com/o/oauth2/token'
+ user_info_url = 'https://www.googleapis.com/oauth2/v3/userinfo?alt=json'
+
+ user_info_scope = ['profile',
+ 'email']
+
+ supported_user_attributes = core.SupportedUserAttributes(
+ id=True,
+ email=True,
+ name=True,
+ first_name=True,
+ last_name=True,
+ locale=True,
+ picture=True
+ )
+
+ def __init__(self, *args, **kwargs):
+ super(Google, self).__init__(*args, **kwargs)
+
+ # Handle special Google requirements to be able to refresh the access
+ # token.
+ if self.offline:
+ if 'access_type' not in self.user_authorization_params:
+ # Google needs access_type=offline param in the user
+ # authorization request.
+ self.user_authorization_params['access_type'] = 'offline'
+ if 'approval_prompt' not in self.user_authorization_params:
+ # And also approval_prompt=force.
+ self.user_authorization_params['approval_prompt'] = 'force'
+
+ @classmethod
+ def _x_request_elements_filter(cls, request_type, request_elements,
+ credentials):
+ """
+ Google doesn't accept client ID and secret to be at the same time in
+ request parameters and in the basic authorization header in the access
+ token request.
+ """
+ if request_type is cls.ACCESS_TOKEN_REQUEST_TYPE:
+ params = request_elements[2]
+ del params['client_id']
+ del params['client_secret']
+ return request_elements
+
+ @staticmethod
+ def _x_user_parser(user, data):
+ emails = data.get('emails', [])
+ if emails:
+ user.email = emails[0].get('value')
+ for email in emails:
+ if email.get('type') == 'account':
+ user.email = email.get('value')
+ break
+
+ user.id = data.get('sub')
+ user.name = data.get('name')
+ user.first_name = data.get('given_name', '')
+ user.last_name = data.get('family_name', '')
+ user.locale = data.get('locale', '')
+ user.picture = data.get('picture', '')
+
+ user.email_verified = data.get("email_verified")
+ user.hosted_domain = data.get("hd")
+ return user
+
+ def _x_scope_parser(self, scope):
+ """
+ Google has space-separated scopes.
+ """
+ return ' '.join(scope)
+
+
+class LinkedIn(OAuth2):
+ """
+ Linked In |oauth2| provider.
+
+ .. note::
+
+ Doesn't support access token refreshment.
+
+ * Dashboard: https://www.linkedin.com/secure/developer
+ * Docs: http://developer.linkedin.com/documents/authentication
+ * API reference: http://developer.linkedin.com/rest
+
+ Supported :class:`.User` properties:
+
+ * city
+ * country
+ * email
+ * first_name
+ * id
+ * last_name
+ * link
+ * name
+ * picture
+
+ Unsupported :class:`.User` properties:
+
+ * birth_date
+ * gender
+ * locale
+ * location
+ * nickname
+ * phone
+ * postal_code
+ * timezone
+ * username
+
+ """
+
+ user_authorization_url = 'https://www.linkedin.com/uas/oauth2/' + \
+ 'authorization'
+ access_token_url = 'https://www.linkedin.com/uas/oauth2/accessToken'
+ user_info_url = ('https://api.linkedin.com/v1/people/~:'
+ '(id,first-name,last-name,formatted-name,location,'
+ 'picture-url,public-profile-url,email-address)'
+ '?format=json')
+
+ user_info_scope = ['r_emailaddress']
+
+ token_request_method = 'GET' # To avoid a bug with OAuth2.0 on Linkedin
+ # http://developer.linkedin.com/forum/unauthorized-invalid-or-expired-token-immediately-after-receiving-oauth2-token
+
+ supported_user_attributes = core.SupportedUserAttributes(
+ city=True,
+ country=True,
+ email=True,
+ first_name=True,
+ id=True,
+ last_name=True,
+ link=True,
+ location=False,
+ name=True,
+ picture=True
+ )
+
+ @classmethod
+ def _x_request_elements_filter(cls, request_type, request_elements,
+ credentials):
+ if request_type == cls.PROTECTED_RESOURCE_REQUEST_TYPE:
+ # LinkedIn too has it's own terminology!
+ url, method, params, headers, body = request_elements
+ params['oauth2_access_token'] = params.pop('access_token')
+ request_elements = core.RequestElements(url, method, params,
+ headers, body)
+
+ return request_elements
+
+ @staticmethod
+ def _x_user_parser(user, data):
+
+ user.first_name = data.get('firstName')
+ user.last_name = data.get('lastName')
+ user.email = data.get('emailAddress')
+ user.name = data.get('formattedName')
+ user.city = user.city = data.get('location', {}).get('name')
+ user.country = data.get('location', {}).get('country', {}).get('code')
+ user.phone = data.get('phoneNumbers', {}).get('values', [{}])[0]\
+ .get('phoneNumber')
+ user.picture = data.get('pictureUrl')
+ user.link = data.get('publicProfileUrl')
+
+ _birthdate = data.get('dateOfBirth', {})
+ if _birthdate:
+ _day = _birthdate.get('day')
+ _month = _birthdate.get('month')
+ _year = _birthdate.get('year')
+ if _day and _month and _year:
+ user.birth_date = datetime.datetime(_year, _month, _day)
+
+ return user
+
+
+class PayPal(OAuth2):
+ """
+ PayPal |oauth2| provider.
+
+ * Dashboard: https://developer.paypal.com/webapps/developer/applications
+ * Docs:
+ https://developer.paypal.com/webapps/developer/docs/integration/direct/make-your-first-call/
+ * API reference: https://developer.paypal.com/webapps/developer/docs/api/
+
+ .. note::
+
+ Paypal doesn't redirect the **user** to authorize your app!
+ It grants you an **access token** based on your **app's** key and
+ secret instead.
+
+ """
+
+ _x_use_authorization_header = True
+
+ supported_user_attributes = core.SupportedUserAttributes()
+
+ @classmethod
+ def _x_request_elements_filter(
+ cls, request_type, request_elements, credentials):
+
+ if request_type == cls.ACCESS_TOKEN_REQUEST_TYPE:
+ url, method, params, headers, body = request_elements
+ params['grant_type'] = 'client_credentials'
+ request_elements = core.RequestElements(
+ url, method, params, headers, body)
+
+ return request_elements
+
+ user_authorization_url = ''
+ access_token_url = 'https://api.sandbox.paypal.com/v1/oauth2/token'
+ user_info_url = ''
+
+
+class Reddit(OAuth2):
+ """
+ Reddit |oauth2| provider.
+
+ .. note::
+
+ Currently credentials refreshment returns
+ ``{"error": "invalid_request"}``.
+
+ * Dashboard: https://ssl.reddit.com/prefs/apps
+ * Docs: https://github.com/reddit/reddit/wiki/OAuth2
+ * API reference: http://www.reddit.com/dev/api
+
+ .. note::
+
+ According to Reddit API
+ `docs `_,
+ you have to include a `User-Agent` header in each API call.
+
+ You can apply a default ``User-Agent`` header for all API calls in the
+ config like this:
+
+ .. code-block:: python
+ :emphasize-lines: 6
+
+ CONFIG = {
+ 'reddit': {
+ 'class_': oauth2.Reddit,
+ 'consumer_key': '#####',
+ 'consumer_secret': '#####',
+ 'access_headers': {'User-Agent': "Andy Pipkin's App"},
+ }
+ }
+
+ Supported :class:`.User` properties:
+
+ * id
+ * username
+
+ Unsupported :class:`.User` properties:
+
+ * birth_date
+ * country
+ * city
+ * email
+ * first_name
+ * gender
+ * last_name
+ * link
+ * locale
+ * location
+ * name
+ * nickname
+ * phone
+ * picture
+ * postal_code
+ * timezone
+
+ """
+
+ user_authorization_url = 'https://ssl.reddit.com/api/v1/authorize'
+ access_token_url = 'https://ssl.reddit.com/api/v1/access_token'
+ user_info_url = 'https://oauth.reddit.com/api/v1/me.json'
+
+ user_info_scope = ['identity']
+
+ supported_user_attributes = core.SupportedUserAttributes(
+ id=True,
+ name=True,
+ username=True
+ )
+
+ def __init__(self, *args, **kwargs):
+ super(Reddit, self).__init__(*args, **kwargs)
+
+ if self.offline:
+ if 'duration' not in self.user_authorization_params:
+ # http://www.reddit.com/r/changelog/comments/11jab9/reddit_change_permanent_oauth_grants_using/
+ self.user_authorization_params['duration'] = 'permanent'
+
+ @classmethod
+ def _x_credentials_parser(cls, credentials, data):
+ if data.get('token_type') == 'bearer':
+ credentials.token_type = cls.BEARER
+ return credentials
+
+ @staticmethod
+ def _x_user_parser(user, data):
+ user.username = data.get('name')
+ return user
+
+
+class Viadeo(OAuth2):
+ """
+ Viadeo |oauth2| provider.
+
+ .. note::
+
+ As stated in the `Viadeo documentation
+ `__:
+
+ Viadeo restrains access to its API.
+ They are now exclusively reserved for its strategic partners.
+
+ * Dashboard: http://dev.viadeo.com/dashboard/
+ * Docs:
+ http://dev.viadeo.com/documentation/authentication/oauth-authentication/
+ * API reference: http://dev.viadeo.com/documentation/
+
+ .. note::
+
+ Viadeo doesn't support **credentials refreshment**.
+ As stated in their
+ `docs
+ `_:
+ "The access token has an infinite time to live."
+
+ """
+
+ user_authorization_url = 'https://secure.viadeo.com/oauth-provider/' + \
+ 'authorize2'
+ access_token_url = 'https://secure.viadeo.com/oauth-provider/access_token2'
+ user_info_url = 'https://api.viadeo.com/me'
+
+ @classmethod
+ def _x_credentials_parser(cls, credentials, data):
+ if data.get('token_type') == 'bearer_token':
+ credentials.token_type = cls.BEARER
+ return credentials
+
+ @staticmethod
+ def _x_refresh_credentials_if(credentials):
+ # Never refresh.
+ return False
+
+ @staticmethod
+ def _x_user_parser(user, data):
+ user.username = data.get('nickname')
+ user.picture = data.get('picture_large')
+ user.picture = data.get('picture_large')
+ user.locale = data.get('language')
+ user.email = data.get('')
+ user.email = data.get('')
+ user.country = data.get('location', {}).get('country')
+ user.city = data.get('location', {}).get('city')
+ user.postal_code = data.get('location', {}).get('zipcode')
+ user.timezone = data.get('location', {}).get('timezone')
+
+ return user
+
+
+class VK(OAuth2):
+ """
+ VK.com |oauth2| provider.
+
+ * Dashboard: http://vk.com/apps?act=manage
+ * Docs: http://vk.com/developers.php?oid=-17680044&p=Authorizing_Sites
+ * API reference: http://vk.com/developers.php?oid=-17680044&p=API_
+ Method_Description
+
+ .. note::
+
+ VK uses a
+ `bitmask scope
+ `_!
+ Use it like this:
+
+ .. code-block:: python
+ :emphasize-lines: 7
+
+ CONFIG = {
+ 'vk': {
+ 'class_': oauth2.VK,
+ 'consumer_key': '#####',
+ 'consumer_secret': '#####',
+ 'id': authomatic.provider_id(),
+ 'scope': ['1024'] # Always a single item.
+ }
+ }
+
+ Supported :class:`.User` properties:
+
+ * birth_date
+ * city
+ * country
+ * first_name
+ * gender
+ * id
+ * last_name
+ * location
+ * name
+ * picture
+ * timezone
+
+ Unsupported :class:`.User` properties:
+
+ * email
+ * link
+ * locale
+ * nickname
+ * phone
+ * postal_code
+ * username
+
+ """
+
+ user_authorization_url = 'http://api.vkontakte.ru/oauth/authorize'
+ access_token_url = 'https://api.vkontakte.ru/oauth/access_token'
+ user_info_url = 'https://api.vk.com/method/getProfiles?' + \
+ 'fields=uid,first_name,last_name,nickname,sex,bdate,' + \
+ 'city,country,timezone,photo_big'
+
+ supported_user_attributes = core.SupportedUserAttributes(
+ birth_date=True,
+ city=True,
+ country=True,
+ first_name=True,
+ gender=True,
+ id=True,
+ last_name=True,
+ location=True,
+ name=True,
+ picture=True,
+ timezone=True,
+ )
+
+ def __init__(self, *args, **kwargs):
+ super(VK, self).__init__(*args, **kwargs)
+
+ if self.offline:
+ if 'offline' not in self.scope:
+ self.scope.append('offline')
+
+ @staticmethod
+ def _x_user_parser(user, data):
+ _resp = data.get('response', [{}])[0]
+
+ _birth_date = _resp.get('bdate')
+ if _birth_date:
+ user.birth_date = datetime.datetime.strptime(
+ _birth_date, '%d.%m.%Y')
+ user.id = _resp.get('uid')
+ user.first_name = _resp.get('first_name')
+ user.gender = _resp.get('sex')
+ user.last_name = _resp.get('last_name')
+ user.nickname = _resp.get('nickname')
+ user.city = _resp.get('city')
+ user.country = _resp.get('country')
+ user.timezone = _resp.get('timezone')
+ user.picture = _resp.get('photo_big')
+
+ return user
+
+
+class WindowsLive(OAuth2):
+ """
+ Windows Live |oauth2| provider.
+
+ * Dashboard: https://account.live.com/developers/applications
+ * Docs: http://msdn.microsoft.com/en-us/library/hh243647.aspx
+ * API explorer: http://isdk.dev.live.com/?mkt=en-us
+
+ Supported :class:`.User` properties:
+
+ * email
+ * first_name
+ * id
+ * last_name
+ * link
+ * locale
+ * name
+ * picture
+
+ Unsupported :class:`.User` properties:
+
+ * birth_date
+ * city
+ * country
+ * gender
+ * nickname
+ * location
+ * phone
+ * postal_code
+ * timezone
+ * username
+
+ """
+
+ user_authorization_url = 'https://login.live.com/oauth20_authorize.srf'
+ access_token_url = 'https://login.live.com/oauth20_token.srf'
+ user_info_url = 'https://apis.live.net/v5.0/me'
+
+ user_info_scope = ['wl.basic', 'wl.emails', 'wl.photos']
+
+ supported_user_attributes = core.SupportedUserAttributes(
+ email=True,
+ first_name=True,
+ id=True,
+ last_name=True,
+ link=True,
+ locale=True,
+ name=True,
+ picture=True
+ )
+
+ def __init__(self, *args, **kwargs):
+ super(WindowsLive, self).__init__(*args, **kwargs)
+
+ if self.offline:
+ if 'wl.offline_access' not in self.scope:
+ self.scope.append('wl.offline_access')
+
+ @classmethod
+ def _x_credentials_parser(cls, credentials, data):
+ if data.get('token_type') == 'bearer':
+ credentials.token_type = cls.BEARER
+ return credentials
+
+ @staticmethod
+ def _x_user_parser(user, data):
+ user.email = data.get('emails', {}).get('preferred')
+ user.picture = 'https://apis.live.net/v5.0/{0}/picture'.format(
+ data.get('id'))
+ return user
+
+
+class Yammer(OAuth2):
+ """
+ Yammer |oauth2| provider.
+
+ * Dashboard: https://www.yammer.com/client_applications
+ * Docs: https://developer.yammer.com/authentication/
+ * API reference: https://developer.yammer.com/restapi/
+
+ Supported :class:`.User` properties:
+
+ * birth_date
+ * city
+ * country
+ * email
+ * first_name
+ * id
+ * last_name
+ * link
+ * locale
+ * location
+ * name
+ * phone
+ * picture
+ * timezone
+ * username
+
+ Unsupported :class:`.User` properties:
+
+ * gender
+ * nickname
+ * postal_code
+
+ """
+
+ user_authorization_url = 'https://www.yammer.com/dialog/oauth'
+ access_token_url = 'https://www.yammer.com/oauth2/access_token.json'
+ user_info_url = 'https://www.yammer.com/api/v1/users/current.json'
+
+ supported_user_attributes = core.SupportedUserAttributes(
+ birth_date=True,
+ city=True,
+ country=True,
+ email=True,
+ first_name=True,
+ id=True,
+ last_name=True,
+ link=True,
+ locale=True,
+ location=True,
+ name=True,
+ phone=True,
+ picture=True,
+ timezone=True,
+ username=True
+ )
+
+ @classmethod
+ def _x_credentials_parser(cls, credentials, data):
+ # import pdb; pdb.set_trace()
+ credentials.token_type = cls.BEARER
+ _access_token = data.get('access_token', {})
+ credentials.token = _access_token.get('token')
+ _expire_in = _access_token.get('expires_at', 0)
+ if _expire_in:
+ credentials.expire_in = _expire_in
+ return credentials
+
+ @staticmethod
+ def _x_user_parser(user, data):
+
+ # Yammer provides most of the user info in the access token request,
+ # but provides more on in user info request.
+ _user = data.get('user', {})
+ if not _user:
+ # If there is "user key", it is token request.
+ _user = data
+
+ user.username = _user.get('name')
+ user.name = _user.get('full_name')
+ user.link = _user.get('web_url')
+ user.picture = _user.get('mugshot_url')
+
+ user.city, user.country = _user.get('location', ',').split(',')
+ user.city = user.city.strip()
+ user.country = user.country.strip()
+ user.locale = _user.get('web_preferences', {}).get('locale')
+
+ # Contact
+ _contact = _user.get('contact', {})
+ user.phone = _contact.get('phone_numbers', [{}])[0].get('number')
+ _emails = _contact.get('email_addresses', [])
+ for email in _emails:
+ if email.get('type', '') == 'primary':
+ user.email = email.get('address')
+ break
+
+ try:
+ user.birth_date = datetime.datetime.strptime(
+ _user.get('birth_date'), "%B %d")
+ except ValueError:
+ user.birth_date = _user.get('birth_date')
+
+ return user
+
+
+class Yandex(OAuth2):
+ """
+ Yandex |oauth2| provider.
+
+ * Dashboard: https://oauth.yandex.com/client/my
+ * Docs:
+ http://api.yandex.com/oauth/doc/dg/reference/obtain-access-token.xml
+ * API reference:
+
+ Supported :class:`.User` properties:
+
+ * id
+ * name
+ * username
+
+ Unsupported :class:`.User` properties:
+
+ * birth_date
+ * city
+ * country
+ * email
+ * first_name
+ * gender
+ * last_name
+ * link
+ * locale
+ * location
+ * nickname
+ * phone
+ * picture
+ * postal_code
+ * timezone
+
+ """
+
+ user_authorization_url = 'https://oauth.yandex.com/authorize'
+ access_token_url = 'https://oauth.yandex.com/token'
+ user_info_url = 'https://login.yandex.ru/info'
+
+ supported_user_attributes = core.SupportedUserAttributes(
+ id=True,
+ name=True,
+ username=True
+ )
+
+ @classmethod
+ def _x_credentials_parser(cls, credentials, data):
+ if data.get('token_type') == 'bearer':
+ credentials.token_type = cls.BEARER
+ return credentials
+
+ @staticmethod
+ def _x_user_parser(user, data):
+
+ # http://api.yandex.ru/login/doc/dg/reference/response.xml
+ user.name = data.get('real_name')
+ user.nickname = data.get('display_name')
+ user.gender = data.get('Sex')
+ user.email = data.get('Default_email')
+ user.username = data.get('login')
+
+ try:
+ user.birth_date = datetime.datetime.strptime(
+ data.get('birthday'), "%Y-%m-%d")
+ except ValueError:
+ user.birth_date = data.get('birthday')
+
+ return user
+
+
+# The provider type ID is generated from this list's indexes!
+# Always append new providers at the end so that ids of existing providers
+# don't change!
+PROVIDER_ID_MAP = [
+ Amazon,
+ Behance,
+ Bitly,
+ Bitbucket,
+ Cosm,
+ DeviantART,
+ Eventbrite,
+ Facebook,
+ Foursquare,
+ GitHub,
+ Google,
+ LinkedIn,
+ OAuth2,
+ PayPal,
+ Reddit,
+ Viadeo,
+ VK,
+ WindowsLive,
+ Yammer,
+ Yandex,
+]
diff --git a/rhodecode/lib/_vendor/authomatic/providers/openid.py b/rhodecode/lib/_vendor/authomatic/providers/openid.py
new file mode 100755
--- /dev/null
+++ b/rhodecode/lib/_vendor/authomatic/providers/openid.py
@@ -0,0 +1,505 @@
+# -*- coding: utf-8 -*-
+"""
+|openid| Providers
+----------------------------------
+
+Providers which implement the |openid|_ protocol based on the
+`python-openid`_ library.
+
+.. warning::
+
+ This providers are dependent on the |pyopenid|_ package.
+
+.. autosummary::
+
+ OpenID
+ Yahoo
+ Google
+
+"""
+
+# We need absolute import to import from openid library which has the same
+# name as this module
+from __future__ import absolute_import
+import datetime
+import logging
+import time
+
+from openid import oidutil
+from openid.consumer import consumer
+from openid.extensions import ax, pape, sreg
+from openid.association import Association
+
+from authomatic import providers
+from authomatic.exceptions import FailureError, CancellationError, OpenIDError
+
+
+__all__ = ['OpenID', 'Yahoo', 'Google']
+
+
+# Suppress openid logging.
+oidutil.log = lambda message, level=0: None
+
+
+REALM_HTML = \
+ """
+
+
+
+
+
+ {body}
+
+"""
+
+
+XRDS_XML = \
+ """
+
+
+
+
+ http://specs.openid.net/auth/2.0/return_to
+ {return_to}
+
+
+
+"""
+
+
+class SessionOpenIDStore(object):
+ """
+ A very primitive session-based implementation of the.
+
+ :class:`openid.store.interface.OpenIDStore` interface of the
+ `python-openid`_ library.
+
+ .. warning::
+
+ Nonces get verified only by their timeout. Use on your own risk!
+
+ """
+
+ @staticmethod
+ def _log(level, message):
+ return None
+
+ ASSOCIATION_KEY = ('authomatic.providers.openid.SessionOpenIDStore:'
+ 'association')
+
+ def __init__(self, session, nonce_timeout=None):
+ """
+ :param int nonce_timeout:
+
+ Nonces older than this in seconds will be considered expired.
+ Default is 600.
+ """
+ self.session = session
+ self.nonce_timeout = nonce_timeout or 600
+
+ def storeAssociation(self, server_url, association):
+ self._log(logging.DEBUG,
+ 'SessionOpenIDStore: Storing association to session.')
+
+ serialized = association.serialize()
+ decoded = serialized.decode('latin-1')
+
+ assoc = decoded
+ # assoc = serialized
+
+ # Always store only one association as a tuple.
+ self.session[self.ASSOCIATION_KEY] = (server_url, association.handle,
+ assoc)
+
+ def getAssociation(self, server_url, handle=None):
+ # Try to get association.
+ assoc = self.session.get(self.ASSOCIATION_KEY)
+ if assoc and assoc[0] == server_url:
+ # If found deserialize and return it.
+ self._log(logging.DEBUG, u'SessionOpenIDStore: Association found.')
+ return Association.deserialize(assoc[2].encode('latin-1'))
+ else:
+ self._log(logging.DEBUG,
+ u'SessionOpenIDStore: Association not found.')
+
+ def removeAssociation(self, server_url, handle):
+ # Just inform the caller that it's gone.
+ return True
+
+ def useNonce(self, server_url, timestamp, salt):
+ # Evaluate expired nonces as false.
+ age = int(time.time()) - int(timestamp)
+ if age < self.nonce_timeout:
+ return True
+ else:
+ self._log(logging.ERROR, u'SessionOpenIDStore: Expired nonce!')
+ return False
+
+
+class OpenID(providers.AuthenticationProvider):
+ """
+ |openid|_ provider based on the `python-openid`_ library.
+ """
+
+ AX = ['http://axschema.org/contact/email',
+ 'http://schema.openid.net/contact/email',
+ 'http://axschema.org/namePerson',
+ 'http://openid.net/schema/namePerson/first',
+ 'http://openid.net/schema/namePerson/last',
+ 'http://openid.net/schema/gender',
+ 'http://openid.net/schema/language/pref',
+ 'http://openid.net/schema/contact/web/default',
+ 'http://openid.net/schema/media/image',
+ 'http://openid.net/schema/timezone']
+
+ AX_REQUIRED = ['http://schema.openid.net/contact/email']
+
+ SREG = ['nickname',
+ 'email',
+ 'fullname',
+ 'dob',
+ 'gender',
+ 'postcode',
+ 'country',
+ 'language',
+ 'timezone']
+
+ PAPE = [
+ 'http://schemas.openid.net/pape/policies/2007/06/'
+ 'multi-factor-physical',
+ 'http://schemas.openid.net/pape/policies/2007/06/multi-factor',
+ 'http://schemas.openid.net/pape/policies/2007/06/phishing-resistant'
+ ]
+
+ def __init__(self, *args, **kwargs):
+ """
+ Accepts additional keyword arguments:
+
+ :param store:
+ Any object which implements
+ :class:`openid.store.interface.OpenIDStore`
+ of the `python-openid`_ library.
+
+ :param bool use_realm:
+ Whether to use `OpenID realm
+ `_
+ If ``True`` the realm HTML document will be accessible at
+ ``{current url}?{realm_param}={realm_param}``
+ e.g. ``http://example.com/path?realm=realm``.
+
+ :param str realm_body:
+ Contents of the HTML body tag of the realm.
+
+ :param str realm_param:
+ Name of the query parameter to be used to serve the realm.
+
+ :param str xrds_param:
+ The name of the query parameter to be used to serve the
+ `XRDS document
+ `_.
+
+ :param list sreg:
+ List of strings of optional
+ `SREG
+ `_
+ fields.
+ Default = :attr:`OpenID.SREG`.
+
+ :param list sreg_required:
+ List of strings of required
+ `SREG
+ `_
+ fields.
+ Default = ``[]``.
+
+ :param list ax:
+ List of strings of optional
+ `AX
+ `_
+ schemas.
+ Default = :attr:`OpenID.AX`.
+
+ :param list ax_required:
+ List of strings of required
+ `AX
+ `_
+ schemas.
+ Default = :attr:`OpenID.AX_REQUIRED`.
+
+ :param list pape:
+ of requested
+ `PAPE
+ `_
+ policies.
+ Default = :attr:`OpenID.PAPE`.
+
+ As well as those inherited from :class:`.AuthenticationProvider`
+ constructor.
+
+ """
+
+ super(OpenID, self).__init__(*args, **kwargs)
+
+ # Allow for other openid store implementations.
+ self.store = self._kwarg(
+ kwargs, 'store', SessionOpenIDStore(
+ self.session))
+
+ # Realm
+ self.use_realm = self._kwarg(kwargs, 'use_realm', True)
+ self.realm_body = self._kwarg(kwargs, 'realm_body', '')
+ self.realm_param = self._kwarg(kwargs, 'realm_param', 'realm')
+ self.xrds_param = self._kwarg(kwargs, 'xrds_param', 'xrds')
+
+ # SREG
+ self.sreg = self._kwarg(kwargs, 'sreg', self.SREG)
+ self.sreg_required = self._kwarg(kwargs, 'sreg_required', [])
+
+ # AX
+ self.ax = self._kwarg(kwargs, 'ax', self.AX)
+ self.ax_required = self._kwarg(kwargs, 'ax_required', self.AX_REQUIRED)
+ # add required schemas to schemas if not already there
+ for i in self.ax_required:
+ if i not in self.ax:
+ self.ax.append(i)
+
+ # PAPE
+ self.pape = self._kwarg(kwargs, 'pape', self.PAPE)
+
+ @staticmethod
+ def _x_user_parser(user, data):
+
+ user.first_name = data.get('ax', {}).get(
+ 'http://openid.net/schema/namePerson/first')
+ user.last_name = data.get('ax', {}).get(
+ 'http://openid.net/schema/namePerson/last')
+ user.id = data.get('guid')
+ user.link = data.get('ax', {}).get(
+ 'http://openid.net/schema/contact/web/default')
+ user.picture = data.get('ax', {}).get(
+ 'http://openid.net/schema/media/image')
+ user.nickname = data.get('sreg', {}).get('nickname')
+ user.country = data.get('sreg', {}).get('country')
+ user.postal_code = data.get('sreg', {}).get('postcode')
+
+ user.name = data.get('sreg', {}).get('fullname') or \
+ data.get('ax', {}).get('http://axschema.org/namePerson')
+
+ user.gender = data.get('sreg', {}).get('gender') or \
+ data.get('ax', {}).get('http://openid.net/schema/gender')
+
+ user.locale = data.get('sreg', {}).get('language') or \
+ data.get('ax', {}).get('http://openid.net/schema/language/pref')
+
+ user.timezone = data.get('sreg', {}).get('timezone') or \
+ data.get('ax', {}).get('http://openid.net/schema/timezone')
+
+ user.email = data.get('sreg', {}).get('email') or \
+ data.get('ax', {}).get('http://axschema.org/contact/email') or \
+ data.get('ax', {}).get('http://schema.openid.net/contact/email')
+
+ if data.get('sreg', {}).get('dob'):
+ user.birth_date = datetime.datetime.strptime(
+ data.get('sreg', {}).get('dob'),
+ '%Y-%m-%d'
+ )
+ else:
+ user.birth_date = None
+
+ return user
+
+ @providers.login_decorator
+ def login(self):
+ # Instantiate consumer
+ self.store._log = self._log
+ oi_consumer = consumer.Consumer(self.session, self.store)
+
+ # handle realm and XRDS if there is only one query parameter
+ if self.use_realm and len(self.params) == 1:
+ realm_request = self.params.get(self.realm_param)
+ xrds_request = self.params.get(self.xrds_param)
+ else:
+ realm_request = None
+ xrds_request = None
+
+ # determine type of request
+ if realm_request:
+ # =================================================================
+ # Realm HTML
+ # =================================================================
+
+ self._log(
+ logging.INFO,
+ u'Writing OpenID realm HTML to the response.')
+ xrds_location = '{u}?{x}={x}'.format(u=self.url, x=self.xrds_param)
+ self.write(
+ REALM_HTML.format(
+ xrds_location=xrds_location,
+ body=self.realm_body))
+
+ elif xrds_request:
+ # =================================================================
+ # XRDS XML
+ # =================================================================
+
+ self._log(
+ logging.INFO,
+ u'Writing XRDS XML document to the response.')
+ self.set_header('Content-Type', 'application/xrds+xml')
+ self.write(XRDS_XML.format(return_to=self.url))
+
+ elif self.params.get('openid.mode'):
+ # =================================================================
+ # Phase 2 after redirect
+ # =================================================================
+
+ self._log(
+ logging.INFO,
+ u'Continuing OpenID authentication procedure after redirect.')
+
+ # complete the authentication process
+ response = oi_consumer.complete(self.params, self.url)
+
+ # on success
+ if response.status == consumer.SUCCESS:
+
+ data = {}
+
+ # get user ID
+ data['guid'] = response.getDisplayIdentifier()
+
+ self._log(logging.INFO, u'Authentication successful.')
+
+ # get user data from AX response
+ ax_response = ax.FetchResponse.fromSuccessResponse(response)
+ if ax_response and ax_response.data:
+ self._log(logging.INFO, u'Got AX data.')
+ ax_data = {}
+ # convert iterable values to their first item
+ for k, v in ax_response.data.items():
+ if v and isinstance(v, (list, tuple)):
+ ax_data[k] = v[0]
+ data['ax'] = ax_data
+
+ # get user data from SREG response
+ sreg_response = sreg.SRegResponse.fromSuccessResponse(response)
+ if sreg_response and sreg_response.data:
+ self._log(logging.INFO, u'Got SREG data.')
+ data['sreg'] = sreg_response.data
+
+ # get data from PAPE response
+ pape_response = pape.Response.fromSuccessResponse(response)
+ if pape_response and pape_response.auth_policies:
+ self._log(logging.INFO, u'Got PAPE data.')
+ data['pape'] = pape_response.auth_policies
+
+ # create user
+ self._update_or_create_user(data)
+
+ # =============================================================
+ # We're done!
+ # =============================================================
+
+ elif response.status == consumer.CANCEL:
+ raise CancellationError(
+ u'User cancelled the verification of ID "{0}"!'.format(
+ response.getDisplayIdentifier()))
+
+ elif response.status == consumer.FAILURE:
+ raise FailureError(response.message)
+
+ elif self.identifier: # As set in AuthenticationProvider.__init__
+ # =================================================================
+ # Phase 1 before redirect
+ # =================================================================
+
+ self._log(
+ logging.INFO,
+ u'Starting OpenID authentication procedure.')
+
+ # get AuthRequest object
+ try:
+ auth_request = oi_consumer.begin(self.identifier)
+ except consumer.DiscoveryFailure as e:
+ raise FailureError(
+ u'Discovery failed for identifier {0}!'.format(
+ self.identifier
+ ),
+ url=self.identifier,
+ original_message=e.message)
+
+ self._log(
+ logging.INFO,
+ u'Service discovery for identifier {0} successful.'.format(
+ self.identifier))
+
+ # add SREG extension
+ # we need to remove required fields from optional fields because
+ # addExtension then raises an error
+ self.sreg = [i for i in self.sreg if i not in self.sreg_required]
+ auth_request.addExtension(
+ sreg.SRegRequest(
+ optional=self.sreg,
+ required=self.sreg_required)
+ )
+
+ # add AX extension
+ ax_request = ax.FetchRequest()
+ # set AX schemas
+ for i in self.ax:
+ required = i in self.ax_required
+ ax_request.add(ax.AttrInfo(i, required=required))
+ auth_request.addExtension(ax_request)
+
+ # add PAPE extension
+ auth_request.addExtension(pape.Request(self.pape))
+
+ # prepare realm and return_to URLs
+ if self.use_realm:
+ realm = return_to = '{u}?{r}={r}'.format(
+ u=self.url, r=self.realm_param)
+ else:
+ realm = return_to = self.url
+
+ url = auth_request.redirectURL(realm, return_to)
+
+ if auth_request.shouldSendRedirect():
+ # can be redirected
+ url = auth_request.redirectURL(realm, return_to)
+ self._log(
+ logging.INFO,
+ u'Redirecting user to {0}.'.format(url))
+ self.redirect(url)
+ else:
+ # must be sent as POST
+ # this writes a html post form with auto-submit
+ self._log(
+ logging.INFO,
+ u'Writing an auto-submit HTML form to the response.')
+ form = auth_request.htmlMarkup(
+ realm, return_to, False, dict(
+ id='openid_form'))
+ self.write(form)
+ else:
+ raise OpenIDError('No identifier specified!')
+
+
+class Yahoo(OpenID):
+ """
+ Yahoo :class:`.OpenID` provider with the :attr:`.identifier` predefined to
+ ``"me.yahoo.com"``.
+ """
+
+ identifier = 'me.yahoo.com'
+
+
+class Google(OpenID):
+ """
+ Google :class:`.OpenID` provider with the :attr:`.identifier` predefined to
+ ``"https://www.google.com/accounts/o8/id"``.
+ """
+
+ identifier = 'https://www.google.com/accounts/o8/id'
diff --git a/rhodecode/lib/_vendor/authomatic/providers/persona.py b/rhodecode/lib/_vendor/authomatic/providers/persona.py
new file mode 100755
--- /dev/null
+++ b/rhodecode/lib/_vendor/authomatic/providers/persona.py
@@ -0,0 +1,6 @@
+# -*- coding: utf-8 -*-
+from authomatic import providers
+
+
+class MozillaPersona(providers.AuthenticationProvider):
+ pass
diff --git a/rhodecode/lib/_vendor/authomatic/six.py b/rhodecode/lib/_vendor/authomatic/six.py
new file mode 100755
--- /dev/null
+++ b/rhodecode/lib/_vendor/authomatic/six.py
@@ -0,0 +1,839 @@
+# -*- coding: utf-8 -*-
+"""Utilities for writing code that runs on Python 2 and 3"""
+
+# Copyright (c) 2010-2015 Benjamin Peterson
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+
+from __future__ import absolute_import
+
+import functools
+import itertools
+import operator
+import sys
+import types
+
+__author__ = "Benjamin Peterson "
+__version__ = "1.9.0"
+
+
+# Useful for very coarse version differentiation.
+PY2 = sys.version_info[0] == 2
+PY3 = sys.version_info[0] == 3
+
+if PY3:
+ string_types = str,
+ integer_types = int,
+ class_types = type,
+ text_type = str
+ binary_type = bytes
+
+ MAXSIZE = sys.maxsize
+else:
+ string_types = basestring,
+ integer_types = (int, long)
+ class_types = (type, types.ClassType)
+ text_type = unicode
+ binary_type = str
+
+ if sys.platform.startswith("java"):
+ # Jython always uses 32 bits.
+ MAXSIZE = int((1 << 31) - 1)
+ else:
+ # It's possible to have sizeof(long) != sizeof(Py_ssize_t).
+ class X(object):
+ def __len__(self):
+ return 1 << 31
+ try:
+ len(X())
+ except OverflowError:
+ # 32-bit
+ MAXSIZE = int((1 << 31) - 1)
+ else:
+ # 64-bit
+ MAXSIZE = int((1 << 63) - 1)
+ del X
+
+
+def _add_doc(func, doc):
+ """Add documentation to a function."""
+ func.__doc__ = doc
+
+
+def _import_module(name):
+ """Import module, returning the module after the last dot."""
+ __import__(name)
+ return sys.modules[name]
+
+
+class _LazyDescr(object):
+
+ def __init__(self, name):
+ self.name = name
+
+ def __get__(self, obj, tp):
+ result = self._resolve()
+ setattr(obj, self.name, result) # Invokes __set__.
+ try:
+ # This is a bit ugly, but it avoids running this again by
+ # removing this descriptor.
+ delattr(obj.__class__, self.name)
+ except AttributeError:
+ pass
+ return result
+
+
+class MovedModule(_LazyDescr):
+
+ def __init__(self, name, old, new=None):
+ super(MovedModule, self).__init__(name)
+ if PY3:
+ if new is None:
+ new = name
+ self.mod = new
+ else:
+ self.mod = old
+
+ def _resolve(self):
+ return _import_module(self.mod)
+
+ def __getattr__(self, attr):
+ _module = self._resolve()
+ value = getattr(_module, attr)
+ setattr(self, attr, value)
+ return value
+
+
+class _LazyModule(types.ModuleType):
+
+ def __init__(self, name):
+ super(_LazyModule, self).__init__(name)
+ self.__doc__ = self.__class__.__doc__
+
+ def __dir__(self):
+ attrs = ["__doc__", "__name__"]
+ attrs += [attr.name for attr in self._moved_attributes]
+ return attrs
+
+ # Subclasses should override this
+ _moved_attributes = []
+
+
+class MovedAttribute(_LazyDescr):
+
+ def __init__(self, name, old_mod, new_mod, old_attr=None, new_attr=None):
+ super(MovedAttribute, self).__init__(name)
+ if PY3:
+ if new_mod is None:
+ new_mod = name
+ self.mod = new_mod
+ if new_attr is None:
+ if old_attr is None:
+ new_attr = name
+ else:
+ new_attr = old_attr
+ self.attr = new_attr
+ else:
+ self.mod = old_mod
+ if old_attr is None:
+ old_attr = name
+ self.attr = old_attr
+
+ def _resolve(self):
+ module = _import_module(self.mod)
+ return getattr(module, self.attr)
+
+
+class _SixMetaPathImporter(object):
+ """
+ A meta path importer to import six.moves and its submodules.
+
+ This class implements a PEP302 finder and loader. It should be compatible
+ with Python 2.5 and all existing versions of Python3
+ """
+ def __init__(self, six_module_name):
+ self.name = six_module_name
+ self.known_modules = {}
+
+ def _add_module(self, mod, *fullnames):
+ for fullname in fullnames:
+ self.known_modules[self.name + "." + fullname] = mod
+
+ def _get_module(self, fullname):
+ return self.known_modules[self.name + "." + fullname]
+
+ def find_module(self, fullname, path=None):
+ if fullname in self.known_modules:
+ return self
+ return None
+
+ def __get_module(self, fullname):
+ try:
+ return self.known_modules[fullname]
+ except KeyError:
+ raise ImportError("This loader does not know module " + fullname)
+
+ def load_module(self, fullname):
+ try:
+ # in case of a reload
+ return sys.modules[fullname]
+ except KeyError:
+ pass
+ mod = self.__get_module(fullname)
+ if isinstance(mod, MovedModule):
+ mod = mod._resolve()
+ else:
+ mod.__loader__ = self
+ sys.modules[fullname] = mod
+ return mod
+
+ def is_package(self, fullname):
+ """
+ Return true, if the named module is a package.
+
+ We need this method to get correct spec objects with
+ Python 3.4 (see PEP451)
+ """
+ return hasattr(self.__get_module(fullname), "__path__")
+
+ def get_code(self, fullname):
+ """Return None
+
+ Required, if is_package is implemented"""
+ self.__get_module(fullname) # eventually raises ImportError
+ return None
+ get_source = get_code # same as get_code
+
+_importer = _SixMetaPathImporter(__name__)
+
+
+class _MovedItems(_LazyModule):
+ """Lazy loading of moved objects"""
+ __path__ = [] # mark as package
+
+
+_moved_attributes = [
+ MovedAttribute("cStringIO", "cStringIO", "io", "StringIO"),
+ MovedAttribute("filter", "itertools", "builtins", "ifilter", "filter"),
+ MovedAttribute("filterfalse", "itertools", "itertools", "ifilterfalse", "filterfalse"),
+ MovedAttribute("input", "__builtin__", "builtins", "raw_input", "input"),
+ MovedAttribute("intern", "__builtin__", "sys"),
+ MovedAttribute("map", "itertools", "builtins", "imap", "map"),
+ MovedAttribute("range", "__builtin__", "builtins", "xrange", "range"),
+ MovedAttribute("reload_module", "__builtin__", "imp", "reload"),
+ MovedAttribute("reduce", "__builtin__", "functools"),
+ MovedAttribute("shlex_quote", "pipes", "shlex", "quote"),
+ MovedAttribute("StringIO", "StringIO", "io"),
+ MovedAttribute("UserDict", "UserDict", "collections"),
+ MovedAttribute("UserList", "UserList", "collections"),
+ MovedAttribute("UserString", "UserString", "collections"),
+ MovedAttribute("xrange", "__builtin__", "builtins", "xrange", "range"),
+ MovedAttribute("zip", "itertools", "builtins", "izip", "zip"),
+ MovedAttribute("zip_longest", "itertools", "itertools", "izip_longest", "zip_longest"),
+
+ MovedModule("builtins", "__builtin__"),
+ MovedModule("configparser", "ConfigParser"),
+ MovedModule("copyreg", "copy_reg"),
+ MovedModule("dbm_gnu", "gdbm", "dbm.gnu"),
+ MovedModule("_dummy_thread", "dummy_thread", "_dummy_thread"),
+ MovedModule("http_cookiejar", "cookielib", "http.cookiejar"),
+ MovedModule("http_cookies", "Cookie", "http.cookies"),
+ MovedModule("html_entities", "htmlentitydefs", "html.entities"),
+ MovedModule("html_parser", "HTMLParser", "html.parser"),
+ MovedModule("http_client", "httplib", "http.client"),
+ MovedModule("email_mime_multipart", "email.MIMEMultipart", "email.mime.multipart"),
+ MovedModule("email_mime_nonmultipart", "email.MIMENonMultipart", "email.mime.nonmultipart"),
+ MovedModule("email_mime_text", "email.MIMEText", "email.mime.text"),
+ MovedModule("email_mime_base", "email.MIMEBase", "email.mime.base"),
+ MovedModule("BaseHTTPServer", "BaseHTTPServer", "http.server"),
+ MovedModule("CGIHTTPServer", "CGIHTTPServer", "http.server"),
+ MovedModule("SimpleHTTPServer", "SimpleHTTPServer", "http.server"),
+ MovedModule("cPickle", "cPickle", "pickle"),
+ MovedModule("queue", "Queue"),
+ MovedModule("reprlib", "repr"),
+ MovedModule("socketserver", "SocketServer"),
+ MovedModule("_thread", "thread", "_thread"),
+ MovedModule("tkinter", "Tkinter"),
+ MovedModule("tkinter_dialog", "Dialog", "tkinter.dialog"),
+ MovedModule("tkinter_filedialog", "FileDialog", "tkinter.filedialog"),
+ MovedModule("tkinter_scrolledtext", "ScrolledText", "tkinter.scrolledtext"),
+ MovedModule("tkinter_simpledialog", "SimpleDialog", "tkinter.simpledialog"),
+ MovedModule("tkinter_tix", "Tix", "tkinter.tix"),
+ MovedModule("tkinter_ttk", "ttk", "tkinter.ttk"),
+ MovedModule("tkinter_constants", "Tkconstants", "tkinter.constants"),
+ MovedModule("tkinter_dnd", "Tkdnd", "tkinter.dnd"),
+ MovedModule("tkinter_colorchooser", "tkColorChooser",
+ "tkinter.colorchooser"),
+ MovedModule("tkinter_commondialog", "tkCommonDialog",
+ "tkinter.commondialog"),
+ MovedModule("tkinter_tkfiledialog", "tkFileDialog", "tkinter.filedialog"),
+ MovedModule("tkinter_font", "tkFont", "tkinter.font"),
+ MovedModule("tkinter_messagebox", "tkMessageBox", "tkinter.messagebox"),
+ MovedModule("tkinter_tksimpledialog", "tkSimpleDialog",
+ "tkinter.simpledialog"),
+ MovedModule("urllib_parse", __name__ + ".moves.urllib_parse", "urllib.parse"),
+ MovedModule("urllib_error", __name__ + ".moves.urllib_error", "urllib.error"),
+ MovedModule("urllib", __name__ + ".moves.urllib", __name__ + ".moves.urllib"),
+ MovedModule("urllib_robotparser", "robotparser", "urllib.robotparser"),
+ MovedModule("xmlrpc_client", "xmlrpclib", "xmlrpc.client"),
+ MovedModule("xmlrpc_server", "SimpleXMLRPCServer", "xmlrpc.server"),
+ MovedModule("winreg", "_winreg"),
+]
+for attr in _moved_attributes:
+ setattr(_MovedItems, attr.name, attr)
+ if isinstance(attr, MovedModule):
+ _importer._add_module(attr, "moves." + attr.name)
+del attr
+
+_MovedItems._moved_attributes = _moved_attributes
+
+moves = _MovedItems(__name__ + ".moves")
+_importer._add_module(moves, "moves")
+
+
+class Module_six_moves_urllib_parse(_LazyModule):
+ """Lazy loading of moved objects in six.moves.urllib_parse"""
+
+
+_urllib_parse_moved_attributes = [
+ MovedAttribute("ParseResult", "urlparse", "urllib.parse"),
+ MovedAttribute("SplitResult", "urlparse", "urllib.parse"),
+ MovedAttribute("parse_qs", "urlparse", "urllib.parse"),
+ MovedAttribute("parse_qsl", "urlparse", "urllib.parse"),
+ MovedAttribute("urldefrag", "urlparse", "urllib.parse"),
+ MovedAttribute("urljoin", "urlparse", "urllib.parse"),
+ MovedAttribute("urlparse", "urlparse", "urllib.parse"),
+ MovedAttribute("urlsplit", "urlparse", "urllib.parse"),
+ MovedAttribute("urlunparse", "urlparse", "urllib.parse"),
+ MovedAttribute("urlunsplit", "urlparse", "urllib.parse"),
+ MovedAttribute("quote", "urllib", "urllib.parse"),
+ MovedAttribute("quote_plus", "urllib", "urllib.parse"),
+ MovedAttribute("unquote", "urllib", "urllib.parse"),
+ MovedAttribute("unquote_plus", "urllib", "urllib.parse"),
+ MovedAttribute("urlencode", "urllib", "urllib.parse"),
+ MovedAttribute("splitquery", "urllib", "urllib.parse"),
+ MovedAttribute("splittag", "urllib", "urllib.parse"),
+ MovedAttribute("splituser", "urllib", "urllib.parse"),
+ MovedAttribute("uses_fragment", "urlparse", "urllib.parse"),
+ MovedAttribute("uses_netloc", "urlparse", "urllib.parse"),
+ MovedAttribute("uses_params", "urlparse", "urllib.parse"),
+ MovedAttribute("uses_query", "urlparse", "urllib.parse"),
+ MovedAttribute("uses_relative", "urlparse", "urllib.parse"),
+]
+for attr in _urllib_parse_moved_attributes:
+ setattr(Module_six_moves_urllib_parse, attr.name, attr)
+del attr
+
+Module_six_moves_urllib_parse._moved_attributes = _urllib_parse_moved_attributes
+
+_importer._add_module(Module_six_moves_urllib_parse(__name__ + ".moves.urllib_parse"),
+ "moves.urllib_parse", "moves.urllib.parse")
+
+
+class Module_six_moves_urllib_error(_LazyModule):
+ """Lazy loading of moved objects in six.moves.urllib_error"""
+
+
+_urllib_error_moved_attributes = [
+ MovedAttribute("URLError", "urllib2", "urllib.error"),
+ MovedAttribute("HTTPError", "urllib2", "urllib.error"),
+ MovedAttribute("ContentTooShortError", "urllib", "urllib.error"),
+]
+for attr in _urllib_error_moved_attributes:
+ setattr(Module_six_moves_urllib_error, attr.name, attr)
+del attr
+
+Module_six_moves_urllib_error._moved_attributes = _urllib_error_moved_attributes
+
+_importer._add_module(Module_six_moves_urllib_error(__name__ + ".moves.urllib.error"),
+ "moves.urllib_error", "moves.urllib.error")
+
+
+class Module_six_moves_urllib_request(_LazyModule):
+ """Lazy loading of moved objects in six.moves.urllib_request"""
+
+
+_urllib_request_moved_attributes = [
+ MovedAttribute("urlopen", "urllib2", "urllib.request"),
+ MovedAttribute("install_opener", "urllib2", "urllib.request"),
+ MovedAttribute("build_opener", "urllib2", "urllib.request"),
+ MovedAttribute("pathname2url", "urllib", "urllib.request"),
+ MovedAttribute("url2pathname", "urllib", "urllib.request"),
+ MovedAttribute("getproxies", "urllib", "urllib.request"),
+ MovedAttribute("Request", "urllib2", "urllib.request"),
+ MovedAttribute("OpenerDirector", "urllib2", "urllib.request"),
+ MovedAttribute("HTTPDefaultErrorHandler", "urllib2", "urllib.request"),
+ MovedAttribute("HTTPRedirectHandler", "urllib2", "urllib.request"),
+ MovedAttribute("HTTPCookieProcessor", "urllib2", "urllib.request"),
+ MovedAttribute("ProxyHandler", "urllib2", "urllib.request"),
+ MovedAttribute("BaseHandler", "urllib2", "urllib.request"),
+ MovedAttribute("HTTPPasswordMgr", "urllib2", "urllib.request"),
+ MovedAttribute("HTTPPasswordMgrWithDefaultRealm", "urllib2", "urllib.request"),
+ MovedAttribute("AbstractBasicAuthHandler", "urllib2", "urllib.request"),
+ MovedAttribute("HTTPBasicAuthHandler", "urllib2", "urllib.request"),
+ MovedAttribute("ProxyBasicAuthHandler", "urllib2", "urllib.request"),
+ MovedAttribute("AbstractDigestAuthHandler", "urllib2", "urllib.request"),
+ MovedAttribute("HTTPDigestAuthHandler", "urllib2", "urllib.request"),
+ MovedAttribute("ProxyDigestAuthHandler", "urllib2", "urllib.request"),
+ MovedAttribute("HTTPHandler", "urllib2", "urllib.request"),
+ MovedAttribute("HTTPSHandler", "urllib2", "urllib.request"),
+ MovedAttribute("FileHandler", "urllib2", "urllib.request"),
+ MovedAttribute("FTPHandler", "urllib2", "urllib.request"),
+ MovedAttribute("CacheFTPHandler", "urllib2", "urllib.request"),
+ MovedAttribute("UnknownHandler", "urllib2", "urllib.request"),
+ MovedAttribute("HTTPErrorProcessor", "urllib2", "urllib.request"),
+ MovedAttribute("urlretrieve", "urllib", "urllib.request"),
+ MovedAttribute("urlcleanup", "urllib", "urllib.request"),
+ MovedAttribute("URLopener", "urllib", "urllib.request"),
+ MovedAttribute("FancyURLopener", "urllib", "urllib.request"),
+ MovedAttribute("proxy_bypass", "urllib", "urllib.request"),
+]
+for attr in _urllib_request_moved_attributes:
+ setattr(Module_six_moves_urllib_request, attr.name, attr)
+del attr
+
+Module_six_moves_urllib_request._moved_attributes = _urllib_request_moved_attributes
+
+_importer._add_module(Module_six_moves_urllib_request(__name__ + ".moves.urllib.request"),
+ "moves.urllib_request", "moves.urllib.request")
+
+
+class Module_six_moves_urllib_response(_LazyModule):
+ """Lazy loading of moved objects in six.moves.urllib_response"""
+
+
+_urllib_response_moved_attributes = [
+ MovedAttribute("addbase", "urllib", "urllib.response"),
+ MovedAttribute("addclosehook", "urllib", "urllib.response"),
+ MovedAttribute("addinfo", "urllib", "urllib.response"),
+ MovedAttribute("addinfourl", "urllib", "urllib.response"),
+]
+for attr in _urllib_response_moved_attributes:
+ setattr(Module_six_moves_urllib_response, attr.name, attr)
+del attr
+
+Module_six_moves_urllib_response._moved_attributes = _urllib_response_moved_attributes
+
+_importer._add_module(Module_six_moves_urllib_response(__name__ + ".moves.urllib.response"),
+ "moves.urllib_response", "moves.urllib.response")
+
+
+class Module_six_moves_urllib_robotparser(_LazyModule):
+ """Lazy loading of moved objects in six.moves.urllib_robotparser"""
+
+
+_urllib_robotparser_moved_attributes = [
+ MovedAttribute("RobotFileParser", "robotparser", "urllib.robotparser"),
+]
+for attr in _urllib_robotparser_moved_attributes:
+ setattr(Module_six_moves_urllib_robotparser, attr.name, attr)
+del attr
+
+Module_six_moves_urllib_robotparser._moved_attributes = _urllib_robotparser_moved_attributes
+
+_importer._add_module(Module_six_moves_urllib_robotparser(__name__ + ".moves.urllib.robotparser"),
+ "moves.urllib_robotparser", "moves.urllib.robotparser")
+
+
+class Module_six_moves_urllib(types.ModuleType):
+ """Create a six.moves.urllib namespace that resembles the Python 3 namespace"""
+ __path__ = [] # mark as package
+ parse = _importer._get_module("moves.urllib_parse")
+ error = _importer._get_module("moves.urllib_error")
+ request = _importer._get_module("moves.urllib_request")
+ response = _importer._get_module("moves.urllib_response")
+ robotparser = _importer._get_module("moves.urllib_robotparser")
+
+ def __dir__(self):
+ return ['parse', 'error', 'request', 'response', 'robotparser']
+
+_importer._add_module(Module_six_moves_urllib(__name__ + ".moves.urllib"),
+ "moves.urllib")
+
+
+def add_move(move):
+ """Add an item to six.moves."""
+ setattr(_MovedItems, move.name, move)
+
+
+def remove_move(name):
+ """Remove item from six.moves."""
+ try:
+ delattr(_MovedItems, name)
+ except AttributeError:
+ try:
+ del moves.__dict__[name]
+ except KeyError:
+ raise AttributeError("no such move, %r" % (name,))
+
+
+if PY3:
+ _meth_func = "__func__"
+ _meth_self = "__self__"
+
+ _func_closure = "__closure__"
+ _func_code = "__code__"
+ _func_defaults = "__defaults__"
+ _func_globals = "__globals__"
+else:
+ _meth_func = "im_func"
+ _meth_self = "im_self"
+
+ _func_closure = "func_closure"
+ _func_code = "func_code"
+ _func_defaults = "func_defaults"
+ _func_globals = "func_globals"
+
+
+try:
+ advance_iterator = next
+except NameError:
+ def advance_iterator(it):
+ return it.next()
+next = advance_iterator
+
+
+try:
+ callable = callable
+except NameError:
+ def callable(obj):
+ return any("__call__" in klass.__dict__ for klass in type(obj).__mro__)
+
+
+if PY3:
+ def get_unbound_function(unbound):
+ return unbound
+
+ create_bound_method = types.MethodType
+
+ Iterator = object
+else:
+ def get_unbound_function(unbound):
+ return unbound.im_func
+
+ def create_bound_method(func, obj):
+ return types.MethodType(func, obj, obj.__class__)
+
+ class Iterator(object):
+
+ def next(self):
+ return type(self).__next__(self)
+
+ callable = callable
+_add_doc(get_unbound_function,
+ """Get the function out of a possibly unbound function""")
+
+
+get_method_function = operator.attrgetter(_meth_func)
+get_method_self = operator.attrgetter(_meth_self)
+get_function_closure = operator.attrgetter(_func_closure)
+get_function_code = operator.attrgetter(_func_code)
+get_function_defaults = operator.attrgetter(_func_defaults)
+get_function_globals = operator.attrgetter(_func_globals)
+
+
+if PY3:
+ def iterkeys(d, **kw):
+ return iter(d.keys(**kw))
+
+ def itervalues(d, **kw):
+ return iter(d.values(**kw))
+
+ def iteritems(d, **kw):
+ return iter(d.items(**kw))
+
+ def iterlists(d, **kw):
+ return iter(d.lists(**kw))
+
+ viewkeys = operator.methodcaller("keys")
+
+ viewvalues = operator.methodcaller("values")
+
+ viewitems = operator.methodcaller("items")
+else:
+ def iterkeys(d, **kw):
+ return iter(d.iterkeys(**kw))
+
+ def itervalues(d, **kw):
+ return iter(d.itervalues(**kw))
+
+ def iteritems(d, **kw):
+ return iter(d.iteritems(**kw))
+
+ def iterlists(d, **kw):
+ return iter(d.iterlists(**kw))
+
+ viewkeys = operator.methodcaller("viewkeys")
+
+ viewvalues = operator.methodcaller("viewvalues")
+
+ viewitems = operator.methodcaller("viewitems")
+
+_add_doc(iterkeys, "Return an iterator over the keys of a dictionary.")
+_add_doc(itervalues, "Return an iterator over the values of a dictionary.")
+_add_doc(iteritems,
+ "Return an iterator over the (key, value) pairs of a dictionary.")
+_add_doc(iterlists,
+ "Return an iterator over the (key, [values]) pairs of a dictionary.")
+
+
+if PY3:
+ def b(s):
+ return s.encode("latin-1")
+ def u(s):
+ return s
+ unichr = chr
+ if sys.version_info[1] <= 1:
+ def int2byte(i):
+ return bytes((i,))
+ else:
+ # This is about 2x faster than the implementation above on 3.2+
+ int2byte = operator.methodcaller("to_bytes", 1, "big")
+ byte2int = operator.itemgetter(0)
+ indexbytes = operator.getitem
+ iterbytes = iter
+ import io
+ StringIO = io.StringIO
+ BytesIO = io.BytesIO
+ _assertCountEqual = "assertCountEqual"
+ _assertRaisesRegex = "assertRaisesRegex"
+ _assertRegex = "assertRegex"
+else:
+ def b(s):
+ return s
+ # Workaround for standalone backslash
+ def u(s):
+ return unicode(s.replace(r'\\', r'\\\\'), "unicode_escape")
+ unichr = unichr
+ int2byte = chr
+ def byte2int(bs):
+ return ord(bs[0])
+ def indexbytes(buf, i):
+ return ord(buf[i])
+ iterbytes = functools.partial(itertools.imap, ord)
+ import StringIO
+ StringIO = BytesIO = StringIO.StringIO
+ _assertCountEqual = "assertItemsEqual"
+ _assertRaisesRegex = "assertRaisesRegexp"
+ _assertRegex = "assertRegexpMatches"
+_add_doc(b, """Byte literal""")
+_add_doc(u, """Text literal""")
+
+
+def assertCountEqual(self, *args, **kwargs):
+ return getattr(self, _assertCountEqual)(*args, **kwargs)
+
+
+def assertRaisesRegex(self, *args, **kwargs):
+ return getattr(self, _assertRaisesRegex)(*args, **kwargs)
+
+
+def assertRegex(self, *args, **kwargs):
+ return getattr(self, _assertRegex)(*args, **kwargs)
+
+
+if PY3:
+ exec_ = getattr(moves.builtins, "exec")
+
+
+ def reraise(tp, value, tb=None):
+ if value is None:
+ value = tp()
+ if value.__traceback__ is not tb:
+ raise value.with_traceback(tb)
+ raise value
+
+else:
+ def exec_(_code_, _globs_=None, _locs_=None):
+ """Execute code in a namespace."""
+ if _globs_ is None:
+ frame = sys._getframe(1)
+ _globs_ = frame.f_globals
+ if _locs_ is None:
+ _locs_ = frame.f_locals
+ del frame
+ elif _locs_ is None:
+ _locs_ = _globs_
+ exec("""exec _code_ in _globs_, _locs_""")
+
+
+ exec_("""def reraise(tp, value, tb=None):
+ raise tp, value, tb
+""")
+
+
+if sys.version_info[:2] == (3, 2):
+ exec_("""def raise_from(value, from_value):
+ if from_value is None:
+ raise value
+ raise value from from_value
+""")
+elif sys.version_info[:2] > (3, 2):
+ exec_("""def raise_from(value, from_value):
+ raise value from from_value
+""")
+else:
+ def raise_from(value, from_value):
+ raise value
+
+
+print_ = getattr(moves.builtins, "print", None)
+if print_ is None:
+ def print_(*args, **kwargs):
+ """The new-style print function for Python 2.4 and 2.5."""
+ fp = kwargs.pop("file", sys.stdout)
+ if fp is None:
+ return
+ def write(data):
+ if not isinstance(data, basestring):
+ data = str(data)
+ # If the file has an encoding, encode unicode with it.
+ if (isinstance(fp, file) and
+ isinstance(data, unicode) and
+ fp.encoding is not None):
+ errors = getattr(fp, "errors", None)
+ if errors is None:
+ errors = "strict"
+ data = data.encode(fp.encoding, errors)
+ fp.write(data)
+ want_unicode = False
+ sep = kwargs.pop("sep", None)
+ if sep is not None:
+ if isinstance(sep, unicode):
+ want_unicode = True
+ elif not isinstance(sep, str):
+ raise TypeError("sep must be None or a string")
+ end = kwargs.pop("end", None)
+ if end is not None:
+ if isinstance(end, unicode):
+ want_unicode = True
+ elif not isinstance(end, str):
+ raise TypeError("end must be None or a string")
+ if kwargs:
+ raise TypeError("invalid keyword arguments to print()")
+ if not want_unicode:
+ for arg in args:
+ if isinstance(arg, unicode):
+ want_unicode = True
+ break
+ if want_unicode:
+ newline = unicode("\n")
+ space = unicode(" ")
+ else:
+ newline = "\n"
+ space = " "
+ if sep is None:
+ sep = space
+ if end is None:
+ end = newline
+ for i, arg in enumerate(args):
+ if i:
+ write(sep)
+ write(arg)
+ write(end)
+if sys.version_info[:2] < (3, 3):
+ _print = print_
+ def print_(*args, **kwargs):
+ fp = kwargs.get("file", sys.stdout)
+ flush = kwargs.pop("flush", False)
+ _print(*args, **kwargs)
+ if flush and fp is not None:
+ fp.flush()
+
+_add_doc(reraise, """Reraise an exception.""")
+
+if sys.version_info[0:2] < (3, 4):
+ def wraps(wrapped, assigned=functools.WRAPPER_ASSIGNMENTS,
+ updated=functools.WRAPPER_UPDATES):
+ def wrapper(f):
+ f = functools.wraps(wrapped, assigned, updated)(f)
+ f.__wrapped__ = wrapped
+ return f
+ return wrapper
+else:
+ wraps = functools.wraps
+
+def with_metaclass(meta, *bases):
+ """Create a base class with a metaclass."""
+ # This requires a bit of explanation: the basic idea is to make a dummy
+ # metaclass for one level of class instantiation that replaces itself with
+ # the actual metaclass.
+ class metaclass(meta):
+ def __new__(cls, name, this_bases, d):
+ return meta(name, bases, d)
+ return type.__new__(metaclass, 'temporary_class', (), {})
+
+
+def add_metaclass(metaclass):
+ """Class decorator for creating a class with a metaclass."""
+ def wrapper(cls):
+ orig_vars = cls.__dict__.copy()
+ slots = orig_vars.get('__slots__')
+ if slots is not None:
+ if isinstance(slots, str):
+ slots = [slots]
+ for slots_var in slots:
+ orig_vars.pop(slots_var)
+ orig_vars.pop('__dict__', None)
+ orig_vars.pop('__weakref__', None)
+ return metaclass(cls.__name__, cls.__bases__, orig_vars)
+ return wrapper
+
+
+def python_2_unicode_compatible(klass):
+ """
+ A decorator that defines __unicode__ and __str__ methods under Python 2.
+ Under Python 3 it does nothing.
+
+ To support Python 2 and 3 with a single code base, define a __str__ method
+ returning text and apply this decorator to the class.
+ """
+ if PY2:
+ if '__str__' not in klass.__dict__:
+ raise ValueError("@python_2_unicode_compatible cannot be applied "
+ "to %s because it doesn't define __str__()." %
+ klass.__name__)
+ klass.__unicode__ = klass.__str__
+ klass.__str__ = lambda self: self.__unicode__().encode('utf-8')
+ return klass
+
+
+# Complete the moves implementation.
+# This code is at the end of this module to speed up module loading.
+# Turn this module into a package.
+__path__ = [] # required for PEP 302 and PEP 451
+__package__ = __name__ # see PEP 366 @ReservedAssignment
+if globals().get("__spec__") is not None:
+ __spec__.submodule_search_locations = [] # PEP 451 @UndefinedVariable
+# Remove other six meta path importers, since they cause problems. This can
+# happen if six is removed from sys.modules and then reloaded. (Setuptools does
+# this for some reason.)
+if sys.meta_path:
+ for i, importer in enumerate(sys.meta_path):
+ # Here's some real nastiness: Another "instance" of the six module might
+ # be floating around. Therefore, we can't use isinstance() to check for
+ # the six meta path importer, since the other six instance will have
+ # inserted an importer with different class.
+ if (type(importer).__name__ == "_SixMetaPathImporter" and
+ importer.name == __name__):
+ del sys.meta_path[i]
+ break
+ del i, importer
+# Finally, add the importer to the meta path import hook.
+sys.meta_path.append(_importer)