mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Remove vendored models in migration service (#74503)
This PR replaces the vendored models in the migration with their equivalent ngalert models. It also replaces the raw SQL selects and inserts with service calls. It also fills in some gaps in the testing suite around: - Migration of alert rules: verifying that the actual data model (queries, conditions) are correct 9a7cfa9 - Secure settings migration: verifying that secure fields remain encrypted for all available notifiers and certain fields migrate from plain text to encrypted secure settings correctly e7d3993 Replacing the checks for custom dashboard ACLs will be replaced in a separate targeted PR as it will be complex enough alone.
This commit is contained in:
@@ -1,284 +0,0 @@
|
||||
package ualert
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"xorm.io/xorm"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/infra/metrics"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
dashver "github.com/grafana/grafana/pkg/services/dashboardversion"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
type roleType string
|
||||
|
||||
const (
|
||||
RoleNone roleType = "None"
|
||||
RoleViewer roleType = "Viewer"
|
||||
RoleEditor roleType = "Editor"
|
||||
RoleAdmin roleType = "Admin"
|
||||
)
|
||||
|
||||
func (r roleType) IsValid() bool {
|
||||
return r == RoleViewer || r == RoleAdmin || r == RoleEditor || r == RoleNone
|
||||
}
|
||||
|
||||
type permissionType int
|
||||
|
||||
type dashboardACL struct {
|
||||
// nolint:stylecheck
|
||||
Id int64
|
||||
OrgID int64 `xorm:"org_id"`
|
||||
DashboardID int64 `xorm:"dashboard_id"`
|
||||
|
||||
UserID int64 `xorm:"user_id"`
|
||||
TeamID int64 `xorm:"team_id"`
|
||||
Role *roleType // pointer to be nullable
|
||||
Permission permissionType
|
||||
|
||||
Created time.Time
|
||||
Updated time.Time
|
||||
}
|
||||
|
||||
func (p dashboardACL) TableName() string { return "dashboard_acl" }
|
||||
|
||||
type folderHelper struct {
|
||||
sess *xorm.Session
|
||||
mg *migrator.Migrator
|
||||
}
|
||||
|
||||
// getOrCreateGeneralFolder returns the general folder under the specific organisation
|
||||
// If the general folder does not exist it creates it.
|
||||
func (m *folderHelper) getOrCreateGeneralFolder(orgID int64) (*dashboard, error) {
|
||||
// there is a unique constraint on org_id, folder_id, title
|
||||
// there are no nested folders so the parent folder id is always 0
|
||||
dashboard := dashboard{OrgId: orgID, FolderId: 0, Title: GENERAL_FOLDER}
|
||||
has, err := m.sess.Get(&dashboard)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
// create folder
|
||||
return m.createGeneralFolder(orgID)
|
||||
}
|
||||
return &dashboard, nil
|
||||
}
|
||||
|
||||
func (m *folderHelper) createGeneralFolder(orgID int64) (*dashboard, error) {
|
||||
return m.createFolder(orgID, GENERAL_FOLDER)
|
||||
}
|
||||
|
||||
// returns the folder of the given dashboard (if exists)
|
||||
func (m *folderHelper) getFolder(dash dashboard, da dashAlert) (dashboard, error) {
|
||||
// get folder if exists
|
||||
folder := dashboard{}
|
||||
if dash.FolderId > 0 {
|
||||
exists, err := m.sess.Where("id=?", dash.FolderId).Get(&folder)
|
||||
if err != nil {
|
||||
return folder, fmt.Errorf("failed to get folder %d: %w", dash.FolderId, err)
|
||||
}
|
||||
if !exists {
|
||||
return folder, fmt.Errorf("folder with id %v not found", dash.FolderId)
|
||||
}
|
||||
if !folder.IsFolder {
|
||||
return folder, fmt.Errorf("id %v is a dashboard not a folder", dash.FolderId)
|
||||
}
|
||||
}
|
||||
return folder, nil
|
||||
}
|
||||
|
||||
// based on sqlstore.saveDashboard()
|
||||
// it should be called from inside a transaction
|
||||
func (m *folderHelper) createFolder(orgID int64, title string) (*dashboard, error) {
|
||||
cmd := saveFolderCommand{
|
||||
OrgId: orgID,
|
||||
FolderId: 0,
|
||||
IsFolder: true,
|
||||
Dashboard: simplejson.NewFromAny(map[string]any{
|
||||
"title": title,
|
||||
}),
|
||||
}
|
||||
dash := cmd.getDashboardModel()
|
||||
dash.setUid(util.GenerateShortUID())
|
||||
|
||||
parentVersion := dash.Version
|
||||
dash.setVersion(1)
|
||||
dash.Created = time.Now()
|
||||
dash.CreatedBy = FOLDER_CREATED_BY
|
||||
dash.Updated = time.Now()
|
||||
dash.UpdatedBy = FOLDER_CREATED_BY
|
||||
metrics.MApiDashboardInsert.Inc()
|
||||
|
||||
if _, err := m.sess.Insert(dash); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dashVersion := &dashver.DashboardVersion{
|
||||
DashboardID: dash.Id,
|
||||
ParentVersion: parentVersion,
|
||||
RestoredFrom: cmd.RestoredFrom,
|
||||
Version: dash.Version,
|
||||
Created: time.Now(),
|
||||
CreatedBy: dash.UpdatedBy,
|
||||
Message: cmd.Message,
|
||||
Data: dash.Data,
|
||||
}
|
||||
|
||||
// insert version entry
|
||||
if _, err := m.sess.Insert(dashVersion); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dash, nil
|
||||
}
|
||||
|
||||
// based on SQLStore.UpdateDashboardACL()
|
||||
// it should be called from inside a transaction
|
||||
func (m *folderHelper) setACL(orgID int64, dashboardID int64, items []*dashboardACL) error {
|
||||
if dashboardID <= 0 {
|
||||
return fmt.Errorf("folder id must be greater than zero for a folder permission")
|
||||
}
|
||||
|
||||
// userPermissionsMap is a map keeping the highest permission per user
|
||||
// for handling conficting inherited (folder) and non-inherited (dashboard) user permissions
|
||||
userPermissionsMap := make(map[int64]*dashboardACL, len(items))
|
||||
// teamPermissionsMap is a map keeping the highest permission per team
|
||||
// for handling conficting inherited (folder) and non-inherited (dashboard) team permissions
|
||||
teamPermissionsMap := make(map[int64]*dashboardACL, len(items))
|
||||
for _, item := range items {
|
||||
if item.UserID != 0 {
|
||||
acl, ok := userPermissionsMap[item.UserID]
|
||||
if !ok {
|
||||
userPermissionsMap[item.UserID] = item
|
||||
} else {
|
||||
if item.Permission > acl.Permission {
|
||||
// the higher permission wins
|
||||
userPermissionsMap[item.UserID] = item
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if item.TeamID != 0 {
|
||||
acl, ok := teamPermissionsMap[item.TeamID]
|
||||
if !ok {
|
||||
teamPermissionsMap[item.TeamID] = item
|
||||
} else {
|
||||
if item.Permission > acl.Permission {
|
||||
// the higher permission wins
|
||||
teamPermissionsMap[item.TeamID] = item
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type keyType struct {
|
||||
UserID int64 `xorm:"user_id"`
|
||||
TeamID int64 `xorm:"team_id"`
|
||||
Role roleType
|
||||
Permission permissionType
|
||||
}
|
||||
// seen keeps track of inserted perrmissions to avoid duplicates (due to inheritance)
|
||||
seen := make(map[keyType]struct{}, len(items))
|
||||
for _, item := range items {
|
||||
if item.UserID == 0 && item.TeamID == 0 && (item.Role == nil || !item.Role.IsValid()) {
|
||||
return dashboards.ErrDashboardACLInfoMissing
|
||||
}
|
||||
|
||||
// ignore duplicate user permissions
|
||||
if item.UserID != 0 {
|
||||
acl, ok := userPermissionsMap[item.UserID]
|
||||
if ok {
|
||||
if acl.Id != item.Id {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ignore duplicate team permissions
|
||||
if item.TeamID != 0 {
|
||||
acl, ok := teamPermissionsMap[item.TeamID]
|
||||
if ok {
|
||||
if acl.Id != item.Id {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
key := keyType{UserID: item.UserID, TeamID: item.TeamID, Role: "", Permission: item.Permission}
|
||||
if item.Role != nil {
|
||||
key.Role = *item.Role
|
||||
}
|
||||
if _, ok := seen[key]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// unset Id so that the new record will get a different one
|
||||
item.Id = 0
|
||||
item.OrgID = orgID
|
||||
item.DashboardID = dashboardID
|
||||
item.Created = time.Now()
|
||||
item.Updated = time.Now()
|
||||
|
||||
m.sess.Nullable("user_id", "team_id")
|
||||
if _, err := m.sess.Insert(item); err != nil {
|
||||
return err
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
}
|
||||
|
||||
// Update dashboard HasACL flag
|
||||
dashboard := dashboards.Dashboard{HasACL: true}
|
||||
_, err := m.sess.Cols("has_acl").Where("id=?", dashboardID).Update(&dashboard)
|
||||
return err
|
||||
}
|
||||
|
||||
// based on SQLStore.GetDashboardACLInfoList()
|
||||
func (m *folderHelper) getACL(orgID, dashboardID int64) ([]*dashboardACL, error) {
|
||||
var err error
|
||||
|
||||
falseStr := m.mg.Dialect.BooleanStr(false)
|
||||
|
||||
result := make([]*dashboardACL, 0)
|
||||
rawSQL := `
|
||||
-- get distinct permissions for the dashboard and its parent folder
|
||||
SELECT DISTINCT
|
||||
da.id,
|
||||
da.user_id,
|
||||
da.team_id,
|
||||
da.permission,
|
||||
da.role
|
||||
FROM dashboard as d
|
||||
LEFT JOIN dashboard folder on folder.id = d.folder_id
|
||||
LEFT JOIN dashboard_acl AS da ON
|
||||
da.dashboard_id = d.id OR
|
||||
da.dashboard_id = d.folder_id OR
|
||||
(
|
||||
-- include default permissions --
|
||||
da.org_id = -1 AND (
|
||||
(folder.id IS NOT NULL AND folder.has_acl = ` + falseStr + `) OR
|
||||
(folder.id IS NULL AND d.has_acl = ` + falseStr + `)
|
||||
)
|
||||
)
|
||||
WHERE d.org_id = ? AND d.id = ? AND da.id IS NOT NULL
|
||||
ORDER BY da.id ASC
|
||||
`
|
||||
err = m.sess.SQL(rawSQL, orgID, dashboardID).Find(&result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// getOrgsThatHaveFolders returns a unique list of organization ID that have at least one folder
|
||||
func (m *folderHelper) getOrgsIDThatHaveFolders() (map[int64]struct{}, error) {
|
||||
// get folder if exists
|
||||
var rows []int64
|
||||
err := m.sess.Table(&dashboard{}).Where("is_folder=?", true).Distinct("org_id").Find(&rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make(map[int64]struct{}, len(rows))
|
||||
for _, s := range rows {
|
||||
result[s] = struct{}{}
|
||||
}
|
||||
return result, nil
|
||||
}
|
Reference in New Issue
Block a user