integration_handler.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617
  1. package api
  2. import (
  3. "context"
  4. "crypto/hmac"
  5. "crypto/sha256"
  6. "encoding/hex"
  7. "encoding/json"
  8. "fmt"
  9. "io/ioutil"
  10. "net/http"
  11. "net/url"
  12. "sort"
  13. "strconv"
  14. "strings"
  15. "github.com/go-chi/chi"
  16. "github.com/google/go-github/github"
  17. "github.com/porter-dev/porter/internal/forms"
  18. "github.com/porter-dev/porter/internal/oauth"
  19. "golang.org/x/oauth2"
  20. "gorm.io/gorm"
  21. "github.com/porter-dev/porter/internal/models/integrations"
  22. ints "github.com/porter-dev/porter/internal/models/integrations"
  23. )
  24. // HandleListClusterIntegrations lists the cluster integrations available to the
  25. // instance
  26. func (app *App) HandleListClusterIntegrations(w http.ResponseWriter, r *http.Request) {
  27. clusters := ints.PorterClusterIntegrations
  28. w.WriteHeader(http.StatusOK)
  29. if err := json.NewEncoder(w).Encode(&clusters); err != nil {
  30. app.handleErrorFormDecoding(err, ErrProjectDecode, w)
  31. return
  32. }
  33. }
  34. // HandleListRegistryIntegrations lists the image registry integrations available to the
  35. // instance
  36. func (app *App) HandleListRegistryIntegrations(w http.ResponseWriter, r *http.Request) {
  37. registries := ints.PorterRegistryIntegrations
  38. w.WriteHeader(http.StatusOK)
  39. if err := json.NewEncoder(w).Encode(&registries); err != nil {
  40. app.handleErrorFormDecoding(err, ErrProjectDecode, w)
  41. return
  42. }
  43. }
  44. // HandleListHelmRepoIntegrations lists the Helm repo integrations available to the
  45. // instance
  46. func (app *App) HandleListHelmRepoIntegrations(w http.ResponseWriter, r *http.Request) {
  47. hrs := ints.PorterHelmRepoIntegrations
  48. w.WriteHeader(http.StatusOK)
  49. if err := json.NewEncoder(w).Encode(&hrs); err != nil {
  50. app.handleErrorFormDecoding(err, ErrProjectDecode, w)
  51. return
  52. }
  53. }
  54. // HandleListRepoIntegrations lists the repo integrations available to the
  55. // instance
  56. func (app *App) HandleListRepoIntegrations(w http.ResponseWriter, r *http.Request) {
  57. repos := ints.PorterGitRepoIntegrations
  58. w.WriteHeader(http.StatusOK)
  59. if err := json.NewEncoder(w).Encode(&repos); err != nil {
  60. app.handleErrorFormDecoding(err, ErrProjectDecode, w)
  61. return
  62. }
  63. }
  64. // HandleCreateGCPIntegration creates a new GCP integration in the DB
  65. func (app *App) HandleCreateGCPIntegration(w http.ResponseWriter, r *http.Request) {
  66. userID, err := app.getUserIDFromRequest(r)
  67. if err != nil {
  68. http.Error(w, err.Error(), http.StatusInternalServerError)
  69. return
  70. }
  71. projID, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
  72. if err != nil || projID == 0 {
  73. app.handleErrorFormDecoding(err, ErrProjectDecode, w)
  74. return
  75. }
  76. form := &forms.CreateGCPIntegrationForm{
  77. UserID: userID,
  78. ProjectID: uint(projID),
  79. }
  80. // decode from JSON to form value
  81. if err := json.NewDecoder(r.Body).Decode(form); err != nil {
  82. app.handleErrorFormDecoding(err, ErrProjectDecode, w)
  83. return
  84. }
  85. // validate the form
  86. if err := app.validator.Struct(form); err != nil {
  87. app.handleErrorFormValidation(err, ErrProjectValidateFields, w)
  88. return
  89. }
  90. // convert the form to a gcp integration
  91. gcp, err := form.ToGCPIntegration()
  92. if err != nil {
  93. app.handleErrorFormDecoding(err, ErrProjectDecode, w)
  94. return
  95. }
  96. // handle write to the database
  97. gcp, err = app.Repo.GCPIntegration.CreateGCPIntegration(gcp)
  98. if err != nil {
  99. app.handleErrorDataWrite(err, w)
  100. return
  101. }
  102. app.Logger.Info().Msgf("New gcp integration created: %d", gcp.ID)
  103. w.WriteHeader(http.StatusCreated)
  104. gcpExt := gcp.Externalize()
  105. if err := json.NewEncoder(w).Encode(gcpExt); err != nil {
  106. app.handleErrorFormDecoding(err, ErrProjectDecode, w)
  107. return
  108. }
  109. }
  110. // HandleCreateAWSIntegration creates a new AWS integration in the DB
  111. func (app *App) HandleCreateAWSIntegration(w http.ResponseWriter, r *http.Request) {
  112. userID, err := app.getUserIDFromRequest(r)
  113. if err != nil {
  114. http.Error(w, err.Error(), http.StatusInternalServerError)
  115. return
  116. }
  117. projID, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
  118. if err != nil || projID == 0 {
  119. app.handleErrorFormDecoding(err, ErrProjectDecode, w)
  120. return
  121. }
  122. form := &forms.CreateAWSIntegrationForm{
  123. UserID: userID,
  124. ProjectID: uint(projID),
  125. }
  126. // decode from JSON to form value
  127. if err := json.NewDecoder(r.Body).Decode(form); err != nil {
  128. app.handleErrorFormDecoding(err, ErrProjectDecode, w)
  129. return
  130. }
  131. // validate the form
  132. if err := app.validator.Struct(form); err != nil {
  133. app.handleErrorFormValidation(err, ErrProjectValidateFields, w)
  134. return
  135. }
  136. // convert the form to a aws integration
  137. aws, err := form.ToAWSIntegration()
  138. if err != nil {
  139. app.handleErrorFormDecoding(err, ErrProjectDecode, w)
  140. return
  141. }
  142. // handle write to the database
  143. aws, err = app.Repo.AWSIntegration.CreateAWSIntegration(aws)
  144. if err != nil {
  145. app.handleErrorDataWrite(err, w)
  146. return
  147. }
  148. app.Logger.Info().Msgf("New aws integration created: %d", aws.ID)
  149. w.WriteHeader(http.StatusCreated)
  150. awsExt := aws.Externalize()
  151. if err := json.NewEncoder(w).Encode(awsExt); err != nil {
  152. app.handleErrorFormDecoding(err, ErrProjectDecode, w)
  153. return
  154. }
  155. }
  156. // HandleOverwriteAWSIntegration overwrites the ID of an AWS integration in the DB
  157. func (app *App) HandleOverwriteAWSIntegration(w http.ResponseWriter, r *http.Request) {
  158. userID, err := app.getUserIDFromRequest(r)
  159. if err != nil {
  160. http.Error(w, err.Error(), http.StatusInternalServerError)
  161. return
  162. }
  163. projID, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
  164. if err != nil || projID == 0 {
  165. app.handleErrorFormDecoding(err, ErrProjectDecode, w)
  166. return
  167. }
  168. awsIntegrationID, err := strconv.ParseUint(chi.URLParam(r, "aws_integration_id"), 0, 64)
  169. if err != nil || awsIntegrationID == 0 {
  170. app.handleErrorFormDecoding(err, ErrProjectDecode, w)
  171. return
  172. }
  173. form := &forms.OverwriteAWSIntegrationForm{
  174. UserID: userID,
  175. ProjectID: uint(projID),
  176. }
  177. // decode from JSON to form value
  178. if err := json.NewDecoder(r.Body).Decode(form); err != nil {
  179. app.handleErrorFormDecoding(err, ErrProjectDecode, w)
  180. return
  181. }
  182. // validate the form
  183. if err := app.validator.Struct(form); err != nil {
  184. app.handleErrorFormValidation(err, ErrProjectValidateFields, w)
  185. return
  186. }
  187. // read the aws integration by ID and overwrite the access id/secret
  188. awsIntegration, err := app.Repo.AWSIntegration.ReadAWSIntegration(uint(awsIntegrationID))
  189. if err != nil {
  190. app.handleErrorFormDecoding(err, ErrProjectDecode, w)
  191. return
  192. }
  193. awsIntegration.AWSAccessKeyID = []byte(form.AWSAccessKeyID)
  194. awsIntegration.AWSSecretAccessKey = []byte(form.AWSSecretAccessKey)
  195. // handle write to the database
  196. awsIntegration, err = app.Repo.AWSIntegration.OverwriteAWSIntegration(awsIntegration)
  197. if err != nil {
  198. app.handleErrorDataWrite(err, w)
  199. return
  200. }
  201. // clear the cluster token cache if cluster_id exists
  202. vals, err := url.ParseQuery(r.URL.RawQuery)
  203. if err != nil {
  204. app.handleErrorDataWrite(err, w)
  205. return
  206. }
  207. if len(vals["cluster_id"]) > 0 {
  208. clusterID, err := strconv.ParseUint(vals["cluster_id"][0], 10, 64)
  209. if err != nil {
  210. app.handleErrorDataWrite(err, w)
  211. return
  212. }
  213. cluster, err := app.Repo.Cluster.ReadCluster(uint(clusterID))
  214. // clear the token
  215. cluster.TokenCache.Token = []byte("")
  216. cluster, err = app.Repo.Cluster.UpdateClusterTokenCache(&cluster.TokenCache)
  217. if err != nil {
  218. app.handleErrorDataWrite(err, w)
  219. return
  220. }
  221. }
  222. app.Logger.Info().Msgf("AWS integration overwritten: %d", awsIntegration.ID)
  223. w.WriteHeader(http.StatusCreated)
  224. awsExt := awsIntegration.Externalize()
  225. if err := json.NewEncoder(w).Encode(awsExt); err != nil {
  226. app.handleErrorFormDecoding(err, ErrProjectDecode, w)
  227. return
  228. }
  229. }
  230. // HandleCreateBasicAuthIntegration creates a new basic auth integration in the DB
  231. func (app *App) HandleCreateBasicAuthIntegration(w http.ResponseWriter, r *http.Request) {
  232. userID, err := app.getUserIDFromRequest(r)
  233. if err != nil {
  234. http.Error(w, err.Error(), http.StatusInternalServerError)
  235. return
  236. }
  237. projID, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
  238. if err != nil || projID == 0 {
  239. app.handleErrorFormDecoding(err, ErrProjectDecode, w)
  240. return
  241. }
  242. form := &forms.CreateBasicAuthIntegrationForm{
  243. UserID: userID,
  244. ProjectID: uint(projID),
  245. }
  246. // decode from JSON to form value
  247. if err := json.NewDecoder(r.Body).Decode(form); err != nil {
  248. app.handleErrorFormDecoding(err, ErrProjectDecode, w)
  249. return
  250. }
  251. // validate the form
  252. if err := app.validator.Struct(form); err != nil {
  253. app.handleErrorFormValidation(err, ErrProjectValidateFields, w)
  254. return
  255. }
  256. // convert the form to a gcp integration
  257. basic, err := form.ToBasicIntegration()
  258. if err != nil {
  259. app.handleErrorFormDecoding(err, ErrProjectDecode, w)
  260. return
  261. }
  262. // handle write to the database
  263. basic, err = app.Repo.BasicIntegration.CreateBasicIntegration(basic)
  264. if err != nil {
  265. app.handleErrorDataWrite(err, w)
  266. return
  267. }
  268. app.Logger.Info().Msgf("New basic integration created: %d", basic.ID)
  269. w.WriteHeader(http.StatusCreated)
  270. basicExt := basic.Externalize()
  271. if err := json.NewEncoder(w).Encode(basicExt); err != nil {
  272. app.handleErrorFormDecoding(err, ErrProjectDecode, w)
  273. return
  274. }
  275. }
  276. // HandleListProjectOAuthIntegrations lists the oauth integrations for the project
  277. func (app *App) HandleListProjectOAuthIntegrations(w http.ResponseWriter, r *http.Request) {
  278. projID, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
  279. if err != nil || projID == 0 {
  280. app.handleErrorFormDecoding(err, ErrProjectDecode, w)
  281. return
  282. }
  283. oauthInts, err := app.Repo.OAuthIntegration.ListOAuthIntegrationsByProjectID(uint(projID))
  284. if err != nil {
  285. app.handleErrorDataRead(err, w)
  286. return
  287. }
  288. res := make([]*integrations.OAuthIntegrationExternal, 0)
  289. for _, oauthInt := range oauthInts {
  290. res = append(res, oauthInt.Externalize())
  291. }
  292. w.WriteHeader(http.StatusOK)
  293. if err := json.NewEncoder(w).Encode(res); err != nil {
  294. app.handleErrorFormDecoding(err, ErrProjectDecode, w)
  295. return
  296. }
  297. }
  298. // verifySignature verifies a signature based on hmac protocal
  299. // https://docs.github.com/en/developers/webhooks-and-events/webhooks/securing-your-webhooks
  300. func verifySignature(secret []byte, signature string, body []byte) bool {
  301. if len(signature) != 71 || !strings.HasPrefix(signature, "sha256=") {
  302. return false
  303. }
  304. actual := make([]byte, 32)
  305. hex.Decode(actual, []byte(signature[7:]))
  306. computed := hmac.New(sha256.New, secret)
  307. computed.Write(body)
  308. return hmac.Equal(computed.Sum(nil), actual)
  309. }
  310. func (app *App) HandleGithubAppEvent(w http.ResponseWriter, r *http.Request) {
  311. payload, err := ioutil.ReadAll(r.Body)
  312. if err != nil {
  313. app.handleErrorInternal(err, w)
  314. return
  315. }
  316. // verify webhook secret
  317. signature := r.Header.Get("X-Hub-Signature-256")
  318. if !verifySignature([]byte(app.GithubAppConf.WebhookSecret), signature, payload) {
  319. http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
  320. return
  321. }
  322. event, err := github.ParseWebHook(github.WebHookType(r), payload)
  323. if err != nil {
  324. app.handleErrorInternal(err, w)
  325. return
  326. }
  327. switch e := event.(type) {
  328. case *github.InstallationEvent:
  329. if *e.Action == "created" {
  330. _, err := app.Repo.GithubAppInstallation.ReadGithubAppInstallationByAccountID(*e.Installation.Account.ID)
  331. if err != nil && err == gorm.ErrRecordNotFound {
  332. // insert account/installation pair into database
  333. _, err := app.Repo.GithubAppInstallation.CreateGithubAppInstallation(&ints.GithubAppInstallation{
  334. AccountID: *e.Installation.Account.ID,
  335. InstallationID: *e.Installation.ID,
  336. })
  337. if err != nil {
  338. app.handleErrorInternal(err, w)
  339. }
  340. return
  341. } else if err != nil {
  342. app.handleErrorInternal(err, w)
  343. return
  344. }
  345. }
  346. if *e.Action == "deleted" {
  347. err := app.Repo.GithubAppInstallation.DeleteGithubAppInstallationByAccountID(*e.Installation.Account.ID)
  348. if err != nil {
  349. app.handleErrorInternal(err, w)
  350. return
  351. }
  352. }
  353. }
  354. }
  355. // HandleGithubAppAuthorize starts the oauth2 flow for a project repo request.
  356. func (app *App) HandleGithubAppAuthorize(w http.ResponseWriter, r *http.Request) {
  357. state := oauth.CreateRandomState()
  358. err := app.populateOAuthSession(w, r, state, false)
  359. if err != nil {
  360. app.handleErrorDataRead(err, w)
  361. return
  362. }
  363. // specify access type offline to get a refresh token
  364. url := app.GithubAppConf.AuthCodeURL(state, oauth2.AccessTypeOffline)
  365. http.Redirect(w, r, url, 302)
  366. }
  367. // HandleGithubAppOauthInit redirects the user to the Porter github app authorization page
  368. func (app *App) HandleGithubAppOauthInit(w http.ResponseWriter, r *http.Request) {
  369. http.Redirect(w, r, app.GithubAppConf.AuthCodeURL("", oauth2.AccessTypeOffline), 302)
  370. }
  371. // HandleGithubAppInstall redirects the user to the Porter github app installation page
  372. func (app *App) HandleGithubAppInstall(w http.ResponseWriter, r *http.Request) {
  373. http.Redirect(w, r, fmt.Sprintf("https://github.com/apps/%s/installations/new", app.GithubAppConf.AppName), 302)
  374. }
  375. // HandleListGithubAppAccessResp is the response returned by HandleListGithubAppAccess
  376. type HandleListGithubAppAccessResp struct {
  377. HasAccess bool `json:"has_access"`
  378. LoginName string `json:"username,omitempty"`
  379. Accounts []string `json:"accounts,omitempty"`
  380. }
  381. // HandleListGithubAppAccess provides basic info on if the current user is authenticated through the GitHub app
  382. // and what accounts/organizations their authentication has access to
  383. func (app *App) HandleListGithubAppAccess(w http.ResponseWriter, r *http.Request) {
  384. tok, err := app.getGithubAppOauthTokenFromRequest(r)
  385. if err != nil {
  386. res := HandleListGithubAppAccessResp{
  387. HasAccess: false,
  388. }
  389. json.NewEncoder(w).Encode(res)
  390. return
  391. }
  392. client := github.NewClient(app.GithubProjectConf.Client(oauth2.NoContext, tok))
  393. opts := &github.ListOptions{
  394. PerPage: 100,
  395. Page: 1,
  396. }
  397. res := HandleListGithubAppAccessResp{
  398. HasAccess: true,
  399. }
  400. for {
  401. orgs, pages, err := client.Organizations.List(context.Background(), "", opts)
  402. if err != nil {
  403. res := HandleListGithubAppAccessResp{
  404. HasAccess: false,
  405. }
  406. json.NewEncoder(w).Encode(res)
  407. return
  408. }
  409. for _, org := range orgs {
  410. res.Accounts = append(res.Accounts, *org.Login)
  411. }
  412. if pages.NextPage == 0 {
  413. break
  414. }
  415. }
  416. AuthUser, _, err := client.Users.Get(context.Background(), "")
  417. if err != nil {
  418. app.handleErrorInternal(err, w)
  419. return
  420. }
  421. res.LoginName = *AuthUser.Login
  422. // check if user has app installed in their account
  423. Installation, err := app.Repo.GithubAppInstallation.ReadGithubAppInstallationByAccountID(*AuthUser.ID)
  424. if err != nil && err != gorm.ErrRecordNotFound {
  425. app.handleErrorInternal(err, w)
  426. return
  427. }
  428. if Installation != nil {
  429. res.Accounts = append(res.Accounts, *AuthUser.Login)
  430. }
  431. sort.Strings(res.Accounts)
  432. json.NewEncoder(w).Encode(res)
  433. }
  434. // getGithubAppOauthTokenFromRequest gets the oauth token from the request based on the currently
  435. // logged in user. Note that this authenticates as the user, rather than the installation.
  436. func (app *App) getGithubAppOauthTokenFromRequest(r *http.Request) (*oauth2.Token, error) {
  437. userID, err := app.getUserIDFromRequest(r)
  438. if err != nil {
  439. return nil, err
  440. }
  441. user, err := app.Repo.User.ReadUser(userID)
  442. if err != nil {
  443. return nil, err
  444. }
  445. oauthInt, err := app.Repo.GithubAppOAuthIntegration.ReadGithubAppOauthIntegration(user.GithubAppIntegrationID)
  446. if err != nil {
  447. return nil, fmt.Errorf("Could not get GH app integration for user %d: %s", user.ID, err.Error())
  448. }
  449. _, _, err = oauth.GetAccessToken(oauthInt.SharedOAuthModel,
  450. &app.GithubAppConf.Config,
  451. oauth.MakeUpdateGithubAppOauthIntegrationFunction(oauthInt, *app.Repo))
  452. if err != nil {
  453. // try again, in case the token got updated
  454. oauthInt2, err := app.Repo.GithubAppOAuthIntegration.ReadGithubAppOauthIntegration(user.GithubAppIntegrationID)
  455. if err != nil {
  456. return nil, err
  457. }
  458. if oauthInt2.Expiry == oauthInt.Expiry {
  459. return nil, err
  460. } else {
  461. oauthInt.AccessToken = oauthInt2.AccessToken
  462. oauthInt.RefreshToken = oauthInt2.RefreshToken
  463. oauthInt.Expiry = oauthInt2.Expiry
  464. }
  465. }
  466. return &oauth2.Token{
  467. AccessToken: string(oauthInt.AccessToken),
  468. RefreshToken: string(oauthInt.RefreshToken),
  469. Expiry: oauthInt.Expiry,
  470. TokenType: "Bearer",
  471. }, nil
  472. }