Browse Source

helm repo basic auth internal impl

Alexander Belanger 5 years ago
parent
commit
ba80eb2f73
34 changed files with 1317 additions and 92 deletions
  1. 2 0
      cmd/app/main.go
  2. 2 0
      cmd/migrate/main.go
  3. 71 16
      internal/helm/loader/loader.go
  4. 82 0
      internal/helm/repo/repo.go
  5. 6 4
      internal/kubernetes/config.go
  6. 1 1
      internal/models/cluster.go
  7. 70 0
      internal/models/helm_repo.go
  8. 56 0
      internal/models/integrations/basic.go
  9. 31 9
      internal/models/integrations/integration.go
  10. 1 1
      internal/models/integrations/oauth.go
  11. 21 29
      internal/models/integrations/token_cache.go
  12. 4 0
      internal/models/project.go
  13. 6 5
      internal/registry/registry.go
  14. 1 1
      internal/repository/cluster.go
  15. 140 0
      internal/repository/gorm/auth.go
  16. 86 0
      internal/repository/gorm/auth_test.go
  17. 1 1
      internal/repository/gorm/cluster.go
  18. 5 3
      internal/repository/gorm/cluster_test.go
  19. 196 0
      internal/repository/gorm/helm_repo.go
  20. 250 0
      internal/repository/gorm/helm_repo_test.go
  21. 55 1
      internal/repository/gorm/helpers_test.go
  22. 4 2
      internal/repository/gorm/registry_test.go
  23. 2 0
      internal/repository/gorm/repository.go
  24. 16 0
      internal/repository/helm_repo.go
  25. 8 0
      internal/repository/integrations.go
  26. 2 0
      internal/repository/repository.go
  27. 64 0
      internal/repository/test/auth.go
  28. 1 1
      internal/repository/test/cluster.go
  29. 125 0
      internal/repository/test/helm_repo.go
  30. 2 0
      internal/repository/test/repository.go
  31. 1 1
      server/api/cluster_handler_test.go
  32. 1 1
      server/api/deploy_handler.go
  33. 1 1
      server/api/integration_handler.go
  34. 3 15
      server/api/template_handler.go

+ 2 - 0
cmd/app/main.go

@@ -40,10 +40,12 @@ func main() {
 		&models.Session{},
 		&models.GitRepo{},
 		&models.Registry{},
+		&models.HelmRepo{},
 		&models.Cluster{},
 		&models.ClusterCandidate{},
 		&models.ClusterResolver{},
 		&ints.KubeIntegration{},
+		&ints.BasicIntegration{},
 		&ints.OIDCIntegration{},
 		&ints.OAuthIntegration{},
 		&ints.GCPIntegration{},

+ 2 - 0
cmd/migrate/main.go

@@ -31,10 +31,12 @@ func main() {
 		&models.Session{},
 		&models.GitRepo{},
 		&models.Registry{},
+		&models.HelmRepo{},
 		&models.Cluster{},
 		&models.ClusterCandidate{},
 		&models.ClusterResolver{},
 		&ints.KubeIntegration{},
+		&ints.BasicIntegration{},
 		&ints.OIDCIntegration{},
 		&ints.OAuthIntegration{},
 		&ints.GCPIntegration{},

+ 71 - 16
internal/helm/loader/loader.go

@@ -7,6 +7,7 @@ import (
 	"net/http"
 	"strings"
 
+	"github.com/porter-dev/porter/internal/models"
 	"k8s.io/helm/pkg/repo"
 	"sigs.k8s.io/yaml"
 
@@ -14,9 +15,47 @@ import (
 	chartloader "helm.sh/helm/v3/pkg/chart/loader"
 )
 
-// LoadRepoIndex loads an index file from a remote Helm repo
-func LoadRepoIndex(indexURL string) (*repo.IndexFile, error) {
-	resp, err := http.Get(indexURL)
+// RepoIndexToPorterChartList converts an index file to a list of porter charts
+func RepoIndexToPorterChartList(index *repo.IndexFile) []*models.PorterChartList {
+	porterCharts := make([]*models.PorterChartList, 0)
+
+	for _, entry := range index.Entries {
+		indexChart := entry[0]
+
+		porterChart := &models.PorterChartList{
+			Name:        indexChart.Name,
+			Description: indexChart.Description,
+			Icon:        indexChart.Icon,
+		}
+
+		porterCharts = append(porterCharts, porterChart)
+	}
+
+	return porterCharts
+}
+
+// BasicAuthClient is just a username/password to set on requests
+type BasicAuthClient struct {
+	Username string
+	Password string
+}
+
+// LoadRepoIndex uses an http request to get the index file and loads it
+func LoadRepoIndex(client *BasicAuthClient, repoURL string) (*repo.IndexFile, error) {
+	trimmedRepoURL := strings.TrimSuffix(strings.TrimSpace(repoURL), "/")
+	indexURL := trimmedRepoURL + "/index.yaml"
+
+	req, err := http.NewRequest("GET", indexURL, nil)
+
+	if err != nil {
+		return nil, err
+	}
+
+	if client.Username != "" {
+		req.SetBasicAuth(client.Username, client.Password)
+	}
+
+	resp, err := http.DefaultClient.Do(req)
 
 	if err != nil {
 		return nil, err
@@ -43,14 +82,14 @@ func LoadRepoIndex(indexURL string) (*repo.IndexFile, error) {
 	return index, nil
 }
 
-// LoadChart returns a Helm3 (v2) chart from a remote repo. If chartVersion is an
-// empty string, the most stable latest version is found.
-//
-// TODO: this is an expensive operation, so after retrieving the digest from the
-// repo index, this should check the digest in the cache
-func LoadChart(repoURL, chartName, chartVersion string) (*chart.Chart, error) {
-	trimmedRepoURL := strings.TrimSuffix(strings.TrimSpace(repoURL), "/")
-	repoIndex, err := LoadRepoIndex(trimmedRepoURL + "/index.yaml")
+// LoadRepoIndexPublic loads an index file from a remote public Helm repo
+func LoadRepoIndexPublic(repoURL string) (*repo.IndexFile, error) {
+	return LoadRepoIndex(&BasicAuthClient{}, repoURL)
+}
+
+// LoadChart uses an http request to fetch a chart from a remote Helm repo
+func LoadChart(client *BasicAuthClient, repoURL, chartName, chartVersion string) (*chart.Chart, error) {
+	repoIndex, err := LoadRepoIndex(client, repoURL)
 
 	if err != nil {
 		return nil, err
@@ -64,12 +103,21 @@ func LoadChart(repoURL, chartName, chartVersion string) (*chart.Chart, error) {
 		return nil, fmt.Errorf("%s:%s no valid download urls", chartName, chartVersion)
 	}
 
+	trimmedRepoURL := strings.TrimSuffix(strings.TrimSpace(repoURL), "/")
 	chartURL := trimmedRepoURL + "/" + strings.TrimPrefix(cv.URLs[0], "/")
 
-	fmt.Println(chartURL)
-
 	// download tgz
-	resp, err := http.Get(chartURL)
+	req, err := http.NewRequest("GET", chartURL, nil)
+
+	if err != nil {
+		return nil, err
+	}
+
+	if client.Username != "" {
+		req.SetBasicAuth(client.Username, client.Password)
+	}
+
+	resp, err := http.DefaultClient.Do(req)
 
 	if err != nil {
 		return nil, err
@@ -79,11 +127,18 @@ func LoadChart(repoURL, chartName, chartVersion string) (*chart.Chart, error) {
 
 	data, err := ioutil.ReadAll(resp.Body)
 
-	// fmt.Println("DATA IS", string(data))
-
 	if err != nil {
 		return nil, err
 	}
 
 	return chartloader.LoadArchive(bytes.NewReader(data))
 }
+
+// LoadChartPublic returns a Helm3 (v2) chart from a remote public repo.
+// If chartVersion is an empty string, the most stable latest version is found.
+//
+// TODO: this is an expensive operation, so after retrieving the digest from the
+// repo index, this should check the digest in the cache
+func LoadChartPublic(repoURL, chartName, chartVersion string) (*chart.Chart, error) {
+	return LoadChart(&BasicAuthClient{}, repoURL, chartName, chartVersion)
+}

+ 82 - 0
internal/helm/repo/repo.go

@@ -0,0 +1,82 @@
+package repo
+
+import (
+	"fmt"
+
+	"github.com/porter-dev/porter/internal/helm/loader"
+	"github.com/porter-dev/porter/internal/models"
+	"helm.sh/helm/v3/pkg/chart"
+
+	"github.com/porter-dev/porter/internal/repository"
+)
+
+// HelmRepo wraps the gorm HelmRepo model
+type HelmRepo models.HelmRepo
+
+// ListCharts lists Porter charts for a given helm repo
+func (hr *HelmRepo) ListCharts(repo repository.Repository) ([]*models.PorterChartList, error) {
+	if hr.BasicAuthIntegrationID != 0 {
+		return hr.listChartsBasic(repo)
+	}
+
+	return nil, fmt.Errorf("error listing charts")
+}
+
+// GetChart retrieves a Porter chart for a given helm repo
+func (hr *HelmRepo) GetChart(
+	repo repository.Repository,
+	chartName, chartVersion string,
+) (*chart.Chart, error) {
+	if hr.BasicAuthIntegrationID != 0 {
+		return hr.getChartBasic(repo, chartName, chartVersion)
+	}
+
+	return nil, fmt.Errorf("error listing charts")
+}
+
+func (hr *HelmRepo) listChartsBasic(
+	repo repository.Repository,
+) ([]*models.PorterChartList, error) {
+	// get the basic auth integration
+	basic, err := repo.BasicIntegration.ReadBasicIntegration(
+		hr.BasicAuthIntegrationID,
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	client := &loader.BasicAuthClient{
+		Username: string(basic.Username),
+		Password: string(basic.Password),
+	}
+
+	repoIndex, err := loader.LoadRepoIndex(client, hr.RepoURL)
+
+	if err != nil {
+		return nil, err
+	}
+
+	return loader.RepoIndexToPorterChartList(repoIndex), nil
+}
+
+func (hr *HelmRepo) getChartBasic(
+	repo repository.Repository,
+	chartName, chartVersion string,
+) (*chart.Chart, error) {
+	// get the basic auth integration
+	basic, err := repo.BasicIntegration.ReadBasicIntegration(
+		hr.BasicAuthIntegrationID,
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	client := &loader.BasicAuthClient{
+		Username: string(basic.Username),
+		Password: string(basic.Password),
+	}
+
+	return loader.LoadChart(client, hr.RepoURL, chartName, chartVersion)
+}

+ 6 - 4
internal/kubernetes/config.go

@@ -315,15 +315,17 @@ func (conf *OutOfClusterConfig) createRawConfigFromCluster() (*api.Config, error
 }
 
 func (conf *OutOfClusterConfig) getTokenCache() (tok *ints.TokenCache, err error) {
-	return &conf.Cluster.TokenCache, nil
+	return &conf.Cluster.TokenCache.TokenCache, nil
 }
 
 func (conf *OutOfClusterConfig) setTokenCache(token string, expiry time.Time) error {
 	_, err := conf.Repo.Cluster.UpdateClusterTokenCache(
-		&ints.TokenCache{
+		&ints.ClusterTokenCache{
 			ClusterID: conf.Cluster.ID,
-			Token:     []byte(token),
-			Expiry:    expiry,
+			TokenCache: ints.TokenCache{
+				Token:  []byte(token),
+				Expiry: expiry,
+			},
 		},
 	)
 

+ 1 - 1
internal/models/cluster.go

@@ -58,7 +58,7 @@ type Cluster struct {
 	AWSIntegrationID  uint
 
 	// A token cache that can be used by an auth mechanism, if desired
-	TokenCache integrations.TokenCache `json:"token_cache"`
+	TokenCache integrations.ClusterTokenCache `json:"token_cache"`
 
 	// CertificateAuthorityData for the cluster, encrypted at rest
 	CertificateAuthorityData []byte `json:"certificate-authority-data,omitempty"`

+ 70 - 0
internal/models/helm_repo.go

@@ -0,0 +1,70 @@
+package models
+
+import (
+	"github.com/porter-dev/porter/internal/models/integrations"
+	"gorm.io/gorm"
+)
+
+// HelmRepo is an integration that can connect to a Helm repository via a
+// set of auth mechanisms
+type HelmRepo struct {
+	gorm.Model
+
+	// Name given to the Helm repository
+	Name string `json:"name"`
+
+	// The project that this integration belongs to
+	ProjectID uint `json:"project_id"`
+
+	// RepoURL is the URL to the helm repo. This varies based on the integration
+	// type. For example, for AWS S3 this may be prefixed with s3://, or for
+	// GCS it may be gs://
+	RepoURL string `json:"repo_name"`
+
+	// ------------------------------------------------------------------
+	// All fields below this line are encrypted before storage
+	// ------------------------------------------------------------------
+	BasicAuthIntegrationID uint
+	GCPIntegrationID       uint
+	AWSIntegrationID       uint
+
+	// A token cache that can be used by an auth mechanism (integration), if desired
+	TokenCache integrations.HelmRepoTokenCache
+}
+
+// HelmRepoExternal is an external HelmRepo to be shared over REST
+type HelmRepoExternal struct {
+	ID uint `json:"id"`
+
+	// The project that this integration belongs to
+	ProjectID uint `json:"project_id"`
+
+	// Name of the repo
+	Name string `json:"name"`
+
+	RepoURL string `json:"repo_name"`
+
+	// The integration service for this registry
+	Service integrations.IntegrationService `json:"service"`
+}
+
+// Externalize generates an external Registry to be shared over REST
+func (hr *HelmRepo) Externalize() *HelmRepoExternal {
+	var serv integrations.IntegrationService
+
+	if hr.BasicAuthIntegrationID != 0 {
+		serv = integrations.HelmRepo
+	} else if hr.AWSIntegrationID != 0 {
+		serv = integrations.S3
+	} else if hr.GCPIntegrationID != 0 {
+		serv = integrations.GCS
+	}
+
+	return &HelmRepoExternal{
+		ID:        hr.ID,
+		ProjectID: hr.ProjectID,
+		Name:      hr.Name,
+		RepoURL:   hr.RepoURL,
+		Service:   serv,
+	}
+}

+ 56 - 0
internal/models/integrations/basic.go

@@ -0,0 +1,56 @@
+package integrations
+
+import "gorm.io/gorm"
+
+// BasicIntegration represents a basic auth mechanism via username/password
+type BasicIntegration struct {
+	gorm.Model
+
+	// The id of the user that linked this auth mechanism
+	UserID uint `json:"user_id"`
+
+	// The project that this integration belongs to
+	ProjectID uint `json:"project_id"`
+
+	// ------------------------------------------------------------------
+	// All fields encrypted before storage.
+	// ------------------------------------------------------------------
+
+	// Username/Password for basic authentication to a cluster
+	Username []byte `json:"username,omitempty"`
+	Password []byte `json:"password,omitempty"`
+}
+
+// BasicIntegrationExternal is a BasicIntegration to be shared over REST
+type BasicIntegrationExternal struct {
+	ID uint `json:"id"`
+
+	// The id of the user that linked this auth mechanism
+	UserID uint `json:"user_id"`
+
+	// The project that this integration belongs to
+	ProjectID uint `json:"project_id"`
+}
+
+// Externalize generates an external BasicIntegration to be shared over REST
+func (b *BasicIntegration) Externalize() *BasicIntegrationExternal {
+	return &BasicIntegrationExternal{
+		ID:        b.ID,
+		UserID:    b.UserID,
+		ProjectID: b.ProjectID,
+	}
+}
+
+// ToProjectIntegration converts an oauth integration to a project integration
+func (b *BasicIntegration) ToProjectIntegration(
+	category string,
+	service IntegrationService,
+) *ProjectIntegration {
+	return &ProjectIntegration{
+		ID:            b.ID,
+		ProjectID:     b.ProjectID,
+		AuthMechanism: "basic",
+		Category:      category,
+		Service:       service,
+	}
+}

+ 31 - 9
internal/models/integrations/integration.go

@@ -5,13 +5,16 @@ type IntegrationService string
 
 // The list of supported third-party services
 const (
-	GKE    IntegrationService = "gke"
-	EKS    IntegrationService = "eks"
-	Kube   IntegrationService = "kube"
-	GCR    IntegrationService = "gcr"
-	ECR    IntegrationService = "ecr"
-	Github IntegrationService = "github"
-	Docker IntegrationService = "docker"
+	GKE      IntegrationService = "gke"
+	GCS      IntegrationService = "gcs"
+	S3       IntegrationService = "s3"
+	HelmRepo IntegrationService = "helm"
+	EKS      IntegrationService = "eks"
+	Kube     IntegrationService = "kube"
+	GCR      IntegrationService = "gcr"
+	ECR      IntegrationService = "ecr"
+	Github   IntegrationService = "github"
+	Docker   IntegrationService = "docker"
 )
 
 // PorterIntegration is a supported integration service, specifying an auth
@@ -74,8 +77,27 @@ var PorterRegistryIntegrations = []PorterIntegration{
 	},
 }
 
-// PorterRepoIntegrations are the supported repo integrations
-var PorterRepoIntegrations = []PorterIntegration{
+// PorterHelmRepoIntegrations are the supported helm repo integrations
+var PorterHelmRepoIntegrations = []PorterIntegration{
+	PorterIntegration{
+		AuthMechanism: "basic",
+		Category:      "helm",
+		Service:       HelmRepo,
+	},
+	PorterIntegration{
+		AuthMechanism: "gcp",
+		Category:      "helm",
+		Service:       GCS,
+	},
+	PorterIntegration{
+		AuthMechanism: "aws",
+		Category:      "helm",
+		Service:       S3,
+	},
+}
+
+// PorterGitRepoIntegrations are the supported git repo integrations
+var PorterGitRepoIntegrations = []PorterIntegration{
 	PorterIntegration{
 		AuthMechanism: "oauth",
 		Category:      "repo",

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

@@ -62,7 +62,7 @@ func (o *OAuthIntegration) Externalize() *OAuthIntegrationExternal {
 	}
 }
 
-// ToProjectIntegration converts a gcp integration to a project integration
+// ToProjectIntegration converts an oauth integration to a project integration
 func (o *OAuthIntegration) ToProjectIntegration(
 	category string,
 	service IntegrationService,

+ 21 - 29
internal/models/integrations/token_cache.go

@@ -6,23 +6,12 @@ import (
 	"gorm.io/gorm"
 )
 
-// GetTokenCacheFunc is a function that retrieves the token and expiry
-// time from the db
-type GetTokenCacheFunc func() (tok *TokenCache, err error)
-
-// SetTokenCacheFunc is a function that updates the token cache
-// with a new token and expiry time
-type SetTokenCacheFunc func(token string, expiry time.Time) error
-
 // TokenCache stores a token and an expiration for the token for a
 // service account. This will never be shared over REST, so no need
 // to externalize.
 type TokenCache struct {
 	gorm.Model
 
-	ClusterID  uint `json:"cluster_id"`
-	RegistryID uint `json:"registry_id"`
-
 	Expiry time.Time `json:"expiry,omitempty"`
 
 	// ------------------------------------------------------------------
@@ -32,37 +21,40 @@ type TokenCache struct {
 	Token []byte `json:"access_token"`
 }
 
+// GetTokenCacheFunc is a function that retrieves the token and expiry
+// time from the db
+type GetTokenCacheFunc func() (tok *TokenCache, err error)
+
+// SetTokenCacheFunc is a function that updates the token cache
+// with a new token and expiry time
+type SetTokenCacheFunc func(token string, expiry time.Time) error
+
 // IsExpired returns true if a token is expired, false otherwise
 func (t *TokenCache) IsExpired() bool {
 	return time.Now().After(t.Expiry)
 }
 
-// GetRegTokenCacheFunc is a function that retrieves the token and expiry
-// time from the db
-type GetRegTokenCacheFunc func() (tok *TokenCache, err error)
+// ClusterTokenCache is a token cache that clusters can use; a foreign
+// key constraint between a Cluster and ClusterTokenCache is created
+type ClusterTokenCache struct {
+	TokenCache
 
-// SetRegTokenCacheFunc is a function that updates the token cache
-// with a new token and expiry time
-type SetRegTokenCacheFunc func(token string, expiry time.Time) error
+	ClusterID uint `json:"cluster_id"`
+}
 
 // RegTokenCache stores a token and an expiration for the JWT token for a
 // Docker registry. This will never be shared over REST, so no need
 // to externalize.
 type RegTokenCache struct {
-	gorm.Model
+	TokenCache
 
 	RegistryID uint `json:"registry_id"`
-
-	Expiry time.Time `json:"expiry,omitempty"`
-
-	// ------------------------------------------------------------------
-	// All fields below this line are encrypted before storage
-	// ------------------------------------------------------------------
-
-	Token []byte `json:"access_token"`
 }
 
-// IsExpired returns true if a token is expired, false otherwise
-func (r *RegTokenCache) IsExpired() bool {
-	return time.Now().After(r.Expiry)
+// HelmRepoTokenCache is a token cache that helm repos can use; a foreign
+// key constraint between a HelmRepo and HelmRepoTokenCache is created
+type HelmRepoTokenCache struct {
+	TokenCache
+
+	HelmRepoID uint `json:"helm_repo_id"`
 }

+ 4 - 0
internal/models/project.go

@@ -23,8 +23,12 @@ type Project struct {
 	Clusters          []Cluster          `json:"clusters"`
 	ClusterCandidates []ClusterCandidate `json:"cluster_candidates"`
 
+	// linked helm repos
+	HelmRepos []HelmRepo `json:"helm_repos"`
+
 	// auth mechanisms
 	KubeIntegrations  []ints.KubeIntegration  `json:"kube_integrations"`
+	BasicIntegrations []ints.BasicIntegration `json:"basic_integrations"`
 	OIDCIntegrations  []ints.OIDCIntegration  `json:"oidc_integrations"`
 	OAuthIntegrations []ints.OAuthIntegration `json:"oauth_integrations"`
 	AWSIntegrations   []ints.AWSIntegration   `json:"aws_integrations"`

+ 6 - 5
internal/registry/registry.go

@@ -216,9 +216,8 @@ func (r *Registry) listECRRepositories(repo repository.Repository) ([]*Repositor
 
 func (r *Registry) getTokenCache() (tok *ints.TokenCache, err error) {
 	return &ints.TokenCache{
-		RegistryID: r.TokenCache.RegistryID,
-		Token:      r.TokenCache.Token,
-		Expiry:     r.TokenCache.Expiry,
+		Token:  r.TokenCache.Token,
+		Expiry: r.TokenCache.Expiry,
 	}, nil
 }
 
@@ -228,9 +227,11 @@ func (r *Registry) setTokenCacheFunc(
 	return func(token string, expiry time.Time) error {
 		_, err := repo.Registry.UpdateRegistryTokenCache(
 			&ints.RegTokenCache{
+				TokenCache: ints.TokenCache{
+					Token:  []byte(token),
+					Expiry: expiry,
+				},
 				RegistryID: r.ID,
-				Token:      []byte(token),
-				Expiry:     expiry,
 			},
 		)
 

+ 1 - 1
internal/repository/cluster.go

@@ -17,6 +17,6 @@ type ClusterRepository interface {
 	ReadCluster(id uint) (*models.Cluster, error)
 	ListClustersByProjectID(projectID uint) ([]*models.Cluster, error)
 	UpdateCluster(cluster *models.Cluster) (*models.Cluster, error)
-	UpdateClusterTokenCache(tokenCache *ints.TokenCache) (*models.Cluster, error)
+	UpdateClusterTokenCache(tokenCache *ints.ClusterTokenCache) (*models.Cluster, error)
 	DeleteCluster(cluster *models.Cluster) error
 }

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

@@ -228,6 +228,146 @@ func (repo *KubeIntegrationRepository) DecryptKubeIntegrationData(
 	return nil
 }
 
+// BasicIntegrationRepository uses gorm.DB for querying the database
+type BasicIntegrationRepository struct {
+	db  *gorm.DB
+	key *[32]byte
+}
+
+// NewBasicIntegrationRepository returns a BasicIntegrationRepository which uses
+// gorm.DB for querying the database. It accepts an encryption key to encrypt
+// sensitive data
+func NewBasicIntegrationRepository(
+	db *gorm.DB,
+	key *[32]byte,
+) repository.BasicIntegrationRepository {
+	return &BasicIntegrationRepository{db, key}
+}
+
+// CreateBasicIntegration creates a new basic auth mechanism
+func (repo *BasicIntegrationRepository) CreateBasicIntegration(
+	am *ints.BasicIntegration,
+) (*ints.BasicIntegration, error) {
+	err := repo.EncryptBasicIntegrationData(am, repo.key)
+
+	if err != nil {
+		return nil, err
+	}
+
+	project := &models.Project{}
+
+	if err := repo.db.Where("id = ?", am.ProjectID).First(&project).Error; err != nil {
+		return nil, err
+	}
+
+	assoc := repo.db.Model(&project).Association("BasicIntegrations")
+
+	if assoc.Error != nil {
+		return nil, assoc.Error
+	}
+
+	if err := assoc.Append(am); err != nil {
+		return nil, err
+	}
+
+	return am, nil
+}
+
+// ReadBasicIntegration finds a basic auth mechanism by id
+func (repo *BasicIntegrationRepository) ReadBasicIntegration(
+	id uint,
+) (*ints.BasicIntegration, error) {
+	basic := &ints.BasicIntegration{}
+
+	if err := repo.db.Where("id = ?", id).First(&basic).Error; err != nil {
+		return nil, err
+	}
+
+	err := repo.DecryptBasicIntegrationData(basic, repo.key)
+
+	if err != nil {
+		return nil, err
+	}
+
+	return basic, nil
+}
+
+// ListBasicIntegrationsByProjectID finds all basic auth mechanisms
+// for a given project id
+func (repo *BasicIntegrationRepository) ListBasicIntegrationsByProjectID(
+	projectID uint,
+) ([]*ints.BasicIntegration, error) {
+	basics := []*ints.BasicIntegration{}
+
+	if err := repo.db.Where("project_id = ?", projectID).Find(&basics).Error; err != nil {
+		return nil, err
+	}
+
+	for _, basic := range basics {
+		repo.DecryptBasicIntegrationData(basic, repo.key)
+	}
+
+	return basics, nil
+}
+
+// EncryptBasicIntegrationData will encrypt the basic integration data before
+// writing to the DB
+func (repo *BasicIntegrationRepository) EncryptBasicIntegrationData(
+	basic *ints.BasicIntegration,
+	key *[32]byte,
+) error {
+	if len(basic.Username) > 0 {
+		cipherData, err := repository.Encrypt(basic.Username, key)
+
+		if err != nil {
+			return err
+		}
+
+		basic.Username = cipherData
+	}
+
+	if len(basic.Password) > 0 {
+		cipherData, err := repository.Encrypt(basic.Password, key)
+
+		if err != nil {
+			return err
+		}
+
+		basic.Password = cipherData
+	}
+
+	return nil
+}
+
+// DecryptBasicIntegrationData will decrypt the basic integration data before
+// returning it from the DB
+func (repo *BasicIntegrationRepository) DecryptBasicIntegrationData(
+	basic *ints.BasicIntegration,
+	key *[32]byte,
+) error {
+	if len(basic.Username) > 0 {
+		plaintext, err := repository.Decrypt(basic.Username, key)
+
+		if err != nil {
+			return err
+		}
+
+		basic.Username = plaintext
+	}
+
+	if len(basic.Password) > 0 {
+		plaintext, err := repository.Decrypt(basic.Password, key)
+
+		if err != nil {
+			return err
+		}
+
+		basic.Password = plaintext
+	}
+
+	return nil
+}
+
 // OIDCIntegrationRepository uses gorm.DB for querying the database
 type OIDCIntegrationRepository struct {
 	db  *gorm.DB

+ 86 - 0
internal/repository/gorm/auth_test.go

@@ -94,6 +94,92 @@ func TestListKubeIntegrationsByProjectID(t *testing.T) {
 	}
 }
 
+func TestCreateBasicIntegration(t *testing.T) {
+	tester := &tester{
+		dbFileName: "./porter_create_basic.db",
+	}
+
+	setupTestEnv(tester, t)
+	initUser(tester, t)
+	initProject(tester, t)
+	defer cleanup(tester, t)
+
+	basic := &ints.BasicIntegration{
+		ProjectID: tester.initProjects[0].ID,
+		UserID:    tester.initUsers[0].ID,
+		Username:  []byte("username"),
+		Password:  []byte("password"),
+	}
+
+	expBasic := *basic
+
+	basic, err := tester.repo.BasicIntegration.CreateBasicIntegration(basic)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	basic, err = tester.repo.BasicIntegration.ReadBasicIntegration(basic.Model.ID)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	// make sure id is 1
+	if basic.Model.ID != 1 {
+		t.Errorf("incorrect basic integration ID: expected %d, got %d\n", 1, basic.Model.ID)
+	}
+
+	// reset fields for deep.Equal
+	basic.Model = orm.Model{}
+
+	if diff := deep.Equal(expBasic, *basic); diff != nil {
+		t.Errorf("incorrect basic integration")
+		t.Error(diff)
+	}
+}
+
+func TestListBasicIntegrationsByProjectID(t *testing.T) {
+	tester := &tester{
+		dbFileName: "./porter_list_basics.db",
+	}
+
+	setupTestEnv(tester, t)
+	initProject(tester, t)
+	initBasicIntegration(tester, t)
+	defer cleanup(tester, t)
+
+	basics, err := tester.repo.BasicIntegration.ListBasicIntegrationsByProjectID(
+		tester.initProjects[0].Model.ID,
+	)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	if len(basics) != 1 {
+		t.Fatalf("length of basic integrations incorrect: expected %d, got %d\n", 1, len(basics))
+	}
+
+	// make sure data is correct
+	expBasic := ints.BasicIntegration{
+		ProjectID: tester.initProjects[0].ID,
+		UserID:    tester.initUsers[0].ID,
+		Username:  []byte("username"),
+		Password:  []byte("password"),
+	}
+
+	basic := basics[0]
+
+	// reset fields for reflect.DeepEqual
+	basic.Model = orm.Model{}
+
+	if diff := deep.Equal(expBasic, *basic); diff != nil {
+		t.Errorf("incorrect basic integration")
+		t.Error(diff)
+	}
+}
+
 func TestCreateOIDCIntegration(t *testing.T) {
 	tester := &tester{
 		dbFileName: "./porter_create_oidc.db",

+ 1 - 1
internal/repository/gorm/cluster.go

@@ -206,7 +206,7 @@ func (repo *ClusterRepository) UpdateCluster(
 
 // UpdateClusterTokenCache updates the token cache for a cluster
 func (repo *ClusterRepository) UpdateClusterTokenCache(
-	tokenCache *ints.TokenCache,
+	tokenCache *ints.ClusterTokenCache,
 ) (*models.Cluster, error) {
 	if tok := tokenCache.Token; len(tok) > 0 {
 		cipherData, err := repository.Encrypt(tok, repo.key)

+ 5 - 3
internal/repository/gorm/cluster_test.go

@@ -357,9 +357,11 @@ func TestUpdateClusterToken(t *testing.T) {
 		Server:                   "https://localhost",
 		KubeIntegrationID:        tester.initKIs[0].ID,
 		CertificateAuthorityData: []byte("-----BEGIN"),
-		TokenCache: ints.TokenCache{
-			Token:  []byte("token-1"),
-			Expiry: time.Now().Add(-1 * time.Hour),
+		TokenCache: ints.ClusterTokenCache{
+			TokenCache: ints.TokenCache{
+				Token:  []byte("token-1"),
+				Expiry: time.Now().Add(-1 * time.Hour),
+			},
 		},
 	}
 

+ 196 - 0
internal/repository/gorm/helm_repo.go

@@ -0,0 +1,196 @@
+package gorm
+
+import (
+	"github.com/porter-dev/porter/internal/models"
+	ints "github.com/porter-dev/porter/internal/models/integrations"
+	"github.com/porter-dev/porter/internal/repository"
+	"gorm.io/gorm"
+)
+
+// HelmRepoRepository uses gorm.DB for querying the database
+type HelmRepoRepository struct {
+	db  *gorm.DB
+	key *[32]byte
+}
+
+// NewHelmRepoRepository returns a HelmRepoRepository which uses
+// gorm.DB for querying the database
+func NewHelmRepoRepository(db *gorm.DB, key *[32]byte) repository.HelmRepoRepository {
+	return &HelmRepoRepository{db, key}
+}
+
+// CreateHelmRepo creates a new helm repo
+func (repo *HelmRepoRepository) CreateHelmRepo(hr *models.HelmRepo) (*models.HelmRepo, error) {
+	err := repo.EncryptHelmRepoData(hr, repo.key)
+
+	if err != nil {
+		return nil, err
+	}
+
+	project := &models.Project{}
+
+	if err := repo.db.Where("id = ?", hr.ProjectID).First(&project).Error; err != nil {
+		return nil, err
+	}
+
+	assoc := repo.db.Model(&project).Association("HelmRepos")
+
+	if assoc.Error != nil {
+		return nil, assoc.Error
+	}
+
+	if err := assoc.Append(hr); err != nil {
+		return nil, err
+	}
+
+	// create a token cache by default
+	assoc = repo.db.Model(hr).Association("TokenCache")
+
+	if assoc.Error != nil {
+		return nil, assoc.Error
+	}
+
+	if err := assoc.Append(&hr.TokenCache); err != nil {
+		return nil, err
+	}
+
+	err = repo.DecryptHelmRepoData(hr, repo.key)
+
+	if err != nil {
+		return nil, err
+	}
+
+	return hr, nil
+}
+
+// ReadHelmRepo gets a helm repo specified by a unique id
+func (repo *HelmRepoRepository) ReadHelmRepo(id uint) (*models.HelmRepo, error) {
+	hr := &models.HelmRepo{}
+
+	if err := repo.db.Preload("TokenCache").Where("id = ?", id).First(&hr).Error; err != nil {
+		return nil, err
+	}
+
+	repo.DecryptHelmRepoData(hr, repo.key)
+
+	return hr, nil
+}
+
+// ListHelmReposByProjectID finds all helm repos
+// for a given project id
+func (repo *HelmRepoRepository) ListHelmReposByProjectID(
+	projectID uint,
+) ([]*models.HelmRepo, error) {
+	hrs := []*models.HelmRepo{}
+
+	if err := repo.db.Preload("TokenCache").Where("project_id = ?", projectID).Find(&hrs).Error; err != nil {
+		return nil, err
+	}
+
+	for _, hr := range hrs {
+		repo.DecryptHelmRepoData(hr, repo.key)
+	}
+
+	return hrs, nil
+}
+
+// UpdateHelmRepo modifies an existing HelmRepo in the database
+func (repo *HelmRepoRepository) UpdateHelmRepo(
+	hr *models.HelmRepo,
+) (*models.HelmRepo, error) {
+	if err := repo.db.Save(hr).Error; err != nil {
+		return nil, err
+	}
+
+	return hr, nil
+}
+
+// UpdateHelmRepoTokenCache updates the helm repo for a registry
+func (repo *HelmRepoRepository) UpdateHelmRepoTokenCache(
+	tokenCache *ints.HelmRepoTokenCache,
+) (*models.HelmRepo, error) {
+	if tok := tokenCache.Token; len(tok) > 0 {
+		cipherData, err := repository.Encrypt(tok, repo.key)
+
+		if err != nil {
+			return nil, err
+		}
+
+		tokenCache.Token = cipherData
+	}
+
+	hr := &models.HelmRepo{}
+
+	if err := repo.db.Where("id = ?", tokenCache.HelmRepoID).First(&hr).Error; err != nil {
+		return nil, err
+	}
+
+	hr.TokenCache.Token = tokenCache.Token
+	hr.TokenCache.Expiry = tokenCache.Expiry
+
+	if err := repo.db.Save(hr).Error; err != nil {
+		return nil, err
+	}
+
+	return hr, nil
+}
+
+// DeleteHelmRepo removes a registry from the db
+func (repo *HelmRepoRepository) DeleteHelmRepo(
+	hr *models.HelmRepo,
+) error {
+	// clear TokenCache association
+	assoc := repo.db.Model(hr).Association("TokenCache")
+
+	if assoc.Error != nil {
+		return assoc.Error
+	}
+
+	if err := assoc.Clear(); err != nil {
+		return err
+	}
+
+	if err := repo.db.Where("id = ?", hr.ID).Delete(&models.HelmRepo{}).Error; err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// EncryptHelmRepoData will encrypt the user's helm repo data before writing
+// to the DB
+func (repo *HelmRepoRepository) EncryptHelmRepoData(
+	hr *models.HelmRepo,
+	key *[32]byte,
+) error {
+	if tok := hr.TokenCache.Token; len(tok) > 0 {
+		cipherData, err := repository.Encrypt(tok, key)
+
+		if err != nil {
+			return err
+		}
+
+		hr.TokenCache.Token = cipherData
+	}
+
+	return nil
+}
+
+// DecryptHelmRepoData will decrypt the user's helm repo data before returning it
+// from the DB
+func (repo *HelmRepoRepository) DecryptHelmRepoData(
+	hr *models.HelmRepo,
+	key *[32]byte,
+) error {
+	if tok := hr.TokenCache.Token; len(tok) > 0 {
+		plaintext, err := repository.Decrypt(tok, key)
+
+		if err != nil {
+			return err
+		}
+
+		hr.TokenCache.Token = plaintext
+	}
+
+	return nil
+}

+ 250 - 0
internal/repository/gorm/helm_repo_test.go

@@ -0,0 +1,250 @@
+package gorm_test
+
+import (
+	"testing"
+	"time"
+
+	"github.com/go-test/deep"
+	"github.com/porter-dev/porter/internal/models"
+	ints "github.com/porter-dev/porter/internal/models/integrations"
+	"gorm.io/gorm"
+	orm "gorm.io/gorm"
+)
+
+func TestCreateHelmRepo(t *testing.T) {
+	tester := &tester{
+		dbFileName: "./porter_create_hr.db",
+	}
+
+	setupTestEnv(tester, t)
+	initProject(tester, t)
+	defer cleanup(tester, t)
+
+	hr := &models.HelmRepo{
+		Name:      "helm-repo-test",
+		RepoURL:   "https://example-repo.com",
+		ProjectID: tester.initProjects[0].Model.ID,
+	}
+
+	hr, err := tester.repo.HelmRepo.CreateHelmRepo(hr)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	hr, err = tester.repo.HelmRepo.ReadHelmRepo(hr.Model.ID)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	// make sure id is 1 and name is "registry-test"
+	if hr.Model.ID != 1 {
+		t.Errorf("incorrect registry ID: expected %d, got %d\n", 1, hr.Model.ID)
+	}
+
+	if hr.Name != "helm-repo-test" {
+		t.Errorf("incorrect helm repo name: expected %s, got %s\n", "helm-repo-test", hr.Name)
+	}
+
+	if hr.RepoURL != "https://example-repo.com" {
+		t.Errorf("incorrect helm repo url: expected %s, got %s\n", "https://example-repo.com", hr.RepoURL)
+	}
+}
+
+func TestListHelmReposByProjectID(t *testing.T) {
+	tester := &tester{
+		dbFileName: "./porter_list_hrs.db",
+	}
+
+	setupTestEnv(tester, t)
+	initProject(tester, t)
+	initHelmRepo(tester, t)
+	defer cleanup(tester, t)
+
+	hrs, err := tester.repo.HelmRepo.ListHelmReposByProjectID(
+		tester.initProjects[0].Model.ID,
+	)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	if len(hrs) != 1 {
+		t.Fatalf("length of helm repos incorrect: expected %d, got %d\n", 1, len(hrs))
+	}
+
+	// make sure data is correct
+	expHelmRepo := models.HelmRepo{
+		Name:      "helm-repo-test",
+		RepoURL:   "https://example-repo.com",
+		ProjectID: tester.initProjects[0].Model.ID,
+	}
+
+	hr := hrs[0]
+
+	// reset fields for reflect.DeepEqual
+	hr.Model = gorm.Model{}
+
+	if diff := deep.Equal(expHelmRepo, *hr); diff != nil {
+		t.Errorf("incorrect helm repo")
+		t.Error(diff)
+	}
+}
+
+func TestUpdateHelmRepo(t *testing.T) {
+	tester := &tester{
+		dbFileName: "./porter_update_hr.db",
+	}
+
+	setupTestEnv(tester, t)
+	initProject(tester, t)
+	initHelmRepo(tester, t)
+	defer cleanup(tester, t)
+
+	hr := tester.initHRs[0]
+
+	hr.Name = "helm-repo-new-name"
+
+	hr, err := tester.repo.HelmRepo.UpdateHelmRepo(
+		hr,
+	)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	hr, err = tester.repo.HelmRepo.ReadHelmRepo(tester.initHRs[0].ID)
+
+	// make sure data is correct
+	expHelmRepo := models.HelmRepo{
+		Name:      "helm-repo-new-name",
+		RepoURL:   "https://example-repo.com",
+		ProjectID: tester.initProjects[0].Model.ID,
+	}
+
+	// reset fields for reflect.DeepEqual
+	hr.Model = orm.Model{}
+
+	if diff := deep.Equal(expHelmRepo, *hr); diff != nil {
+		t.Errorf("incorrect helm repo")
+		t.Error(diff)
+	}
+}
+
+func TestUpdateHelmRepoToken(t *testing.T) {
+	tester := &tester{
+		dbFileName: "./porter_test_update_hr_token.db",
+	}
+
+	setupTestEnv(tester, t)
+	initProject(tester, t)
+	defer cleanup(tester, t)
+
+	hr := &models.HelmRepo{
+		Name:      "helm-repo-test",
+		RepoURL:   "https://example-repo.com",
+		ProjectID: tester.initProjects[0].Model.ID,
+		TokenCache: ints.HelmRepoTokenCache{
+			TokenCache: ints.TokenCache{
+				Token:  []byte("token-1"),
+				Expiry: time.Now().Add(-1 * time.Hour),
+			},
+		},
+	}
+
+	hr, err := tester.repo.HelmRepo.CreateHelmRepo(hr)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	hr, err = tester.repo.HelmRepo.ReadHelmRepo(hr.Model.ID)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	// make sure helm repo id of token is 1
+	if hr.TokenCache.HelmRepoID != 1 {
+		t.Fatalf("incorrect helm repo id in token cache: expected %d, got %d\n", 1, hr.TokenCache.HelmRepoID)
+	}
+
+	// make sure old token is expired
+	if isExpired := hr.TokenCache.IsExpired(); !isExpired {
+		t.Fatalf("token was not expired\n")
+	}
+
+	if string(hr.TokenCache.Token) != "token-1" {
+		t.Errorf("incorrect token in cache: expected %s, got %s\n", "token-1", hr.TokenCache.Token)
+	}
+
+	hr.TokenCache.Token = []byte("token-2")
+	hr.TokenCache.Expiry = time.Now().Add(24 * time.Hour)
+
+	hr, err = tester.repo.HelmRepo.UpdateHelmRepoTokenCache(&hr.TokenCache)
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+	hr, err = tester.repo.HelmRepo.ReadHelmRepo(hr.Model.ID)
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	// make sure id is 1
+	if hr.Model.ID != 1 {
+		t.Errorf("incorrect helm repo ID: expected %d, got %d\n", 1, hr.Model.ID)
+	}
+
+	// make sure new token is correct and not expired
+	if hr.TokenCache.HelmRepoID != 1 {
+		t.Fatalf("incorrect helm repo ID in token cache: expected %d, got %d\n", 1, hr.TokenCache.HelmRepoID)
+	}
+
+	if isExpired := hr.TokenCache.IsExpired(); isExpired {
+		t.Fatalf("token was expired\n")
+	}
+
+	if string(hr.TokenCache.Token) != "token-2" {
+		t.Errorf("incorrect token in cache: expected %s, got %s\n", "token-2", hr.TokenCache.Token)
+	}
+}
+
+// func TestDeleteRegistry(t *testing.T) {
+// 	tester := &tester{
+// 		dbFileName: "./porter_delete_registry.db",
+// 	}
+
+// 	setupTestEnv(tester, t)
+// 	initProject(tester, t)
+// 	initRegistry(tester, t)
+// 	defer cleanup(tester, t)
+
+// 	reg, err := tester.repo.Registry.ReadRegistry(tester.initRegs[0].Model.ID)
+
+// 	if err != nil {
+// 		t.Fatalf("%v\n", err)
+// 	}
+
+// 	err = tester.repo.Registry.DeleteRegistry(reg)
+
+// 	if err != nil {
+// 		t.Fatalf("%v\n", err)
+// 	}
+
+// 	_, err = tester.repo.Registry.ReadRegistry(tester.initRegs[0].Model.ID)
+
+// 	if err != orm.ErrRecordNotFound {
+// 		t.Fatalf("incorrect error: expected %v, got %v\n", orm.ErrRecordNotFound, err)
+// 	}
+
+// 	regs, err := tester.repo.Registry.ListRegistriesByProjectID(tester.initProjects[0].Model.ID)
+
+// 	if err != nil {
+// 		t.Fatalf("%v\n", err)
+// 	}
+
+// 	if len(regs) != 0 {
+// 		t.Fatalf("length of clusters was not 0")
+// 	}
+// }

+ 55 - 1
internal/repository/gorm/helpers_test.go

@@ -21,8 +21,10 @@ type tester struct {
 	initGRs      []*models.GitRepo
 	initRegs     []*models.Registry
 	initClusters []*models.Cluster
+	initHRs      []*models.HelmRepo
 	initCCs      []*models.ClusterCandidate
 	initKIs      []*ints.KubeIntegration
+	initBasics   []*ints.BasicIntegration
 	initOIDCs    []*ints.OIDCIntegration
 	initOAuths   []*ints.OAuthIntegration
 	initGCPs     []*ints.GCPIntegration
@@ -49,16 +51,19 @@ func setupTestEnv(tester *tester, t *testing.T) {
 		&models.Session{},
 		&models.GitRepo{},
 		&models.Registry{},
+		&models.HelmRepo{},
 		&models.Cluster{},
 		&models.ClusterCandidate{},
 		&models.ClusterResolver{},
 		&ints.KubeIntegration{},
+		&ints.BasicIntegration{},
 		&ints.OIDCIntegration{},
 		&ints.OAuthIntegration{},
 		&ints.GCPIntegration{},
 		&ints.AWSIntegration{},
-		&ints.TokenCache{},
+		&ints.ClusterTokenCache{},
 		&ints.RegTokenCache{},
+		&ints.HelmRepoTokenCache{},
 	)
 
 	if err != nil {
@@ -159,6 +164,33 @@ func initKubeIntegration(tester *tester, t *testing.T) {
 	tester.initKIs = append(tester.initKIs, ki)
 }
 
+func initBasicIntegration(tester *tester, t *testing.T) {
+	t.Helper()
+
+	if len(tester.initProjects) == 0 {
+		initProject(tester, t)
+	}
+
+	if len(tester.initUsers) == 0 {
+		initUser(tester, t)
+	}
+
+	basic := &ints.BasicIntegration{
+		ProjectID: tester.initProjects[0].ID,
+		UserID:    tester.initUsers[0].ID,
+		Username:  []byte("username"),
+		Password:  []byte("password"),
+	}
+
+	basic, err := tester.repo.BasicIntegration.CreateBasicIntegration(basic)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	tester.initBasics = append(tester.initBasics, basic)
+}
+
 func initOIDCIntegration(tester *tester, t *testing.T) {
 	t.Helper()
 
@@ -380,3 +412,25 @@ func initRegistry(tester *tester, t *testing.T) {
 
 	tester.initRegs = append(tester.initRegs, reg)
 }
+
+func initHelmRepo(tester *tester, t *testing.T) {
+	t.Helper()
+
+	if len(tester.initProjects) == 0 {
+		initProject(tester, t)
+	}
+
+	hr := &models.HelmRepo{
+		Name:      "helm-repo-test",
+		RepoURL:   "https://example-repo.com",
+		ProjectID: tester.initProjects[0].Model.ID,
+	}
+
+	hr, err := tester.repo.HelmRepo.CreateHelmRepo(hr)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	tester.initHRs = append(tester.initHRs, hr)
+}

+ 4 - 2
internal/repository/gorm/registry_test.go

@@ -138,8 +138,10 @@ func TestUpdateRegistryToken(t *testing.T) {
 		Name:      "registry-test",
 		ProjectID: tester.initProjects[0].Model.ID,
 		TokenCache: ints.RegTokenCache{
-			Token:  []byte("token-1"),
-			Expiry: time.Now().Add(-1 * time.Hour),
+			TokenCache: ints.TokenCache{
+				Token:  []byte("token-1"),
+				Expiry: time.Now().Add(-1 * time.Hour),
+			},
 		},
 	}
 

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

@@ -14,8 +14,10 @@ func NewRepository(db *gorm.DB, key *[32]byte) *repository.Repository {
 		Project:          NewProjectRepository(db),
 		GitRepo:          NewGitRepoRepository(db, key),
 		Cluster:          NewClusterRepository(db, key),
+		HelmRepo:         NewHelmRepoRepository(db, key),
 		Registry:         NewRegistryRepository(db, key),
 		KubeIntegration:  NewKubeIntegrationRepository(db, key),
+		BasicIntegration: NewBasicIntegrationRepository(db, key),
 		OIDCIntegration:  NewOIDCIntegrationRepository(db, key),
 		OAuthIntegration: NewOAuthIntegrationRepository(db, key),
 		GCPIntegration:   NewGCPIntegrationRepository(db, key),

+ 16 - 0
internal/repository/helm_repo.go

@@ -0,0 +1,16 @@
+package repository
+
+import (
+	"github.com/porter-dev/porter/internal/models"
+	ints "github.com/porter-dev/porter/internal/models/integrations"
+)
+
+// HelmRepoRepository represents the set of queries on the HelmRepo model
+type HelmRepoRepository interface {
+	CreateHelmRepo(repo *models.HelmRepo) (*models.HelmRepo, error)
+	ReadHelmRepo(id uint) (*models.HelmRepo, error)
+	ListHelmReposByProjectID(projectID uint) ([]*models.HelmRepo, error)
+	UpdateHelmRepo(repo *models.HelmRepo) (*models.HelmRepo, error)
+	UpdateHelmRepoTokenCache(tokenCache *ints.HelmRepoTokenCache) (*models.HelmRepo, error)
+	DeleteHelmRepo(repo *models.HelmRepo) error
+}

+ 8 - 0
internal/repository/integrations.go

@@ -12,6 +12,14 @@ type KubeIntegrationRepository interface {
 	ListKubeIntegrationsByProjectID(projectID uint) ([]*ints.KubeIntegration, error)
 }
 
+// BasicIntegrationRepository represents the set of queries on the "basic" auth
+// mechanism
+type BasicIntegrationRepository interface {
+	CreateBasicIntegration(am *ints.BasicIntegration) (*ints.BasicIntegration, error)
+	ReadBasicIntegration(id uint) (*ints.BasicIntegration, error)
+	ListBasicIntegrationsByProjectID(projectID uint) ([]*ints.BasicIntegration, error)
+}
+
 // OIDCIntegrationRepository represents the set of queries on the OIDC auth
 // mechanism
 type OIDCIntegrationRepository interface {

+ 2 - 0
internal/repository/repository.go

@@ -7,8 +7,10 @@ type Repository struct {
 	Session          SessionRepository
 	GitRepo          GitRepoRepository
 	Cluster          ClusterRepository
+	HelmRepo         HelmRepoRepository
 	Registry         RegistryRepository
 	KubeIntegration  KubeIntegrationRepository
+	BasicIntegration BasicIntegrationRepository
 	OIDCIntegration  OIDCIntegrationRepository
 	OAuthIntegration OAuthIntegrationRepository
 	GCPIntegration   GCPIntegrationRepository

+ 64 - 0
internal/repository/test/auth.go

@@ -73,6 +73,70 @@ func (repo *KubeIntegrationRepository) ListKubeIntegrationsByProjectID(
 	return res, nil
 }
 
+// BasicIntegrationRepository implements repository.BasicIntegrationRepository
+type BasicIntegrationRepository struct {
+	canQuery          bool
+	basicIntegrations []*ints.BasicIntegration
+}
+
+// NewBasicIntegrationRepository will return errors if canQuery is false
+func NewBasicIntegrationRepository(canQuery bool) repository.BasicIntegrationRepository {
+	return &BasicIntegrationRepository{
+		canQuery,
+		[]*ints.BasicIntegration{},
+	}
+}
+
+// CreateBasicIntegration creates a new basic auth mechanism
+func (repo *BasicIntegrationRepository) CreateBasicIntegration(
+	am *ints.BasicIntegration,
+) (*ints.BasicIntegration, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot write database")
+	}
+
+	repo.basicIntegrations = append(repo.basicIntegrations, am)
+	am.ID = uint(len(repo.basicIntegrations))
+
+	return am, nil
+}
+
+// ReadBasicIntegration finds a basic auth mechanism by id
+func (repo *BasicIntegrationRepository) ReadBasicIntegration(
+	id uint,
+) (*ints.BasicIntegration, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot read from database")
+	}
+
+	if int(id-1) >= len(repo.basicIntegrations) || repo.basicIntegrations[id-1] == nil {
+		return nil, gorm.ErrRecordNotFound
+	}
+
+	index := int(id - 1)
+	return repo.basicIntegrations[index], nil
+}
+
+// ListBasicIntegrationsByProjectID finds all basic auth mechanisms
+// for a given project id
+func (repo *BasicIntegrationRepository) ListBasicIntegrationsByProjectID(
+	projectID uint,
+) ([]*ints.BasicIntegration, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot read from database")
+	}
+
+	res := make([]*ints.BasicIntegration, 0)
+
+	for _, basicAM := range repo.basicIntegrations {
+		if basicAM.ProjectID == projectID {
+			res = append(res, basicAM)
+		}
+	}
+
+	return res, nil
+}
+
 // OIDCIntegrationRepository implements repository.OIDCIntegrationRepository
 type OIDCIntegrationRepository struct {
 	canQuery         bool

+ 1 - 1
internal/repository/test/cluster.go

@@ -164,7 +164,7 @@ func (repo *ClusterRepository) UpdateCluster(
 
 // UpdateClusterTokenCache updates the token cache for a cluster
 func (repo *ClusterRepository) UpdateClusterTokenCache(
-	tokenCache *ints.TokenCache,
+	tokenCache *ints.ClusterTokenCache,
 ) (*models.Cluster, error) {
 	if !repo.canQuery {
 		return nil, errors.New("Cannot write database")

+ 125 - 0
internal/repository/test/helm_repo.go

@@ -0,0 +1,125 @@
+package test
+
+import (
+	"errors"
+
+	"github.com/porter-dev/porter/internal/models"
+	ints "github.com/porter-dev/porter/internal/models/integrations"
+	"github.com/porter-dev/porter/internal/repository"
+	"gorm.io/gorm"
+)
+
+// HelmRepoRepository implements repository.HelmRepoRepository
+type HelmRepoRepository struct {
+	canQuery  bool
+	helmRepos []*models.HelmRepo
+}
+
+// NewHelmRepoRepository will return errors if canQuery is false
+func NewHelmRepoRepository(canQuery bool) repository.HelmRepoRepository {
+	return &HelmRepoRepository{
+		canQuery,
+		[]*models.HelmRepo{},
+	}
+}
+
+// CreateHelmRepo creates a new repoistry
+func (repo *HelmRepoRepository) CreateHelmRepo(
+	hr *models.HelmRepo,
+) (*models.HelmRepo, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot write database")
+	}
+
+	repo.helmRepos = append(repo.helmRepos, hr)
+	hr.ID = uint(len(repo.helmRepos))
+
+	return hr, nil
+}
+
+// ReadHelmRepo finds a repoistry by id
+func (repo *HelmRepoRepository) ReadHelmRepo(
+	id uint,
+) (*models.HelmRepo, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot read from database")
+	}
+
+	if int(id-1) >= len(repo.helmRepos) || repo.helmRepos[id-1] == nil {
+		return nil, gorm.ErrRecordNotFound
+	}
+
+	index := int(id - 1)
+	return repo.helmRepos[index], nil
+}
+
+// ListHelmReposByProjectID finds all repoistries
+// for a given project id
+func (repo *HelmRepoRepository) ListHelmReposByProjectID(
+	projectID uint,
+) ([]*models.HelmRepo, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot read from database")
+	}
+
+	res := make([]*models.HelmRepo, 0)
+
+	for _, hr := range repo.helmRepos {
+		if hr != nil && hr.ProjectID == projectID {
+			res = append(res, hr)
+		}
+	}
+
+	return res, nil
+}
+
+// UpdateHelmRepo modifies an existing HelmRepo in the database
+func (repo *HelmRepoRepository) UpdateHelmRepo(
+	hr *models.HelmRepo,
+) (*models.HelmRepo, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot write database")
+	}
+
+	if int(hr.ID-1) >= len(repo.helmRepos) || repo.helmRepos[hr.ID-1] == nil {
+		return nil, gorm.ErrRecordNotFound
+	}
+
+	index := int(hr.ID - 1)
+	repo.helmRepos[index] = hr
+
+	return hr, nil
+}
+
+// UpdateHelmRepoTokenCache updates the token cache for a repoistry
+func (repo *HelmRepoRepository) UpdateHelmRepoTokenCache(
+	tokenCache *ints.HelmRepoTokenCache,
+) (*models.HelmRepo, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot write database")
+	}
+
+	index := int(tokenCache.HelmRepoID - 1)
+	repo.helmRepos[index].TokenCache.Token = tokenCache.Token
+	repo.helmRepos[index].TokenCache.Expiry = tokenCache.Expiry
+
+	return repo.helmRepos[index], nil
+}
+
+// DeleteHelmRepo removes a repoistry from the array by setting it to nil
+func (repo *HelmRepoRepository) DeleteHelmRepo(
+	hr *models.HelmRepo,
+) error {
+	if !repo.canQuery {
+		return errors.New("Cannot write database")
+	}
+
+	if int(hr.ID-1) >= len(repo.helmRepos) || repo.helmRepos[hr.ID-1] == nil {
+		return gorm.ErrRecordNotFound
+	}
+
+	index := int(hr.ID - 1)
+	repo.helmRepos[index] = nil
+
+	return nil
+}

+ 2 - 0
internal/repository/test/repository.go

@@ -12,9 +12,11 @@ func NewRepository(canQuery bool) *repository.Repository {
 		Session:          NewSessionRepository(canQuery),
 		Project:          NewProjectRepository(canQuery),
 		Cluster:          NewClusterRepository(canQuery),
+		HelmRepo:         NewHelmRepoRepository(canQuery),
 		Registry:         NewRegistryRepository(canQuery),
 		GitRepo:          NewGitRepoRepository(canQuery),
 		KubeIntegration:  NewKubeIntegrationRepository(canQuery),
+		BasicIntegration: NewBasicIntegrationRepository(canQuery),
 		OIDCIntegration:  NewOIDCIntegrationRepository(canQuery),
 		OAuthIntegration: NewOAuthIntegrationRepository(canQuery),
 		GCPIntegration:   NewGCPIntegrationRepository(canQuery),

+ 1 - 1
server/api/cluster_handler_test.go

@@ -250,7 +250,7 @@ var createProjectClusterCandidatesTests = []*clusterTest{
 					Name:                     "cluster-test",
 					Server:                   "https://10.10.10.10",
 					OIDCIntegrationID:        1,
-					TokenCache:               integrations.TokenCache{},
+					TokenCache:               integrations.ClusterTokenCache{},
 					CertificateAuthorityData: []byte("-----BEGIN CER"),
 				}
 

+ 1 - 1
server/api/deploy_handler.go

@@ -37,7 +37,7 @@ func (app *App) HandleDeployTemplate(w http.ResponseWriter, r *http.Request) {
 
 	getChartForm.PopulateRepoURLFromQueryParams(vals)
 
-	chart, err := loader.LoadChart(getChartForm.RepoURL, getChartForm.Name, getChartForm.Version)
+	chart, err := loader.LoadChartPublic(getChartForm.RepoURL, getChartForm.Name, getChartForm.Version)
 
 	if err != nil {
 		app.handleErrorFormDecoding(err, ErrReleaseDecode, w)

+ 1 - 1
server/api/integration_handler.go

@@ -40,7 +40,7 @@ func (app *App) HandleListRegistryIntegrations(w http.ResponseWriter, r *http.Re
 // HandleListRepoIntegrations lists the repo integrations available to the
 // instance
 func (app *App) HandleListRepoIntegrations(w http.ResponseWriter, r *http.Request) {
-	repos := ints.PorterRepoIntegrations
+	repos := ints.PorterGitRepoIntegrations
 
 	w.WriteHeader(http.StatusOK)
 

+ 3 - 15
server/api/template_handler.go

@@ -18,26 +18,14 @@ import (
 // TODO: test and reduce fragility (handle untar/parse error for individual charts)
 // TODO: separate markdown retrieval into its own query if necessary
 func (app *App) HandleListTemplates(w http.ResponseWriter, r *http.Request) {
-	repoIndex, err := loader.LoadRepoIndex("https://porter-dev.github.io/chart-repo/index.yaml")
+	repoIndex, err := loader.LoadRepoIndexPublic("https://porter-dev.github.io/chart-repo")
 
 	if err != nil {
 		app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
 		return
 	}
 
-	// Loop over charts in index.yaml
-	porterCharts := []models.PorterChartList{}
-
-	for _, entry := range repoIndex.Entries {
-		indexChart := entry[0]
-
-		porterChart := models.PorterChartList{}
-		porterChart.Name = indexChart.Name
-		porterChart.Description = indexChart.Description
-		porterChart.Icon = indexChart.Icon
-
-		porterCharts = append(porterCharts, porterChart)
-	}
+	porterCharts := loader.RepoIndexToPorterChartList(repoIndex)
 
 	json.NewEncoder(w).Encode(porterCharts)
 }
@@ -68,7 +56,7 @@ func (app *App) HandleReadTemplate(w http.ResponseWriter, r *http.Request) {
 
 	form.PopulateRepoURLFromQueryParams(vals)
 
-	chart, err := loader.LoadChart(form.RepoURL, form.Name, form.Version)
+	chart, err := loader.LoadChartPublic(form.RepoURL, form.Name, form.Version)
 
 	if err != nil {
 		app.handleErrorFormDecoding(err, ErrReleaseDecode, w)