Alexander Belanger 5 лет назад
Родитель
Сommit
e556d78c0e

+ 6 - 0
cmd/migrate/main.go

@@ -23,6 +23,12 @@ func main() {
 	}
 
 	err = db.AutoMigrate(
+		&models.Project{},
+		&models.Role{},
+		&models.ServiceAccount{},
+		&models.ServiceAccountAction{},
+		&models.ServiceAccountCandidate{},
+		&models.Cluster{},
 		&models.User{},
 		&models.Session{},
 	)

+ 2 - 1
internal/forms/action.go

@@ -2,6 +2,7 @@ package forms
 
 import (
 	"encoding/base64"
+	"strings"
 
 	"github.com/porter-dev/porter/internal/kubernetes"
 	"github.com/porter-dev/porter/internal/models"
@@ -74,7 +75,7 @@ func (sar *ServiceAccountActionResolver) PopulateServiceAccount(
 			AuthMechanism:     sar.SACandidate.AuthMechanism,
 			LocationOfOrigin:  authInfo.LocationOfOrigin,
 			Impersonate:       authInfo.Impersonate,
-			ImpersonateGroups: authInfo.ImpersonateGroups,
+			ImpersonateGroups: strings.Join(authInfo.ImpersonateGroups, ","),
 		}
 	} else {
 		doesClusterExist := false

+ 2 - 1
internal/kubernetes/kubeconfig.go

@@ -2,6 +2,7 @@ package kubernetes
 
 import (
 	"errors"
+	"strings"
 
 	"github.com/porter-dev/porter/internal/models"
 	"k8s.io/client-go/tools/clientcmd"
@@ -276,7 +277,7 @@ func createRawConfigFromServiceAccount(
 	authInfoMap[authInfoName] = &api.AuthInfo{
 		LocationOfOrigin:  sa.LocationOfOrigin,
 		Impersonate:       sa.Impersonate,
-		ImpersonateGroups: sa.ImpersonateGroups,
+		ImpersonateGroups: strings.Split(sa.ImpersonateGroups, ","),
 	}
 
 	switch sa.AuthMechanism {

+ 9 - 4
internal/models/serviceaccount.go

@@ -28,7 +28,12 @@ type ServiceAccountCandidate struct {
 	ClusterName     string `json:"cluster_name"`
 	ClusterEndpoint string `json:"cluster_endpoint"`
 	AuthMechanism   string `json:"auth_mechanism"`
-	Kubeconfig      []byte `json:"kubeconfig"`
+
+	// ------------------------------------------------------------------
+	// All fields below this line are encrypted before storage
+	// ------------------------------------------------------------------
+
+	Kubeconfig []byte `json:"kubeconfig"`
 }
 
 // ServiceAccountCandidateExternal represents the ServiceAccountCandidate type that is
@@ -80,9 +85,9 @@ type ServiceAccount struct {
 	AuthMechanism string `json:"auth_mechanism"`
 
 	// These fields are used by all auth mechanisms
-	LocationOfOrigin string
-	Impersonate      string `json:"act-as,omitempty"`
-	// ImpersonateGroups []string `json:"act-as-groups,omitempty"`
+	LocationOfOrigin  string
+	Impersonate       string `json:"act-as,omitempty"`
+	ImpersonateGroups string `json:"act-as-groups,omitempty"`
 
 	// ------------------------------------------------------------------
 	// All fields below this line are encrypted before storage

+ 10 - 1
internal/repository/gorm/project_test.go

@@ -17,6 +17,7 @@ import (
 
 type tester struct {
 	repo             *repository.Repository
+	key              *[32]byte
 	dbFileName       string
 	initProjects     []*models.Project
 	initSACandidates []*models.ServiceAccountCandidate
@@ -51,7 +52,15 @@ func setupTestEnv(tester *tester, t *testing.T) {
 		t.Fatalf("%v\n", err)
 	}
 
-	tester.repo = gorm.NewRepository(db)
+	var key [32]byte
+
+	for i, b := range []byte("__random_strong_encryption_key__") {
+		key[i] = b
+	}
+
+	tester.key = &key
+
+	tester.repo = gorm.NewRepository(db, &key)
 }
 
 func cleanup(tester *tester, t *testing.T) {

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

@@ -7,11 +7,11 @@ import (
 
 // NewRepository returns a Repository which uses
 // gorm.DB for querying the database
-func NewRepository(db *gorm.DB) *repository.Repository {
+func NewRepository(db *gorm.DB, key *[32]byte) *repository.Repository {
 	return &repository.Repository{
 		User:           NewUserRepository(db),
 		Session:        NewSessionRepository(db),
 		Project:        NewProjectRepository(db),
-		ServiceAccount: NewServiceAccountRepository(db),
+		ServiceAccount: NewServiceAccountRepository(db, key),
 	}
 }

+ 189 - 4
internal/repository/gorm/serviceaccount.go

@@ -8,19 +8,27 @@ import (
 
 // ServiceAccountRepository uses gorm.DB for querying the database
 type ServiceAccountRepository struct {
-	db *gorm.DB
+	db  *gorm.DB
+	key *[32]byte
 }
 
 // NewServiceAccountRepository returns a ServiceAccountRepository which uses
-// gorm.DB for querying the database
-func NewServiceAccountRepository(db *gorm.DB) repository.ServiceAccountRepository {
-	return &ServiceAccountRepository{db}
+// gorm.DB for querying the database. It accepts an encryption key to encrypt
+// sensitive data
+func NewServiceAccountRepository(db *gorm.DB, key *[32]byte) repository.ServiceAccountRepository {
+	return &ServiceAccountRepository{db, key}
 }
 
 // CreateServiceAccountCandidate creates a new service account candidate
 func (repo *ServiceAccountRepository) CreateServiceAccountCandidate(
 	saCandidate *models.ServiceAccountCandidate,
 ) (*models.ServiceAccountCandidate, error) {
+	err := repo.EncryptServiceAccountCandidateData(saCandidate, repo.key)
+
+	if err != nil {
+		return nil, err
+	}
+
 	project := &models.Project{}
 
 	if err := repo.db.Where("id = ?", saCandidate.ProjectID).First(&project).Error; err != nil {
@@ -71,6 +79,12 @@ func (repo *ServiceAccountRepository) ListServiceAccountCandidatesByProjectID(
 func (repo *ServiceAccountRepository) CreateServiceAccount(
 	sa *models.ServiceAccount,
 ) (*models.ServiceAccount, error) {
+	err := repo.EncryptServiceAccountData(sa, repo.key)
+
+	if err != nil {
+		return nil, err
+	}
+
 	project := &models.Project{}
 
 	if err := repo.db.Where("id = ?", sa.ProjectID).First(&project).Error; err != nil {
@@ -117,3 +131,174 @@ func (repo *ServiceAccountRepository) ListServiceAccountsByProjectID(
 
 	return sas, nil
 }
+
+// EncryptServiceAccountData will encrypt the user's service account data before writing
+// to the DB
+func (repo *ServiceAccountRepository) EncryptServiceAccountData(
+	sa *models.ServiceAccount,
+	key *[32]byte,
+) error {
+	if len(sa.ClientCertificateData) > 0 {
+		cipherData, err := repository.Encrypt(sa.ClientCertificateData, key)
+
+		if err != nil {
+			return err
+		}
+
+		sa.ClientCertificateData = cipherData
+	}
+
+	if len(sa.ClientKeyData) > 0 {
+		cipherData, err := repository.Encrypt(sa.ClientKeyData, key)
+
+		if err != nil {
+			return err
+		}
+
+		sa.ClientKeyData = cipherData
+	}
+
+	if sa.Token != "" {
+		cipherData, err := repository.Encrypt([]byte(sa.Token), key)
+
+		if err != nil {
+			return err
+		}
+
+		sa.Token = string(cipherData)
+	}
+
+	if sa.Username != "" {
+		cipherData, err := repository.Encrypt([]byte(sa.Username), key)
+
+		if err != nil {
+			return err
+		}
+
+		sa.Username = string(cipherData)
+	}
+
+	if sa.Password != "" {
+		cipherData, err := repository.Encrypt([]byte(sa.Password), key)
+
+		if err != nil {
+			return err
+		}
+
+		sa.Password = string(cipherData)
+	}
+
+	if len(sa.KeyData) > 0 {
+		cipherData, err := repository.Encrypt(sa.KeyData, key)
+
+		if err != nil {
+			return err
+		}
+
+		sa.KeyData = cipherData
+	}
+
+	if sa.PrevToken != "" {
+		cipherData, err := repository.Encrypt([]byte(sa.PrevToken), key)
+
+		if err != nil {
+			return err
+		}
+
+		sa.PrevToken = string(cipherData)
+	}
+
+	if sa.OIDCIssuerURL != "" {
+		cipherData, err := repository.Encrypt([]byte(sa.OIDCIssuerURL), key)
+
+		if err != nil {
+			return err
+		}
+
+		sa.OIDCIssuerURL = string(cipherData)
+	}
+
+	if sa.OIDCClientID != "" {
+		cipherData, err := repository.Encrypt([]byte(sa.OIDCClientID), key)
+
+		if err != nil {
+			return err
+		}
+
+		sa.OIDCClientID = string(cipherData)
+	}
+
+	if sa.OIDCClientSecret != "" {
+		cipherData, err := repository.Encrypt([]byte(sa.OIDCClientSecret), key)
+
+		if err != nil {
+			return err
+		}
+
+		sa.OIDCClientSecret = string(cipherData)
+	}
+
+	if sa.OIDCCertificateAuthorityData != "" {
+		cipherData, err := repository.Encrypt([]byte(sa.OIDCCertificateAuthorityData), key)
+
+		if err != nil {
+			return err
+		}
+
+		sa.OIDCCertificateAuthorityData = string(cipherData)
+	}
+
+	if sa.OIDCIDToken != "" {
+		cipherData, err := repository.Encrypt([]byte(sa.OIDCIDToken), key)
+
+		if err != nil {
+			return err
+		}
+
+		sa.OIDCIDToken = string(cipherData)
+	}
+
+	if sa.OIDCRefreshToken != "" {
+		cipherData, err := repository.Encrypt([]byte(sa.OIDCRefreshToken), key)
+
+		if err != nil {
+			return err
+		}
+
+		sa.OIDCRefreshToken = string(cipherData)
+	}
+
+	for i, cluster := range sa.Clusters {
+		if len(cluster.CertificateAuthorityData) > 0 {
+			cipherData, err := repository.Encrypt(cluster.CertificateAuthorityData, key)
+
+			if err != nil {
+				return err
+			}
+
+			cluster.CertificateAuthorityData = cipherData
+			sa.Clusters[i] = cluster
+		}
+	}
+
+	return nil
+}
+
+// EncryptServiceAccountCandidateData will encrypt the service account candidate data before
+// writing to the DB
+func (repo *ServiceAccountRepository) EncryptServiceAccountCandidateData(
+	saCandidate *models.ServiceAccountCandidate,
+	key *[32]byte,
+) error {
+	if len(saCandidate.Kubeconfig) > 0 {
+		cipherData, err := repository.Encrypt(saCandidate.Kubeconfig, key)
+
+		if err != nil {
+			return err
+		}
+
+		saCandidate.Kubeconfig = cipherData
+	}
+
+	return nil
+}

+ 44 - 8
internal/repository/gorm/serviceaccount_test.go

@@ -3,6 +3,8 @@ package gorm_test
 import (
 	"testing"
 
+	"github.com/porter-dev/porter/internal/repository"
+
 	"github.com/go-test/deep"
 	"github.com/porter-dev/porter/internal/models"
 	orm "gorm.io/gorm"
@@ -17,6 +19,7 @@ func initServiceAccountCandidate(tester *tester, t *testing.T) {
 		ClusterName:     "cluster-test",
 		ClusterEndpoint: "https://localhost",
 		AuthMechanism:   models.X509,
+		Kubeconfig:      []byte("current-context: testing\n"),
 		Actions: []models.ServiceAccountAction{
 			models.ServiceAccountAction{
 				Name:     models.TokenDataAction,
@@ -45,8 +48,9 @@ func initServiceAccount(tester *tester, t *testing.T) {
 		ClientKeyData:         []byte("-----BEGIN"),
 		Clusters: []models.Cluster{
 			models.Cluster{
-				Name:   "cluster-test",
-				Server: "https://localhost",
+				Name:                     "cluster-test",
+				Server:                   "https://localhost",
+				CertificateAuthorityData: []byte("-----BEGIN"),
 			},
 		},
 	}
@@ -75,6 +79,7 @@ func TestCreateServiceAccountCandidate(t *testing.T) {
 		ClusterName:     "cluster-test",
 		ClusterEndpoint: "https://localhost",
 		AuthMechanism:   models.X509,
+		Kubeconfig:      []byte("current-context: testing\n"),
 	}
 
 	saCandidate, err := tester.repo.ServiceAccount.CreateServiceAccountCandidate(saCandidate)
@@ -101,6 +106,7 @@ func TestCreateServiceAccountCandidate(t *testing.T) {
 		ClusterName:     "cluster-test",
 		ClusterEndpoint: "https://localhost",
 		AuthMechanism:   models.X509,
+		Kubeconfig:      []byte("current-context: testing\n"),
 		Actions:         []models.ServiceAccountAction{},
 	}
 
@@ -108,6 +114,10 @@ func TestCreateServiceAccountCandidate(t *testing.T) {
 
 	// reset fields for reflect.DeepEqual
 	copySACandidate.Model = orm.Model{}
+	copySACandidate.Kubeconfig, _ = repository.Decrypt(
+		copySACandidate.Kubeconfig,
+		tester.key,
+	)
 
 	if diff := deep.Equal(copySACandidate, expSACandidate); diff != nil {
 		t.Errorf("incorrect sa candidate")
@@ -153,6 +163,7 @@ func TestCreateServiceAccountCandidateWithAction(t *testing.T) {
 		ClusterName:     "cluster-test",
 		ClusterEndpoint: "https://localhost",
 		AuthMechanism:   models.X509,
+		Kubeconfig:      []byte("current-context: testing\n"),
 		Actions: []models.ServiceAccountAction{
 			models.ServiceAccountAction{
 				ServiceAccountCandidateID: 1,
@@ -166,6 +177,10 @@ func TestCreateServiceAccountCandidateWithAction(t *testing.T) {
 
 	// reset fields for reflect.DeepEqual
 	copySACandidate.Model = orm.Model{}
+	copySACandidate.Kubeconfig, _ = repository.Decrypt(
+		copySACandidate.Kubeconfig,
+		tester.key,
+	)
 	copySACandidate.Actions[0].Model = orm.Model{}
 
 	if diff := deep.Equal(copySACandidate, expSACandidate); diff != nil {
@@ -203,6 +218,7 @@ func TestListServiceAccountCandidatesByProjectID(t *testing.T) {
 		ClusterName:     "cluster-test",
 		ClusterEndpoint: "https://localhost",
 		AuthMechanism:   models.X509,
+		Kubeconfig:      []byte("current-context: testing\n"),
 		Actions: []models.ServiceAccountAction{
 			models.ServiceAccountAction{
 				ServiceAccountCandidateID: 1,
@@ -216,6 +232,10 @@ func TestListServiceAccountCandidatesByProjectID(t *testing.T) {
 
 	// reset fields for reflect.DeepEqual
 	copySACandidate.Model = orm.Model{}
+	copySACandidate.Kubeconfig, _ = repository.Decrypt(
+		copySACandidate.Kubeconfig,
+		tester.key,
+	)
 	copySACandidate.Actions[0].Model = orm.Model{}
 
 	if diff := deep.Equal(copySACandidate, expSACandidate); diff != nil {
@@ -272,6 +292,8 @@ func TestCreateServiceAccount(t *testing.T) {
 
 	// reset fields for reflect.DeepEqual
 	copySA.Model = orm.Model{}
+	copySA.ClientCertificateData, _ = repository.Decrypt(copySA.ClientCertificateData, tester.key)
+	copySA.ClientKeyData, _ = repository.Decrypt(copySA.ClientKeyData, tester.key)
 
 	if diff := deep.Equal(copySA, expSA); diff != nil {
 		t.Errorf("incorrect service account")
@@ -319,9 +341,10 @@ func TestCreateServiceAccountWithCluster(t *testing.T) {
 		ClientKeyData:         []byte("-----BEGIN"),
 		Clusters: []models.Cluster{
 			models.Cluster{
-				ServiceAccountID: 1,
-				Name:             "cluster-test",
-				Server:           "https://localhost",
+				ServiceAccountID:         1,
+				Name:                     "cluster-test",
+				Server:                   "https://localhost",
+				CertificateAuthorityData: []byte("-----BEGIN"),
 			},
 		},
 	}
@@ -330,7 +353,13 @@ func TestCreateServiceAccountWithCluster(t *testing.T) {
 
 	// reset fields for reflect.DeepEqual
 	copySA.Model = orm.Model{}
+	copySA.ClientCertificateData, _ = repository.Decrypt(copySA.ClientCertificateData, tester.key)
+	copySA.ClientKeyData, _ = repository.Decrypt(copySA.ClientKeyData, tester.key)
 	copySA.Clusters[0].Model = orm.Model{}
+	copySA.Clusters[0].CertificateAuthorityData, _ = repository.Decrypt(
+		copySA.Clusters[0].CertificateAuthorityData,
+		tester.key,
+	)
 
 	if diff := deep.Equal(copySA, expSA); diff != nil {
 		t.Errorf("incorrect service account")
@@ -369,9 +398,10 @@ func TestListServiceAccountsByProjectID(t *testing.T) {
 		ClientKeyData:         []byte("-----BEGIN"),
 		Clusters: []models.Cluster{
 			models.Cluster{
-				ServiceAccountID: 1,
-				Name:             "cluster-test",
-				Server:           "https://localhost",
+				ServiceAccountID:         1,
+				Name:                     "cluster-test",
+				Server:                   "https://localhost",
+				CertificateAuthorityData: []byte("-----BEGIN"),
 			},
 		},
 	}
@@ -380,7 +410,13 @@ func TestListServiceAccountsByProjectID(t *testing.T) {
 
 	// reset fields for reflect.DeepEqual
 	copySA.Model = orm.Model{}
+	copySA.ClientCertificateData, _ = repository.Decrypt(copySA.ClientCertificateData, tester.key)
+	copySA.ClientKeyData, _ = repository.Decrypt(copySA.ClientKeyData, tester.key)
 	copySA.Clusters[0].Model = orm.Model{}
+	copySA.Clusters[0].CertificateAuthorityData, _ = repository.Decrypt(
+		copySA.Clusters[0].CertificateAuthorityData,
+		tester.key,
+	)
 
 	if diff := deep.Equal(copySA, expSA); diff != nil {
 		t.Errorf("incorrect service account")