integration_handler.go 13 KB

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