Basing sessions on UUIDs must be done in phases. First all users must obtain an UUID. Once a backfill has given all previous users UUIDs and new users are being generated with UUIDs, then we can actually change the session to be based on that value.
278 lines
9.3 KiB
Python
278 lines
9.3 KiB
Python
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 with id: %s' % user_db_id)
|
|
return _LoginWrappedDBUser(user_db_id)
|
|
|
|
|
|
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 (%s)' % (db_user.username, db_user.uuid))
|
|
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()),
|
|
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,
|
|
'registry': host,
|
|
'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([str(repository.namespace_user.id), repository.name], json.dumps({
|
|
'build_uuid': build_request.uuid,
|
|
'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
|