blockerbugs: remove hotfix, already released
This commit is contained in:
parent
714fc09d7a
commit
3cb6f5ca9d
2 changed files with 0 additions and 415 deletions
|
@ -1,406 +0,0 @@
|
||||||
#!/usr/bin/env python2 -tt
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
|
||||||
# Copyright (C) 2013-2014 Red Hat, Inc.
|
|
||||||
# This file is part of python-fedora
|
|
||||||
#
|
|
||||||
# python-fedora is free software; you can redistribute it and/or
|
|
||||||
# modify it under the terms of the GNU Lesser General Public
|
|
||||||
# License as published by the Free Software Foundation; either
|
|
||||||
# version 2.1 of the License, or (at your option) any later version.
|
|
||||||
#
|
|
||||||
# python-fedora 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
|
|
||||||
# Lesser General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Lesser General Public
|
|
||||||
# License along with python-fedora; if not, see <http://www.gnu.org/licenses/>
|
|
||||||
#
|
|
||||||
|
|
||||||
"""Base client for application relying on OpenID for authentication.
|
|
||||||
|
|
||||||
.. moduleauthor:: Pierre-Yves Chibon <pingou@fedoraproject.org>
|
|
||||||
.. moduleauthor:: Toshio Kuratomi <toshio@fedoraproject.org>
|
|
||||||
|
|
||||||
.. versionadded: 0.3.35
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
# :F0401: Unable to import : Disabled because these will either import on py3
|
|
||||||
# or py2 not both.
|
|
||||||
# :E0611: No name $X in module: This was renamed in python3
|
|
||||||
|
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
import sqlite3
|
|
||||||
from collections import defaultdict
|
|
||||||
from functools import partial
|
|
||||||
|
|
||||||
# pylint: disable-msg=F0401
|
|
||||||
try:
|
|
||||||
# pylint: disable-msg=E0611
|
|
||||||
# Python 3
|
|
||||||
from urllib.parse import urljoin
|
|
||||||
except ImportError:
|
|
||||||
# Python 2
|
|
||||||
from urlparse import urljoin
|
|
||||||
# pylint: enable-msg=F0401,E0611
|
|
||||||
|
|
||||||
|
|
||||||
import requests
|
|
||||||
from functools import wraps
|
|
||||||
from munch import munchify
|
|
||||||
from kitchen.text.converters import to_bytes
|
|
||||||
|
|
||||||
from fedora import __version__
|
|
||||||
from fedora.client import AuthError, LoginRequiredError, ServerError
|
|
||||||
from fedora.client.openidproxyclient import (
|
|
||||||
OpenIdProxyClient, absolute_url, openid_login)
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
b_SESSION_DIR = os.path.join(os.path.expanduser('~'), '.fedora')
|
|
||||||
b_SESSION_FILE = os.path.join(b_SESSION_DIR, 'baseclient-sessions.sqlite')
|
|
||||||
|
|
||||||
|
|
||||||
def requires_login(func):
|
|
||||||
"""
|
|
||||||
Decorator function for get or post requests requiring login.
|
|
||||||
|
|
||||||
Decorate a controller method that requires the user to be authenticated.
|
|
||||||
Example::
|
|
||||||
|
|
||||||
from fedora.client.openidbaseclient import requires_login
|
|
||||||
|
|
||||||
@requires_login
|
|
||||||
def rename_user(new_name):
|
|
||||||
user = new_name
|
|
||||||
# [...]
|
|
||||||
"""
|
|
||||||
def _decorator(request, *args, **kwargs):
|
|
||||||
""" Run the function and check if it redirected to the openid form.
|
|
||||||
"""
|
|
||||||
output = func(request, *args, **kwargs)
|
|
||||||
if output and \
|
|
||||||
'<title>OpenID transaction in progress</title>' in output.text:
|
|
||||||
raise LoginRequiredError(
|
|
||||||
'{0} requires a logged in user'.format(output.url))
|
|
||||||
return output
|
|
||||||
return wraps(func)(_decorator)
|
|
||||||
|
|
||||||
|
|
||||||
class OpenIdBaseClient(OpenIdProxyClient):
|
|
||||||
|
|
||||||
""" A client for interacting with web services relying on openid auth. """
|
|
||||||
|
|
||||||
def __init__(self, base_url, login_url=None, useragent=None, debug=False,
|
|
||||||
insecure=False, openid_insecure=False, username=None,
|
|
||||||
session_id=None, session_name='session',
|
|
||||||
openid_session_id=None, openid_session_name='FAS_OPENID',
|
|
||||||
cache_session=True, retries=None, timeout=None):
|
|
||||||
"""Client for interacting with web services relying on fas_openid auth.
|
|
||||||
|
|
||||||
:arg base_url: Base of every URL used to contact the server
|
|
||||||
:kwarg login_url: The url to the login endpoint of the application.
|
|
||||||
If none are specified, it uses the default `/login`.
|
|
||||||
:kwarg useragent: Useragent string to use. If not given, default to
|
|
||||||
"Fedora OpenIdBaseClient/VERSION"
|
|
||||||
:kwarg debug: If True, log debug information
|
|
||||||
:kwarg insecure: If True, do not check server certificates against
|
|
||||||
their CA's. This means that man-in-the-middle attacks are
|
|
||||||
possible against the `BaseClient`. You might turn this option on
|
|
||||||
for testing against a local version of a server with a self-signed
|
|
||||||
certificate but it should be off in production.
|
|
||||||
:kwarg openid_insecure: If True, do not check the openid server
|
|
||||||
certificates against their CA's. This means that man-in-the-
|
|
||||||
middle attacks are possible against the `BaseClient`. You might
|
|
||||||
turn this option on for testing against a local version of a
|
|
||||||
server with a self-signed certificate but it should be off in
|
|
||||||
production.
|
|
||||||
:kwarg username: Username for establishing authenticated connections
|
|
||||||
:kwarg session_id: id of the user's session
|
|
||||||
:kwarg session_name: name of the cookie to use with session handling
|
|
||||||
:kwarg openid_session_id: id of the user's openid session
|
|
||||||
:kwarg openid_session_name: name of the cookie to use with openid
|
|
||||||
session handling
|
|
||||||
:kwarg cache_session: If set to true, cache the user's session data on
|
|
||||||
the filesystem between runs
|
|
||||||
:kwarg retries: if we get an unknown or possibly transient error from
|
|
||||||
the server, retry this many times. Setting this to a negative
|
|
||||||
number makes it try forever. Defaults to zero, no retries.
|
|
||||||
:kwarg timeout: A float describing the timeout of the connection. The
|
|
||||||
timeout only affects the connection process itself, not the
|
|
||||||
downloading of the response body. Defaults to 120 seconds.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
# These are also needed by OpenIdProxyClient
|
|
||||||
self.useragent = useragent or 'Fedora BaseClient/%(version)s' % {
|
|
||||||
'version': __version__}
|
|
||||||
self.base_url = base_url
|
|
||||||
self.login_url = login_url or urljoin(self.base_url, '/login')
|
|
||||||
self.debug = debug
|
|
||||||
self.insecure = insecure
|
|
||||||
self.openid_insecure = openid_insecure
|
|
||||||
self.retries = retries
|
|
||||||
self.timeout = timeout
|
|
||||||
self.session_name = session_name
|
|
||||||
self.openid_session_name = openid_session_name
|
|
||||||
|
|
||||||
# These are specific to OpenIdBaseClient
|
|
||||||
self.username = username
|
|
||||||
self.cache_session = cache_session
|
|
||||||
|
|
||||||
# Make sure the database for storing the session cookies exists
|
|
||||||
if cache_session:
|
|
||||||
self._db = self._initialize_session_cache()
|
|
||||||
if not self._db:
|
|
||||||
self.cache_session = False
|
|
||||||
|
|
||||||
# Session cookie that identifies this user to the application
|
|
||||||
self._session_id_map = defaultdict(str)
|
|
||||||
if session_id:
|
|
||||||
self.session_id = session_id
|
|
||||||
if openid_session_id:
|
|
||||||
self.openid_session_id = openid_session_id
|
|
||||||
|
|
||||||
# python-requests session. Holds onto cookies
|
|
||||||
self._session = requests.session()
|
|
||||||
|
|
||||||
def _initialize_session_cache(self):
|
|
||||||
# Note -- fallback to returning None on any problems as this isn't
|
|
||||||
# critical. It just makes it so that we don't have to ask the user
|
|
||||||
# for their password over and over.
|
|
||||||
if not os.path.isdir(b_SESSION_DIR):
|
|
||||||
try:
|
|
||||||
os.makedirs(b_SESSION_DIR, mode=0o755)
|
|
||||||
except OSError as err:
|
|
||||||
log.warning('Unable to create {file}: {error}'.format(
|
|
||||||
file=b_SESSION_DIR, error=err))
|
|
||||||
return None
|
|
||||||
|
|
||||||
if not os.path.exists(b_SESSION_FILE):
|
|
||||||
dbcon = sqlite3.connect(b_SESSION_FILE)
|
|
||||||
cursor = dbcon.cursor()
|
|
||||||
try:
|
|
||||||
cursor.execute('create table sessions (username text,'
|
|
||||||
' base_url text, session_id text,'
|
|
||||||
' primary key (username, base_url))')
|
|
||||||
except sqlite3.DatabaseError as err:
|
|
||||||
# Probably not a database
|
|
||||||
log.warning('Unable to initialize {file}: {error}'.format(
|
|
||||||
file=b_SESSION_FILE, error=err))
|
|
||||||
return None
|
|
||||||
dbcon.commit()
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
dbcon = sqlite3.connect(b_SESSION_FILE)
|
|
||||||
except sqlite3.OperationalError as err:
|
|
||||||
# Likely permission denied
|
|
||||||
log.warning('Unable to connect to {file}: {error}'.format(
|
|
||||||
file=b_SESSION_FILE, error=err))
|
|
||||||
return None
|
|
||||||
return dbcon
|
|
||||||
|
|
||||||
def _get_id(self, base_url=None):
|
|
||||||
# base_url is only sent as a param if we're looking for the openid
|
|
||||||
# session
|
|
||||||
base_url = base_url or self.base_url
|
|
||||||
|
|
||||||
username = self.username or ''
|
|
||||||
|
|
||||||
session_id_key = '{}:{}'.format(base_url, username)
|
|
||||||
if self._session_id_map[session_id_key]:
|
|
||||||
# Cached in memory
|
|
||||||
return self._session_id_map[session_id_key]
|
|
||||||
|
|
||||||
if username and self.cache_session:
|
|
||||||
# Look for a session on disk
|
|
||||||
cursor = self._db.cursor()
|
|
||||||
cursor.execute('select * from sessions where'
|
|
||||||
' username = ? and base_url = ?',
|
|
||||||
(username, base_url))
|
|
||||||
found_sessions = cursor.fetchall()
|
|
||||||
if found_sessions:
|
|
||||||
self._session_id_map[session_id_key] = found_sessions[0]
|
|
||||||
|
|
||||||
if not self._session_id_map[session_id_key]:
|
|
||||||
log.debug('No session cached for "{username}"'.format(
|
|
||||||
username=self.username))
|
|
||||||
return self._session_id_map[session_id_key]
|
|
||||||
|
|
||||||
def _set_id(self, session_id, base_url=None):
|
|
||||||
#if not self.username:
|
|
||||||
# base_url is only sent as a param if we're looking for the openid
|
|
||||||
# session
|
|
||||||
base_url = base_url or self.base_url
|
|
||||||
|
|
||||||
username = self.username or ''
|
|
||||||
|
|
||||||
# Cache in memory
|
|
||||||
session_id_key = '{}:{}'.format(base_url, username)
|
|
||||||
self._session_id_map[session_id_key] = session_id
|
|
||||||
|
|
||||||
if username and self.cache_session:
|
|
||||||
# Save to disk as well
|
|
||||||
cursor = self._db.cursor()
|
|
||||||
try:
|
|
||||||
cursor.exectue('insert into sessions'
|
|
||||||
' (session_id, username, base_url)'
|
|
||||||
' values (?, ?, ?)',
|
|
||||||
(session_id, username, base_url))
|
|
||||||
except sqlite3.IntegrityError:
|
|
||||||
# Record already exists for that username and url
|
|
||||||
cursor.execute('update sessions set session_id = ? where'
|
|
||||||
' username = ? and base_url = ?',
|
|
||||||
(session_id, username, base_url))
|
|
||||||
cursor.commit()
|
|
||||||
|
|
||||||
def _del_id(self, base_url=None):
|
|
||||||
# base_url is only sent as a param if we're looking for the openid
|
|
||||||
# session
|
|
||||||
base_url = base_url or self.base_url
|
|
||||||
|
|
||||||
username = self.username or ''
|
|
||||||
|
|
||||||
# Remove from the in-memory cache
|
|
||||||
session_id_key = '{}:{}'.format(base_url, username)
|
|
||||||
del self._session_id_map[session_id_key]
|
|
||||||
|
|
||||||
if username and self.cache_session:
|
|
||||||
# Remove from the disk cache as well
|
|
||||||
cursor = self._db.cursor()
|
|
||||||
cursor.execute('delete from sessions where'
|
|
||||||
' username = ? and base_url = ?',
|
|
||||||
(username, base_url))
|
|
||||||
|
|
||||||
session_id = property(
|
|
||||||
_get_id,
|
|
||||||
_set_id,
|
|
||||||
_del_id,
|
|
||||||
"""The session_id.
|
|
||||||
|
|
||||||
The session id is saved in a file in case it is needed in
|
|
||||||
consecutive runs of BaseClient.
|
|
||||||
""")
|
|
||||||
|
|
||||||
openid_session_id = property(
|
|
||||||
partial(_get_id, base_url='FAS_OPENID'),
|
|
||||||
partial(_set_id, base_url='FAS_OPENID'),
|
|
||||||
partial(_del_id, base_url='FAS_OPENID'),
|
|
||||||
"""The openid_session_id.
|
|
||||||
|
|
||||||
The openid session id is saved in a file in case it is needed in
|
|
||||||
consecutive runs of BaseClient.
|
|
||||||
""")
|
|
||||||
|
|
||||||
@requires_login
|
|
||||||
def _authed_post(self, url, params=None, data=None, **kwargs):
|
|
||||||
""" Return the request object of a post query."""
|
|
||||||
response = self._session.post(url, params=params, data=data, **kwargs)
|
|
||||||
return response
|
|
||||||
|
|
||||||
@requires_login
|
|
||||||
def _authed_get(self, url, params=None, data=None, **kwargs):
|
|
||||||
""" Return the request object of a get query."""
|
|
||||||
response = self._session.get(url, params=params, data=data, **kwargs)
|
|
||||||
return response
|
|
||||||
|
|
||||||
def send_request(self, method, auth=False, verb='POST', **kwargs):
|
|
||||||
"""Make an HTTP request to a server method.
|
|
||||||
|
|
||||||
The given method is called with any parameters set in req_params. If
|
|
||||||
auth is True, then the request is made with an authenticated session
|
|
||||||
cookie.
|
|
||||||
|
|
||||||
:arg method: Method to call on the server. It's a url fragment that
|
|
||||||
comes after the :attr:`base_url` set in :meth:`__init__`.
|
|
||||||
:kwarg retries: if we get an unknown or possibly transient error from
|
|
||||||
the server, retry this many times. Setting this to a negative
|
|
||||||
number makes it try forever. Default to use the :attr:`retries`
|
|
||||||
value set on the instance or in :meth:`__init__` (which defaults
|
|
||||||
to zero, no retries).
|
|
||||||
:kwarg timeout: A float describing the timeout of the connection. The
|
|
||||||
timeout only affects the connection process itself, not the
|
|
||||||
downloading of the response body. Default to use the
|
|
||||||
:attr:`timeout` value set on the instance or in :meth:`__init__`
|
|
||||||
(which defaults to 120s).
|
|
||||||
:kwarg auth: If True perform auth to the server, else do not.
|
|
||||||
:kwarg req_params: Extra parameters to send to the server.
|
|
||||||
:kwarg file_params: dict of files where the key is the name of the
|
|
||||||
file field used in the remote method and the value is the local
|
|
||||||
path of the file to be uploaded. If you want to pass multiple
|
|
||||||
files to a single file field, pass the paths as a list of paths.
|
|
||||||
:kwarg verb: HTTP verb to use. GET and POST are currently supported.
|
|
||||||
POST is the default.
|
|
||||||
"""
|
|
||||||
# Decide on the set of auth cookies to use
|
|
||||||
|
|
||||||
method = absolute_url(self.base_url, method)
|
|
||||||
|
|
||||||
self._authed_verb_dispatcher = {(False, 'POST'): self._session.post,
|
|
||||||
(False, 'GET'): self._session.get,
|
|
||||||
(True, 'POST'): self._authed_post,
|
|
||||||
(True, 'GET'): self._authed_get}
|
|
||||||
try:
|
|
||||||
func = self._authed_verb_dispatcher[(auth, verb)]
|
|
||||||
except KeyError:
|
|
||||||
raise Exception('Unknown HTTP verb')
|
|
||||||
|
|
||||||
if auth:
|
|
||||||
auth_params = {'session_id': self.session_id,
|
|
||||||
'openid_session_id': self.openid_session_id}
|
|
||||||
try:
|
|
||||||
output = func(method, auth_params, **kwargs)
|
|
||||||
except LoginRequiredError:
|
|
||||||
raise AuthError()
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
output = func(method, **kwargs)
|
|
||||||
except LoginRequiredError:
|
|
||||||
raise AuthError()
|
|
||||||
|
|
||||||
try:
|
|
||||||
data = output.json
|
|
||||||
# Compatibility with newer python-requests
|
|
||||||
if callable(data):
|
|
||||||
data = data()
|
|
||||||
except ValueError as e:
|
|
||||||
# The response wasn't JSON data
|
|
||||||
raise ServerError(
|
|
||||||
method, output.status_code, 'Error returned from'
|
|
||||||
' json module while processing %(url)s: %(err)s\n%(output)s' %
|
|
||||||
{
|
|
||||||
'url': to_bytes(method),
|
|
||||||
'err': to_bytes(e),
|
|
||||||
'output': to_bytes(output.text),
|
|
||||||
})
|
|
||||||
|
|
||||||
data = munchify(data)
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
def login(self, username, password, otp=None):
|
|
||||||
""" Open a session for the user.
|
|
||||||
|
|
||||||
Log in the user with the specified username and password
|
|
||||||
against the FAS OpenID server.
|
|
||||||
|
|
||||||
:arg username: the FAS username of the user that wants to log in
|
|
||||||
:arg password: the FAS password of the user that wants to log in
|
|
||||||
:kwarg otp: currently unused. Eventually a way to send an otp to the
|
|
||||||
API that the API can use.
|
|
||||||
|
|
||||||
"""
|
|
||||||
response = openid_login(
|
|
||||||
session=self._session,
|
|
||||||
login_url=self.login_url,
|
|
||||||
username=username,
|
|
||||||
password=password,
|
|
||||||
otp=otp,
|
|
||||||
openid_insecure=self.openid_insecure)
|
|
||||||
return response
|
|
||||||
|
|
||||||
__all__ = ('OpenIdBaseClient', 'requires_login')
|
|
|
@ -52,12 +52,3 @@
|
||||||
tags:
|
tags:
|
||||||
- config
|
- config
|
||||||
- blockerbugs
|
- blockerbugs
|
||||||
|
|
||||||
# hotfix openidbaseclient.py in python-fedora so it doesn't blow up during sync.
|
|
||||||
# fix is in upstream, waiting for new release: https://github.com/fedora-infra/python-fedora/commit/a5a66e8e744ad1c54788e00d78bd6b66e67fe83b
|
|
||||||
- name: hotfix python-fedora so it doesn't blow up
|
|
||||||
when: env == "production"
|
|
||||||
copy: src=python-fedora-openidbaseclient-hotfix.py dest=/usr/lib/python2.7/site-packages/fedora/client/openidbaseclient.py owner=root group=root mode=0644
|
|
||||||
tags:
|
|
||||||
- blockerbugs
|
|
||||||
- hotfix
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue