package legacy import ( "context" "database/sql" "encoding/json" "fmt" "path/filepath" "strconv" "sync" "time" "github.com/grafana/authlib/claims" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/utils/ptr" "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/utils" dashboardsV0 "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" gapiutil "github.com/grafana/grafana/pkg/services/apiserver/utils" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/provisioning" "github.com/grafana/grafana/pkg/storage/legacysql" "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate" ) var ( _ DashboardAccess = (*dashboardSqlAccess)(nil) ) type dashboardRow struct { // The numeric version for this dashboard RV int64 // Dashboard resource Dash *dashboardsV0.Dashboard // The folder UID (needed for access control checks) FolderUID string // The token we can use that will start a new connection that includes // this same dashboard token *continueToken } type dashboardSqlAccess struct { sql legacysql.LegacyDatabaseProvider namespacer request.NamespaceMapper provisioning provisioning.ProvisioningService // Use for writing (not reading) dashStore dashboards.Store softDelete bool // Typically one... the server wrapper subscribers []chan *resource.WrittenEvent mutex sync.Mutex } func NewDashboardAccess(sql legacysql.LegacyDatabaseProvider, namespacer request.NamespaceMapper, dashStore dashboards.Store, provisioning provisioning.ProvisioningService, softDelete bool, ) DashboardAccess { return &dashboardSqlAccess{ sql: sql, namespacer: namespacer, dashStore: dashStore, provisioning: provisioning, softDelete: softDelete, } } func (a *dashboardSqlAccess) getRows(ctx context.Context, sql *legacysql.LegacyDatabaseHelper, query *DashboardQuery) (*rowsWrapper, error) { if len(query.Labels) > 0 { return nil, fmt.Errorf("labels not yet supported") // if query.Requirements.Folder != nil { // args = append(args, *query.Requirements.Folder) // sqlcmd = fmt.Sprintf("%s AND dashboard.folder_uid=?$%d", sqlcmd, len(args)) // } } req := newQueryReq(sql, query) tmpl := sqlQueryDashboards if query.UseHistoryTable() && query.GetTrash { return nil, fmt.Errorf("trash not included in history table") } rawQuery, err := sqltemplate.Execute(tmpl, req) if err != nil { return nil, fmt.Errorf("execute template %q: %w", tmpl.Name(), err) } q := rawQuery // q = sqltemplate.RemoveEmptyLines(rawQuery) // fmt.Printf(">>%s [%+v]", q, req.GetArgs()) rows, err := sql.DB.GetSqlxSession().Query(ctx, q, req.GetArgs()...) if err != nil { if rows != nil { _ = rows.Close() } rows = nil } return &rowsWrapper{ rows: rows, a: a, // This looks up rules from the permissions on a user canReadDashboard: func(scopes ...string) bool { return true // ??? }, // accesscontrol.Checker(user, dashboards.ActionDashboardsRead), }, err } var _ resource.ListIterator = (*rowsWrapper)(nil) type rowsWrapper struct { a *dashboardSqlAccess rows *sql.Rows canReadDashboard func(scopes ...string) bool // Current row *dashboardRow err error } func (a *dashboardSqlAccess) Namespaces(ctx context.Context) ([]string, error) { return nil, fmt.Errorf("not implemented") } func (r *rowsWrapper) Close() error { if r.rows == nil { return nil } return r.rows.Close() } func (r *rowsWrapper) Next() bool { if r.err != nil { return false } var err error // breaks after first readable value for r.rows.Next() { r.row, err = r.a.scanRow(r.rows) if err != nil { r.err = err return false } if r.row != nil { d := r.row // Access control checker scopes := []string{dashboards.ScopeDashboardsProvider.GetResourceScopeUID(d.Dash.Name)} if d.FolderUID != "" { // Copied from searchV2... not sure the logic is right scopes = append(scopes, dashboards.ScopeFoldersProvider.GetResourceScopeUID(d.FolderUID)) } if !r.canReadDashboard(scopes...) { continue } // returns the first folder it can return true } } return false } // ContinueToken implements resource.ListIterator. func (r *rowsWrapper) ContinueToken() string { return r.row.token.String() } // Error implements resource.ListIterator. func (r *rowsWrapper) Error() error { return r.err } // Name implements resource.ListIterator. func (r *rowsWrapper) Name() string { return r.row.Dash.Name } // Namespace implements resource.ListIterator. func (r *rowsWrapper) Namespace() string { return r.row.Dash.Namespace } // ResourceVersion implements resource.ListIterator. func (r *rowsWrapper) ResourceVersion() int64 { return r.row.RV } // Value implements resource.ListIterator. func (r *rowsWrapper) Value() []byte { b, err := json.Marshal(r.row.Dash) r.err = err return b } func (a *dashboardSqlAccess) scanRow(rows *sql.Rows) (*dashboardRow, error) { dash := &dashboardsV0.Dashboard{ TypeMeta: dashboardsV0.DashboardResourceInfo.TypeMeta(), ObjectMeta: metav1.ObjectMeta{Annotations: make(map[string]string)}, } row := &dashboardRow{Dash: dash} var dashboard_id int64 var orgId int64 var folder_uid sql.NullString var updated time.Time var updatedBy sql.NullString var updatedByID sql.NullInt64 var deleted sql.NullTime var created time.Time var createdBy sql.NullString var createdByID sql.NullInt64 var message sql.NullString var plugin_id string var origin_name sql.NullString var origin_path sql.NullString var origin_ts sql.NullInt64 var origin_hash sql.NullString var data []byte // the dashboard JSON var version int64 err := rows.Scan(&orgId, &dashboard_id, &dash.Name, &folder_uid, &deleted, &plugin_id, &origin_name, &origin_path, &origin_hash, &origin_ts, &created, &createdBy, &createdByID, &updated, &updatedBy, &updatedByID, &version, &message, &data, ) row.token = &continueToken{orgId: orgId, id: dashboard_id} if err == nil { row.RV = getResourceVersion(dashboard_id, version) dash.ResourceVersion = fmt.Sprintf("%d", row.RV) dash.Namespace = a.namespacer(orgId) dash.UID = gapiutil.CalculateClusterWideUID(dash) dash.SetCreationTimestamp(metav1.NewTime(created)) meta, err := utils.MetaAccessor(dash) if err != nil { return nil, err } meta.SetUpdatedTimestamp(&updated) meta.SetCreatedBy(getUserID(createdBy, createdByID)) meta.SetUpdatedBy(getUserID(updatedBy, updatedByID)) if deleted.Valid { meta.SetDeletionTimestamp(ptr.To(metav1.NewTime(deleted.Time))) } if message.String != "" { meta.SetMessage(message.String) } if folder_uid.String != "" { meta.SetFolder(folder_uid.String) row.FolderUID = folder_uid.String } if origin_name.String != "" { ts := time.Unix(origin_ts.Int64, 0) resolvedPath := a.provisioning.GetDashboardProvisionerResolvedPath(origin_name.String) originPath, err := filepath.Rel( resolvedPath, origin_path.String, ) if err != nil { return nil, err } meta.SetOriginInfo(&utils.ResourceOriginInfo{ Name: origin_name.String, Path: originPath, Hash: origin_hash.String, Timestamp: &ts, }) } else if plugin_id != "" { meta.SetOriginInfo(&utils.ResourceOriginInfo{ Name: "plugin", Path: plugin_id, }) } if len(data) > 0 { err = dash.Spec.UnmarshalJSON(data) if err != nil { return row, err } } // add it so we can get it from the body later dash.Spec.Set("id", dashboard_id) } return row, err } func getUserID(v sql.NullString, id sql.NullInt64) string { if v.Valid && v.String != "" { return identity.NewTypedIDString(claims.TypeUser, v.String) } if id.Valid && id.Int64 == -1 { return identity.NewTypedIDString(claims.TypeProvisioning, "") } return "" } // DeleteDashboard implements DashboardAccess. func (a *dashboardSqlAccess) DeleteDashboard(ctx context.Context, orgId int64, uid string) (*dashboardsV0.Dashboard, bool, error) { dash, _, err := a.GetDashboard(ctx, orgId, uid, 0) if err != nil { return nil, false, err } if a.softDelete { err = a.dashStore.SoftDeleteDashboard(ctx, orgId, uid) if err == nil && dash != nil { now := metav1.NewTime(time.Now()) dash.DeletionTimestamp = &now return dash, true, err } return dash, false, err } id := dash.Spec.GetNestedInt64("id") if id == 0 { return nil, false, fmt.Errorf("could not find id in saved body") } err = a.dashStore.DeleteDashboard(ctx, &dashboards.DeleteDashboardCommand{ OrgID: orgId, ID: id, }) if err != nil { return nil, false, err } return dash, true, nil } // SaveDashboard implements DashboardAccess. func (a *dashboardSqlAccess) SaveDashboard(ctx context.Context, orgId int64, dash *dashboardsV0.Dashboard) (*dashboardsV0.Dashboard, bool, error) { created := false user, ok := claims.From(ctx) if !ok || user == nil { return nil, created, fmt.Errorf("no user found in context") } if dash.Name != "" { dash.Spec.Set("uid", dash.Name) // Get the previous version to set the internal ID old, _ := a.dashStore.GetDashboard(ctx, &dashboards.GetDashboardQuery{ OrgID: orgId, UID: dash.Name, }) if old != nil { dash.Spec.Set("id", old.ID) } else { dash.Spec.Remove("id") // existing of "id" makes it an update created = true } } else { dash.Spec.Remove("id") dash.Spec.Remove("uid") } var userID int64 idClaims := user.GetIdentity() if claims.IsIdentityType(idClaims.IdentityType(), claims.TypeUser) { var err error userID, err = identity.UserIdentifier(idClaims.Subject()) if err != nil { return nil, false, err } } meta, err := utils.MetaAccessor(dash) if err != nil { return nil, false, err } out, err := a.dashStore.SaveDashboard(ctx, dashboards.SaveDashboardCommand{ OrgID: orgId, Dashboard: simplejson.NewFromAny(dash.Spec.UnstructuredContent()), FolderUID: meta.GetFolder(), Overwrite: true, // already passed the revisionVersion checks! UserID: userID, }) if err != nil { return nil, false, err } if out != nil { created = (out.Created.Unix() == out.Updated.Unix()) // and now? } dash, _, err = a.GetDashboard(ctx, orgId, out.UID, 0) return dash, created, err } func (a *dashboardSqlAccess) GetLibraryPanels(ctx context.Context, query LibraryPanelQuery) (*dashboardsV0.LibraryPanelList, error) { limit := int(query.Limit) query.Limit += 1 // for continue if query.OrgID == 0 { return nil, fmt.Errorf("expected non zero orgID") } sql, err := a.sql(ctx) if err != nil { return nil, err } req := newLibraryQueryReq(sql, &query) rawQuery, err := sqltemplate.Execute(sqlQueryPanels, req) if err != nil { return nil, fmt.Errorf("execute template %q: %w", sqlQueryPanels.Name(), err) } q := rawQuery res := &dashboardsV0.LibraryPanelList{} rows, err := sql.DB.GetSqlxSession().Query(ctx, q, req.GetArgs()...) defer func() { if rows != nil { _ = rows.Close() } }() if err != nil { return nil, err } type panel struct { ID int64 UID string FolderUID string Created time.Time CreatedBy string Updated time.Time UpdatedBy string Name string Type string Description string Model []byte } var lastID int64 for rows.Next() { p := panel{} err = rows.Scan(&p.ID, &p.UID, &p.FolderUID, &p.Created, &p.CreatedBy, &p.Updated, &p.UpdatedBy, &p.Name, &p.Type, &p.Description, &p.Model, ) if err != nil { return res, err } lastID = p.ID item := dashboardsV0.LibraryPanel{ ObjectMeta: metav1.ObjectMeta{ Name: p.UID, CreationTimestamp: metav1.NewTime(p.Created), ResourceVersion: strconv.FormatInt(p.Updated.UnixMilli(), 10), }, Spec: dashboardsV0.LibraryPanelSpec{}, } status := &dashboardsV0.LibraryPanelStatus{ Missing: v0alpha1.Unstructured{}, } err = json.Unmarshal(p.Model, &item.Spec) if err != nil { return nil, err } err = json.Unmarshal(p.Model, &status.Missing.Object) if err != nil { return nil, err } if item.Spec.Title != p.Name { status.Warnings = append(item.Status.Warnings, fmt.Sprintf("title mismatch (expected: %s)", p.Name)) } if item.Spec.Description != p.Description { status.Warnings = append(item.Status.Warnings, fmt.Sprintf("description mismatch (expected: %s)", p.Description)) } if item.Spec.Type != p.Type { status.Warnings = append(item.Status.Warnings, fmt.Sprintf("type mismatch (expected: %s)", p.Type)) } item.Status = status // Remove the properties we are already showing for _, k := range []string{"type", "pluginVersion", "title", "description", "options", "fieldConfig", "datasource", "targets", "libraryPanel"} { delete(status.Missing.Object, k) } meta, err := utils.MetaAccessor(&item) if err != nil { return nil, err } meta.SetFolder(p.FolderUID) meta.SetCreatedBy(p.CreatedBy) meta.SetUpdatedBy(p.UpdatedBy) meta.SetUpdatedTimestamp(&p.Updated) meta.SetOriginInfo(&utils.ResourceOriginInfo{ Name: "SQL", Path: strconv.FormatInt(p.ID, 10), }) res.Items = append(res.Items, item) if len(res.Items) > limit { res.Continue = strconv.FormatInt(lastID, 10) break } } if query.UID == "" { rv, err := sql.GetResourceVersion(ctx, "library_element", "updated") if err == nil { res.ResourceVersion = strconv.FormatInt(rv, 10) } } return res, err }