Source code for coaster.app
"""
App configuration
=================
"""
from flask import Flask
from flask.sessions import SecureCookieSessionInterface
import itsdangerous
from . import logger
from .auth import current_auth
from .views import current_view
__all__ = [
'KeyRotationWrapper',
'RotatingKeySecureCookieSessionInterface',
'Flask',
'init_app',
]
_additional_config = {
'dev': 'development.py',
'development': 'development.py',
'test': 'testing.py',
'testing': 'testing.py',
'prod': 'production.py',
'production': 'production.py',
}
[docs]class KeyRotationWrapper:
"""
Wrapper to support multiple secret keys in itsdangerous.
The first secret key is used for all operations, but if it causes a BadSignature
exception, the other secret keys are tried in order.
:param cls: Signing class from itsdangerous (eg: URLSafeTimedSerializer)
:param secret_keys: List of secret keys
:param kwargs: Arguments to pass to each signer/serializer
"""
def __init__(self, cls, secret_keys, **kwargs):
if isinstance(secret_keys, str):
raise ValueError("Secret keys must be a list")
self._engines = [cls(key, **kwargs) for key in secret_keys]
def __getattr__(self, attr):
item = getattr(self._engines[0], attr)
return self._make_wrapper(attr) if callable(item) else item
def _make_wrapper(self, attr):
def wrapper(*args, **kwargs):
last = len(self._engines) - 1
for counter, engine in enumerate(self._engines):
try:
return getattr(engine, attr)(*args, **kwargs)
except itsdangerous.exc.BadSignature:
if counter == last:
# We've run out of engines. Raise error to caller
raise
return wrapper
[docs]class RotatingKeySecureCookieSessionInterface(SecureCookieSessionInterface):
"""Replaces the serializer with key rotation support"""
def get_signing_serializer(self, app):
if not app.config.get('SECRET_KEYS'):
return None
signer_kwargs = {
'key_derivation': self.key_derivation,
'digest_method': self.digest_method,
}
return KeyRotationWrapper(
itsdangerous.URLSafeTimedSerializer,
app.config['SECRET_KEYS'],
salt=self.salt,
serializer=self.serializer,
signer_kwargs=signer_kwargs,
)
[docs]def init_app(app, init_logging=True):
"""
Configure an app depending on the environment. Loads settings from a file
named ``settings.py`` in the instance folder, followed by additional
settings from one of ``development.py``, ``production.py`` or
``testing.py``. Typical usage::
from flask import Flask
import coaster.app
app = Flask(__name__, instance_relative_config=True)
coaster.app.init_app(app) # Guess environment automatically
:func:`init_app` also configures logging by calling
:func:`coaster.logger.init_app`.
:param app: App to be configured
:param bool init_logging: Call `coaster.logger.init_app` (default `True`)
"""
# Make current_auth available to app templates
app.jinja_env.globals['current_auth'] = current_auth
# Make the current view available to app templates
app.jinja_env.globals['current_view'] = current_view
# Disable Flask-SQLAlchemy events.
# Apps that want it can turn it back on in their config
app.config.setdefault('SQLALCHEMY_TRACK_MODIFICATIONS', False)
# Load config from the app's settings.py
load_config_from_file(app, 'settings.py')
# Load additional settings from the app's environment-specific config file:
# Flask sets ``ENV`` configuration variable based on ``FLASK_ENV`` environment
# variable. So we can directly get it from ``app.config['ENV']``.
# Lowercase because that's how flask defines it.
# ref: https://flask.palletsprojects.com/en/1.1.x/config/#environment-and-debug-features
additional = _additional_config.get(app.config['ENV'].lower())
if additional:
load_config_from_file(app, additional)
if init_logging:
logger.init_app(app)
def load_config_from_file(app, filepath):
"""Helper function to load config from a specified file"""
try:
app.config.from_pyfile(filepath)
return True
except IOError:
app.logger.warning(
"Did not find settings file %s for additional settings, skipping it",
filepath,
)
return False