V8.5.12 security patch (#486) (#55495)

* Data source: prevent from using auth proxy header as custom data source header (#477)

* apply security changes for auth proxy permission escalation

* add links to CVE

* remove duplicate check

* apply security fix for admin only folder migration (#484)

Co-authored-by: Karl Persson <kalle.persson@grafana.com>

Co-authored-by: Karl Persson <kalle.persson@grafana.com>
This commit is contained in:
Ieva
2022-09-20 16:40:52 +01:00
committed by GitHub
co-authored by Karl Persson
parent 8806b8fc1b
commit 3282afc648
10 changed files with 309 additions and 73 deletions
+30
View File
@@ -4,19 +4,23 @@ import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"sort"
"strconv"
"strings"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/api/datasource"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins/adapters"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/datasources/permissions"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/web"
)
@@ -241,6 +245,26 @@ func validateURL(cmdType string, url string) response.Response {
return nil
}
// validateJSONData prevents the user from adding a custom header with name that matches the auth proxy header name.
// This is done to prevent data source proxy from being used to circumvent auth proxy.
// For more context take a look at CVE-2022-35957
func validateJSONData(jsonData *simplejson.Json, cfg *setting.Cfg) error {
if jsonData == nil || !cfg.AuthProxyEnabled {
return nil
}
for key, value := range jsonData.MustMap() {
if strings.HasPrefix(key, "httpHeaderName") {
header := fmt.Sprint(value)
if http.CanonicalHeaderKey(header) == http.CanonicalHeaderKey(cfg.AuthProxyHeaderName) {
datasourcesLogger.Error("Forbidden to add a data source header with a name equal to auth proxy header name", "headerName", key)
return errors.New("validation error, invalid header name specified")
}
}
}
return nil
}
// POST /api/datasources/
func (hs *HTTPServer) AddDataSource(c *models.ReqContext) response.Response {
cmd := models.AddDataSourceCommand{}
@@ -256,6 +280,9 @@ func (hs *HTTPServer) AddDataSource(c *models.ReqContext) response.Response {
return resp
}
}
if err := validateJSONData(cmd.JsonData, hs.Cfg); err != nil {
return response.Error(http.StatusBadRequest, "Failed to add datasource", err)
}
if err := hs.DataSourcesService.AddDataSource(c.Req.Context(), &cmd); err != nil {
if errors.Is(err, models.ErrDataSourceNameExists) || errors.Is(err, models.ErrDataSourceUidExists) {
@@ -289,6 +316,9 @@ func (hs *HTTPServer) UpdateDataSource(c *models.ReqContext) response.Response {
if resp := validateURL(cmd.Type, cmd.Url); resp != nil {
return resp
}
if err := validateJSONData(cmd.JsonData, hs.Cfg); err != nil {
return response.Error(http.StatusBadRequest, "Failed to update datasource", err)
}
ds, err := hs.getRawDataSourceById(c.Req.Context(), cmd.Id, cmd.OrgId)
if err != nil {
+65
View File
@@ -15,6 +15,7 @@ import (
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/datasources"
@@ -82,6 +83,7 @@ func TestAddDataSource_InvalidURL(t *testing.T) {
sc := setupScenarioContext(t, "/api/datasources")
hs := &HTTPServer{
DataSourcesService: &dataSourcesServiceMock{},
Cfg: setting.NewCfg(),
}
sc.m.Post(sc.url, routing.Wrap(func(c *models.ReqContext) response.Response {
@@ -108,6 +110,7 @@ func TestAddDataSource_URLWithoutProtocol(t *testing.T) {
DataSourcesService: &dataSourcesServiceMock{
expectedDatasource: &models.DataSource{},
},
Cfg: setting.NewCfg(),
}
sc := setupScenarioContext(t, "/api/datasources")
@@ -127,10 +130,42 @@ func TestAddDataSource_URLWithoutProtocol(t *testing.T) {
assert.Equal(t, 200, sc.resp.Code)
}
// Using a custom header whose name matches the name specified for auth proxy header should fail
func TestAddDataSource_InvalidJSONData(t *testing.T) {
hs := &HTTPServer{
DataSourcesService: &dataSourcesServiceMock{},
Cfg: setting.NewCfg(),
}
sc := setupScenarioContext(t, "/api/datasources")
hs.Cfg = setting.NewCfg()
hs.Cfg.AuthProxyEnabled = true
hs.Cfg.AuthProxyHeaderName = "X-AUTH-PROXY-HEADER"
jsonData := simplejson.New()
jsonData.Set("httpHeaderName1", hs.Cfg.AuthProxyHeaderName)
sc.m.Post(sc.url, routing.Wrap(func(c *models.ReqContext) response.Response {
c.Req.Body = mockRequestBody(models.AddDataSourceCommand{
Name: "Test",
Url: "localhost:5432",
Access: "direct",
Type: "test",
JsonData: jsonData,
})
return hs.AddDataSource(c)
}))
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
assert.Equal(t, 400, sc.resp.Code)
}
// Updating data sources with invalid URLs should lead to an error.
func TestUpdateDataSource_InvalidURL(t *testing.T) {
hs := &HTTPServer{
DataSourcesService: &dataSourcesServiceMock{},
Cfg: setting.NewCfg(),
}
sc := setupScenarioContext(t, "/api/datasources/1234")
@@ -149,6 +184,35 @@ func TestUpdateDataSource_InvalidURL(t *testing.T) {
assert.Equal(t, 400, sc.resp.Code)
}
// Using a custom header whose name matches the name specified for auth proxy header should fail
func TestUpdateDataSource_InvalidJSONData(t *testing.T) {
hs := &HTTPServer{
DataSourcesService: &dataSourcesServiceMock{},
Cfg: setting.NewCfg(),
}
sc := setupScenarioContext(t, "/api/datasources/1234")
hs.Cfg.AuthProxyEnabled = true
hs.Cfg.AuthProxyHeaderName = "X-AUTH-PROXY-HEADER"
jsonData := simplejson.New()
jsonData.Set("httpHeaderName1", hs.Cfg.AuthProxyHeaderName)
sc.m.Put(sc.url, routing.Wrap(func(c *models.ReqContext) response.Response {
c.Req.Body = mockRequestBody(models.AddDataSourceCommand{
Name: "Test",
Url: "localhost:5432",
Access: "direct",
Type: "test",
JsonData: jsonData,
})
return hs.AddDataSource(c)
}))
sc.fakeReqWithParams("PUT", sc.url, map[string]string{}).exec()
assert.Equal(t, 400, sc.resp.Code)
}
// Updating data sources with URLs not specifying protocol should work.
func TestUpdateDataSource_URLWithoutProtocol(t *testing.T) {
const name = "Test"
@@ -158,6 +222,7 @@ func TestUpdateDataSource_URLWithoutProtocol(t *testing.T) {
DataSourcesService: &dataSourcesServiceMock{
expectedDatasource: &models.DataSource{},
},
Cfg: setting.NewCfg(),
}
sc := setupScenarioContext(t, "/api/datasources/1234")
@@ -309,7 +309,7 @@ func (s *Service) httpClientOptions(ds *models.DataSource) (*sdkhttpclient.Optio
}
opts := &sdkhttpclient.Options{
Timeouts: timeouts,
Headers: s.getCustomHeaders(ds.JsonData, s.DecryptedValues(ds)),
Headers: GetCustomHeaders(ds.JsonData, s.DecryptedValues(ds), s.cfg),
Labels: map[string]string{
"datasource_name": ds.Name,
"datasource_uid": ds.Uid,
@@ -438,16 +438,17 @@ func (s *Service) getTimeout(ds *models.DataSource) time.Duration {
return time.Duration(timeout) * time.Second
}
// getCustomHeaders returns a map with all the to be set headers
// GetCustomHeaders returns a map with all the to be set headers
// The map key represents the HeaderName and the value represents this header's value
func (s *Service) getCustomHeaders(jsonData *simplejson.Json, decryptedValues map[string]string) map[string]string {
func GetCustomHeaders(jsonData *simplejson.Json, decryptedValues map[string]string, cfg *setting.Cfg) map[string]string {
headers := make(map[string]string)
if jsonData == nil {
return headers
}
index := 1
index := 0
for {
index++
headerNameSuffix := fmt.Sprintf("httpHeaderName%d", index)
headerValueSuffix := fmt.Sprintf("httpHeaderValue%d", index)
@@ -457,10 +458,16 @@ func (s *Service) getCustomHeaders(jsonData *simplejson.Json, decryptedValues ma
break
}
// skip a header with name that corresponds to auth proxy header's name
// to make sure that data source proxy isn't used to circumvent auth proxy.
// For more context take a look at CVE-2022-35957
if cfg.AuthProxyEnabled && http.CanonicalHeaderKey(key) == http.CanonicalHeaderKey(cfg.AuthProxyHeaderName) {
continue
}
if val, ok := decryptedValues[headerValueSuffix]; ok {
headers[key] = val
}
index++
}
return headers
@@ -445,7 +445,7 @@ func TestService_GetHttpTransport(t *testing.T) {
SecureJsonData: map[string][]byte{"httpHeaderValue1": encryptedData},
}
headers := dsService.getCustomHeaders(json, map[string]string{"httpHeaderValue1": "Bearer xf5yhfkpsnmgo"})
headers := GetCustomHeaders(json, map[string]string{"httpHeaderValue1": "Bearer xf5yhfkpsnmgo"}, dsService.cfg)
require.Equal(t, "Bearer xf5yhfkpsnmgo", headers["Authorization"])
// 1. Start HTTP test server which checks the request headers
@@ -537,6 +537,83 @@ func TestService_GetHttpTransport(t *testing.T) {
require.NotNil(t, configuredOpts.SigV4)
require.Equal(t, "es", configuredOpts.SigV4.Service)
})
t.Run("Should not add a header with key that corresponds to auth proxies key name", func(t *testing.T) {
provider := httpclient.NewProvider()
cfg.AuthProxyEnabled = true
cfg.AuthProxyHeaderName = "X-AUTH-PROXY-HEADER"
allowedHeaderName := "X-ALLOWED-HEADER"
allowedHeaderValue := "X-ALLOWED-HEADER_value"
jsonData := simplejson.New()
jsonData.Set("httpHeaderName1", allowedHeaderName)
jsonData.Set("httpHeaderName2", cfg.AuthProxyHeaderName)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := ProvideService(nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
encryptedJsonData, err := secretsService.EncryptJsonData(
context.Background(),
map[string]string{
"httpHeaderValue1": allowedHeaderValue,
"httpHeaderValue2": "admin",
}, secrets.WithoutScope())
require.NoError(t, err)
ds := models.DataSource{
Id: 1,
Url: "http://k8s:8001",
Type: "Kubernetes",
JsonData: jsonData,
SecureJsonData: encryptedJsonData,
}
headers := GetCustomHeaders(jsonData, map[string]string{"httpHeaderValue2": "admin", "httpHeaderValue1": "X-ALLOWED-HEADER_value"}, dsService.cfg)
require.Equal(t, "", headers[cfg.AuthProxyHeaderName], "header with auth proxy header name should have been stripped")
require.Equal(t, allowedHeaderValue, headers[allowedHeaderName], "other headers should stay intact")
// 1. Start HTTP test server which checks the request headers
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get(cfg.AuthProxyHeaderName) != "" {
w.WriteHeader(http.StatusInternalServerError)
_, err := w.Write([]byte("Did not expect the header to be set"))
require.NoError(t, err)
return
}
if r.Header.Get(allowedHeaderName) != allowedHeaderValue {
w.WriteHeader(http.StatusInternalServerError)
_, err := w.Write([]byte("Expected the allowed header to be set"))
require.NoError(t, err)
return
}
w.WriteHeader(200)
_, err := w.Write([]byte("Ok"))
require.NoError(t, err)
}))
defer backend.Close()
// 2. Get HTTP transport from datasource which uses the test server as backend
ds.Url = backend.URL
rt, err := dsService.GetHTTPTransport(&ds, provider)
require.NoError(t, err)
require.NotNil(t, rt)
// 3. Send test request which should not have a header with name that matches auth proxy header
req := httptest.NewRequest("GET", backend.URL+"/test-headers", nil)
res, err := rt.RoundTrip(req)
require.NoError(t, err)
t.Cleanup(func() {
err := res.Body.Close()
require.NoError(t, err)
})
body, err := ioutil.ReadAll(res.Body)
require.NoError(t, err)
bodyStr := string(body)
require.Equal(t, "Ok", bodyStr)
})
}
func TestService_getTimeout(t *testing.T) {
+9 -35
View File
@@ -11,12 +11,12 @@ import (
"strings"
"time"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/expr"
"github.com/grafana/grafana/pkg/expr/classic"
"github.com/grafana/grafana/pkg/infra/log"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/datasources/service"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/secrets"
"github.com/grafana/grafana/pkg/setting"
@@ -147,7 +147,7 @@ type AlertExecCtx struct {
}
// GetExprRequest validates the condition, gets the datasource information and creates an expr.Request from it.
func GetExprRequest(ctx AlertExecCtx, data []models.AlertQuery, now time.Time, dsCacheService datasources.CacheService, secretsService secrets.Service) (*expr.Request, error) {
func GetExprRequest(ctx AlertExecCtx, data []models.AlertQuery, now time.Time, dsCacheService datasources.CacheService, secretsService secrets.Service, cfg *setting.Cfg) (*expr.Request, error) {
req := &expr.Request{
OrgId: ctx.OrgID,
Headers: map[string]string{
@@ -197,7 +197,7 @@ func GetExprRequest(ctx AlertExecCtx, data []models.AlertQuery, now time.Time, d
if err != nil {
return nil, err
}
customHeaders := getCustomHeaders(ds.JsonData, decryptedData)
customHeaders := service.GetCustomHeaders(ds.JsonData, decryptedData, cfg)
for k, v := range customHeaders {
if _, ok := req.Headers[k]; !ok {
req.Headers[k] = v
@@ -220,40 +220,14 @@ func GetExprRequest(ctx AlertExecCtx, data []models.AlertQuery, now time.Time, d
return req, nil
}
func getCustomHeaders(jsonData *simplejson.Json, decryptedValues map[string]string) map[string]string {
headers := make(map[string]string)
if jsonData == nil {
return headers
}
index := 1
for {
headerNameSuffix := fmt.Sprintf("httpHeaderName%d", index)
headerValueSuffix := fmt.Sprintf("httpHeaderValue%d", index)
key := jsonData.Get(headerNameSuffix).MustString()
if key == "" {
// No (more) header values are available
break
}
if val, ok := decryptedValues[headerValueSuffix]; ok {
headers[key] = val
}
index++
}
return headers
}
type NumberValueCapture struct {
Var string // RefID
Labels data.Labels
Value *float64
}
func executeCondition(ctx AlertExecCtx, c *models.Condition, now time.Time, exprService *expr.Service, dsCacheService datasources.CacheService, secretsService secrets.Service) ExecutionResults {
execResp, err := executeQueriesAndExpressions(ctx, c.Data, now, exprService, dsCacheService, secretsService)
func executeCondition(ctx AlertExecCtx, c *models.Condition, now time.Time, exprService *expr.Service, dsCacheService datasources.CacheService, secretsService secrets.Service, cfg *setting.Cfg) ExecutionResults {
execResp, err := executeQueriesAndExpressions(ctx, c.Data, now, exprService, dsCacheService, secretsService, cfg)
if err != nil {
return ExecutionResults{Error: err}
}
@@ -336,7 +310,7 @@ func executeCondition(ctx AlertExecCtx, c *models.Condition, now time.Time, expr
return result
}
func executeQueriesAndExpressions(ctx AlertExecCtx, data []models.AlertQuery, now time.Time, exprService *expr.Service, dsCacheService datasources.CacheService, secretsService secrets.Service) (resp *backend.QueryDataResponse, err error) {
func executeQueriesAndExpressions(ctx AlertExecCtx, data []models.AlertQuery, now time.Time, exprService *expr.Service, dsCacheService datasources.CacheService, secretsService secrets.Service, cfg *setting.Cfg) (resp *backend.QueryDataResponse, err error) {
defer func() {
if e := recover(); e != nil {
ctx.Log.Error("alert rule panic", "error", e, "stack", string(debug.Stack()))
@@ -349,7 +323,7 @@ func executeQueriesAndExpressions(ctx AlertExecCtx, data []models.AlertQuery, no
}
}()
queryDataReq, err := GetExprRequest(ctx, data, now, dsCacheService, secretsService)
queryDataReq, err := GetExprRequest(ctx, data, now, dsCacheService, secretsService, cfg)
if err != nil {
return nil, err
}
@@ -590,7 +564,7 @@ func (e *evaluatorImpl) ConditionEval(condition *models.Condition, now time.Time
alertExecCtx := AlertExecCtx{OrgID: condition.OrgID, Ctx: alertCtx, ExpressionsEnabled: e.cfg.ExpressionsEnabled, Log: e.log}
execResult := executeCondition(alertExecCtx, condition, now, expressionService, e.dataSourceCache, e.secretsService)
execResult := executeCondition(alertExecCtx, condition, now, expressionService, e.dataSourceCache, e.secretsService, e.cfg)
evalResults := evaluateExecutionResult(execResult, now)
return evalResults, nil
@@ -603,7 +577,7 @@ func (e *evaluatorImpl) QueriesAndExpressionsEval(orgID int64, data []models.Ale
alertExecCtx := AlertExecCtx{OrgID: orgID, Ctx: alertCtx, ExpressionsEnabled: e.cfg.ExpressionsEnabled, Log: e.log}
execResult, err := executeQueriesAndExpressions(alertExecCtx, data, now, expressionService, e.dataSourceCache, e.secretsService)
execResult, err := executeQueriesAndExpressions(alertExecCtx, data, now, expressionService, e.dataSourceCache, e.secretsService, e.cfg)
if err != nil {
return nil, fmt.Errorf("failed to execute conditions: %w", err)
}
+2 -27
View File
@@ -3,7 +3,6 @@ package query
import (
"context"
"fmt"
"strings"
"time"
"github.com/grafana/grafana/pkg/api/dtos"
@@ -14,6 +13,7 @@ import (
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/adapters"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/datasources/service"
"github.com/grafana/grafana/pkg/services/oauthtoken"
"github.com/grafana/grafana/pkg/services/secrets"
"github.com/grafana/grafana/pkg/setting"
@@ -23,11 +23,6 @@ import (
"github.com/grafana/grafana-plugin-sdk-go/backend"
)
const (
headerName = "httpHeaderName"
headerValue = "httpHeaderValue"
)
func ProvideService(
cfg *setting.Cfg,
dataSourceCache datasources.CacheService,
@@ -146,7 +141,7 @@ func (s *Service) handleQueryData(ctx context.Context, user *models.SignedInUser
}
}
for k, v := range customHeaders(ds.JsonData, instanceSettings.DecryptedSecureJSONData) {
for k, v := range service.GetCustomHeaders(ds.JsonData, instanceSettings.DecryptedSecureJSONData, s.cfg) {
req.Headers[k] = v
}
@@ -167,26 +162,6 @@ type parsedRequest struct {
parsedQueries []parsedQuery
}
func customHeaders(jsonData *simplejson.Json, decryptedJsonData map[string]string) map[string]string {
if jsonData == nil {
return nil
}
data := jsonData.MustMap()
headers := map[string]string{}
for k := range data {
if strings.HasPrefix(k, headerName) {
if header, ok := data[k].(string); ok {
valueKey := strings.ReplaceAll(k, headerName, headerValue)
headers[header] = decryptedJsonData[valueKey]
}
}
}
return headers
}
func (s *Service) parseMetricRequest(ctx context.Context, user *models.SignedInUser, skipCache bool, reqDTO dtos.MetricRequest) (*parsedRequest, error) {
if len(reqDTO.Queries) == 0 {
return nil, NewErrBadQuery("no queries found")
+2 -1
View File
@@ -14,6 +14,7 @@ import (
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/services/query"
"github.com/grafana/grafana/pkg/services/secrets"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/require"
)
@@ -64,7 +65,7 @@ func setup() *testContext {
dataSourceCache: dc,
oauthTokenService: tc,
pluginRequestValidator: rv,
queryService: query.ProvideService(nil, dc, nil, rv, sc, pc, tc),
queryService: query.ProvideService(setting.NewCfg(), dc, nil, rv, sc, pc, tc),
}
}
@@ -0,0 +1,100 @@
package accesscontrol
import (
"strings"
"xorm.io/xorm"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
)
func AddAdminOnlyMigration(mg *migrator.Migrator) {
mg.AddMigration("admin only folder/dashboard permission", &adminOnlyMigrator{})
}
type adminOnlyMigrator struct {
migrator.MigrationBase
}
func (m *adminOnlyMigrator) SQL(dialect migrator.Dialect) string {
return CodeMigrationSQL
}
func (m *adminOnlyMigrator) Exec(sess *xorm.Session, mg *migrator.Migrator) error {
logger := log.New("admin-permissions-only-migrator")
type model struct {
UID string `xorm:"uid"`
OrgID int64 `xorm:"org_id"`
IsFolder bool `xorm:"is_folder"`
}
var models []model
// Find all dashboards and folders that should have only admin permission in acl
// When a dashboard or folder only has admin permission the acl table should be empty and the has_acl set to true
sql := `
SELECT res.uid, res.is_folder, res.org_id
FROM (SELECT dashboard.id, dashboard.uid, dashboard.is_folder, dashboard.org_id, count(dashboard_acl.id) as count
FROM dashboard
LEFT JOIN dashboard_acl ON dashboard.id = dashboard_acl.dashboard_id
WHERE dashboard.has_acl IS TRUE
GROUP BY dashboard.id) as res
WHERE res.count = 0
`
if err := sess.SQL(sql).Find(&models); err != nil {
return err
}
for _, model := range models {
var scope string
// set scope based on type
if model.IsFolder {
scope = "folders:uid:" + model.UID
} else {
scope = "dashboards:uid:" + model.UID
}
// Find all managed editor and viewer permissions with scopes to folder or dashboard
sql = `
SELECT r.id
FROM role r
LEFT JOIN permission p on r.id = p.role_id
WHERE p.scope = ?
AND r.org_id = ?
AND r.name IN ('managed:builtins:editor:permissions', 'managed:builtins:viewer:permissions')
GROUP BY r.id
`
var roleIDS []int64
if err := sess.SQL(sql, scope, model.OrgID).Find(&roleIDS); err != nil {
return err
}
if len(roleIDS) == 0 {
continue
}
msg := "removing viewer and editor permissions on "
if model.IsFolder {
msg += "folder"
} else {
msg += "dashboard"
}
logger.Info(msg, "uid", model.UID)
// Remove managed permission for editors and viewers if there was any
removeSQL := `DELETE FROM permission WHERE scope = ? AND role_id IN(?` + strings.Repeat(", ?", len(roleIDS)-1) + `) `
params := []interface{}{removeSQL, scope}
for _, id := range roleIDS {
params = append(params, id)
}
if _, err := sess.Exec(params...); err != nil {
return err
}
}
return nil
}
@@ -69,6 +69,7 @@ type dashboard struct {
FolderID int64 `xorm:"folder_id"`
OrgID int64 `xorm:"org_id"`
IsFolder bool
HasAcl bool `xorm:"has_acl"`
}
func (m dashboardPermissionsMigrator) Exec(sess *xorm.Session, migrator *migrator.Migrator) error {
@@ -76,8 +77,8 @@ func (m dashboardPermissionsMigrator) Exec(sess *xorm.Session, migrator *migrato
m.dialect = migrator.Dialect
var dashboards []dashboard
if err := m.sess.SQL("SELECT id, is_folder, folder_id, org_id FROM dashboard").Find(&dashboards); err != nil {
return err
if err := m.sess.SQL("SELECT id, is_folder, folder_id, org_id, has_acl FROM dashboard").Find(&dashboards); err != nil {
return fmt.Errorf("failed to list dashboards: %w", err)
}
var acl []models.DashboardAcl
@@ -108,7 +109,7 @@ func (m dashboardPermissionsMigrator) migratePermissions(dashboards []dashboard,
permissionMap[d.OrgID] = map[string][]*ac.Permission{}
}
if (d.IsFolder || d.FolderID == 0) && len(acls) == 0 {
if (d.IsFolder || d.FolderID == 0) && len(acls) == 0 && !d.HasAcl {
permissionMap[d.OrgID]["managed:builtins:editor:permissions"] = append(
permissionMap[d.OrgID]["managed:builtins:editor:permissions"],
m.mapPermission(d.ID, models.PERMISSION_EDIT, d.IsFolder)...,
@@ -91,7 +91,13 @@ func (*OSSMigrations) AddMigration(mg *Migrator) {
addCommentMigrations(mg)
}
}
accesscontrol.AddManagedFolderAlertActionsMigration(mg)
if mg.Cfg != nil && mg.Cfg.IsFeatureToggleEnabled != nil {
if mg.Cfg.IsFeatureToggleEnabled(featuremgmt.FlagAccesscontrol) {
accesscontrol.AddManagedFolderAlertActionsMigration(mg)
accesscontrol.AddAdminOnlyMigration(mg)
}
}
}
func addMigrationLogMigrations(mg *Migrator) {