import logging import urlparse import json import string import datetime from flask import make_response, render_template, request, abort, session from flask.ext.login import login_user, UserMixin from flask.ext.principal import identity_changed from random import SystemRandom from data import model from data.database import db from app import app, login_manager, dockerfile_build_queue, notification_queue, oauth_apps from auth.permissions import QuayDeferredPermissionUser from auth import scopes from endpoints.api.discovery import swagger_route_data from werkzeug.routing import BaseConverter from functools import wraps from config import getFrontendVisibleConfig from external_libraries import get_external_javascript, get_external_css from endpoints.notificationhelper import spawn_notification from util.useremails import CannotSendEmailException import features logger = logging.getLogger(__name__) profile = logging.getLogger('application.profiler') route_data = None class RepoPathConverter(BaseConverter): regex = '[\.a-zA-Z0-9_\-]+/[\.a-zA-Z0-9_\-]+' weight = 200 app.url_map.converters['repopath'] = RepoPathConverter def route_show_if(value): def decorator(f): @wraps(f) def decorated_function(*args, **kwargs): if not value: abort(404) return f(*args, **kwargs) return decorated_function return decorator def route_hide_if(value): def decorator(f): @wraps(f) def decorated_function(*args, **kwargs): if value: abort(404) return f(*args, **kwargs) return decorated_function return decorator def get_route_data(): global route_data if route_data: return route_data route_data = swagger_route_data(include_internal=True, compact=True) return route_data def truthy_param(param): return param not in {False, 'false', 'False', '0', 'FALSE', '', 'null'} def param_required(param_name): def wrapper(wrapped): @wraps(wrapped) def decorated(*args, **kwargs): if param_name not in request.args: abort(make_response('Required param: %s' % param_name, 400)) return wrapped(*args, **kwargs) return decorated return wrapper @login_manager.user_loader def load_user(user_db_id): logger.debug('User loader loading deferred user id: %s' % user_db_id) try: user_db_id_int = int(user_db_id) return _LoginWrappedDBUser(user_db_id_int) except ValueError: return None class _LoginWrappedDBUser(UserMixin): def __init__(self, user_db_id, db_user=None): self._db_id = user_db_id self._db_user = db_user def db_user(self): if not self._db_user: self._db_user = model.get_user_by_id(self._db_id) return self._db_user def is_authenticated(self): return self.db_user() is not None def is_active(self): return self.db_user().verified def get_id(self): return unicode(self._db_id) def common_login(db_user): if login_user(_LoginWrappedDBUser(db_user.id, db_user)): logger.debug('Successfully signed in as: %s' % db_user.username) new_identity = QuayDeferredPermissionUser(db_user.id, 'user_db_id', {scopes.DIRECT_LOGIN}) identity_changed.send(app, identity=new_identity) session['login_time'] = datetime.datetime.now() return True else: logger.debug('User could not be logged in, inactive?.') return False @app.errorhandler(model.DataModelException) def handle_dme(ex): logger.exception(ex) return make_response(json.dumps({'message': ex.message}), 400) @app.errorhandler(CannotSendEmailException) def handle_emailexception(ex): message = 'Could not send email. Please contact an administrator and report this problem.' return make_response(json.dumps({'message': message}), 400) def random_string(): random = SystemRandom() return ''.join([random.choice(string.ascii_uppercase + string.digits) for _ in range(8)]) def list_files(path, extension): import os def matches(f): return os.path.splitext(f)[1] == '.' + extension def join_path(dp, f): # Remove the static/ prefix. It is added in the template. return os.path.join(dp, f)[len('static/'):] filepath = 'static/' + path return [join_path(dp, f) for dp, dn, files in os.walk(filepath) for f in files if matches(f)] SAVED_CACHE_STRING = random_string() def render_page_template(name, **kwargs): if app.config.get('DEBUGGING', False): # If DEBUGGING is enabled, then we load the full set of individual JS and CSS files # from the file system. library_styles = list_files('lib', 'css') main_styles = list_files('css', 'css') library_scripts = list_files('lib', 'js') main_scripts = list_files('js', 'js') cache_buster = 'debugging' file_lists = [library_styles, main_styles, library_scripts, main_scripts] for file_list in file_lists: file_list.sort() else: library_styles = [] main_styles = ['dist/quay-frontend.css'] library_scripts = [] main_scripts = ['dist/quay-frontend.min.js'] cache_buster = SAVED_CACHE_STRING external_styles = get_external_css(local=not app.config.get('USE_CDN', True)) external_scripts = get_external_javascript(local=not app.config.get('USE_CDN', True)) def get_oauth_config(): oauth_config = {} for oauth_app in oauth_apps: oauth_config[oauth_app.key_name] = oauth_app.get_public_config() return oauth_config contact_href = None if len(app.config.get('CONTACT_INFO', [])) == 1: contact_href = app.config['CONTACT_INFO'][0] resp = make_response(render_template(name, route_data=json.dumps(get_route_data()), external_styles=external_styles, external_scripts=external_scripts, main_styles=main_styles, library_styles=library_styles, main_scripts=main_scripts, library_scripts=library_scripts, feature_set=json.dumps(features.get_features()), config_set=json.dumps(getFrontendVisibleConfig(app.config)), oauth_set=json.dumps(get_oauth_config()), scope_set=json.dumps(scopes.ALL_SCOPES), mixpanel_key=app.config.get('MIXPANEL_KEY', ''), google_analytics_key=app.config.get('GOOGLE_ANALYTICS_KEY', ''), sentry_public_dsn=app.config.get('SENTRY_PUBLIC_DSN', ''), is_debug=str(app.config.get('DEBUGGING', False)).lower(), show_chat=features.OLARK_CHAT, cache_buster=cache_buster, has_billing=features.BILLING, contact_href=contact_href, **kwargs)) resp.headers['X-FRAME-OPTIONS'] = 'DENY' return resp def check_repository_usage(user_or_org, plan_found): private_repos = model.get_private_repo_count(user_or_org.username) repos_allowed = plan_found['privateRepos'] if private_repos > repos_allowed: model.create_notification('over_private_usage', user_or_org, {'namespace': user_or_org.username}) else: model.delete_notifications_by_kind(user_or_org, 'over_private_usage') def start_build(repository, dockerfile_id, tags, build_name, subdir, manual, trigger=None, pull_robot_name=None): host = urlparse.urlparse(request.url).netloc repo_path = '%s/%s/%s' % (host, repository.namespace_user.username, repository.name) token = model.create_access_token(repository, 'write') logger.debug('Creating build %s with repo %s tags %s and dockerfile_id %s', build_name, repo_path, tags, dockerfile_id) job_config = { 'docker_tags': tags, 'repository': repo_path, 'build_subdir': subdir } with app.config['DB_TRANSACTION_FACTORY'](db): build_request = model.create_repository_build(repository, token, job_config, dockerfile_id, build_name, trigger, pull_robot_name=pull_robot_name) dockerfile_build_queue.put([repository.namespace_user.username, repository.name], json.dumps({ 'build_uuid': build_request.uuid, 'namespace': repository.namespace_user.username, 'repository': repository.name, 'pull_credentials': model.get_pull_credentials(pull_robot_name) if pull_robot_name else None }), retries_remaining=1) # Add the build to the repo's log. metadata = { 'repo': repository.name, 'namespace': repository.namespace_user.username, 'fileid': dockerfile_id, 'manual': manual, } if trigger: metadata['trigger_id'] = trigger.uuid metadata['config'] = json.loads(trigger.config) metadata['service'] = trigger.service.name model.log_action('build_dockerfile', repository.namespace_user.username, ip=request.remote_addr, metadata=metadata, repository=repository) # Add notifications for the build queue. profile.debug('Adding notifications for repository') event_data = { 'build_id': build_request.uuid, 'build_name': build_name, 'docker_tags': tags, 'is_manual': manual } if trigger: event_data['trigger_id'] = trigger.uuid event_data['trigger_kind'] = trigger.service.name spawn_notification(repository, 'build_queued', event_data, subpage='build?current=%s' % build_request.uuid, pathargs=['build', build_request.uuid]) return build_request