import logging from flask import (abort, redirect, request, url_for, make_response, Response, Blueprint, send_from_directory, jsonify, send_file) from flask.ext.login import current_user from urlparse import urlparse from health.healthcheck import get_healthchecker from data import model from data.database import db from app import app, billing as stripe, build_logs, avatar, signer, log_archive, config_provider from auth.auth import require_session_login, process_oauth from auth.permissions import (AdministerOrganizationPermission, ReadRepositoryPermission, SuperUserPermission, AdministerRepositoryPermission, ModifyRepositoryPermission) from util.invoice import renderInvoiceToPdf from util.seo import render_snapshot from util.cache import no_cache from endpoints.common import common_login, render_page_template, route_show_if, param_required from endpoints.decorators import anon_protect, anon_allowed from endpoints.csrf import csrf_protect, generate_csrf_token, verify_csrf from buildtrigger.customhandler import CustomBuildTrigger from buildtrigger.bitbuckethandler import BitbucketBuildTrigger from buildtrigger.triggerutil import TriggerProviderException from buildtrigger.basehandler import BuildTriggerHandler from util.names import parse_repository_name, parse_repository_name_and_tag from util.useremails import send_email_changed from util.systemlogs import build_logs_archive from util.headers import parse_basic_auth from auth import scopes import features import json logger = logging.getLogger(__name__) # Capture the unverified SSL errors. logging.captureWarnings(True) web = Blueprint('web', __name__) STATUS_TAGS = app.config['STATUS_TAGS'] @web.route('/', methods=['GET'], defaults={'path': ''}) @no_cache def index(path, **kwargs): return render_page_template('index.html', **kwargs) @web.route('/500', methods=['GET']) def internal_error_display(): return render_page_template('500.html') @web.errorhandler(404) @web.route('/404', methods=['GET']) def not_found_error_display(e = None): resp = render_page_template('404.html') resp.status_code = 404 return resp @web.route('/organization/', methods=['GET']) @no_cache def org_view(path): return index('') @web.route('/user/', methods=['GET']) @no_cache def user_view(path): return index('') @web.route('/snapshot', methods=['GET']) @web.route('/snapshot/', methods=['GET']) @web.route('/snapshot/', methods=['GET']) def snapshot(path = ''): parsed = urlparse(request.url) final_url = '%s://%s/%s' % (parsed.scheme, 'localhost', path) result = render_snapshot(final_url) if result: return result abort(404) @web.route('/aci-signing-key') @no_cache @anon_protect def aci_signing_key(): if not signer.name: abort(404) return send_file(signer.public_key_path) @web.route('/plans/') @no_cache @route_show_if(features.BILLING) def plans(): return index('') @web.route('/guide/') @no_cache def guide(): return index('') @web.route('/tour/') @web.route('/tour/') @no_cache def tour(path = ''): return index('') @web.route('/tutorial/') @no_cache def tutorial(): return index('') @web.route('/organizations/') @web.route('/organizations/new/') @no_cache def organizations(): return index('') @web.route('/user/') @no_cache def user(): return index('') @web.route('/superuser/') @no_cache @route_show_if(features.SUPER_USERS) def superuser(): return index('') @web.route('/setup/') @no_cache @route_show_if(features.SUPER_USERS) def setup(): return index('') @web.route('/signin/') @no_cache def signin(redirect=None): return index('') @web.route('/contact/') @no_cache def contact(): return index('') @web.route('/about/') @no_cache def about(): return index('') @web.route('/new/') @no_cache def new(): return index('') @web.route('/confirminvite') @no_cache def confirm_invite(): code = request.values['code'] return index('', code=code) @web.route('/repository/', defaults={'path': ''}) @web.route('/repository/', methods=['GET']) @no_cache def repository(path): return index('') @web.route('/starred/') @no_cache def starred(): return index('') @web.route('/security/') @no_cache def security(): return index('') @web.route('/__exp/') @no_cache def exp(expname): return index('') @web.route('/v1') @web.route('/v1/') @no_cache def v1(): return index('') # TODO(jschorr): Remove this mirrored endpoint once we migrate ELB. @web.route('/health', methods=['GET']) @web.route('/health/instance', methods=['GET']) @no_cache def instance_health(): checker = get_healthchecker(app, config_provider) (data, status_code) = checker.check_instance() response = jsonify(dict(data=data, status_code=status_code)) response.status_code = status_code return response # TODO(jschorr): Remove this mirrored endpoint once we migrate pingdom. @web.route('/status', methods=['GET']) @web.route('/health/endtoend', methods=['GET']) @no_cache def endtoend_health(): checker = get_healthchecker(app, config_provider) (data, status_code) = checker.check_endtoend() response = jsonify(dict(data=data, status_code=status_code)) response.status_code = status_code return response @web.route('/health/dbrevision', methods=['GET']) @route_show_if(features.BILLING) # Since this is only used in production. @no_cache def dbrevision_health(): # Find the revision from the database. result = db.execute_sql('select * from alembic_version limit 1').fetchone() db_revision = result[0] # Find the local revision from the file system. with open('ALEMBIC_HEAD', 'r') as f: local_revision = f.readline().split(' ')[0] data = { 'db_revision': db_revision, 'local_revision': local_revision, } status_code = 200 if db_revision == local_revision else 400 response = jsonify(dict(data=data, status_code=status_code)) response.status_code = status_code return response @web.route('/tos', methods=['GET']) @no_cache def tos(): return render_page_template('tos.html') @web.route('/disclaimer', methods=['GET']) @no_cache def disclaimer(): return render_page_template('disclaimer.html') @web.route('/privacy', methods=['GET']) @no_cache def privacy(): return render_page_template('privacy.html') @web.route('/robots.txt', methods=['GET']) @no_cache def robots(): return send_from_directory('static', 'robots.txt') @web.route('/buildlogs/', methods=['GET']) @route_show_if(features.BUILD_SUPPORT) @require_session_login def buildlogs(build_uuid): found_build = model.build.get_repository_build(build_uuid) if not found_build: abort(403) repo = found_build.repository if not ModifyRepositoryPermission(repo.namespace_user.username, repo.name).can(): abort(403) # If the logs have been archived, just return a URL of the completed archive if found_build.logs_archived: return redirect(log_archive.get_file_url(found_build.uuid)) _, logs = build_logs.get_log_entries(found_build.uuid, 0) response = jsonify({ 'logs': [log for log in logs] }) response.headers["Content-Disposition"] = "attachment;filename=" + found_build.uuid + ".json" return response @web.route('/receipt', methods=['GET']) @route_show_if(features.BILLING) @require_session_login def receipt(): if not current_user.is_authenticated(): abort(401) return invoice_id = request.args.get('id') if invoice_id: invoice = stripe.Invoice.retrieve(invoice_id) if invoice: user_or_org = model.user.get_user_or_org_by_customer_id(invoice.customer) if user_or_org: if user_or_org.organization: admin_org = AdministerOrganizationPermission(user_or_org.username) if not admin_org.can(): abort(404) return else: if not user_or_org.username == current_user.db_user().username: abort(404) return file_data = renderInvoiceToPdf(invoice, user_or_org) return Response(file_data, mimetype="application/pdf", headers={"Content-Disposition": "attachment;filename=receipt.pdf"}) abort(404) @web.route('/authrepoemail', methods=['GET']) @route_show_if(features.MAILING) def confirm_repo_email(): code = request.values['code'] record = None try: record = model.repository.confirm_email_authorization_for_repo(code) except model.DataModelException as ex: return render_page_template('confirmerror.html', error_message=ex.message) message = """ Your E-mail address has been authorized to receive notifications for repository %s/%s. """ % (app.config['PREFERRED_URL_SCHEME'], app.config['SERVER_HOSTNAME'], record.repository.namespace_user.username, record.repository.name, record.repository.namespace_user.username, record.repository.name) return render_page_template('message.html', message=message) @web.route('/confirm', methods=['GET']) @route_show_if(features.MAILING) @anon_allowed def confirm_email(): code = request.values['code'] user = None new_email = None try: user, new_email, old_email = model.user.confirm_user_email(code) except model.DataModelException as ex: return render_page_template('confirmerror.html', error_message=ex.message) if new_email: send_email_changed(user.username, old_email, new_email) common_login(user) return redirect(url_for('web.user', tab='email') if new_email else url_for('web.index')) @web.route('/recovery', methods=['GET']) @route_show_if(features.MAILING) @anon_allowed def confirm_recovery(): code = request.values['code'] user = model.user.validate_reset_code(code) if user: common_login(user) return redirect(url_for('web.user')) else: abort(403) @web.route('/repository//status', methods=['GET']) @parse_repository_name @anon_protect def build_status_badge(namespace, repository): token = request.args.get('token', None) is_public = model.repository.repository_is_public(namespace, repository) if not is_public: repo = model.repository.get_repository(namespace, repository) if not repo or token != repo.badge_token: abort(404) # Lookup the tags for the repository. tags = model.tag.list_repository_tags(namespace, repository) is_empty = len(list(tags)) == 0 recent_build = model.build.get_recent_repository_build(namespace, repository) if not is_empty and (not recent_build or recent_build.phase == 'complete'): status_name = 'ready' elif recent_build and recent_build.phase == 'error': status_name = 'failed' elif recent_build and recent_build.phase != 'complete': status_name = 'building' else: status_name = 'none' if request.headers.get('If-None-Match') == status_name: return Response(status=304) response = make_response(STATUS_TAGS[status_name]) response.content_type = 'image/svg+xml' response.headers['Cache-Control'] = 'no-cache' response.headers['ETag'] = status_name return response class FlaskAuthorizationProvider(model.oauth.DatabaseAuthorizationProvider): def get_authorized_user(self): return current_user.db_user() def _make_response(self, body='', headers=None, status_code=200): return make_response(body, status_code, headers) @web.route('/oauth/authorizeapp', methods=['POST']) @csrf_protect def authorize_application(): if not current_user.is_authenticated(): abort(401) return provider = FlaskAuthorizationProvider() client_id = request.form.get('client_id', None) redirect_uri = request.form.get('redirect_uri', None) scope = request.form.get('scope', None) # Add the access token. return provider.get_token_response('token', client_id, redirect_uri, scope=scope) @web.route(app.config['LOCAL_OAUTH_HANDLER'], methods=['GET']) def oauth_local_handler(): if not current_user.is_authenticated(): abort(401) return if not request.args.get('scope'): return render_page_template("message.html", message="Authorization canceled") else: return render_page_template("generatedtoken.html") @web.route('/oauth/denyapp', methods=['POST']) @csrf_protect def deny_application(): if not current_user.is_authenticated(): abort(401) return provider = FlaskAuthorizationProvider() client_id = request.form.get('client_id', None) redirect_uri = request.form.get('redirect_uri', None) scope = request.form.get('scope', None) # Add the access token. return provider.get_auth_denied_response('token', client_id, redirect_uri, scope=scope) @web.route('/oauth/authorize', methods=['GET']) @no_cache @param_required('client_id') @param_required('redirect_uri') @param_required('scope') def request_authorization_code(): provider = FlaskAuthorizationProvider() response_type = request.args.get('response_type', 'code') client_id = request.args.get('client_id', None) redirect_uri = request.args.get('redirect_uri', None) scope = request.args.get('scope', None) if (not current_user.is_authenticated() or not provider.validate_has_scopes(client_id, current_user.db_user().username, scope)): if not provider.validate_redirect_uri(client_id, redirect_uri): current_app = provider.get_application_for_client_id(client_id) if not current_app: abort(404) return provider._make_redirect_error_response(current_app.redirect_uri, 'redirect_uri_mismatch') # Load the scope information. scope_info = scopes.get_scope_information(scope) if not scope_info: abort(404) return # Load the application information. oauth_app = provider.get_application_for_client_id(client_id) app_email = oauth_app.avatar_email or oauth_app.organization.email oauth_app_view = { 'name': oauth_app.name, 'description': oauth_app.description, 'url': oauth_app.application_uri, 'avatar': json.dumps(avatar.get_data(oauth_app.name, app_email, 'app')), 'organization': { 'name': oauth_app.organization.username, 'avatar': json.dumps(avatar.get_data_for_org(oauth_app.organization)) } } # Show the authorization page. has_dangerous_scopes = any([check_scope['dangerous'] for check_scope in scope_info]) return render_page_template('oauthorize.html', scopes=scope_info, has_dangerous_scopes=has_dangerous_scopes, application=oauth_app_view, enumerate=enumerate, client_id=client_id, redirect_uri=redirect_uri, scope=scope, csrf_token_val=generate_csrf_token()) if response_type == 'token': return provider.get_token_response(response_type, client_id, redirect_uri, scope=scope) else: return provider.get_authorization_code(response_type, client_id, redirect_uri, scope=scope) @web.route('/oauth/access_token', methods=['POST']) @no_cache @param_required('grant_type', allow_body=True) @param_required('client_id', allow_body=True) @param_required('redirect_uri', allow_body=True) @param_required('code', allow_body=True) @param_required('scope', allow_body=True) def exchange_code_for_token(): grant_type = request.values.get('grant_type', None) client_id = request.values.get('client_id', None) client_secret = request.values.get('client_id', None) redirect_uri = request.values.get('redirect_uri', None) code = request.values.get('code', None) scope = request.values.get('scope', None) # Sometimes OAuth2 clients place the client id/secret in the Auth header. basic_header = parse_basic_auth(request.headers.get('Authorization')) if basic_header is not None: client_id = basic_header[0] or client_id client_secret = basic_header[1] or client_secret provider = FlaskAuthorizationProvider() return provider.get_token(grant_type, client_id, client_secret, redirect_uri, code, scope=scope) @web.route('/systemlogsarchive', methods=['GET']) @process_oauth @route_show_if(features.SUPER_USERS) @no_cache def download_logs_archive(): # Note: We cannot use the decorator here because this is a GET method. That being said, this # information is sensitive enough that we want the extra protection. verify_csrf() if SuperUserPermission().can(): archive_data = build_logs_archive(app) return Response(archive_data, mimetype="application/octet-stream", headers={"Content-Disposition": "attachment;filename=erlogs.tar.gz"}) abort(403) @web.route('/bitbucket/setup/', methods=['GET']) @require_session_login @parse_repository_name @route_show_if(features.BITBUCKET_BUILD) def attach_bitbucket_trigger(namespace, repository_name): permission = AdministerRepositoryPermission(namespace, repository_name) if permission.can(): repo = model.repository.get_repository(namespace, repository_name) if not repo: msg = 'Invalid repository: %s/%s' % (namespace, repository_name) abort(404, message=msg) trigger = model.build.create_build_trigger(repo, BitbucketBuildTrigger.service_name(), None, current_user.db_user()) try: oauth_info = BuildTriggerHandler.get_handler(trigger).get_oauth_url() config = { 'access_token': oauth_info['access_token'] } access_token_secret = oauth_info['access_token_secret'] model.build.update_build_trigger(trigger, config, auth_token=access_token_secret) return redirect(oauth_info['url']) except TriggerProviderException: trigger.delete_instance() abort(400, message='Could not retrieve OAuth URL from Bitbucket') abort(403) @web.route('/customtrigger/setup/', methods=['GET']) @require_session_login @parse_repository_name def attach_custom_build_trigger(namespace, repository_name): permission = AdministerRepositoryPermission(namespace, repository_name) if permission.can(): repo = model.repository.get_repository(namespace, repository_name) if not repo: msg = 'Invalid repository: %s/%s' % (namespace, repository_name) abort(404, message=msg) trigger = model.build.create_build_trigger(repo, CustomBuildTrigger.service_name(), None, current_user.db_user()) repo_path = '%s/%s' % (namespace, repository_name) full_url = '%s%s%s' % (url_for('web.repository', path=repo_path), '?tab=builds&newtrigger=', trigger.uuid) logger.debug('Redirecting to full url: %s', full_url) return redirect(full_url) abort(403) @web.route('/') @no_cache @process_oauth @parse_repository_name_and_tag @anon_protect def redirect_to_repository(namespace, reponame, tag_name): permission = ReadRepositoryPermission(namespace, reponame) is_public = model.repository.repository_is_public(namespace, reponame) if request.args.get('ac-discovery', 0) == 1: return index('') if permission.can() or is_public: repository_name = '/'.join([namespace, reponame]) return redirect(url_for('web.repository', path=repository_name, tag=tag_name)) abort(404) @web.route('/') @no_cache @process_oauth @anon_protect def redirect_to_namespace(namespace): user_or_org = model.user.get_user_or_org(namespace) if not user_or_org: abort(404) if user_or_org.organization: return redirect(url_for('web.org_view', path=namespace)) else: return redirect(url_for('web.user_view', path=namespace))