Przeglądaj źródła

add github-app endpoints

Alexander Belanger 4 lat temu
rodzic
commit
fae49da5eb

+ 88 - 0
api/server/handlers/gitinstallation/get_accounts.go

@@ -0,0 +1,88 @@
+package gitinstallation
+
+import (
+	"context"
+	"net/http"
+	"sort"
+
+	"github.com/google/go-github/github"
+	"github.com/porter-dev/porter/api/server/authz"
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/types"
+	"golang.org/x/oauth2"
+	"gorm.io/gorm"
+)
+
+type GetGithubAppAccountsHandler struct {
+	handlers.PorterHandlerReadWriter
+	authz.KubernetesAgentGetter
+}
+
+func NewGetGithubAppAccountsHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *GetGithubAppAccountsHandler {
+	return &GetGithubAppAccountsHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+func (c *GetGithubAppAccountsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	tok, err := GetGithubAppOauthTokenFromRequest(c.Config(), r)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrForbidden(err))
+		return
+	}
+
+	client := github.NewClient(c.Config().GithubAppConf.Client(oauth2.NoContext, tok))
+	res := &types.GetGithubAppAccountsResponse{}
+
+	for {
+		orgs, pages, err := client.Organizations.List(context.Background(), "", &github.ListOptions{
+			PerPage: 100,
+			Page:    1,
+		})
+
+		if err != nil {
+			continue
+		}
+
+		for _, org := range orgs {
+			res.Accounts = append(res.Accounts, *org.Login)
+		}
+
+		if pages.NextPage == 0 {
+			break
+		}
+	}
+
+	authUser, _, err := client.Users.Get(context.Background(), "")
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	res.Username = *authUser.Login
+
+	// check if user has app installed in their account
+	installation, err := c.Repo().GithubAppInstallation().ReadGithubAppInstallationByAccountID(*authUser.ID)
+
+	if err != nil && err != gorm.ErrRecordNotFound {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	if installation != nil {
+		res.Accounts = append(res.Accounts, *authUser.Login)
+	}
+
+	sort.Strings(res.Accounts)
+
+	c.WriteResult(w, r, res)
+}

+ 25 - 0
api/server/handlers/gitinstallation/install.go

@@ -0,0 +1,25 @@
+package gitinstallation
+
+import (
+	"fmt"
+	"net/http"
+
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared/config"
+)
+
+type GithubAppInstallHandler struct {
+	handlers.PorterHandler
+}
+
+func NewGithubAppInstallHandler(
+	config *config.Config,
+) *GithubAppInstallHandler {
+	return &GithubAppInstallHandler{
+		PorterHandler: handlers.NewDefaultPorterHandler(config, nil, nil),
+	}
+}
+
+func (c *GithubAppInstallHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	http.Redirect(w, r, fmt.Sprintf("https://github.com/apps/%s/installations/new", c.Config().GithubAppConf.AppName), 302)
+}

+ 152 - 0
api/server/handlers/gitinstallation/oauth_callback.go

@@ -0,0 +1,152 @@
+package gitinstallation
+
+import (
+	"fmt"
+	"net/http"
+
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/models/integrations"
+	"golang.org/x/oauth2"
+)
+
+type GithubAppOAuthCallbackHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+func NewGithubAppOAuthCallbackHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *GithubAppOAuthCallbackHandler {
+	return &GithubAppOAuthCallbackHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+func (c *GithubAppOAuthCallbackHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	user, _ := r.Context().Value(types.UserScope).(*models.User)
+
+	session, err := c.Config().Store.Get(r, c.Config().ServerConf.CookieName)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	token, err := c.Config().GithubAppConf.Exchange(oauth2.NoContext, r.URL.Query().Get("code"))
+
+	if err != nil || !token.Valid() {
+		if session.Values["query_params"] != "" {
+			http.Redirect(w, r, fmt.Sprintf("/dashboard?%s", session.Values["query_params"]), 302)
+		} else {
+			http.Redirect(w, r, "/dashboard", 302)
+		}
+
+		return
+	}
+
+	oauthInt := &integrations.GithubAppOAuthIntegration{
+		SharedOAuthModel: integrations.SharedOAuthModel{
+			AccessToken:  []byte(token.AccessToken),
+			RefreshToken: []byte(token.RefreshToken),
+			Expiry:       token.Expiry,
+		},
+		UserID: user.ID,
+	}
+
+	oauthInt, err = c.Repo().GithubAppOAuthIntegration().CreateGithubAppOAuthIntegration(oauthInt)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	user.GithubAppIntegrationID = oauthInt.ID
+
+	user, err = c.Repo().User().UpdateUser(user)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	if session.Values["query_params"] != "" {
+		http.Redirect(w, r, fmt.Sprintf("/dashboard?%s", session.Values["query_params"]), 302)
+	} else {
+		http.Redirect(w, r, "/dashboard", 302)
+	}
+}
+
+// func (app *App) HandleGithubAppOAuthCallback(w http.ResponseWriter, r *http.Request) {
+// 	session, err := app.Store.Get(r, app.ServerConf.CookieName)
+
+// 	if err != nil {
+// 		app.handleErrorDataRead(err, w)
+// 		return
+// 	}
+
+// 	token, err := app.GithubAppConf.Exchange(oauth2.NoContext, r.URL.Query().Get("code"))
+
+// 	if err != nil || !token.Valid() {
+// 		if session.Values["query_params"] != "" {
+// 			http.Redirect(w, r, fmt.Sprintf("/dashboard?%s", session.Values["query_params"]), 302)
+// 		} else {
+// 			http.Redirect(w, r, "/dashboard", 302)
+// 		}
+// 		return
+// 	}
+
+// 	fmt.Println("exchange happaned")
+// 	fmt.Println(token.AccessToken)
+// 	fmt.Println(token.RefreshToken)
+
+// 	userID, err := app.getUserIDFromRequest(r)
+
+// 	if err != nil {
+// 		app.handleErrorInternal(err, w)
+// 		return
+// 	}
+
+// 	user, err := app.Repo.User().ReadUser(userID)
+
+// 	if err != nil {
+// 		app.handleErrorInternal(err, w)
+// 		return
+// 	}
+
+// 	oauthInt := &integrations.GithubAppOAuthIntegration{
+// 		SharedOAuthModel: integrations.SharedOAuthModel{
+// 			AccessToken:  []byte(token.AccessToken),
+// 			RefreshToken: []byte(token.RefreshToken),
+// 			Expiry:       token.Expiry,
+// 		},
+// 		UserID: user.ID,
+// 	}
+
+// 	oauthInt, err = app.Repo.GithubAppOAuthIntegration().CreateGithubAppOAuthIntegration(oauthInt)
+
+// 	if err != nil {
+// 		app.handleErrorInternal(err, w)
+// 		return
+// 	}
+
+// 	user.GithubAppIntegrationID = oauthInt.ID
+
+// 	user, err = app.Repo.User().UpdateUser(user)
+
+// 	if err != nil {
+// 		app.handleErrorInternal(err, w)
+// 		return
+// 	}
+
+// 	if session.Values["query_params"] != "" {
+// 		http.Redirect(w, r, fmt.Sprintf("/dashboard?%s", session.Values["query_params"]), 302)
+// 	} else {
+// 		http.Redirect(w, r, "/dashboard", 302)
+// 	}
+// }

+ 25 - 0
api/server/handlers/gitinstallation/oauth_start.go

@@ -0,0 +1,25 @@
+package gitinstallation
+
+import (
+	"net/http"
+
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"golang.org/x/oauth2"
+)
+
+type GithubAppOAuthStartHandler struct {
+	handlers.PorterHandler
+}
+
+func NewGithubAppOAuthStartHandler(
+	config *config.Config,
+) *GithubAppOAuthStartHandler {
+	return &GithubAppOAuthStartHandler{
+		PorterHandler: handlers.NewDefaultPorterHandler(config, nil, nil),
+	}
+}
+
+func (c *GithubAppOAuthStartHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	http.Redirect(w, r, c.Config().GithubAppConf.AuthCodeURL("", oauth2.AccessTypeOffline), 302)
+}

+ 115 - 0
api/server/handlers/gitinstallation/webhook.go

@@ -0,0 +1,115 @@
+package gitinstallation
+
+import (
+	"crypto/hmac"
+	"crypto/sha256"
+	"encoding/hex"
+	"io/ioutil"
+	"net/http"
+	"strings"
+
+	"github.com/google/go-github/github"
+	"github.com/porter-dev/porter/api/server/authz"
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"gorm.io/gorm"
+
+	ints "github.com/porter-dev/porter/internal/models/integrations"
+)
+
+type GithubAppWebhookHandler struct {
+	handlers.PorterHandlerReadWriter
+	authz.KubernetesAgentGetter
+}
+
+func NewGithubAppWebhookHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *GithubAppWebhookHandler {
+	return &GithubAppWebhookHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+func (c *GithubAppWebhookHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	payload, err := ioutil.ReadAll(r.Body)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	// verify webhook secret
+	signature := r.Header.Get("X-Hub-Signature-256")
+
+	if !verifySignature([]byte(c.Config().GithubAppConf.WebhookSecret), signature, payload) {
+		c.HandleAPIError(w, r, apierrors.NewErrForbidden(err))
+		return
+	}
+
+	event, err := github.ParseWebHook(github.WebHookType(r), payload)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	switch e := event.(type) {
+	case *github.InstallationEvent:
+		if *e.Action == "created" {
+			_, err := c.Repo().GithubAppInstallation().ReadGithubAppInstallationByAccountID(*e.Installation.Account.ID)
+
+			if err != nil && err == gorm.ErrRecordNotFound {
+				// insert account/installation pair into database
+				_, err := c.Repo().GithubAppInstallation().CreateGithubAppInstallation(&ints.GithubAppInstallation{
+					AccountID:      *e.Installation.Account.ID,
+					InstallationID: *e.Installation.ID,
+				})
+
+				if err != nil {
+					c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+				}
+
+				return
+			} else if err != nil {
+				c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+				return
+			}
+		}
+		if *e.Action == "deleted" {
+			err := c.Repo().GithubAppInstallation().DeleteGithubAppInstallationByAccountID(*e.Installation.Account.ID)
+
+			if err != nil {
+				c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+				return
+			}
+		}
+	}
+}
+
+// verifySignature verifies a signature based on hmac protocal
+// https://docs.github.com/en/developers/webhooks-and-events/webhooks/securing-your-webhooks
+func verifySignature(secret []byte, signature string, body []byte) bool {
+	if len(signature) != 71 || !strings.HasPrefix(signature, "sha256=") {
+		return false
+	}
+
+	actual := make([]byte, 32)
+	_, err := hex.Decode(actual, []byte(signature[7:]))
+
+	if err != nil {
+		return false
+	}
+
+	computed := hmac.New(sha256.New, secret)
+	_, err = computed.Write(body)
+
+	if err != nil {
+		return false
+	}
+
+	return hmac.Equal(computed.Sum(nil), actual)
+}

+ 72 - 0
api/server/router/base.go

@@ -2,6 +2,7 @@ package router
 
 import (
 	"github.com/go-chi/chi"
+	"github.com/porter-dev/porter/api/server/handlers/gitinstallation"
 	"github.com/porter-dev/porter/api/server/handlers/metadata"
 	"github.com/porter-dev/porter/api/server/handlers/release"
 	"github.com/porter-dev/porter/api/server/handlers/user"
@@ -218,5 +219,76 @@ func GetBaseRoutes(
 		Router:   r,
 	})
 
+	//  GET /api/integrations/github-app/oauth -> gitinstallation.NewGithubAppOAuthStartHandler
+	githubAppOAuthStartEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: "/integrations/github-app/oauth",
+			},
+			Scopes: []types.PermissionScope{},
+		},
+	)
+
+	githubAppOAuthStartHandler := gitinstallation.NewGithubAppOAuthStartHandler(
+		config,
+	)
+
+	routes = append(routes, &Route{
+		Endpoint: githubAppOAuthStartEndpoint,
+		Handler:  githubAppOAuthStartHandler,
+		Router:   r,
+	})
+
+	//  GET /api/integrations/github-app/install
+	githubAppInstallEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: "/integrations/github-app/install",
+			},
+			Scopes: []types.PermissionScope{},
+		},
+	)
+
+	githubAppInstallHandler := gitinstallation.NewGithubAppInstallHandler(
+		config,
+	)
+
+	routes = append(routes, &Route{
+		Endpoint: githubAppInstallEndpoint,
+		Handler:  githubAppInstallHandler,
+		Router:   r,
+	})
+
+	//  POST /api/integrations/github-app/webhook
+	githubAppWebhookEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbCreate,
+			Method: types.HTTPVerbPost,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: "/integrations/github-app/webhook",
+			},
+			Scopes: []types.PermissionScope{},
+		},
+	)
+
+	githubAppWebhookHandler := gitinstallation.NewGithubAppWebhookHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &Route{
+		Endpoint: githubAppWebhookEndpoint,
+		Handler:  githubAppWebhookHandler,
+		Router:   r,
+	})
+
 	return routes
 }

+ 51 - 0
api/server/router/user.go

@@ -4,6 +4,7 @@ import (
 	"fmt"
 
 	"github.com/go-chi/chi"
+	"github.com/porter-dev/porter/api/server/handlers/gitinstallation"
 	"github.com/porter-dev/porter/api/server/handlers/project"
 	"github.com/porter-dev/porter/api/server/handlers/template"
 	"github.com/porter-dev/porter/api/server/handlers/user"
@@ -317,5 +318,55 @@ func getUserRoutes(
 		Router:   r,
 	})
 
+	//  GET /api/oauth/github-app/callback -> gitinstallation.GithubAppOAuthCallbackHandler
+	githubAppOAuthCallbackEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: "/oauth/github-app/callback",
+			},
+			Scopes: []types.PermissionScope{types.UserScope},
+		},
+	)
+
+	githubAppOAuthCallbackHandler := gitinstallation.NewGithubAppOAuthCallbackHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &Route{
+		Endpoint: githubAppOAuthCallbackEndpoint,
+		Handler:  githubAppOAuthCallbackHandler,
+		Router:   r,
+	})
+
+	//  GET /api/integrations/github-app/accounts -> gitinstallation.NewGetGithubAppAccountsHandler
+	githubAppAccountsEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: "/integrations/github-app/callback",
+			},
+			Scopes: []types.PermissionScope{types.UserScope},
+		},
+	)
+
+	githubAppAccountsHandler := gitinstallation.NewGetGithubAppAccountsHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &Route{
+		Endpoint: githubAppAccountsEndpoint,
+		Handler:  githubAppAccountsHandler,
+		Router:   r,
+	})
+
 	return routes
 }

+ 5 - 0
api/types/git_installation.go

@@ -65,3 +65,8 @@ type GetTarballURLResponse struct {
 	URLString       string `json:"url"`
 	LatestCommitSHA string `json:"latest_commit_sha"`
 }
+
+type GetGithubAppAccountsResponse struct {
+	Username string   `json:"username,omitempty"`
+	Accounts []string `json:"accounts,omitempty"`
+}

+ 6 - 6
docs/developing/backend-refactor-status.md

@@ -7,11 +7,11 @@
 | <li>- [x] `GET /api/email/verify/finalize`                                                                                  | AB          |                 |             |                  |
 | <li>- [x] `POST /api/email/verify/initiate`                                                                                 | AB          |                 |             | yes              |
 | <li>- [ ] `GET /api/integrations/cluster`                                                                                   |             |                 |             |                  |
-| <li>- [ ] `GET /api/integrations/github-app/access`                                                                         |             |                 |             |                  |
-| <li>- [ ] `GET /api/integrations/github-app/authorize`                                                                      |             |                 |             |                  |
-| <li>- [ ] `GET /api/integrations/github-app/install`                                                                        |             |                 |             |                  |
-| <li>- [ ] `GET /api/integrations/github-app/oauth`                                                                          |             |                 |             |                  |
-| <li>- [ ] `POST /api/integrations/github-app/webhook`                                                                       |             |                 |             |                  |
+| <li>- [X] `GET /api/integrations/github-app/access`                                                                         | AB          |                 |             |                  |
+| <li>- [X] `GET /api/integrations/github-app/authorize`                                                                      | AB          |                 |             |                  |
+| <li>- [X] `GET /api/integrations/github-app/install`                                                                        | AB          |                 |             |                  |
+| <li>- [X] `GET /api/integrations/github-app/oauth`                                                                          | AB          |                 |             |                  |
+| <li>- [X] `POST /api/integrations/github-app/webhook`                                                                       | AB          |                 |             |                  |
 | <li>- [ ] `GET /api/integrations/helm`                                                                                      |             |                 |             |                  |
 | <li>- [ ] `GET /api/integrations/registry`                                                                                  |             |                 |             |                  |
 | <li>- [ ] `GET /api/integrations/repo`                                                                                      |             |                 |             |                  |
@@ -19,7 +19,7 @@
 | <li>- [x] `POST /api/login`                                                                                                 | AB          |                 |             | yes              |
 | <li>- [x] `POST /api/logout`                                                                                                | AB          |                 |             | yes              |
 | <li>- [ ] `GET /api/oauth/digitalocean/callback`                                                                            |             |                 |             |                  |
-| <li>- [ ] `GET /api/oauth/github-app/callback`                                                                              |             |                 |             |                  |
+| <li>- [ ] `GET /api/oauth/github-app/callback`                                                                              | AB          |                 |             |                  |
 | <li>- [ ] `GET /api/oauth/github/callback`                                                                                  |             |                 |             |                  |
 | <li>- [ ] `GET /api/oauth/google/callback`                                                                                  |             |                 |             |                  |
 | <li>- [ ] `GET /api/oauth/login/github`                                                                                     |             |                 |             |                  |