##// END OF EJS Templates
repositories: allow updating repository settings for users without store-in-root permissions...
repositories: allow updating repository settings for users without store-in-root permissions in case repository name didn't change. - when an user owns repository in root location, and isn't allow to create repositories in root before we failed to allow this user to update such repository settings due to this validation. We'll now check if name didn't change and in this case allow to update since this doesn't store any new data in root location.

File last commit:

r3912:9bf26830 default
r4415:fc1f6c1b default
Show More
__init__.py
1012 lines | 29.5 KiB | text/x-python | PythonLexer
# -*- 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 = """
<html>
<head>
<title>ERROR: {error}</title>
</head>
<body style="font-family: sans-serif">
<h4>The Authomatic library encountered an error!</h4>
<h1>{error}</h1>
<pre>{traceback}</pre>
</body>
</html>
"""
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 <http://docs.python.org/2/library/logging.html>`_ 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 <http://oauth.net/2/>`_ or `OAuth 1.0a
<http://oauth.net/core/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 <http://openid.net/>`_.
"""
#: 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,
]