From be1f8d00ce1f88577896a33cb8d4983bc0d0e615 Mon Sep 17 00:00:00 2001 From: petruki <31597636+petruki@users.noreply.github.com> Date: Fri, 6 Sep 2024 21:08:02 -0700 Subject: [PATCH] Added token to account including AES encryption --- .env.test | 1 + .github/workflows/master.yml | 1 + src/controller/account_test.go | 35 ++++++++++++++++++++++++- src/controller/controller_test.go | 3 ++- src/model/account.go | 1 + src/repository/account.go | 43 ++++++++++++++++++++++++++----- src/utils/util.go | 38 +++++++++++++++++++++++++++ src/utils/util_test.go | 37 ++++++++++++++++++++++++++ 8 files changed, 150 insertions(+), 9 deletions(-) diff --git a/.env.test b/.env.test index 914a0a5..45a4e8d 100644 --- a/.env.test +++ b/.env.test @@ -1,6 +1,7 @@ PORT=8000 MONGO_URI=mongodb://localhost:27017 MONGO_DB=switcher-gitops-test +GIT_TOKEN_PRIVATE_KEY= SWITCHER_API_URL=https://switcherapi.com/api SWITCHER_API_JWT_SECRET= diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 69a23de..1f6d869 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -36,6 +36,7 @@ jobs: GO_ENV: test MONGODB_URI: mongodb://127.0.0.1:27017 MONGO_DB: switcher-gitops-test + GIT_TOKEN_PRIVATE_KEY: ${{ secrets.GIT_TOKEN_PRIVATE_KEY }} GIT_USER: ${{ secrets.GIT_USER }} GIT_TOKEN: ${{ secrets.GIT_TOKEN }} GIT_TOKEN_READ_ONLY: ${{ secrets.GIT_TOKEN_READ_ONLY }} diff --git a/src/controller/account_test.go b/src/controller/account_test.go index 50f3d5c..46260c9 100644 --- a/src/controller/account_test.go +++ b/src/controller/account_test.go @@ -9,7 +9,9 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/switcherapi/switcher-gitops/src/config" "github.com/switcherapi/switcher-gitops/src/model" + "github.com/switcherapi/switcher-gitops/src/utils" ) const NOT_FOUND = "/not-found" @@ -28,6 +30,9 @@ func TestCreateAccountHandler(t *testing.T) { assert.Equal(t, http.StatusCreated, response.Code) assert.Nil(t, err) assert.Equal(t, accountV1.Repository, accountResponse.Repository) + + token, _ := utils.Decrypt(accountResponse.Token, config.GetEnv("GIT_TOKEN_PRIVATE_KEY")) + assert.Equal(t, accountV1.Token, token) }) t.Run("Should not create an account - invalid request", func(t *testing.T) { @@ -103,7 +108,35 @@ func TestUpdateAccountHandler(t *testing.T) { assert.Equal(t, http.StatusOK, response.Code) assert.Nil(t, err) - assert.Equal(t, accountV2.Repository, accountResponse.Repository) + assert.Equal(t, accountV1.Repository, accountResponse.Repository) + assert.NotEmpty(t, accountResponse.Token) + assert.NotEqual(t, accountV1.Branch, accountResponse.Branch) + assert.NotEqual(t, accountV1.Settings.Window, accountResponse.Settings.Window) + assert.NotEqual(t, accountV1.Settings.Active, accountResponse.Settings.Active) + }) + + t.Run("Should update account token only", func(t *testing.T) { + // Create an account + accountController.CreateAccountHandler(givenAccountRequest(accountV1)) + + // Test + accountRequest := accountV1 + accountRequest.Token = "new-token" + + payload, _ := json.Marshal(accountRequest) + req, _ := http.NewRequest(http.MethodPut, accountController.RouteAccountPath+"/"+accountV1.Domain.ID, bytes.NewBuffer(payload)) + response := executeRequest(req) + + // Assert + var accountResponse model.Account + err := json.NewDecoder(response.Body).Decode(&accountResponse) + + assert.Equal(t, http.StatusOK, response.Code) + assert.Nil(t, err) + assert.Equal(t, accountV1.Repository, accountResponse.Repository) + + encryptedToken := utils.Encrypt(accountV1.Token, config.GetEnv("GIT_TOKEN_PRIVATE_KEY")) + assert.NotEqual(t, encryptedToken, accountResponse.Token) }) t.Run("Should not update an account - invalid request", func(t *testing.T) { diff --git a/src/controller/controller_test.go b/src/controller/controller_test.go index 221c7a6..f720b35 100644 --- a/src/controller/controller_test.go +++ b/src/controller/controller_test.go @@ -60,6 +60,7 @@ func executeRequest(req *http.Request) *httptest.ResponseRecorder { var accountV1 = model.Account{ Repository: "switcherapi/switcher-gitops", Branch: "master", + Token: "github_pat_123", Domain: model.DomainDetails{ ID: "123-controller-test", Name: "Switcher GitOps", @@ -77,7 +78,7 @@ var accountV1 = model.Account{ var accountV2 = model.Account{ Repository: "switcherapi/switcher-gitops", - Branch: "master", + Branch: "main", Domain: model.DomainDetails{ ID: "123-controller-test", Name: "Switcher GitOps", diff --git a/src/model/account.go b/src/model/account.go index 04e2099..09091bb 100644 --- a/src/model/account.go +++ b/src/model/account.go @@ -26,6 +26,7 @@ type Account struct { ID primitive.ObjectID `bson:"_id,omitempty"` Repository string `json:"repository"` Branch string `json:"branch"` + Token string `json:"token"` Environment string `json:"environment"` Domain DomainDetails `json:"domain"` Settings Settings `json:"settings"` diff --git a/src/repository/account.go b/src/repository/account.go index 05b8a48..4c82da1 100644 --- a/src/repository/account.go +++ b/src/repository/account.go @@ -5,7 +5,9 @@ import ( "log" "time" + "github.com/switcherapi/switcher-gitops/src/config" "github.com/switcherapi/switcher-gitops/src/model" + "github.com/switcherapi/switcher-gitops/src/utils" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/mongo" @@ -46,6 +48,9 @@ func (repo *AccountRepositoryMongo) Create(account *model.Account) (*model.Accou collection, ctx, cancel := getDbContext(repo) defer cancel() + // Encrypt token before saving + account.Token = utils.Encrypt(account.Token, config.GetEnv("GIT_TOKEN_PRIVATE_KEY")) + result, err := collection.InsertOne(ctx, account) if err != nil { return nil, err @@ -107,17 +112,24 @@ func (repo *AccountRepositoryMongo) Update(account *model.Account) (*model.Accou collection, ctx, cancel := getDbContext(repo) defer cancel() - filter := primitive.M{domainIdFilter: account.Domain.ID} - update := primitive.M{ - "$set": account, + // Encrypt token before saving + if account.Token != "" { + account.Token = utils.Encrypt(account.Token, config.GetEnv("GIT_TOKEN_PRIVATE_KEY")) } - result := collection.FindOneAndUpdate(ctx, filter, update) - if result.Err() != nil { - return nil, result.Err() + filter := primitive.M{domainIdFilter: account.Domain.ID} + update := getUpdateFields(account) + + var updatedAccount model.Account + err := collection.FindOneAndUpdate(ctx, filter, update, options.FindOneAndUpdate(). + SetReturnDocument(options.After)). + Decode(&updatedAccount) + + if err != nil { + return nil, err } - return account, nil + return &updatedAccount, nil } func (repo *AccountRepositoryMongo) DeleteByAccountId(accountId string) error { @@ -168,3 +180,20 @@ func registerAccountRepositoryValidators(db *mongo.Database) { log.Fatal(err) } } + +func getUpdateFields(account *model.Account) primitive.M { + accountMap := utils.ToMapFromObject(account) + + update := primitive.M{ + "$set": accountMap, + } + + deleteEmpty(accountMap, "token") + return update +} + +func deleteEmpty(setMap map[string]interface{}, key string) { + if setMap[key] == "" { + delete(setMap, key) + } +} diff --git a/src/utils/util.go b/src/utils/util.go index 0706812..3297b6c 100644 --- a/src/utils/util.go +++ b/src/utils/util.go @@ -2,7 +2,11 @@ package utils import ( "bytes" + "crypto/aes" + "crypto/cipher" + "encoding/base64" "encoding/json" + "errors" "os" ) @@ -29,3 +33,37 @@ func ToJsonFromObject(object interface{}) string { json, _ := json.MarshalIndent(object, "", " ") return string(json) } + +func ToMapFromObject(obj interface{}) map[string]interface{} { + var result map[string]interface{} + jsonData, _ := json.Marshal(obj) + json.Unmarshal(jsonData, &result) + return result +} + +func Encrypt(plaintext string, privateKey string) string { + aes, _ := aes.NewCipher([]byte(privateKey)) + gcm, _ := cipher.NewGCM(aes) + + nonce := make([]byte, gcm.NonceSize()) + ciphertext := gcm.Seal(nonce, nonce, []byte(plaintext), nil) + + return base64.StdEncoding.EncodeToString(ciphertext) +} + +func Decrypt(encodedPlaintext string, privateKey string) (string, error) { + decodedText, _ := base64.StdEncoding.DecodeString(encodedPlaintext) + + aes, _ := aes.NewCipher([]byte(privateKey)) + gcm, _ := cipher.NewGCM(aes) + + nonceSize := gcm.NonceSize() + if len(decodedText) < nonceSize { + return "", errors.New("ciphertext too short") + } + + nonce, ciphertext := decodedText[:nonceSize], decodedText[nonceSize:] + plaintext, _ := gcm.Open(nil, []byte(nonce), []byte(ciphertext), nil) + + return string(plaintext), nil +} diff --git a/src/utils/util_test.go b/src/utils/util_test.go index 975ea5a..4a01d2f 100644 --- a/src/utils/util_test.go +++ b/src/utils/util_test.go @@ -5,15 +5,28 @@ import ( "strings" "testing" + "github.com/switcherapi/switcher-gitops/src/config" "github.com/switcherapi/switcher-gitops/src/model" ) +func TestMain(m *testing.M) { + os.Setenv("GO_ENV", "test") + config.InitEnv() + m.Run() +} + func TestToJsonFromObject(t *testing.T) { account := givenAccount(true) actual := ToJsonFromObject(account) AssertNotNil(t, actual) } +func TestToMapFromObject(t *testing.T) { + account := givenAccount(true) + actual := ToMapFromObject(account) + AssertNotNil(t, actual) +} + func TestFormatJSON(t *testing.T) { account := givenAccount(true) accountJSON := ToJsonFromObject(account) @@ -32,6 +45,30 @@ func TestReadJsonFileToObject(t *testing.T) { AssertContains(t, json, "Release 1") } +func TestEncrypDecrypt(t *testing.T) { + privatKey := config.GetEnv("GIT_TOKEN_PRIVATE_KEY") + text := "github_pat_XXXXXXXXXXXXXXXXXXXXXX_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" + + encrypted := Encrypt(text, privatKey) + AssertNotNil(t, encrypted) + + decrypted, err := Decrypt(encrypted, privatKey) + AssertNil(t, err) + AssertEqual(t, text, decrypted) +} + +func TestEncrypDecryptError(t *testing.T) { + privatKey := config.GetEnv("GIT_TOKEN_PRIVATE_KEY") + text := "github_pat..." + + encrypted := Encrypt(text, privatKey) + AssertNotNil(t, encrypted) + + decrypted, err := Decrypt("invalid", privatKey) + AssertNotNil(t, err) + AssertEqual(t, "", decrypted) +} + // Fixtures func givenAccount(active bool) model.Account {