import logging from flask import request, make_response, Blueprint from app import billing as stripe from data import model from auth.decorators import process_auth from auth.permissions import ModifyRepositoryPermission from util.invoice import renderInvoiceToHtml from util.useremails import send_invoice_email, send_subscription_change, send_payment_failed from util.http import abort from buildtrigger.basehandler import BuildTriggerHandler from buildtrigger.triggerutil import (ValidationRequestException, SkipRequestException, InvalidPayloadException) from endpoints.building import start_build, MaximumBuildsQueuedException logger = logging.getLogger(__name__) webhooks = Blueprint('webhooks', __name__) @webhooks.route('/stripe', methods=['POST']) def stripe_webhook(): request_data = request.get_json() logger.debug('Stripe webhook call: %s', request_data) customer_id = request_data.get('data', {}).get('object', {}).get('customer', None) user = model.user.get_user_or_org_by_customer_id(customer_id) if customer_id else None event_type = request_data['type'] if 'type' in request_data else None if event_type == 'charge.succeeded': invoice_id = request_data['data']['object']['invoice'] if user and user.invoice_email: # Lookup the invoice. invoice = stripe.Invoice.retrieve(invoice_id) if invoice: invoice_html = renderInvoiceToHtml(invoice, user) send_invoice_email(user.invoice_email_address or user.email, invoice_html) elif event_type.startswith('customer.subscription.'): cust_email = user.email if user is not None else 'unknown@domain.com' quay_username = user.username if user is not None else 'unknown' change_type = '' if event_type.endswith('.deleted'): plan_id = request_data['data']['object']['plan']['id'] change_type = 'canceled %s' % plan_id send_subscription_change(change_type, customer_id, cust_email, quay_username) elif event_type.endswith('.created'): plan_id = request_data['data']['object']['plan']['id'] change_type = 'subscribed %s' % plan_id send_subscription_change(change_type, customer_id, cust_email, quay_username) elif event_type.endswith('.updated'): if 'previous_attributes' in request_data['data']: if 'plan' in request_data['data']['previous_attributes']: old_plan = request_data['data']['previous_attributes']['plan']['id'] new_plan = request_data['data']['object']['plan']['id'] change_type = 'switched %s -> %s' % (old_plan, new_plan) send_subscription_change(change_type, customer_id, cust_email, quay_username) elif event_type == 'invoice.payment_failed': if user: send_payment_failed(user.email, user.username) return make_response('Okay') @webhooks.route('/push//trigger/', methods=['POST']) @webhooks.route('/push/trigger/', methods=['POST'], defaults={'repository': ''}) @process_auth def build_trigger_webhook(trigger_uuid, **kwargs): logger.debug('Webhook received with uuid %s', trigger_uuid) try: trigger = model.build.get_build_trigger(trigger_uuid) except model.InvalidBuildTriggerException: # It is ok to return 404 here, since letting an attacker know that a trigger UUID is valid # doesn't leak anything abort(404) namespace = trigger.repository.namespace_user.username repository = trigger.repository.name permission = ModifyRepositoryPermission(namespace, repository) if permission.can(): handler = BuildTriggerHandler.get_handler(trigger) logger.debug('Passing webhook request to handler %s', handler) try: prepared = handler.handle_trigger_request(request) except ValidationRequestException: logger.debug('Handler reported a validation exception: %s', handler) # This was just a validation request, we don't need to build anything return make_response('Okay') except SkipRequestException: logger.debug('Handler reported to skip the build: %s', handler) # The build was requested to be skipped return make_response('Okay') except InvalidPayloadException as ipe: logger.exception('Invalid payload') # The payload was malformed abort(400, message=ipe.message) pull_robot_name = model.build.get_pull_robot_name(trigger) repo = model.repository.get_repository(namespace, repository) try: start_build(repo, prepared, pull_robot_name=pull_robot_name) except MaximumBuildsQueuedException: abort(429, message='Maximum queued build rate exceeded.') return make_response('Okay') abort(403)