Skip to content

[usage] Add a placeholder Stripe webhook #11776

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 6 commits into from
Closed

Conversation

andrew-farries
Copy link
Contributor

@andrew-farries andrew-farries commented Aug 1, 2022

Description

Add a placeholder Stripe webhook to the usage component.

In later PRs, the webhook will receive invoice.finalized events from Stripe so that we can update instance usage records to show that they have been included in a particular invoice.

Related Issue(s)

Part of #9036 and #10937

How to test

  • Install the stripe CLI into the workspace and run stripe login.
  • Port forward to the database in the preview environment:
kubectl port-forward svc/mysql 3306:3306
  • Run the usage component locally:
DB_USERNAME=gitpod DB_HOST=localhost DB_PORT=3306 DB_PASSWORD=<>  go run . run  
  • Forward stripe events to the local webhook endpoint:
stripe listen --skip-verify --forward-to localhost:9002/webhook
  • Trigger a series of Stripe events:
stripe trigger invoice.finalized

This should start a series of Stripe events (setting up a customer and payment methods), each of which should be handled with a 200 OK by the webhook:

image

Release Notes

NONE

Documentation

Werft options:

  • /werft with-preview

@andrew-farries andrew-farries requested a review from a team August 1, 2022 14:52
@github-actions github-actions bot added the team: webapp Issue belongs to the WebApp team label Aug 1, 2022
@werft-gitpod-dev-com
Copy link

started the job as gitpod-build-af-stripe-webhook.7 because the annotations in the pull request description changed
(with .werft/ from main)

@andrew-farries
Copy link
Contributor Author

I've put the webhook on the usage component which means we will need to set up ingress (via proxy) to allow the usage component to be reachable by Stripe.

An alternative, would be to put the hook on server as we already have other webhook handling there. usage feels like the right place for this though.

Andrew Farries added 2 commits August 1, 2022 14:59
Register the handler with the underlying baseserver.
@@ -133,3 +136,7 @@ func registerGRPCServices(srv *baseserver.Server, conn *gorm.DB, stripeClient *s
}
return nil
}

func registerHttpHandlers(srv *baseserver.Server, h *stripe.WebhookHandler) {
srv.HTTPMux().HandleFunc("/webhook", h.Handle)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd recommend prefixing this with something like /stripe/invoices/webhook. This makes it easier to debug when you see just a request HTTP path without knowing what it does.

Copy link
Contributor Author

@andrew-farries andrew-farries Aug 2, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done: b0d3d89

}

func (h *WebhookHandler) Handle(w http.ResponseWriter, req *http.Request) {
const maxBodyBytes = int64(65536)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe there' a middleware for this

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I couldn't find any middleware that does exactly this. Did you have something in mind?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found one in the echo library but I think it's not worth importing.

https://echo.labstack.com/middleware/body-limit/

Comment on lines 27 to 39
payload, err := ioutil.ReadAll(req.Body)
if err != nil {
log.WithError(err).Error("Stripe webhook error when reading request body")
w.WriteHeader(http.StatusServiceUnavailable)
return
}

event := stripe.Event{}
if err := json.Unmarshal(payload, &event); err != nil {
log.WithError(err).Error("Stripe webhook error while parsing event payload")
w.WriteHeader(http.StatusBadRequest)
return
}
Copy link
Member

@easyCZ easyCZ Aug 1, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// pseudo from memory
dec := json.Unmarshaller(http.Body)
if err := dec.Unmarshal(&event); err != nil { ... }

This should work. You shouldn't need to read all of the body into memory first and only then start unmarshalling. By using the above, you can stream unmarshal and avoid some buffering.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return &WebhookHandler{}
}

func (h *WebhookHandler) Handle(w http.ResponseWriter, req *http.Request) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also worth wrapping in middleware which rejects non application/json content type.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@easyCZ
Copy link
Member

easyCZ commented Aug 1, 2022

I've put the webhook on the usage component which means we will need to set up ingress (via proxy) to allow the usage component to be reachable by Stripe.

I'd avoid this. We don't have AuthN on the usage component so it shouldn't be directly exposed to the internet.

An alternative, would be to put the hook on server as we already have other webhook handling there. usage feels like the right place for this though.

Do this, but proxy from Server to usage - server becomes the authN ingress, but you can keep logic in usage.

@easyCZ
Copy link
Member

easyCZ commented Aug 1, 2022

Thinking about this, I'd actually implement the Usage part as a gRPC method which the server would delegate. I think in practice all we would need is something like BillingService.FinalizeInvoice(..)

@andrew-farries
Copy link
Contributor Author

I've put the webhook on the usage component which means we will need to set up ingress (via proxy) to allow the usage component to be reachable by Stripe.

I'd avoid this. We don't have AuthN on the usage component so it shouldn't be directly exposed to the internet.

Without AuthN on the usage gRPC methods, we could still expose just the webhook though right?

My preference for putting the webhook on the usage component comes simply from not wanting to load yet more functionality onto server.

Andrew Farries added 4 commits August 2, 2022 05:50
Give it a more descriptive name.
To ensure requests have `application/json` `Content-Type` header set.
@easyCZ
Copy link
Member

easyCZ commented Aug 2, 2022

Without AuthN on the usage gRPC methods, we could still expose just the webhook though right?

That's fair, my worry is the added complexity where you now have to reason through some parts of it being public, while others not. It increases the threat surface for the system but worse it causes extra cognitive load on the engineers. The current (unfavourable) case where only server is exposed at least keeps the assumptions constant

Copy link
Member

@easyCZ easyCZ left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm okay to go with this for now, to unblock you on other PRs but I'd like to understand how others feel about this congitive overhead as they will be the ones on-call for this.

/hold

@andrew-farries
Copy link
Contributor Author

andrew-farries commented Aug 2, 2022

Closing, in favour of #11806 now that we've agreed that the public-api is a better home for the webhook.

@andrew-farries andrew-farries deleted the af/stripe-webhook branch August 2, 2022 12:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants