Просмотр исходного кода

Merge pull request #3830 from raintank/apiPlugin

Add secureJsonData field to appSettings model
Torkel Ödegaard 10 лет назад
Родитель
Сommit
12460af0ec

+ 9 - 2
pkg/api/app_routes.go

@@ -94,8 +94,15 @@ func NewApiPluginProxy(ctx *middleware.Context, proxyPath string, route *plugins
 				ctx.JsonApiErr(500, "failed to get AppSettings.", err)
 				return
 			}
-
-			err = t.Execute(&contentBuf, query.Result.JsonData)
+			type templateData struct {
+				JsonData       map[string]interface{}
+				SecureJsonData map[string]string
+			}
+			data := templateData{
+				JsonData:       query.Result.JsonData,
+				SecureJsonData: query.Result.SecureJsonData.Decrypt(),
+			}
+			err = t.Execute(&contentBuf, data)
 			if err != nil {
 				ctx.JsonApiErr(500, fmt.Sprintf("failed to execute header content template for header %s.", header.Name), err)
 				return

+ 24 - 9
pkg/models/app_settings.go

@@ -3,6 +3,9 @@ package models
 import (
 	"errors"
 	"time"
+
+	"github.com/grafana/grafana/pkg/setting"
+	"github.com/grafana/grafana/pkg/util"
 )
 
 var (
@@ -10,25 +13,37 @@ var (
 )
 
 type AppSettings struct {
-	Id       int64
-	AppId    string
-	OrgId    int64
-	Enabled  bool
-	Pinned   bool
-	JsonData map[string]interface{}
+	Id             int64
+	AppId          string
+	OrgId          int64
+	Enabled        bool
+	Pinned         bool
+	JsonData       map[string]interface{}
+	SecureJsonData SecureJsonData
 
 	Created time.Time
 	Updated time.Time
 }
 
+type SecureJsonData map[string][]byte
+
+func (s SecureJsonData) Decrypt() map[string]string {
+	decrypted := make(map[string]string)
+	for key, data := range s {
+		decrypted[key] = string(util.Decrypt(data, setting.SecretKey))
+	}
+	return decrypted
+}
+
 // ----------------------
 // COMMANDS
 
 // Also acts as api DTO
 type UpdateAppSettingsCmd struct {
-	Enabled  bool                   `json:"enabled"`
-	Pinned   bool                   `json:"pinned"`
-	JsonData map[string]interface{} `json:"jsonData"`
+	Enabled        bool                   `json:"enabled"`
+	Pinned         bool                   `json:"pinned"`
+	JsonData       map[string]interface{} `json:"jsonData"`
+	SecureJsonData map[string]string      `json:"secureJsonData"`
 
 	AppId string `json:"-"`
 	OrgId int64  `json:"-"`

+ 18 - 7
pkg/services/sqlstore/app_settings.go

@@ -5,6 +5,8 @@ import (
 
 	"github.com/grafana/grafana/pkg/bus"
 	m "github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/setting"
+	"github.com/grafana/grafana/pkg/util"
 )
 
 func init() {
@@ -40,18 +42,27 @@ func UpdateAppSettings(cmd *m.UpdateAppSettingsCmd) error {
 		sess.UseBool("enabled")
 		sess.UseBool("pinned")
 		if !exists {
+			// encrypt secureJsonData
+			secureJsonData := make(map[string][]byte)
+			for key, data := range cmd.SecureJsonData {
+				secureJsonData[key] = util.Encrypt([]byte(data), setting.SecretKey)
+			}
 			app = m.AppSettings{
-				AppId:    cmd.AppId,
-				OrgId:    cmd.OrgId,
-				Enabled:  cmd.Enabled,
-				Pinned:   cmd.Pinned,
-				JsonData: cmd.JsonData,
-				Created:  time.Now(),
-				Updated:  time.Now(),
+				AppId:          cmd.AppId,
+				OrgId:          cmd.OrgId,
+				Enabled:        cmd.Enabled,
+				Pinned:         cmd.Pinned,
+				JsonData:       cmd.JsonData,
+				SecureJsonData: secureJsonData,
+				Created:        time.Now(),
+				Updated:        time.Now(),
 			}
 			_, err = sess.Insert(&app)
 			return err
 		} else {
+			for key, data := range cmd.SecureJsonData {
+				app.SecureJsonData[key] = util.Encrypt([]byte(data), setting.SecretKey)
+			}
 			app.Updated = time.Now()
 			app.Enabled = cmd.Enabled
 			app.JsonData = cmd.JsonData

+ 6 - 3
pkg/services/sqlstore/migrations/app_settings.go

@@ -4,7 +4,7 @@ import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
 
 func addAppSettingsMigration(mg *Migrator) {
 
-	appSettingsV1 := Table{
+	appSettingsV2 := Table{
 		Name: "app_settings",
 		Columns: []*Column{
 			{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
@@ -13,6 +13,7 @@ func addAppSettingsMigration(mg *Migrator) {
 			{Name: "enabled", Type: DB_Bool, Nullable: false},
 			{Name: "pinned", Type: DB_Bool, Nullable: false},
 			{Name: "json_data", Type: DB_Text, Nullable: true},
+			{Name: "secure_json_data", Type: DB_Text, Nullable: true},
 			{Name: "created", Type: DB_DateTime, Nullable: false},
 			{Name: "updated", Type: DB_DateTime, Nullable: false},
 		},
@@ -21,8 +22,10 @@ func addAppSettingsMigration(mg *Migrator) {
 		},
 	}
 
-	mg.AddMigration("create app_settings table v1", NewAddTableMigration(appSettingsV1))
+	mg.AddMigration("Drop old table app_settings v1", NewDropTableMigration("app_settings"))
+
+	mg.AddMigration("create app_settings table v2", NewAddTableMigration(appSettingsV2))
 
 	//-------  indexes ------------------
-	addTableIndicesMigrations(mg, "v3", appSettingsV1)
+	addTableIndicesMigrations(mg, "v3", appSettingsV2)
 }

+ 66 - 0
pkg/util/encryption.go

@@ -0,0 +1,66 @@
+package util
+
+import (
+	"crypto/aes"
+	"crypto/cipher"
+	"crypto/rand"
+	"crypto/sha256"
+	"io"
+
+	"github.com/grafana/grafana/pkg/log"
+)
+
+const saltLength = 8
+
+func Decrypt(payload []byte, secret string) []byte {
+	salt := payload[:saltLength]
+	key := encryptionKeyToBytes(secret, string(salt))
+
+	block, err := aes.NewCipher(key)
+	if err != nil {
+		log.Fatal(4, err.Error())
+	}
+
+	// The IV needs to be unique, but not secure. Therefore it's common to
+	// include it at the beginning of the ciphertext.
+	if len(payload) < aes.BlockSize {
+		log.Fatal(4, "payload too short")
+	}
+	iv := payload[saltLength : saltLength+aes.BlockSize]
+	payload = payload[saltLength+aes.BlockSize:]
+
+	stream := cipher.NewCFBDecrypter(block, iv)
+
+	// XORKeyStream can work in-place if the two arguments are the same.
+	stream.XORKeyStream(payload, payload)
+	return payload
+}
+
+func Encrypt(payload []byte, secret string) []byte {
+	salt := GetRandomString(saltLength)
+
+	key := encryptionKeyToBytes(secret, salt)
+	block, err := aes.NewCipher(key)
+	if err != nil {
+		log.Fatal(4, err.Error())
+	}
+
+	// The IV needs to be unique, but not secure. Therefore it's common to
+	// include it at the beginning of the ciphertext.
+	ciphertext := make([]byte, saltLength+aes.BlockSize+len(payload))
+	copy(ciphertext[:saltLength], []byte(salt))
+	iv := ciphertext[saltLength : saltLength+aes.BlockSize]
+	if _, err := io.ReadFull(rand.Reader, iv); err != nil {
+		log.Fatal(4, err.Error())
+	}
+
+	stream := cipher.NewCFBEncrypter(block, iv)
+	stream.XORKeyStream(ciphertext[saltLength+aes.BlockSize:], payload)
+
+	return ciphertext
+}
+
+// Key needs to be 32bytes
+func encryptionKeyToBytes(secret, salt string) []byte {
+	return PBKDF2([]byte(secret), []byte(salt), 10000, 32, sha256.New)
+}

+ 27 - 0
pkg/util/encryption_test.go

@@ -0,0 +1,27 @@
+package util
+
+import (
+	"testing"
+
+	. "github.com/smartystreets/goconvey/convey"
+)
+
+func TestEncryption(t *testing.T) {
+
+	Convey("When getting encryption key", t, func() {
+
+		key := encryptionKeyToBytes("secret", "salt")
+		So(len(key), ShouldEqual, 32)
+
+		key = encryptionKeyToBytes("a very long secret key that is larger then 32bytes", "salt")
+		So(len(key), ShouldEqual, 32)
+	})
+
+	Convey("When decrypting basic payload", t, func() {
+		encrypted := Encrypt([]byte("grafana"), "1234")
+		decrypted := Decrypt(encrypted, "1234")
+
+		So(string(decrypted), ShouldEqual, "grafana")
+	})
+
+}

+ 1 - 0
public/app/features/apps/edit_ctrl.ts

@@ -24,6 +24,7 @@ export class AppEditCtrl {
       enabled: this.appModel.enabled,
       pinned: this.appModel.pinned,
       jsonData: this.appModel.jsonData,
+      secureJsonData: this.appModel.secureJsonData,
     }, options);
 
     this.backendSrv.post(`/api/org/apps/${this.$routeParams.appId}/settings`, updateCmd).then(function() {