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)