mirror of
https://github.com/quay/quay.git
synced 2025-04-19 21:42:17 +03:00
* chore: drop deprecated tables and remove unused code * isort imports * migration: check for table existence before drop
208 lines
8.2 KiB
Python
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)
|