integration_handler.go 14 KB

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