ara/ara/wsgi_sqlite.py

148 lines
5.9 KiB
Python

# Copyright (c) 2018 Red Hat, Inc.
#
# This file is part of ARA Records Ansible.
#
# ARA is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# ARA 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 General Public License
# along with ARA. If not, see <http://www.gnu.org/licenses/>.
# A WSGI script to load the ARA web application against a variable database
# location requested over HTTP.
# Can be configured using environment variables (i.e, Apache SetEnv) with the
# following variables:
#
# ARA_WSGI_USE_VIRTUALENV
# Enable virtual environment usage if ARA is installed in a virtual
# environment.
# Defaults to '0', set to '1' to enable.
# ARA_WSGI_VIRTUALENV_PATH
# When using a virtual environment, where the virtualenv is located.
# Defaults to None, set to the absolute path of your virtualenv.
# ARA_WSGI_TMPDIR_MAX_AGE
# This WSGI middleware creates temporary directories which should be
# discarded on a regular basis to avoid them accumulating.
# This is a duration, in seconds, before cleaning directories up.
# Defaults to 3600.
# ARA_WSGI_LOG_ROOT
# Absolute path on the filesystem that matches the DocumentRoot of your
# webserver vhost.
# Defaults to '/srv/static/logs'.
# ARA_WSGI_DATABASE_DIRECTORY
# Subdirectory in which ARA sqlite databases are expected to reside in.
# For example, 'ara-report' would expect:
# http://logserver/some/path/ara-report/ansible.sqlite
# This variable should match the 'WSGIScriptAliasMatch' pattern of your
# webserver vhost.
# Defaults to 'ara-report'
import logging
import os
import re
import shutil
import six
import time
if (int(os.getenv('ARA_WSGI_USE_VIRTUALENV', 0)) == 1 and
os.getenv('ARA_WSGI_VIRTUALENV_PATH')):
activate_this = os.getenv('ARA_WSGI_VIRTUALENV_PATH')
if six.PY2:
execfile(activate_this, dict(__file__=activate_this)) # nosec
else:
exec(open(activate_this).read()) # nosec
TMPDIR_MAX_AGE = int(os.getenv('ARA_WSGI_TMPDIR_MAX_AGE', 3600))
LOG_ROOT = os.getenv('ARA_WSGI_LOG_ROOT', '/srv/static/logs')
DATABASE_DIRECTORY = os.getenv('ARA_WSGI_DATABASE_DIRECTORY', 'ara-report')
logger = logging.getLogger('ara.wsgi_sqlite')
if not logger.handlers:
logging.basicConfig(format='%(name)s:%(levelname)s:%(message)s')
def bad_request(environ, start_response, message):
logger.error('HTTP 400: %s' % message)
message = """
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>400 Bad Request</title>
<h1>Bad Request</h1>
<p>%s</p>""" % message
status = '400 Bad Request'
response_headers = [('Content-Type', 'text/html')]
start_response(status, response_headers)
return [message]
def application(environ, start_response):
request = environ['REQUEST_URI']
match = re.search('/(?P<path>.*/{}/)'.format(DATABASE_DIRECTORY), request)
if not match:
return bad_request(environ, start_response,
'No "/{}/" in URL.'.format(DATABASE_DIRECTORY))
path = os.path.abspath(os.path.join(LOG_ROOT, match.group('path')))
# Ensure we don't escape outside LOG_ROOT and we are looking at a
# valid directory
if not path.startswith(LOG_ROOT) or not os.path.isdir(path):
logger.error('Directory access violation: %s' % path)
return bad_request(environ, start_response, 'No directory found.')
database = os.path.join(path, 'ansible.sqlite')
if not os.path.isfile(database):
return bad_request(environ, start_response, 'No ARA database found.')
# ARA and Ansible (when loading configuration) both expect a directory
# they are able to write to, this can be safely discarded.
# Nothing is read from here and there is therefore no security risks.
# It needs to be at a known location in order to be able to clean it up
# so it doesn't accumulate needless directories and files.
# TODO: ARA 1.0 no longer requires temporary directories, clean this up.
tmpdir = '/tmp/ara_wsgi_sqlite' # nosec
if os.path.exists(tmpdir):
# Periodically delete this directory to avoid accumulating directories
# and files endlessly
now = time.time()
if now - TMPDIR_MAX_AGE > os.path.getmtime(tmpdir):
shutil.rmtree(tmpdir, ignore_errors=True)
os.environ['ANSIBLE_LOCAL_TEMP'] = os.path.join(tmpdir, '.ansible')
os.environ['ARA_DIR'] = os.path.join(tmpdir, '.ara')
# Path to the ARA database
os.environ['ARA_DATABASE'] = 'sqlite:///{}'.format(database)
# The intent is that we are dealing with databases that already exist.
# Therefore, we're not really interested in creating the database and
# making sure that the SQL migrations are done. Toggle that off.
# This needs to be a string, we're setting an environment variable
os.environ['ARA_AUTOCREATE_DATABASE'] = 'false'
msg = 'Request {request} mapped to {database} with root {root}'.format(
request=request,
database='sqlite:///{}'.format(database),
root=match.group('path')
)
logger.debug(msg)
from ara.webapp import create_app
try:
app = create_app()
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///{}'.format(database)
app.config['APPLICATION_ROOT'] = match.group('path')
return app(environ, start_response)
except Exception as e:
# We're staying relatively vague on purpose to avoid disclosure
logger.error('ARA bootstrap failure for %s: %s' % (database, str(e)))
return bad_request(environ, start_response, 'ARA bootstrap failure.')
def main():
return application