Pārlūkot izejas kodu

add gitlab app oauth

Mohammed Nafees 4 gadi atpakaļ
vecāks
revīzija
a10b9cb6a0

+ 31 - 2
api/server/handlers/handler.go

@@ -17,7 +17,14 @@ type PorterHandler interface {
 	Repo() repository.Repository
 	HandleAPIError(w http.ResponseWriter, r *http.Request, err apierrors.RequestError)
 	HandleAPIErrorNoWrite(w http.ResponseWriter, r *http.Request, err apierrors.RequestError)
-	PopulateOAuthSession(w http.ResponseWriter, r *http.Request, state string, isProject bool) error
+	PopulateOAuthSession(
+		w http.ResponseWriter,
+		r *http.Request,
+		state string,
+		isUser, isProject bool,
+		integrationClient types.OAuthIntegrationClient,
+		integrationID uint,
+	) error
 }
 
 type PorterHandlerWriter interface {
@@ -81,7 +88,14 @@ func IgnoreAPIError(w http.ResponseWriter, r *http.Request, err apierrors.Reques
 	return
 }
 
-func (d *DefaultPorterHandler) PopulateOAuthSession(w http.ResponseWriter, r *http.Request, state string, isProject bool) error {
+func (d *DefaultPorterHandler) PopulateOAuthSession(
+	w http.ResponseWriter,
+	r *http.Request,
+	state string,
+	isProject, isUser bool,
+	integrationClient types.OAuthIntegrationClient,
+	integrationID uint,
+) error {
 	session, err := d.Config().Store.Get(r, d.Config().ServerConf.CookieName)
 
 	if err != nil {
@@ -106,6 +120,21 @@ func (d *DefaultPorterHandler) PopulateOAuthSession(w http.ResponseWriter, r *ht
 		session.Values["project_id"] = project.ID
 	}
 
+	if isUser {
+		user, _ := r.Context().Value(types.UserScope).(*models.User)
+
+		if user == nil {
+			return fmt.Errorf("could not read user")
+		}
+
+		session.Values["user_id"] = user.ID
+	}
+
+	if integrationID != 0 && len(integrationClient) > 0 {
+		session.Values["integration_id"] = integrationID
+		session.Values["integration_client"] = integrationClient
+	}
+
 	if err := session.Save(r, w); err != nil {
 		return err
 	}

+ 111 - 0
api/server/handlers/oauth_callback/gitlab.go

@@ -0,0 +1,111 @@
+package oauth_callback
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"net/http"
+	"net/url"
+
+	"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/internal/models/integrations"
+	"gorm.io/gorm"
+)
+
+type OAuthCallbackGitlabHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+func NewOAuthCallbackGitlabHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *OAuthCallbackGitlabHandler {
+	return &OAuthCallbackGitlabHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+func (p *OAuthCallbackGitlabHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	session, err := p.Config().Store.Get(r, p.Config().ServerConf.CookieName)
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	if _, ok := session.Values["state"]; !ok {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	if r.URL.Query().Get("state") != session.Values["state"] {
+		p.HandleAPIError(w, r, apierrors.NewErrForbidden(err))
+		return
+	}
+
+	token, err := p.Config().DOConf.Exchange(context.Background(), r.URL.Query().Get("code"))
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrForbidden(err))
+		return
+	}
+
+	if !token.Valid() {
+		p.HandleAPIError(w, r, apierrors.NewErrForbidden(fmt.Errorf("invalid token")))
+		return
+	}
+
+	userID, _ := session.Values["user_id"].(uint)
+	projID, _ := session.Values["project_id"].(uint)
+	integrationID := session.Values["integration_id"].(uint)
+
+	_, err = p.Repo().GitlabIntegration().ReadGitlabIntegration(projID, integrationID)
+
+	if err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			p.HandleAPIError(w, r, apierrors.NewErrForbidden(
+				fmt.Errorf("gitlab integration with id %d not found in project %d",
+					integrationID, projID),
+			))
+			return
+		}
+
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	oauthInt := &integrations.GitlabAppOAuthIntegration{
+		SharedOAuthModel: integrations.SharedOAuthModel{
+			AccessToken:  []byte(token.AccessToken),
+			RefreshToken: []byte(token.RefreshToken),
+		},
+		UserID:        userID,
+		ProjectID:     projID,
+		IntegrationID: integrationID,
+	}
+
+	// create the oauth integration first
+	_, err = p.Repo().GitlabAppOAuthIntegration().CreateGitlabAppOAuthIntegration(oauthInt)
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	if redirectStr, ok := session.Values["redirect_uri"].(string); ok && redirectStr != "" {
+		// attempt to parse the redirect uri, if it fails just redirect to dashboard
+		redirectURI, err := url.Parse(redirectStr)
+
+		if err != nil {
+			http.Redirect(w, r, "/dashboard", http.StatusFound)
+		}
+
+		http.Redirect(w, r, fmt.Sprintf("%s?%s", redirectURI.Path, redirectURI.RawQuery), http.StatusFound)
+	} else {
+		http.Redirect(w, r, "/dashboard", http.StatusFound)
+	}
+}

+ 1 - 1
api/server/handlers/project_oauth/digitalocean.go

@@ -29,7 +29,7 @@ func NewProjectOAuthDOHandler(
 func (p *ProjectOAuthDOHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	state := oauth.CreateRandomState()
 
-	if err := p.PopulateOAuthSession(w, r, state, true); err != nil {
+	if err := p.PopulateOAuthSession(w, r, state, false, true, "", 0); err != nil {
 		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 	}

+ 71 - 0
api/server/handlers/project_oauth/gitlab.go

@@ -0,0 +1,71 @@
+package project_oauth
+
+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/commonutils"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/server/shared/requestutils"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/oauth"
+	"golang.org/x/oauth2"
+	"gorm.io/gorm"
+)
+
+type ProjectOAuthGitlabHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+func NewProjectOAuthGitlabHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *ProjectOAuthGitlabHandler {
+	return &ProjectOAuthGitlabHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+func (p *ProjectOAuthGitlabHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+
+	integrationID, reqErr := requestutils.GetURLParamUint(r, "integration_id")
+
+	if reqErr != nil {
+		p.HandleAPIError(w, r, reqErr)
+		return
+	}
+
+	giIntegration, err := p.Repo().GitlabIntegration().ReadGitlabIntegration(proj.ID, integrationID)
+
+	if err != nil {
+		if err == gorm.ErrRecordNotFound {
+			p.HandleAPIError(w, r, apierrors.NewErrForbidden(
+				fmt.Errorf("gitlab integration with id %d not found in project %d", integrationID, proj.ID),
+			))
+		} else {
+			p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		}
+
+		return
+	}
+
+	state := oauth.CreateRandomState()
+
+	if err := p.PopulateOAuthSession(w, r, state, true, true, types.OAuthGitlab, integrationID); err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	gitlabConf := commonutils.GetGitlabOAuthConf(p.Config(), giIntegration)
+
+	// specify access type offline to get a refresh token
+	url := gitlabConf.AuthCodeURL(state, oauth2.AccessTypeOffline)
+
+	http.Redirect(w, r, url, http.StatusFound)
+}

+ 1 - 1
api/server/handlers/project_oauth/slack.go

@@ -29,7 +29,7 @@ func NewProjectOAuthSlackHandler(
 func (p *ProjectOAuthSlackHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	state := oauth.CreateRandomState()
 
-	if err := p.PopulateOAuthSession(w, r, state, true); err != nil {
+	if err := p.PopulateOAuthSession(w, r, state, false, true, "", 0); err != nil {
 		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 	}

+ 1 - 1
api/server/handlers/user/github_start.go

@@ -29,7 +29,7 @@ func NewUserOAuthGithubHandler(
 func (p *UserOAuthGithubHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	state := oauth.CreateRandomState()
 
-	if err := p.PopulateOAuthSession(w, r, state, false); err != nil {
+	if err := p.PopulateOAuthSession(w, r, state, false, false, "", 0); err != nil {
 		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 	}

+ 1 - 1
api/server/handlers/user/google_start.go

@@ -29,7 +29,7 @@ func NewUserOAuthGoogleHandler(
 func (p *UserOAuthGoogleHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	state := oauth.CreateRandomState()
 
-	if err := p.PopulateOAuthSession(w, r, state, false); err != nil {
+	if err := p.PopulateOAuthSession(w, r, state, false, false, "", 0); err != nil {
 		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 	}

+ 24 - 0
api/server/router/oauth_callback.go

@@ -74,5 +74,29 @@ func GetOAuthCallbackRoutes(
 		Router:   r,
 	})
 
+	// GET /api/oauth/gitlab/callback -> oauth_callback.NewOAuthCallbackGitlabHandler
+	gitlabEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/gitlab/callback",
+			},
+		},
+	)
+
+	gitlabHandler := oauth_callback.NewOAuthCallbackGitlabHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &Route{
+		Endpoint: gitlabEndpoint,
+		Handler:  gitlabHandler,
+		Router:   r,
+	})
+
 	return routes
 }

+ 28 - 0
api/server/router/project_oauth.go

@@ -109,5 +109,33 @@ func getProjectOAuthRoutes(
 		Router:   r,
 	})
 
+	// GET /api/projects/{project_id}/oauth/gitlab -> project_integration.NewProjectOAuthGitlabHandler
+	gitlabEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/gitlab",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+			},
+		},
+	)
+
+	gitlabHandler := project_oauth.NewProjectOAuthGitlabHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &Route{
+		Endpoint: gitlabEndpoint,
+		Handler:  gitlabHandler,
+		Router:   r,
+	})
+
 	return routes, newPath
 }

+ 20 - 0
api/server/shared/commonutils/gitlab.go

@@ -0,0 +1,20 @@
+package commonutils
+
+import (
+	"github.com/porter-dev/porter/api/server/shared/config"
+	ints "github.com/porter-dev/porter/internal/models/integrations"
+	"golang.org/x/oauth2"
+)
+
+func GetGitlabOAuthConf(conf *config.Config, giIntegration *ints.GitlabIntegration) *oauth2.Config {
+	return &oauth2.Config{
+		ClientID:     string(giIntegration.AppClientID),
+		ClientSecret: string(giIntegration.AppClientSecret),
+		Endpoint: oauth2.Endpoint{
+			AuthURL:  giIntegration.InstanceURL + "/oauth/authorize",
+			TokenURL: giIntegration.InstanceURL + "/oauth/token",
+		},
+		RedirectURL: conf.ServerConf.ServerURL + "/api/oauth/gitlab/callback",
+		Scopes:      []string{"api"},
+	}
+}

+ 1 - 0
api/types/project_integration.go

@@ -7,6 +7,7 @@ const (
 	OAuthGithub       OAuthIntegrationClient = "github"
 	OAuthDigitalOcean OAuthIntegrationClient = "do"
 	OAuthGoogle       OAuthIntegrationClient = "google"
+	OAuthGitlab       OAuthIntegrationClient = "gitlab"
 )
 
 // OAuthIntegrationClient is the name of an OAuth mechanism client

+ 5 - 1
internal/models/integrations/gitlab.go

@@ -2,7 +2,7 @@ package integrations
 
 import "gorm.io/gorm"
 
-// GitlabIntegration takes care of Gitlab related auth mechanisms and data
+// GitlabIntegration takes care of Gitlab app related data
 type GitlabIntegration struct {
 	gorm.Model
 
@@ -12,6 +12,10 @@ type GitlabIntegration struct {
 	// URL of the Gitlab instance to talk to
 	InstanceURL string `json:"instance_url"`
 
+	// ------------------------------------------------------------------
+	// All fields encrypted before storage.
+	// ------------------------------------------------------------------
+
 	// Gitlab instance-wide app's client ID
 	AppClientID []byte `json:"app_client_id"`
 

+ 26 - 10
internal/models/integrations/oauth.go

@@ -72,16 +72,6 @@ func (g *OAuthIntegration) PopulateTargetMetadata() {
 	}
 }
 
-// GithubAppOAuthIntegration is the model used for storing github app oauth data
-// Unlike the above, this model is tied to a specific user, not a project
-type GithubAppOAuthIntegration struct {
-	gorm.Model
-	SharedOAuthModel
-
-	// The id of the user that linked this auth mechanism
-	UserID uint `json:"user_id"`
-}
-
 // ToOAuthIntegrationType generates an external OAuthIntegration to be shared over REST
 func (o *OAuthIntegration) ToOAuthIntegrationType() *types.OAuthIntegration {
 	return &types.OAuthIntegration{
@@ -94,3 +84,29 @@ func (o *OAuthIntegration) ToOAuthIntegrationType() *types.OAuthIntegration {
 		TargetName:  o.TargetName,
 	}
 }
+
+// GithubAppOAuthIntegration is the model used for storing github app oauth data
+// Unlike the above, this model is tied to a specific user, not a project
+type GithubAppOAuthIntegration struct {
+	gorm.Model
+	SharedOAuthModel
+
+	// The id of the user that linked this auth mechanism
+	UserID uint `json:"user_id"`
+}
+
+// GitlabAppOAuthIntegration is the model used for storing gitlab app oauth data
+// Unlike the above, this model is tied to a specific user, not a project
+type GitlabAppOAuthIntegration struct {
+	gorm.Model
+	SharedOAuthModel
+
+	// The id of the user that linked with this auth mechanism
+	UserID uint `json:"user_id"`
+
+	// The id of the project that linked with this auth mechanism
+	ProjectID uint `json:"project_id"`
+
+	// The id of the gitlab integration linked with this auth mechanism
+	IntegrationID uint `json:"integration_id"`
+}

+ 26 - 0
internal/repository/gorm/auth.go

@@ -1730,3 +1730,29 @@ func (repo *GitlabIntegrationRepository) DecryptGitlabIntegrationData(
 
 	return nil
 }
+
+// GitlabAppOAuthIntegrationRepository uses gorm.DB for querying the database
+type GitlabAppOAuthIntegrationRepository struct {
+	db             *gorm.DB
+	key            *[32]byte
+	storageBackend credentials.CredentialStorage
+}
+
+// NewGitlabAppOAuthIntegrationRepository returns a GitlabAppOAuthIntegrationRepository which uses
+// gorm.DB for querying the database
+func NewGitlabAppOAuthIntegrationRepository(
+	db *gorm.DB,
+	key *[32]byte,
+	storageBackend credentials.CredentialStorage,
+) repository.GitlabAppOAuthIntegrationRepository {
+	return &GitlabAppOAuthIntegrationRepository{db, key, storageBackend}
+}
+
+func (repo *GitlabAppOAuthIntegrationRepository) CreateGitlabAppOAuthIntegration(
+	gi *ints.GitlabAppOAuthIntegration,
+) (*ints.GitlabAppOAuthIntegration, error) {
+	if err := repo.db.Create(gi).Error; err != nil {
+		return nil, err
+	}
+	return gi, nil
+}

+ 6 - 0
internal/repository/gorm/repository.go

@@ -34,6 +34,7 @@ type GormRepository struct {
 	githubAppOAuthIntegration repository.GithubAppOAuthIntegrationRepository
 	slackIntegration          repository.SlackIntegrationRepository
 	gitlabIntegration         repository.GitlabIntegrationRepository
+	gitlabAppOAuthIntegration repository.GitlabAppOAuthIntegrationRepository
 	notificationConfig        repository.NotificationConfigRepository
 	jobNotificationConfig     repository.JobNotificationConfigRepository
 	buildEvent                repository.BuildEventRepository
@@ -154,6 +155,10 @@ func (t *GormRepository) GitlabIntegration() repository.GitlabIntegrationReposit
 	return t.gitlabIntegration
 }
 
+func (t *GormRepository) GitlabAppOAuthIntegration() repository.GitlabAppOAuthIntegrationRepository {
+	return t.gitlabAppOAuthIntegration
+}
+
 func (t *GormRepository) NotificationConfig() repository.NotificationConfigRepository {
 	return t.notificationConfig
 }
@@ -225,6 +230,7 @@ func NewRepository(db *gorm.DB, key *[32]byte, storageBackend credentials.Creden
 		githubAppOAuthIntegration: NewGithubAppOAuthIntegrationRepository(db),
 		slackIntegration:          NewSlackIntegrationRepository(db, key),
 		gitlabIntegration:         NewGitlabIntegrationRepository(db, key, storageBackend),
+		gitlabAppOAuthIntegration: NewGitlabAppOAuthIntegrationRepository(db, key, storageBackend),
 		notificationConfig:        NewNotificationConfigRepository(db),
 		jobNotificationConfig:     NewJobNotificationConfigRepository(db),
 		buildEvent:                NewBuildEventRepository(db),

+ 6 - 1
internal/repository/integrations.go

@@ -89,7 +89,12 @@ type GithubAppInstallationRepository interface {
 
 // GitlabIntegrationRepository represents the set of queries on the GitlabIntegration model
 type GitlabIntegrationRepository interface {
-	CreateGitlabIntegration(user *ints.GitlabIntegration) (*ints.GitlabIntegration, error)
+	CreateGitlabIntegration(gi *ints.GitlabIntegration) (*ints.GitlabIntegration, error)
 	ReadGitlabIntegration(projectID, id uint) (*ints.GitlabIntegration, error)
 	ListGitlabIntegrationsByProjectID(projectID uint) ([]*ints.GitlabIntegration, error)
 }
+
+// GitlabAppOAuthIntegrationRepository represents the set of queries on the GitlabOAuthIntegration model
+type GitlabAppOAuthIntegrationRepository interface {
+	CreateGitlabAppOAuthIntegration(gi *ints.GitlabAppOAuthIntegration) (*ints.GitlabAppOAuthIntegration, error)
+}

+ 2 - 0
internal/repository/repository.go

@@ -27,6 +27,8 @@ type Repository interface {
 	GithubAppInstallation() GithubAppInstallationRepository
 	GithubAppOAuthIntegration() GithubAppOAuthIntegrationRepository
 	SlackIntegration() SlackIntegrationRepository
+	GitlabIntegration() GitlabIntegrationRepository
+	GitlabAppOAuthIntegration() GitlabAppOAuthIntegrationRepository
 	NotificationConfig() NotificationConfigRepository
 	JobNotificationConfig() JobNotificationConfigRepository
 	BuildEvent() BuildEventRepository