Skip to content

Added token to account including AES encryption #31

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Sep 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.test
Original file line number Diff line number Diff line change
@@ -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=
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/master.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
35 changes: 34 additions & 1 deletion src/controller/account_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
3 changes: 2 additions & 1 deletion src/controller/controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
1 change: 1 addition & 0 deletions src/model/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
43 changes: 36 additions & 7 deletions src/repository/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
}
38 changes: 38 additions & 0 deletions src/utils/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ package utils

import (
"bytes"
"crypto/aes"
"crypto/cipher"
"encoding/base64"
"encoding/json"
"errors"
"os"
)

Expand All @@ -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
}
37 changes: 37 additions & 0 deletions src/utils/util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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 {
Expand Down