Commit 12460af0 by Torkel Ödegaard

Merge pull request #3830 from raintank/apiPlugin

Add secureJsonData field to appSettings model
parents 34eb5ace 2e9272c7
......@@ -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
......
......@@ -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:"-"`
......
......@@ -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
......
......@@ -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)
}
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)
}
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")
})
}
......@@ -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() {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment