Merge remote-tracking branch 'brett/itsdangerous'
* brett/itsdangerous: Call is_updated instead of testing it boolean. Harden It's Dangerous key management. First tests for the Session class. Set a starting value for session.send_new_cookie. Remove beaker stuff from the code. Delete the session cookie on an empty session. Back sessions with It's Dangerous. Improve fs security for itsdangerous secret. Docs for get_timed_signer_url. Basic itsdangerous infrastructure. Conflicts: mediagoblin/tests/test_cache.py
This commit is contained in:
commit
8021cc5605
@ -25,7 +25,7 @@ from werkzeug.exceptions import HTTPException
|
|||||||
from werkzeug.routing import RequestRedirect
|
from werkzeug.routing import RequestRedirect
|
||||||
|
|
||||||
from mediagoblin import meddleware, __version__
|
from mediagoblin import meddleware, __version__
|
||||||
from mediagoblin.tools import common, translate, template
|
from mediagoblin.tools import common, session, translate, template
|
||||||
from mediagoblin.tools.response import render_http_exception
|
from mediagoblin.tools.response import render_http_exception
|
||||||
from mediagoblin.tools.theme import register_themes
|
from mediagoblin.tools.theme import register_themes
|
||||||
from mediagoblin.tools import request as mg_request
|
from mediagoblin.tools import request as mg_request
|
||||||
@ -34,8 +34,9 @@ from mediagoblin.init.celery import setup_celery_from_config
|
|||||||
from mediagoblin.init.plugins import setup_plugins
|
from mediagoblin.init.plugins import setup_plugins
|
||||||
from mediagoblin.init import (get_jinja_loader, get_staticdirector,
|
from mediagoblin.init import (get_jinja_loader, get_staticdirector,
|
||||||
setup_global_and_app_config, setup_locales, setup_workbench, setup_database,
|
setup_global_and_app_config, setup_locales, setup_workbench, setup_database,
|
||||||
setup_storage, setup_beaker_cache)
|
setup_storage)
|
||||||
from mediagoblin.tools.pluginapi import PluginManager
|
from mediagoblin.tools.pluginapi import PluginManager
|
||||||
|
from mediagoblin.tools.crypto import setup_crypto
|
||||||
|
|
||||||
|
|
||||||
_log = logging.getLogger(__name__)
|
_log = logging.getLogger(__name__)
|
||||||
@ -66,6 +67,8 @@ class MediaGoblinApp(object):
|
|||||||
# Open and setup the config
|
# Open and setup the config
|
||||||
global_config, app_config = setup_global_and_app_config(config_path)
|
global_config, app_config = setup_global_and_app_config(config_path)
|
||||||
|
|
||||||
|
setup_crypto()
|
||||||
|
|
||||||
##########################################
|
##########################################
|
||||||
# Setup other connections / useful objects
|
# Setup other connections / useful objects
|
||||||
##########################################
|
##########################################
|
||||||
@ -100,9 +103,6 @@ class MediaGoblinApp(object):
|
|||||||
# set up staticdirector tool
|
# set up staticdirector tool
|
||||||
self.staticdirector = get_staticdirector(app_config)
|
self.staticdirector = get_staticdirector(app_config)
|
||||||
|
|
||||||
# set up caching
|
|
||||||
self.cache = setup_beaker_cache()
|
|
||||||
|
|
||||||
# Setup celery, if appropriate
|
# Setup celery, if appropriate
|
||||||
if setup_celery and not app_config.get('celery_setup_elsewhere'):
|
if setup_celery and not app_config.get('celery_setup_elsewhere'):
|
||||||
if os.environ.get('CELERY_ALWAYS_EAGER', 'false').lower() == 'true':
|
if os.environ.get('CELERY_ALWAYS_EAGER', 'false').lower() == 'true':
|
||||||
@ -157,7 +157,8 @@ class MediaGoblinApp(object):
|
|||||||
|
|
||||||
## Attach utilities to the request object
|
## Attach utilities to the request object
|
||||||
# Do we really want to load this via middleware? Maybe?
|
# Do we really want to load this via middleware? Maybe?
|
||||||
request.session = request.environ['beaker.session']
|
session_manager = session.SessionManager()
|
||||||
|
request.session = session_manager.load_session_from_cookie(request)
|
||||||
# Attach self as request.app
|
# Attach self as request.app
|
||||||
# Also attach a few utilities from request.app for convenience?
|
# Also attach a few utilities from request.app for convenience?
|
||||||
request.app = self
|
request.app = self
|
||||||
@ -226,6 +227,8 @@ class MediaGoblinApp(object):
|
|||||||
response = render_http_exeption(
|
response = render_http_exeption(
|
||||||
request, e, e.get_description(environ))
|
request, e, e.get_description(environ))
|
||||||
|
|
||||||
|
session_manager.save_session_to_cookie(request.session, response)
|
||||||
|
|
||||||
return response(environ, start_response)
|
return response(environ, start_response)
|
||||||
|
|
||||||
def __call__(self, environ, start_response):
|
def __call__(self, environ, start_response):
|
||||||
|
@ -14,6 +14,9 @@ sql_engine = string(default="sqlite:///%(here)s/mediagoblin.db")
|
|||||||
# Where temporary files used in processing and etc are kept
|
# Where temporary files used in processing and etc are kept
|
||||||
workbench_path = string(default="%(here)s/user_dev/media/workbench")
|
workbench_path = string(default="%(here)s/user_dev/media/workbench")
|
||||||
|
|
||||||
|
# Where to store cryptographic sensible data
|
||||||
|
crypto_path = string(default="%(here)s/user_dev/crypto")
|
||||||
|
|
||||||
# Where mediagoblin-builtin static assets are kept
|
# Where mediagoblin-builtin static assets are kept
|
||||||
direct_remote_path = string(default="/mgoblin_static/")
|
direct_remote_path = string(default="/mgoblin_static/")
|
||||||
|
|
||||||
|
@ -14,8 +14,6 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from beaker.cache import CacheManager
|
|
||||||
from beaker.util import parse_cache_config_options
|
|
||||||
import jinja2
|
import jinja2
|
||||||
|
|
||||||
from mediagoblin.tools import staticdirect
|
from mediagoblin.tools import staticdirect
|
||||||
@ -146,16 +144,3 @@ def setup_workbench():
|
|||||||
workbench_manager = WorkbenchManager(app_config['workbench_path'])
|
workbench_manager = WorkbenchManager(app_config['workbench_path'])
|
||||||
|
|
||||||
setup_globals(workbench_manager=workbench_manager)
|
setup_globals(workbench_manager=workbench_manager)
|
||||||
|
|
||||||
|
|
||||||
def setup_beaker_cache():
|
|
||||||
"""
|
|
||||||
Setup the Beaker Cache manager.
|
|
||||||
"""
|
|
||||||
cache_config = mg_globals.global_config['beaker.cache']
|
|
||||||
cache_config = dict(
|
|
||||||
[(u'cache.%s' % key, value)
|
|
||||||
for key, value in cache_config.iteritems()])
|
|
||||||
cache = CacheManager(**parse_cache_config_options(cache_config))
|
|
||||||
setup_globals(cache=cache)
|
|
||||||
return cache
|
|
||||||
|
@ -29,9 +29,6 @@ import threading
|
|||||||
# SQL database engine
|
# SQL database engine
|
||||||
database = None
|
database = None
|
||||||
|
|
||||||
# beaker's cache manager
|
|
||||||
cache = None
|
|
||||||
|
|
||||||
# should be the same as the
|
# should be the same as the
|
||||||
public_store = None
|
public_store = None
|
||||||
queue_store = None
|
queue_store = None
|
||||||
|
@ -1,50 +0,0 @@
|
|||||||
# GNU MediaGoblin -- federated, autonomous media hosting
|
|
||||||
# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU Affero General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# 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 Affero 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
|
|
||||||
from mediagoblin import mg_globals
|
|
||||||
|
|
||||||
|
|
||||||
DATA_TO_CACHE = {
|
|
||||||
'herp': 'derp',
|
|
||||||
'lol': 'cats'}
|
|
||||||
|
|
||||||
|
|
||||||
def _get_some_data(key):
|
|
||||||
"""
|
|
||||||
Stuid function that makes use of some caching.
|
|
||||||
"""
|
|
||||||
some_data_cache = mg_globals.cache.get_cache('sum_data')
|
|
||||||
if some_data_cache.has_key(key):
|
|
||||||
return some_data_cache.get(key)
|
|
||||||
|
|
||||||
value = DATA_TO_CACHE.get(key)
|
|
||||||
some_data_cache.put(key, value)
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
def test_cache_working(test_app):
|
|
||||||
some_data_cache = mg_globals.cache.get_cache('sum_data')
|
|
||||||
assert not some_data_cache.has_key('herp')
|
|
||||||
assert _get_some_data('herp') == 'derp'
|
|
||||||
assert some_data_cache.get('herp') == 'derp'
|
|
||||||
# should get the same value again
|
|
||||||
assert _get_some_data('herp') == 'derp'
|
|
||||||
|
|
||||||
# now we force-change it, but the function should use the cached
|
|
||||||
# version
|
|
||||||
some_data_cache.put('herp', 'pred')
|
|
||||||
assert _get_some_data('herp') == 'pred'
|
|
30
mediagoblin/tests/test_session.py
Normal file
30
mediagoblin/tests/test_session.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# GNU MediaGoblin -- federated, autonomous media hosting
|
||||||
|
# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# 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 Affero 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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from mediagoblin.tools import session
|
||||||
|
|
||||||
|
def test_session():
|
||||||
|
sess = session.Session()
|
||||||
|
assert not sess
|
||||||
|
assert not sess.is_updated()
|
||||||
|
sess['user_id'] = 27
|
||||||
|
assert sess
|
||||||
|
assert not sess.is_updated()
|
||||||
|
sess.save()
|
||||||
|
assert sess.is_updated()
|
||||||
|
sess.delete()
|
||||||
|
assert not sess
|
||||||
|
assert sess.is_updated()
|
@ -45,9 +45,7 @@ TEST_USER_DEV = pkg_resources.resource_filename(
|
|||||||
'mediagoblin.tests', 'test_user_dev')
|
'mediagoblin.tests', 'test_user_dev')
|
||||||
|
|
||||||
|
|
||||||
USER_DEV_DIRECTORIES_TO_SETUP = [
|
USER_DEV_DIRECTORIES_TO_SETUP = ['media/public', 'media/queue']
|
||||||
'media/public', 'media/queue',
|
|
||||||
'beaker/sessions/data', 'beaker/sessions/lock']
|
|
||||||
|
|
||||||
BAD_CELERY_MESSAGE = """\
|
BAD_CELERY_MESSAGE = """\
|
||||||
Sorry, you *absolutely* must run tests with the
|
Sorry, you *absolutely* must run tests with the
|
||||||
|
110
mediagoblin/tools/crypto.py
Normal file
110
mediagoblin/tools/crypto.py
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
# GNU MediaGoblin -- federated, autonomous media hosting
|
||||||
|
# Copyright (C) 2013 MediaGoblin contributors. See AUTHORS.
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# 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 Affero 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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import errno
|
||||||
|
import itsdangerous
|
||||||
|
import logging
|
||||||
|
import os.path
|
||||||
|
import random
|
||||||
|
import tempfile
|
||||||
|
from mediagoblin import mg_globals
|
||||||
|
|
||||||
|
_log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# Use the system (hardware-based) random number generator if it exists.
|
||||||
|
# -- this optimization is lifted from Django
|
||||||
|
try:
|
||||||
|
getrandbits = random.SystemRandom().getrandbits
|
||||||
|
except AttributeError:
|
||||||
|
getrandbits = random.getrandbits
|
||||||
|
|
||||||
|
|
||||||
|
__itsda_secret = None
|
||||||
|
|
||||||
|
|
||||||
|
def load_key(filename):
|
||||||
|
global __itsda_secret
|
||||||
|
key_file = open(filename)
|
||||||
|
try:
|
||||||
|
__itsda_secret = key_file.read()
|
||||||
|
finally:
|
||||||
|
key_file.close()
|
||||||
|
|
||||||
|
def create_key(key_dir, key_filepath):
|
||||||
|
global __itsda_secret
|
||||||
|
old_umask = os.umask(077)
|
||||||
|
key_file = None
|
||||||
|
try:
|
||||||
|
if not os.path.isdir(key_dir):
|
||||||
|
os.makedirs(key_dir)
|
||||||
|
_log.info("Created %s", dirname)
|
||||||
|
key = str(getrandbits(192))
|
||||||
|
key_file = tempfile.NamedTemporaryFile(dir=key_dir, suffix='.bin',
|
||||||
|
delete=False)
|
||||||
|
key_file.write(key)
|
||||||
|
key_file.flush()
|
||||||
|
os.rename(key_file.name, key_filepath)
|
||||||
|
key_file.close()
|
||||||
|
finally:
|
||||||
|
os.umask(old_umask)
|
||||||
|
if (key_file is not None) and (not key_file.closed):
|
||||||
|
key_file.close()
|
||||||
|
os.unlink(key_file.name)
|
||||||
|
__itsda_secret = key
|
||||||
|
_log.info("Saved new key for It's Dangerous")
|
||||||
|
|
||||||
|
def setup_crypto():
|
||||||
|
global __itsda_secret
|
||||||
|
key_dir = mg_globals.app_config["crypto_path"]
|
||||||
|
key_filepath = os.path.join(key_dir, 'itsdangeroussecret.bin')
|
||||||
|
try:
|
||||||
|
load_key(key_filepath)
|
||||||
|
except IOError, error:
|
||||||
|
if error.errno != errno.ENOENT:
|
||||||
|
raise
|
||||||
|
create_key(key_dir, key_filepath)
|
||||||
|
|
||||||
|
def get_timed_signer_url(namespace):
|
||||||
|
"""
|
||||||
|
This gives a basic signing/verifying object.
|
||||||
|
|
||||||
|
The namespace makes sure signed tokens can't be used in
|
||||||
|
a different area. Like using a forgot-password-token as
|
||||||
|
a session cookie.
|
||||||
|
|
||||||
|
Basic usage:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
_signer = None
|
||||||
|
TOKEN_VALID_DAYS = 10
|
||||||
|
def setup():
|
||||||
|
global _signer
|
||||||
|
_signer = get_timed_signer_url("session cookie")
|
||||||
|
def create_token(obj):
|
||||||
|
return _signer.dumps(obj)
|
||||||
|
def parse_token(token):
|
||||||
|
# This might raise an exception in case
|
||||||
|
# of an invalid token, or an expired token.
|
||||||
|
return _signer.loads(token, max_age=TOKEN_VALID_DAYS*24*3600)
|
||||||
|
|
||||||
|
For more details see
|
||||||
|
http://pythonhosted.org/itsdangerous/#itsdangerous.URLSafeTimedSerializer
|
||||||
|
"""
|
||||||
|
assert __itsda_secret is not None
|
||||||
|
return itsdangerous.URLSafeTimedSerializer(__itsda_secret,
|
||||||
|
salt=namespace)
|
@ -35,4 +35,4 @@ def setup_user_in_request(request):
|
|||||||
# Something's wrong... this user doesn't exist? Invalidate
|
# Something's wrong... this user doesn't exist? Invalidate
|
||||||
# this session.
|
# this session.
|
||||||
_log.warn("Killing session for user id %r", request.session['user_id'])
|
_log.warn("Killing session for user id %r", request.session['user_id'])
|
||||||
request.session.invalidate()
|
request.session.delete()
|
||||||
|
67
mediagoblin/tools/session.py
Normal file
67
mediagoblin/tools/session.py
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
# GNU MediaGoblin -- federated, autonomous media hosting
|
||||||
|
# Copyright (C) 2013 MediaGoblin contributors. See AUTHORS.
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# 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 Affero 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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import itsdangerous
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import crypto
|
||||||
|
|
||||||
|
_log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class Session(dict):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.send_new_cookie = False
|
||||||
|
dict.__init__(self, *args, **kwargs)
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
self.send_new_cookie = True
|
||||||
|
|
||||||
|
def is_updated(self):
|
||||||
|
return self.send_new_cookie
|
||||||
|
|
||||||
|
def delete(self):
|
||||||
|
self.clear()
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
|
||||||
|
class SessionManager(object):
|
||||||
|
def __init__(self, cookie_name='MGSession', namespace=None):
|
||||||
|
if namespace is None:
|
||||||
|
namespace = cookie_name
|
||||||
|
self.signer = crypto.get_timed_signer_url(namespace)
|
||||||
|
self.cookie_name = cookie_name
|
||||||
|
|
||||||
|
def load_session_from_cookie(self, request):
|
||||||
|
cookie = request.cookies.get(self.cookie_name)
|
||||||
|
if not cookie:
|
||||||
|
return Session()
|
||||||
|
### FIXME: Future cookie-blacklisting code
|
||||||
|
# m = BadCookie.query.filter_by(cookie = cookie)
|
||||||
|
# if m:
|
||||||
|
# _log.warn("Bad cookie received: %s", m.reason)
|
||||||
|
# raise BadRequest()
|
||||||
|
try:
|
||||||
|
return Session(self.signer.loads(cookie))
|
||||||
|
except itsdangerous.BadData:
|
||||||
|
return Session()
|
||||||
|
|
||||||
|
def save_session_to_cookie(self, session, response):
|
||||||
|
if not session.is_updated():
|
||||||
|
return
|
||||||
|
elif not session:
|
||||||
|
response.delete_cookie(self.cookie_name)
|
||||||
|
else:
|
||||||
|
response.set_cookie(self.cookie_name, self.signer.dumps(session))
|
@ -17,7 +17,7 @@ use = egg:Paste#urlmap
|
|||||||
|
|
||||||
[app:mediagoblin]
|
[app:mediagoblin]
|
||||||
use = egg:mediagoblin#app
|
use = egg:mediagoblin#app
|
||||||
filter-with = beaker
|
# filter-with = beaker
|
||||||
config = %(here)s/mediagoblin_local.ini %(here)s/mediagoblin.ini
|
config = %(here)s/mediagoblin_local.ini %(here)s/mediagoblin.ini
|
||||||
|
|
||||||
[loggers]
|
[loggers]
|
||||||
|
2
setup.py
2
setup.py
@ -43,7 +43,6 @@ setup(
|
|||||||
install_requires=[
|
install_requires=[
|
||||||
'setuptools',
|
'setuptools',
|
||||||
'PasteScript',
|
'PasteScript',
|
||||||
'beaker',
|
|
||||||
'wtforms',
|
'wtforms',
|
||||||
'py-bcrypt',
|
'py-bcrypt',
|
||||||
'pytest',
|
'pytest',
|
||||||
@ -61,6 +60,7 @@ setup(
|
|||||||
'sqlalchemy>=0.7.0',
|
'sqlalchemy>=0.7.0',
|
||||||
'sqlalchemy-migrate',
|
'sqlalchemy-migrate',
|
||||||
'mock',
|
'mock',
|
||||||
|
'itsdangerous',
|
||||||
## This is optional!
|
## This is optional!
|
||||||
# 'translitcodec',
|
# 'translitcodec',
|
||||||
## For now we're expecting that users will install this from
|
## For now we're expecting that users will install this from
|
||||||
|
Loading…
x
Reference in New Issue
Block a user