upstash.go 7.4 KB

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