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 ...@@ -94,8 +94,15 @@ func NewApiPluginProxy(ctx *middleware.Context, proxyPath string, route *plugins
ctx.JsonApiErr(500, "failed to get AppSettings.", err) ctx.JsonApiErr(500, "failed to get AppSettings.", err)
return return
} }
type templateData struct {
err = t.Execute(&contentBuf, query.Result.JsonData) 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 { if err != nil {
ctx.JsonApiErr(500, fmt.Sprintf("failed to execute header content template for header %s.", header.Name), err) ctx.JsonApiErr(500, fmt.Sprintf("failed to execute header content template for header %s.", header.Name), err)
return return
......
...@@ -3,6 +3,9 @@ package models ...@@ -3,6 +3,9 @@ package models
import ( import (
"errors" "errors"
"time" "time"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
) )
var ( var (
...@@ -10,25 +13,37 @@ var ( ...@@ -10,25 +13,37 @@ var (
) )
type AppSettings struct { type AppSettings struct {
Id int64 Id int64
AppId string AppId string
OrgId int64 OrgId int64
Enabled bool Enabled bool
Pinned bool Pinned bool
JsonData map[string]interface{} JsonData map[string]interface{}
SecureJsonData SecureJsonData
Created time.Time Created time.Time
Updated 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 // COMMANDS
// Also acts as api DTO // Also acts as api DTO
type UpdateAppSettingsCmd struct { type UpdateAppSettingsCmd struct {
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
Pinned bool `json:"pinned"` Pinned bool `json:"pinned"`
JsonData map[string]interface{} `json:"jsonData"` JsonData map[string]interface{} `json:"jsonData"`
SecureJsonData map[string]string `json:"secureJsonData"`
AppId string `json:"-"` AppId string `json:"-"`
OrgId int64 `json:"-"` OrgId int64 `json:"-"`
......
...@@ -5,6 +5,8 @@ import ( ...@@ -5,6 +5,8 @@ import (
"github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models" m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
) )
func init() { func init() {
...@@ -40,18 +42,27 @@ func UpdateAppSettings(cmd *m.UpdateAppSettingsCmd) error { ...@@ -40,18 +42,27 @@ func UpdateAppSettings(cmd *m.UpdateAppSettingsCmd) error {
sess.UseBool("enabled") sess.UseBool("enabled")
sess.UseBool("pinned") sess.UseBool("pinned")
if !exists { 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{ app = m.AppSettings{
AppId: cmd.AppId, AppId: cmd.AppId,
OrgId: cmd.OrgId, OrgId: cmd.OrgId,
Enabled: cmd.Enabled, Enabled: cmd.Enabled,
Pinned: cmd.Pinned, Pinned: cmd.Pinned,
JsonData: cmd.JsonData, JsonData: cmd.JsonData,
Created: time.Now(), SecureJsonData: secureJsonData,
Updated: time.Now(), Created: time.Now(),
Updated: time.Now(),
} }
_, err = sess.Insert(&app) _, err = sess.Insert(&app)
return err return err
} else { } else {
for key, data := range cmd.SecureJsonData {
app.SecureJsonData[key] = util.Encrypt([]byte(data), setting.SecretKey)
}
app.Updated = time.Now() app.Updated = time.Now()
app.Enabled = cmd.Enabled app.Enabled = cmd.Enabled
app.JsonData = cmd.JsonData app.JsonData = cmd.JsonData
......
...@@ -4,7 +4,7 @@ import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator" ...@@ -4,7 +4,7 @@ import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
func addAppSettingsMigration(mg *Migrator) { func addAppSettingsMigration(mg *Migrator) {
appSettingsV1 := Table{ appSettingsV2 := Table{
Name: "app_settings", Name: "app_settings",
Columns: []*Column{ Columns: []*Column{
{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, {Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
...@@ -13,6 +13,7 @@ func addAppSettingsMigration(mg *Migrator) { ...@@ -13,6 +13,7 @@ func addAppSettingsMigration(mg *Migrator) {
{Name: "enabled", Type: DB_Bool, Nullable: false}, {Name: "enabled", Type: DB_Bool, Nullable: false},
{Name: "pinned", Type: DB_Bool, Nullable: false}, {Name: "pinned", Type: DB_Bool, Nullable: false},
{Name: "json_data", Type: DB_Text, Nullable: true}, {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: "created", Type: DB_DateTime, Nullable: false},
{Name: "updated", Type: DB_DateTime, Nullable: false}, {Name: "updated", Type: DB_DateTime, Nullable: false},
}, },
...@@ -21,8 +22,10 @@ func addAppSettingsMigration(mg *Migrator) { ...@@ -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 ------------------ //------- 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 { ...@@ -24,6 +24,7 @@ export class AppEditCtrl {
enabled: this.appModel.enabled, enabled: this.appModel.enabled,
pinned: this.appModel.pinned, pinned: this.appModel.pinned,
jsonData: this.appModel.jsonData, jsonData: this.appModel.jsonData,
secureJsonData: this.appModel.secureJsonData,
}, options); }, options);
this.backendSrv.post(`/api/org/apps/${this.$routeParams.appId}/settings`, updateCmd).then(function() { 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