upstash.go 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. package oauth_callback
  2. import (
  3. "bytes"
  4. "context"
  5. "encoding/json"
  6. "fmt"
  7. "io"
  8. "net/http"
  9. "net/url"
  10. "time"
  11. "github.com/porter-dev/porter/api/server/handlers"
  12. "github.com/porter-dev/porter/api/server/shared"
  13. "github.com/porter-dev/porter/api/server/shared/apierrors"
  14. "github.com/porter-dev/porter/api/server/shared/config"
  15. "github.com/porter-dev/porter/internal/models/integrations"
  16. "github.com/porter-dev/porter/internal/telemetry"
  17. )
  18. // OAuthCallbackUpstashHandler is the handler responding to the upstash oauth callback
  19. type OAuthCallbackUpstashHandler struct {
  20. handlers.PorterHandlerReadWriter
  21. }
  22. // UpstashApiKeyEndpoint is the endpoint to fetch the upstash developer api key
  23. // nolint:gosec // Not a security key
  24. const UpstashApiKeyEndpoint = "https://api.upstash.com/apikey"
  25. // NewOAuthCallbackUpstashHandler generates a new OAuthCallbackUpstashHandler
  26. func NewOAuthCallbackUpstashHandler(
  27. config *config.Config,
  28. decoderValidator shared.RequestDecoderValidator,
  29. writer shared.ResultWriter,
  30. ) *OAuthCallbackUpstashHandler {
  31. return &OAuthCallbackUpstashHandler{
  32. PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
  33. }
  34. }
  35. // ServeHTTP gets the upstash oauth token from the callback code, uses it to create a developer api token, then creates a new upstash integration
  36. func (p *OAuthCallbackUpstashHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  37. ctx, span := telemetry.NewSpan(r.Context(), "serve-oauth-callback-upstash")
  38. defer span.End()
  39. r = r.Clone(ctx)
  40. session, err := p.Config().Store.Get(r, p.Config().ServerConf.CookieName)
  41. if err != nil {
  42. err = telemetry.Error(ctx, span, err, "session could not be retrieved")
  43. p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  44. return
  45. }
  46. if _, ok := session.Values["state"]; !ok {
  47. err = telemetry.Error(ctx, span, nil, "state not found in session")
  48. p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  49. return
  50. }
  51. if r.URL.Query().Get("state") != session.Values["state"] {
  52. err = telemetry.Error(ctx, span, nil, "state does not match")
  53. p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  54. return
  55. }
  56. projID, ok := session.Values["project_id"].(uint)
  57. if !ok {
  58. err = telemetry.Error(ctx, span, nil, "project id not found in session")
  59. p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  60. return
  61. }
  62. telemetry.WithAttributes(span,
  63. telemetry.AttributeKV{Key: "project-id", Value: projID},
  64. )
  65. if projID == 0 {
  66. err = telemetry.Error(ctx, span, nil, "project id not found in session")
  67. p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  68. return
  69. }
  70. code := r.URL.Query().Get("code")
  71. if code == "" {
  72. err = telemetry.Error(ctx, span, nil, "code not found in query params")
  73. p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusForbidden))
  74. return
  75. }
  76. token, err := p.Config().UpstashConf.Exchange(ctx, code)
  77. if err != nil {
  78. err = telemetry.Error(ctx, span, err, "exchange failed")
  79. p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusForbidden))
  80. return
  81. }
  82. if !token.Valid() {
  83. err = telemetry.Error(ctx, span, nil, "invalid token")
  84. p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusForbidden))
  85. return
  86. }
  87. // make an http call to https://api.upstash.com/apikey with authorization: bearer <access_token>
  88. // to get the api key
  89. apiKey, err := fetchUpstashApiKey(ctx, token.AccessToken)
  90. if err != nil {
  91. err = telemetry.Error(ctx, span, err, "error fetching upstash api key")
  92. p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  93. return
  94. }
  95. oauthInt := integrations.UpstashIntegration{
  96. SharedOAuthModel: integrations.SharedOAuthModel{
  97. AccessToken: []byte(token.AccessToken),
  98. RefreshToken: []byte(token.RefreshToken),
  99. Expiry: token.Expiry,
  100. },
  101. ProjectID: projID,
  102. DeveloperApiKey: []byte(apiKey),
  103. }
  104. _, err = p.Repo().UpstashIntegration().Insert(ctx, oauthInt)
  105. if err != nil {
  106. err = telemetry.Error(ctx, span, err, "error creating oauth integration")
  107. p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  108. return
  109. }
  110. redirect := "/dashboard"
  111. if redirectStr, ok := session.Values["redirect_uri"].(string); ok && redirectStr != "" {
  112. redirectURI, err := url.Parse(redirectStr)
  113. if err == nil {
  114. redirect = fmt.Sprintf("%s?%s", redirectURI.Path, redirectURI.RawQuery)
  115. }
  116. }
  117. http.Redirect(w, r, redirect, http.StatusFound)
  118. }
  119. // UpstashApiKeyRequest is the request body to fetch the upstash developer api key
  120. type UpstashApiKeyRequest struct {
  121. Name string `json:"name"`
  122. }
  123. // UpstashApiKeyResponse is the response body to fetch the upstash developer api key
  124. type UpstashApiKeyResponse struct {
  125. ApiKey string `json:"api_key"`
  126. }
  127. func fetchUpstashApiKey(ctx context.Context, accessToken string) (string, error) {
  128. ctx, span := telemetry.NewSpan(ctx, "fetch-upstash-api-key")
  129. defer span.End()
  130. data := UpstashApiKeyRequest{
  131. Name: fmt.Sprintf("PORTER_API_KEY_%d", time.Now().Unix()),
  132. }
  133. jsonData, err := json.Marshal(data)
  134. if err != nil {
  135. return "", telemetry.Error(ctx, span, err, "error marshalling request body")
  136. }
  137. req, err := http.NewRequestWithContext(ctx, http.MethodPost, UpstashApiKeyEndpoint, bytes.NewBuffer(jsonData))
  138. if err != nil {
  139. return "", telemetry.Error(ctx, span, err, "error creating request")
  140. }
  141. // Set the Authorization header
  142. req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", accessToken))
  143. req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
  144. resp, err := http.DefaultClient.Do(req)
  145. if err != nil {
  146. return "", telemetry.Error(ctx, span, err, "error sending request")
  147. }
  148. defer resp.Body.Close() // nolint: errcheck
  149. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "status-code", Value: resp.StatusCode})
  150. if resp.StatusCode != http.StatusOK {
  151. body, err := io.ReadAll(resp.Body)
  152. if err != nil {
  153. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "read-response-body-error", Value: err.Error()})
  154. }
  155. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "response-body", Value: string(body)})
  156. return "", telemetry.Error(ctx, span, nil, "unexpected status code")
  157. }
  158. body, err := io.ReadAll(resp.Body)
  159. if err != nil {
  160. return "", telemetry.Error(ctx, span, err, "error reading response body")
  161. }
  162. var responseData UpstashApiKeyResponse
  163. err = json.Unmarshal(body, &responseData)
  164. if err != nil {
  165. return "", telemetry.Error(ctx, span, err, "error unmarshalling response body")
  166. }
  167. return responseData.ApiKey, nil
  168. }