Alexander Belanger před 5 roky
rodič
revize
2025e90f84

+ 3 - 0
internal/config/config.go

@@ -33,6 +33,9 @@ type ServerConf struct {
 
 	GithubClientID     string `env:"GITHUB_CLIENT_ID"`
 	GithubClientSecret string `env:"GITHUB_CLIENT_SECRET"`
+
+	DOClientID     string `env:"DO_CLIENT_ID"`
+	DOClientSecret string `env:"DO_CLIENT_SECRET"`
 }
 
 // DBConf is the database configuration: if generated from environment variables,

+ 9 - 6
internal/helm/config.go

@@ -8,6 +8,7 @@ import (
 	"github.com/porter-dev/porter/internal/logger"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/repository"
+	"golang.org/x/oauth2"
 	"helm.sh/helm/v3/pkg/action"
 	"helm.sh/helm/v3/pkg/chartutil"
 	"helm.sh/helm/v3/pkg/kube"
@@ -19,10 +20,11 @@ import (
 // Form represents the options for connecting to a cluster and
 // creating a Helm agent
 type Form struct {
-	Cluster   *models.Cluster `form:"required"`
-	Repo      *repository.Repository
-	Storage   string `json:"storage" form:"oneof=secret configmap memory" default:"secret"`
-	Namespace string `json:"namespace"`
+	Cluster           *models.Cluster `form:"required"`
+	Repo              *repository.Repository
+	DigitalOceanOAuth *oauth2.Config
+	Storage           string `json:"storage" form:"oneof=secret configmap memory" default:"secret"`
+	Namespace         string `json:"namespace"`
 }
 
 // GetAgentOutOfClusterConfig creates a new Agent from outside the cluster using
@@ -30,8 +32,9 @@ type Form struct {
 func GetAgentOutOfClusterConfig(form *Form, l *logger.Logger) (*Agent, error) {
 	// create a kubernetes agent
 	conf := &kubernetes.OutOfClusterConfig{
-		Cluster: form.Cluster,
-		Repo:    form.Repo,
+		Cluster:           form.Cluster,
+		Repo:              form.Repo,
+		DigitalOceanOAuth: form.DigitalOceanOAuth,
 	}
 
 	k8sAgent, err := kubernetes.GetAgentOutOfClusterConfig(conf)

+ 22 - 0
internal/kubernetes/config.go

@@ -8,7 +8,9 @@ import (
 	"time"
 
 	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/oauth"
 	"github.com/porter-dev/porter/internal/repository"
+	"golang.org/x/oauth2"
 	"k8s.io/apimachinery/pkg/api/meta"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/cli-runtime/pkg/genericclioptions"
@@ -88,6 +90,9 @@ func GetAgentTesting(objects ...runtime.Object) *Agent {
 type OutOfClusterConfig struct {
 	Cluster *models.Cluster
 	Repo    *repository.Repository
+
+	// Only required if using DigitalOcean OAuth as an auth mechanism
+	DigitalOceanOAuth *oauth2.Config
 }
 
 // ToRESTConfig creates a kubernetes REST client factory -- it calls ClientConfig on
@@ -291,6 +296,23 @@ func (conf *OutOfClusterConfig) createRawConfigFromCluster() (*api.Config, error
 			return nil, err
 		}
 
+		// add this as a bearer token
+		authInfoMap[authInfoName].Token = tok
+	case models.DO:
+		oauthInt, err := conf.Repo.OAuthIntegration.ReadOAuthIntegration(
+			cluster.DOIntegrationID,
+		)
+
+		if err != nil {
+			return nil, err
+		}
+
+		tok, err := oauth.GetAccessToken(oauthInt, conf.DigitalOceanOAuth, *conf.Repo)
+
+		if err != nil {
+			return nil, err
+		}
+
 		// add this as a bearer token
 		authInfoMap[authInfoName].Token = tok
 	default:

+ 2 - 0
internal/models/cluster.go

@@ -18,6 +18,7 @@ const (
 	OIDC   ClusterAuth = "oidc"
 	GCP    ClusterAuth = "gcp-sa"
 	AWS    ClusterAuth = "aws-sa"
+	DO     ClusterAuth = "do-oauth"
 	Local  ClusterAuth = "local"
 )
 
@@ -58,6 +59,7 @@ type Cluster struct {
 	OIDCIntegrationID uint
 	GCPIntegrationID  uint
 	AWSIntegrationID  uint
+	DOIntegrationID   uint
 
 	// A token cache that can be used by an auth mechanism, if desired
 	TokenCache integrations.ClusterTokenCache `json:"token_cache"`

+ 3 - 1
internal/models/integrations/oauth.go

@@ -1,6 +1,8 @@
 package integrations
 
-import "gorm.io/gorm"
+import (
+	"gorm.io/gorm"
+)
 
 // OAuthIntegrationClient is the name of an OAuth mechanism client
 type OAuthIntegrationClient string

+ 1 - 0
internal/models/registry.go

@@ -28,6 +28,7 @@ type Registry struct {
 
 	GCPIntegrationID uint
 	AWSIntegrationID uint
+	DOIntegrationID  uint
 
 	// A token cache that can be used by an auth mechanism (integration), if desired
 	TokenCache integrations.RegTokenCache

+ 49 - 0
internal/oauth/config.go

@@ -1,9 +1,12 @@
 package oauth
 
 import (
+	"context"
 	"crypto/rand"
 	"encoding/base64"
 
+	"github.com/porter-dev/porter/internal/models/integrations"
+	"github.com/porter-dev/porter/internal/repository"
 	"golang.org/x/oauth2"
 )
 
@@ -27,6 +30,19 @@ func NewGithubClient(cfg *Config) *oauth2.Config {
 	}
 }
 
+func NewDigitalOceanClient(cfg *Config) *oauth2.Config {
+	return &oauth2.Config{
+		ClientID:     cfg.ClientID,
+		ClientSecret: cfg.ClientSecret,
+		Endpoint: oauth2.Endpoint{
+			AuthURL:  "https://cloud.digitalocean.com/v1/oauth/authorize",
+			TokenURL: "https://cloud.digitalocean.com/v1/oauth/token",
+		},
+		RedirectURL: cfg.BaseURL + "/api/oauth/digitalocean/callback",
+		Scopes:      cfg.Scopes,
+	}
+}
+
 func CreateRandomState() string {
 	b := make([]byte, 16)
 	rand.Read(b)
@@ -35,3 +51,36 @@ func CreateRandomState() string {
 
 	return state
 }
+
+// GetAccessToken retrieves an access token for a given client. It updates the
+// access token in the DB if necessary
+func GetAccessToken(
+	o *integrations.OAuthIntegration,
+	conf *oauth2.Config,
+	repo repository.Repository,
+) (string, error) {
+	tokSource := conf.TokenSource(context.TODO(), &oauth2.Token{
+		AccessToken:  string(o.AccessToken),
+		RefreshToken: string(o.RefreshToken),
+		TokenType:    "Bearer",
+	})
+
+	token, err := tokSource.Token()
+
+	if err != nil {
+		return "", err
+	}
+
+	if token.AccessToken != string(o.AccessToken) {
+		o.AccessToken = []byte(token.AccessToken)
+		o.RefreshToken = []byte(token.RefreshToken)
+
+		o, err = repo.OAuthIntegration.UpdateOAuthIntegration(o)
+
+		if err != nil {
+			return "", err
+		}
+	}
+
+	return token.AccessToken, nil
+}

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

@@ -670,6 +670,29 @@ func (repo *OAuthIntegrationRepository) ListOAuthIntegrationsByProjectID(
 	return oauths, nil
 }
 
+// UpdateOAuthIntegration modifies an existing oauth integration in the database
+func (repo *OAuthIntegrationRepository) UpdateOAuthIntegration(
+	am *ints.OAuthIntegration,
+) (*ints.OAuthIntegration, error) {
+	err := repo.EncryptOAuthIntegrationData(am, repo.key)
+
+	if err != nil {
+		return nil, err
+	}
+
+	if err := repo.db.Save(am).Error; err != nil {
+		return nil, err
+	}
+
+	err = repo.DecryptOAuthIntegrationData(am, repo.key)
+
+	if err != nil {
+		return nil, err
+	}
+
+	return am, nil
+}
+
 // EncryptOAuthIntegrationData will encrypt the oauth integration data before
 // writing to the DB
 func (repo *OAuthIntegrationRepository) EncryptOAuthIntegrationData(

+ 12 - 0
internal/repository/gorm/cluster.go

@@ -197,10 +197,22 @@ func (repo *ClusterRepository) ListClustersByProjectID(
 func (repo *ClusterRepository) UpdateCluster(
 	cluster *models.Cluster,
 ) (*models.Cluster, error) {
+	err := repo.EncryptClusterData(cluster, repo.key)
+
+	if err != nil {
+		return nil, err
+	}
+
 	if err := repo.db.Save(cluster).Error; err != nil {
 		return nil, err
 	}
 
+	err = repo.DecryptClusterData(cluster, repo.key)
+
+	if err != nil {
+		return nil, err
+	}
+
 	return cluster, nil
 }
 

+ 1 - 0
internal/repository/integrations.go

@@ -34,6 +34,7 @@ type OAuthIntegrationRepository interface {
 	CreateOAuthIntegration(am *ints.OAuthIntegration) (*ints.OAuthIntegration, error)
 	ReadOAuthIntegration(id uint) (*ints.OAuthIntegration, error)
 	ListOAuthIntegrationsByProjectID(projectID uint) ([]*ints.OAuthIntegration, error)
+	UpdateOAuthIntegration(am *ints.OAuthIntegration) (*ints.OAuthIntegration, error)
 }
 
 // AWSIntegrationRepository represents the set of queries on the AWS auth

+ 18 - 0
internal/repository/memory/auth.go

@@ -265,6 +265,24 @@ func (repo *OAuthIntegrationRepository) ListOAuthIntegrationsByProjectID(
 	return res, nil
 }
 
+// UpdateOAuthIntegration updates an oauth integration in the DB
+func (repo *OAuthIntegrationRepository) UpdateOAuthIntegration(
+	am *ints.OAuthIntegration,
+) (*ints.OAuthIntegration, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot write database")
+	}
+
+	if int(am.ID-1) >= len(repo.oIntegrations) || repo.oIntegrations[am.ID-1] == nil {
+		return nil, gorm.ErrRecordNotFound
+	}
+
+	index := int(am.ID - 1)
+	repo.oIntegrations[index] = am
+
+	return am, nil
+}
+
 // AWSIntegrationRepository implements repository.AWSIntegrationRepository
 type AWSIntegrationRepository struct {
 	canQuery        bool

+ 10 - 0
server/api/api.go

@@ -69,6 +69,7 @@ type App struct {
 
 	// oauth-specific clients
 	GithubConf *oauth2.Config
+	DOConf     *oauth2.Config
 
 	db         *gorm.DB
 	validator  *vr.Validate
@@ -124,5 +125,14 @@ func New(conf *AppConfig) (*App, error) {
 		})
 	}
 
+	if sc := conf.ServerConf; sc.DOClientID != "" && sc.DOClientSecret != "" {
+		app.DOConf = oauth.NewGithubClient(&oauth.Config{
+			ClientID:     sc.DOClientID,
+			ClientSecret: sc.DOClientSecret,
+			Scopes:       []string{"read", "write"},
+			BaseURL:      sc.ServerURL,
+		})
+	}
+
 	return app, nil
 }

+ 2 - 1
server/api/deploy_handler.go

@@ -57,7 +57,8 @@ func (app *App) HandleDeployTemplate(w http.ResponseWriter, r *http.Request) {
 	form := &forms.InstallChartTemplateForm{
 		ReleaseForm: &forms.ReleaseForm{
 			Form: &helm.Form{
-				Repo: app.Repo,
+				Repo:              app.Repo,
+				DigitalOceanOAuth: app.DOConf,
 			},
 		},
 		ChartTemplateForm: &forms.ChartTemplateForm{},

+ 10 - 5
server/api/k8s_handler.go

@@ -35,7 +35,8 @@ func (app *App) HandleListNamespaces(w http.ResponseWriter, r *http.Request) {
 	// get the filter options
 	form := &forms.K8sForm{
 		OutOfClusterConfig: &kubernetes.OutOfClusterConfig{
-			Repo: app.Repo,
+			Repo:              app.Repo,
+			DigitalOceanOAuth: app.DOConf,
 		},
 	}
 
@@ -95,7 +96,8 @@ func (app *App) HandleGetPodLogs(w http.ResponseWriter, r *http.Request) {
 	// get the filter options
 	form := &forms.K8sForm{
 		OutOfClusterConfig: &kubernetes.OutOfClusterConfig{
-			Repo: app.Repo,
+			Repo:              app.Repo,
+			DigitalOceanOAuth: app.DOConf,
 		},
 	}
 
@@ -158,7 +160,8 @@ func (app *App) HandleGetIngress(w http.ResponseWriter, r *http.Request) {
 	// get the filter options
 	form := &forms.K8sForm{
 		OutOfClusterConfig: &kubernetes.OutOfClusterConfig{
-			Repo: app.Repo,
+			Repo:              app.Repo,
+			DigitalOceanOAuth: app.DOConf,
 		},
 	}
 
@@ -214,7 +217,8 @@ func (app *App) HandleListPods(w http.ResponseWriter, r *http.Request) {
 	// get the filter options
 	form := &forms.K8sForm{
 		OutOfClusterConfig: &kubernetes.OutOfClusterConfig{
-			Repo: app.Repo,
+			Repo:              app.Repo,
+			DigitalOceanOAuth: app.DOConf,
 		},
 	}
 
@@ -277,7 +281,8 @@ func (app *App) HandleStreamControllerStatus(w http.ResponseWriter, r *http.Requ
 	// get the filter options
 	form := &forms.K8sForm{
 		OutOfClusterConfig: &kubernetes.OutOfClusterConfig{
-			Repo: app.Repo,
+			Repo:              app.Repo,
+			DigitalOceanOAuth: app.DOConf,
 		},
 	}
 

+ 22 - 11
server/api/release_handler.go

@@ -34,7 +34,8 @@ func (app *App) HandleListReleases(w http.ResponseWriter, r *http.Request) {
 	form := &forms.ListReleaseForm{
 		ReleaseForm: &forms.ReleaseForm{
 			Form: &helm.Form{
-				Repo: app.Repo,
+				Repo:              app.Repo,
+				DigitalOceanOAuth: app.DOConf,
 			},
 		},
 		ListFilter: &helm.ListFilter{},
@@ -80,7 +81,8 @@ func (app *App) HandleGetRelease(w http.ResponseWriter, r *http.Request) {
 	form := &forms.GetReleaseForm{
 		ReleaseForm: &forms.ReleaseForm{
 			Form: &helm.Form{
-				Repo: app.Repo,
+				Repo:              app.Repo,
+				DigitalOceanOAuth: app.DOConf,
 			},
 		},
 		Name:     name,
@@ -113,7 +115,8 @@ func (app *App) HandleGetRelease(w http.ResponseWriter, r *http.Request) {
 	// get the filter options
 	k8sForm := &forms.K8sForm{
 		OutOfClusterConfig: &kubernetes.OutOfClusterConfig{
-			Repo: app.Repo,
+			Repo:              app.Repo,
+			DigitalOceanOAuth: app.DOConf,
 		},
 	}
 
@@ -187,7 +190,8 @@ func (app *App) HandleGetReleaseComponents(w http.ResponseWriter, r *http.Reques
 	form := &forms.GetReleaseForm{
 		ReleaseForm: &forms.ReleaseForm{
 			Form: &helm.Form{
-				Repo: app.Repo,
+				Repo:              app.Repo,
+				DigitalOceanOAuth: app.DOConf,
 			},
 		},
 		Name:     name,
@@ -243,7 +247,8 @@ func (app *App) HandleGetReleaseControllers(w http.ResponseWriter, r *http.Reque
 	form := &forms.GetReleaseForm{
 		ReleaseForm: &forms.ReleaseForm{
 			Form: &helm.Form{
-				Repo: app.Repo,
+				Repo:              app.Repo,
+				DigitalOceanOAuth: app.DOConf,
 			},
 		},
 		Name:     name,
@@ -283,7 +288,8 @@ func (app *App) HandleGetReleaseControllers(w http.ResponseWriter, r *http.Reque
 	// get the filter options
 	k8sForm := &forms.K8sForm{
 		OutOfClusterConfig: &kubernetes.OutOfClusterConfig{
-			Repo: app.Repo,
+			Repo:              app.Repo,
+			DigitalOceanOAuth: app.DOConf,
 		},
 	}
 
@@ -369,7 +375,8 @@ func (app *App) HandleListReleaseHistory(w http.ResponseWriter, r *http.Request)
 	form := &forms.ListReleaseHistoryForm{
 		ReleaseForm: &forms.ReleaseForm{
 			Form: &helm.Form{
-				Repo: app.Repo,
+				Repo:              app.Repo,
+				DigitalOceanOAuth: app.DOConf,
 			},
 		},
 		Name: name,
@@ -449,7 +456,8 @@ func (app *App) HandleUpgradeRelease(w http.ResponseWriter, r *http.Request) {
 	form := &forms.UpgradeReleaseForm{
 		ReleaseForm: &forms.ReleaseForm{
 			Form: &helm.Form{
-				Repo: app.Repo,
+				Repo:              app.Repo,
+				DigitalOceanOAuth: app.DOConf,
 			},
 		},
 		Name: name,
@@ -520,7 +528,8 @@ func (app *App) HandleReleaseDeployHook(w http.ResponseWriter, r *http.Request)
 	form := &forms.UpgradeReleaseForm{
 		ReleaseForm: &forms.ReleaseForm{
 			Form: &helm.Form{
-				Repo: app.Repo,
+				Repo:              app.Repo,
+				DigitalOceanOAuth: app.DOConf,
 			},
 		},
 		Name: name,
@@ -617,7 +626,8 @@ func (app *App) HandleReleaseDeployWebhook(w http.ResponseWriter, r *http.Reques
 	form := &forms.UpgradeReleaseForm{
 		ReleaseForm: &forms.ReleaseForm{
 			Form: &helm.Form{
-				Repo: app.Repo,
+				Repo:              app.Repo,
+				DigitalOceanOAuth: app.DOConf,
 			},
 		},
 		Name: release.Name,
@@ -689,7 +699,8 @@ func (app *App) HandleRollbackRelease(w http.ResponseWriter, r *http.Request) {
 	form := &forms.RollbackReleaseForm{
 		ReleaseForm: &forms.ReleaseForm{
 			Form: &helm.Form{
-				Repo: app.Repo,
+				Repo:              app.Repo,
+				DigitalOceanOAuth: app.DOConf,
 			},
 		},
 		Name: name,