1
0
mirror of https://github.com/quay/quay.git synced 2025-04-19 21:42:17 +03:00
quay/endpoints/webhooks.py
Kenny Lee Sin Cheong 5f63b3a7bb
chore: drop deprecated tables and remove unused code (PROJQUAY-522) (#2089)
* chore: drop deprecated tables and remove unused code

* isort imports

* migration: check for table existence before drop
2023-08-25 12:17:24 -04:00

208 lines
8.2 KiB
Python

import logging
from flask import Blueprint, make_response, request
from app import app
from app import billing as stripe
from auth.decorators import process_auth
from auth.permissions import ModifyRepositoryPermission
from buildtrigger.basehandler import BuildTriggerHandler
from buildtrigger.triggerutil import (
InvalidPayloadException,
SkipRequestException,
ValidationRequestException,
)
from data import model
from data.database import RepositoryState
from data.logs_model import logs_model
from endpoints.building import (
BuildTriggerDisabledException,
MaximumBuildsQueuedException,
start_build,
)
from util.http import abort
from util.invoice import renderInvoiceToHtml
from util.useremails import (
send_invoice_email,
send_payment_failed,
send_subscription_change,
)
logger = logging.getLogger(__name__)
webhooks = Blueprint("webhooks", __name__)
@webhooks.route("/stripe", methods=["POST"])
def stripe_webhook():
def _stripe_checkout_log_action(kind, namespace_name, performer_name, ip, metadata=None):
if not metadata:
metadata = {}
performer = data.model.users.get_user(performer_name)
logs_model.log_action(
kind,
namespace_name,
performer=performer,
ip=ip,
)
request_data = request.get_json()
logger.debug("Stripe webhook call: %s", request_data)
customer_id = request_data.get("data", {}).get("object", {}).get("customer", None)
namespace = 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"]
namespace = model.user.get_user_or_org_by_customer_id(customer_id) if customer_id else None
if namespace:
# Increase the namespace's build allowance, since we had a successful charge.
build_maximum = app.config.get("BILLED_NAMESPACE_MAXIMUM_BUILD_COUNT")
if build_maximum is not None:
model.user.increase_maximum_build_count(namespace, build_maximum)
if namespace.invoice_email:
# Lookup the invoice.
invoice = stripe.Invoice.retrieve(invoice_id)
if invoice:
invoice_html = renderInvoiceToHtml(invoice, namespace)
send_invoice_email(
namespace.invoice_email_address or namespace.email, invoice_html
)
elif event_type.startswith("customer.subscription."):
cust_email = namespace.email if namespace is not None else "unknown@domain.com"
quay_username = namespace.username if namespace is not None else "unknown"
change_type = ""
if event_type.endswith(".deleted"):
plan_id = request_data["data"]["object"]["plan"]["id"]
requested = bool(request_data.get("request"))
if requested:
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 namespace:
send_payment_failed(namespace.email, namespace.username)
elif event_type == "checkout.session.completed":
mode = request_data["data"]["object"]["mode"]
if mode == "setup":
setup_intent = stripe.SetupIntent.retrieve(
request_data["data"]["object"]["setup_intent"]
)
setup_intent_metadata = setup_intent["metadata"]
payment_method = setup_intent["payment_method"]
customer = setup_intent["customer"]
subscription = setup_intent_metadata["subscription_id"]
stripe.Customer.modify(
customer,
invoice_settings={"default_payment_method": payment_method},
)
stripe.Subscription.modify(
subscription,
default_payment_method=payment_method,
)
_stripe_checkout_log_action(
setup_intent_metadata["kind"],
setup_intent_metadata["namespace"],
setup_intent_metadata["performer"],
setup_intent_metadata["ip"],
)
elif mode == "subscription":
sub = stripe.Subscription.retrieve(request_data["data"]["object"]["subscription"])
sub_metadata = sub["metadata"]
_stripe_checkout_log_action(
sub_metadata["kind"],
sub_metadata["namespace"],
sub_metadata["performer"],
sub_metadata["ip"],
metadata={"plan": sub_metadata["plan"]},
)
return make_response("Okay")
@webhooks.route("/push/<repopath:repository>/trigger/<trigger_uuid>", methods=["POST"])
@webhooks.route("/push/trigger/<trigger_uuid>", 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)
# Ensure we are not currently in read-only mode.
if app.config.get("REGISTRY_STATE", "normal") == "readonly":
abort(503, "System is currently in read-only mode")
# Ensure the trigger has permission.
namespace = trigger.repository.namespace_user.username
repository = trigger.repository.name
if ModifyRepositoryPermission(namespace, repository).can():
handler = BuildTriggerHandler.get_handler(trigger)
if trigger.repository.kind.name != "image":
abort(501, "Build triggers cannot be invoked on application repositories")
if trigger.repository.state != RepositoryState.NORMAL:
abort(503, "Repository is currently in read only or mirror mode")
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=str(ipe))
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.")
except BuildTriggerDisabledException:
logger.debug("Build trigger %s is disabled", trigger_uuid)
abort(
400,
message="This build trigger is currently disabled. Please re-enable to continue.",
)
return make_response("Okay")
abort(403)