integration_handler.go 13 KB

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