mirror of
https://github.com/grafana/grafana.git
synced 2024-11-29 20:24:18 -06:00
867 lines
31 KiB
Go
867 lines
31 KiB
Go
package libraryelements
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/grafana/grafana/pkg/api/dtos"
|
|
"github.com/grafana/grafana/pkg/infra/db"
|
|
"github.com/grafana/grafana/pkg/kinds/librarypanel"
|
|
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
|
"github.com/grafana/grafana/pkg/services/auth/identity"
|
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
|
"github.com/grafana/grafana/pkg/services/libraryelements/model"
|
|
"github.com/grafana/grafana/pkg/services/org"
|
|
"github.com/grafana/grafana/pkg/services/search"
|
|
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
|
"github.com/grafana/grafana/pkg/setting"
|
|
"github.com/grafana/grafana/pkg/util"
|
|
)
|
|
|
|
const (
|
|
selectLibraryElementDTOWithMeta = `
|
|
SELECT DISTINCT
|
|
le.name, le.id, le.org_id, le.folder_id, le.uid, le.kind, le.type, le.description, le.model, le.created, le.created_by, le.updated, le.updated_by, le.version
|
|
, u1.login AS created_by_name
|
|
, u1.email AS created_by_email
|
|
, u2.login AS updated_by_name
|
|
, u2.email AS updated_by_email
|
|
, (SELECT COUNT(connection_id) FROM ` + model.LibraryElementConnectionTableName + ` WHERE element_id = le.id AND kind=1) AS connected_dashboards`
|
|
)
|
|
|
|
// redundant SELECT to trick mysql's optimizer
|
|
const deleteInvalidConnections = `
|
|
DELETE FROM library_element_connection
|
|
WHERE connection_id IN (
|
|
SELECT connection_id FROM (
|
|
SELECT connection_id as id FROM library_element_connection
|
|
WHERE element_id=? AND connection_id NOT IN (SELECT id as connection_id from dashboard)
|
|
) as dummy
|
|
)`
|
|
|
|
func getFromLibraryElementDTOWithMeta(dialect migrator.Dialect) string {
|
|
user := dialect.Quote("user")
|
|
userJoin := `
|
|
FROM library_element AS le
|
|
LEFT JOIN ` + user + ` AS u1 ON le.created_by = u1.id
|
|
LEFT JOIN ` + user + ` AS u2 ON le.updated_by = u2.id
|
|
`
|
|
return userJoin
|
|
}
|
|
|
|
func syncFieldsWithModel(libraryElement *model.LibraryElement) error {
|
|
var modelLibraryElement map[string]any
|
|
if err := json.Unmarshal(libraryElement.Model, &modelLibraryElement); err != nil {
|
|
return err
|
|
}
|
|
|
|
if modelLibraryElement == nil {
|
|
modelLibraryElement = make(map[string]any)
|
|
}
|
|
|
|
if model.LibraryElementKind(libraryElement.Kind) == model.VariableElement {
|
|
modelLibraryElement["name"] = libraryElement.Name
|
|
}
|
|
if modelLibraryElement["type"] != nil {
|
|
libraryElement.Type = modelLibraryElement["type"].(string)
|
|
} else {
|
|
modelLibraryElement["type"] = libraryElement.Type
|
|
}
|
|
if modelLibraryElement["description"] != nil {
|
|
libraryElement.Description = modelLibraryElement["description"].(string)
|
|
} else {
|
|
modelLibraryElement["description"] = libraryElement.Description
|
|
}
|
|
syncedModel, err := json.Marshal(&modelLibraryElement)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
libraryElement.Model = syncedModel
|
|
|
|
return nil
|
|
}
|
|
|
|
func GetLibraryElement(dialect migrator.Dialect, session *db.Session, uid string, orgID int64) (model.LibraryElementWithMeta, error) {
|
|
elements := make([]model.LibraryElementWithMeta, 0)
|
|
sql := selectLibraryElementDTOWithMeta +
|
|
", coalesce(dashboard.title, 'General') AS folder_name" +
|
|
", coalesce(dashboard.uid, '') AS folder_uid" +
|
|
getFromLibraryElementDTOWithMeta(dialect) +
|
|
" LEFT JOIN dashboard AS dashboard ON dashboard.id = le.folder_id" +
|
|
" WHERE le.uid=? AND le.org_id=?"
|
|
sess := session.SQL(sql, uid, orgID)
|
|
err := sess.Find(&elements)
|
|
if err != nil {
|
|
return model.LibraryElementWithMeta{}, err
|
|
}
|
|
if len(elements) == 0 {
|
|
return model.LibraryElementWithMeta{}, model.ErrLibraryElementNotFound
|
|
}
|
|
if len(elements) > 1 {
|
|
return model.LibraryElementWithMeta{}, fmt.Errorf("found %d elements, while expecting at most one", len(elements))
|
|
}
|
|
|
|
return elements[0], nil
|
|
}
|
|
|
|
// createLibraryElement adds a library element.
|
|
func (l *LibraryElementService) createLibraryElement(c context.Context, signedInUser identity.Requester, cmd model.CreateLibraryElementCommand) (model.LibraryElementDTO, error) {
|
|
if err := l.requireSupportedElementKind(cmd.Kind); err != nil {
|
|
return model.LibraryElementDTO{}, err
|
|
}
|
|
createUID := cmd.UID
|
|
if len(createUID) == 0 {
|
|
createUID = util.GenerateShortUID()
|
|
} else {
|
|
if !util.IsValidShortUID(createUID) {
|
|
return model.LibraryElementDTO{}, model.ErrLibraryElementInvalidUID
|
|
} else if util.IsShortUIDTooLong(createUID) {
|
|
return model.LibraryElementDTO{}, model.ErrLibraryElementUIDTooLong
|
|
}
|
|
}
|
|
|
|
updatedModel := cmd.Model
|
|
var err error
|
|
if cmd.Kind == int64(model.PanelElement) {
|
|
updatedModel, err = l.addUidToLibraryPanel(cmd.Model, createUID)
|
|
if err != nil {
|
|
return model.LibraryElementDTO{}, err
|
|
}
|
|
}
|
|
|
|
userID := int64(0)
|
|
namespaceID, identifier := signedInUser.GetNamespacedID()
|
|
switch namespaceID {
|
|
case identity.NamespaceUser, identity.NamespaceServiceAccount:
|
|
userID, err = identity.IntIdentifier(namespaceID, identifier)
|
|
if err != nil {
|
|
l.log.Warn("Error while parsing userID", "namespaceID", namespaceID, "userID", identifier)
|
|
}
|
|
}
|
|
|
|
element := model.LibraryElement{
|
|
OrgID: signedInUser.GetOrgID(),
|
|
FolderID: cmd.FolderID, // nolint:staticcheck
|
|
UID: createUID,
|
|
Name: cmd.Name,
|
|
Model: updatedModel,
|
|
Version: 1,
|
|
Kind: cmd.Kind,
|
|
|
|
Created: time.Now(),
|
|
Updated: time.Now(),
|
|
|
|
CreatedBy: userID,
|
|
UpdatedBy: userID,
|
|
}
|
|
|
|
if err := syncFieldsWithModel(&element); err != nil {
|
|
return model.LibraryElementDTO{}, err
|
|
}
|
|
|
|
err = l.SQLStore.WithTransactionalDbSession(c, func(session *db.Session) error {
|
|
if l.features.IsEnabled(c, featuremgmt.FlagLibraryPanelRBAC) {
|
|
allowed, err := l.AccessControl.Evaluate(c, signedInUser, ac.EvalPermission(ActionLibraryPanelsCreate, dashboards.ScopeFoldersProvider.GetResourceScopeUID(*cmd.FolderUID)))
|
|
if !allowed {
|
|
return fmt.Errorf("insufficient permissions for creating library panel in folder with UID %s", *cmd.FolderUID)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
// nolint:staticcheck
|
|
if err := l.requireEditPermissionsOnFolder(c, signedInUser, cmd.FolderID); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if _, err := session.Insert(&element); err != nil {
|
|
if l.SQLStore.GetDialect().IsUniqueConstraintViolation(err) {
|
|
return model.ErrLibraryElementAlreadyExists
|
|
}
|
|
return err
|
|
}
|
|
return nil
|
|
})
|
|
|
|
dto := model.LibraryElementDTO{
|
|
ID: element.ID,
|
|
OrgID: element.OrgID,
|
|
FolderID: element.FolderID, // nolint:staticcheck
|
|
UID: element.UID,
|
|
Name: element.Name,
|
|
Kind: element.Kind,
|
|
Type: element.Type,
|
|
Description: element.Description,
|
|
Model: element.Model,
|
|
Version: element.Version,
|
|
Meta: model.LibraryElementDTOMeta{
|
|
ConnectedDashboards: 0,
|
|
Created: element.Created,
|
|
Updated: element.Updated,
|
|
CreatedBy: librarypanel.LibraryElementDTOMetaUser{
|
|
Id: element.CreatedBy,
|
|
Name: signedInUser.GetLogin(),
|
|
AvatarUrl: dtos.GetGravatarUrl(signedInUser.GetEmail()),
|
|
},
|
|
UpdatedBy: librarypanel.LibraryElementDTOMetaUser{
|
|
Id: element.UpdatedBy,
|
|
Name: signedInUser.GetLogin(),
|
|
AvatarUrl: dtos.GetGravatarUrl(signedInUser.GetEmail()),
|
|
},
|
|
},
|
|
}
|
|
|
|
return dto, err
|
|
}
|
|
|
|
// deleteLibraryElement deletes a library element.
|
|
func (l *LibraryElementService) deleteLibraryElement(c context.Context, signedInUser identity.Requester, uid string) (int64, error) {
|
|
var elementID int64
|
|
err := l.SQLStore.WithTransactionalDbSession(c, func(session *db.Session) error {
|
|
element, err := GetLibraryElement(l.SQLStore.GetDialect(), session, uid, signedInUser.GetOrgID())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// nolint:staticcheck
|
|
if err := l.requireEditPermissionsOnFolder(c, signedInUser, element.FolderID); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Delete any hanging/invalid connections
|
|
if _, err = session.Exec(deleteInvalidConnections, element.ID); err != nil {
|
|
return err
|
|
}
|
|
|
|
var connectionIDs []struct {
|
|
ConnectionID int64 `xorm:"connection_id"`
|
|
}
|
|
sql := "SELECT connection_id FROM library_element_connection WHERE element_id=?"
|
|
if err := session.SQL(sql, element.ID).Find(&connectionIDs); err != nil {
|
|
return err
|
|
} else if len(connectionIDs) > 0 {
|
|
return model.ErrLibraryElementHasConnections
|
|
}
|
|
|
|
result, err := session.Exec("DELETE FROM library_element WHERE id=?", element.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if rowsAffected, err := result.RowsAffected(); err != nil {
|
|
return err
|
|
} else if rowsAffected != 1 {
|
|
return model.ErrLibraryElementNotFound
|
|
}
|
|
|
|
elementID = element.ID
|
|
return nil
|
|
})
|
|
return elementID, err
|
|
}
|
|
|
|
// getLibraryElements gets a Library Element where param == value
|
|
func (l *LibraryElementService) getLibraryElements(c context.Context, store db.DB, cfg *setting.Cfg, signedInUser identity.Requester, params []Pair, features featuremgmt.FeatureToggles, cmd model.GetLibraryElementCommand) ([]model.LibraryElementDTO, error) {
|
|
libraryElements := make([]model.LibraryElementWithMeta, 0)
|
|
|
|
recursiveQueriesAreSupported, err := store.RecursiveQueriesAreSupported()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = store.WithDbSession(c, func(session *db.Session) error {
|
|
builder := db.NewSqlBuilder(cfg, features, store.GetDialect(), recursiveQueriesAreSupported)
|
|
builder.Write(selectLibraryElementDTOWithMeta)
|
|
builder.Write(", ? as folder_name ", cmd.FolderName)
|
|
builder.Write(", '' as folder_uid ")
|
|
builder.Write(getFromLibraryElementDTOWithMeta(store.GetDialect()))
|
|
// nolint:staticcheck
|
|
writeParamSelectorSQL(&builder, append(params, Pair{"folder_id", cmd.FolderID})...)
|
|
builder.Write(" UNION ")
|
|
builder.Write(selectLibraryElementDTOWithMeta)
|
|
builder.Write(", dashboard.title as folder_name ")
|
|
builder.Write(", dashboard.uid as folder_uid ")
|
|
builder.Write(getFromLibraryElementDTOWithMeta(store.GetDialect()))
|
|
builder.Write(" INNER JOIN dashboard AS dashboard on le.folder_id = dashboard.id AND le.folder_id <> 0")
|
|
writeParamSelectorSQL(&builder, params...)
|
|
builder.WriteDashboardPermissionFilter(signedInUser, dashboards.PERMISSION_VIEW, "")
|
|
builder.Write(` OR dashboard.id=0`)
|
|
if err := session.SQL(builder.GetSQLString(), builder.GetParams()...).Find(&libraryElements); err != nil {
|
|
return err
|
|
}
|
|
if len(libraryElements) == 0 {
|
|
return model.ErrLibraryElementNotFound
|
|
}
|
|
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return []model.LibraryElementDTO{}, err
|
|
}
|
|
|
|
leDtos := make([]model.LibraryElementDTO, len(libraryElements))
|
|
for i, libraryElement := range libraryElements {
|
|
var updatedModel json.RawMessage
|
|
if libraryElement.Kind == int64(model.PanelElement) {
|
|
updatedModel, err = l.addUidToLibraryPanel(libraryElement.Model, libraryElement.UID)
|
|
if err != nil {
|
|
return []model.LibraryElementDTO{}, err
|
|
}
|
|
}
|
|
|
|
leDtos[i] = model.LibraryElementDTO{
|
|
ID: libraryElement.ID,
|
|
OrgID: libraryElement.OrgID,
|
|
FolderID: libraryElement.FolderID, // nolint:staticcheck
|
|
FolderUID: libraryElement.FolderUID,
|
|
UID: libraryElement.UID,
|
|
Name: libraryElement.Name,
|
|
Kind: libraryElement.Kind,
|
|
Type: libraryElement.Type,
|
|
Description: libraryElement.Description,
|
|
Model: updatedModel,
|
|
Version: libraryElement.Version,
|
|
Meta: model.LibraryElementDTOMeta{
|
|
FolderName: libraryElement.FolderName,
|
|
FolderUID: libraryElement.FolderUID,
|
|
ConnectedDashboards: libraryElement.ConnectedDashboards,
|
|
Created: libraryElement.Created,
|
|
Updated: libraryElement.Updated,
|
|
CreatedBy: librarypanel.LibraryElementDTOMetaUser{
|
|
Id: libraryElement.CreatedBy,
|
|
Name: libraryElement.CreatedByName,
|
|
AvatarUrl: dtos.GetGravatarUrl(libraryElement.CreatedByEmail),
|
|
},
|
|
UpdatedBy: librarypanel.LibraryElementDTOMetaUser{
|
|
Id: libraryElement.UpdatedBy,
|
|
Name: libraryElement.UpdatedByName,
|
|
AvatarUrl: dtos.GetGravatarUrl(libraryElement.UpdatedByEmail),
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
return leDtos, nil
|
|
}
|
|
|
|
// getLibraryElementByUid gets a Library Element by uid.
|
|
func (l *LibraryElementService) getLibraryElementByUid(c context.Context, signedInUser identity.Requester, cmd model.GetLibraryElementCommand) (model.LibraryElementDTO, error) {
|
|
libraryElements, err := l.getLibraryElements(c, l.SQLStore, l.Cfg, signedInUser, []Pair{{key: "org_id", value: signedInUser.GetOrgID()}, {key: "uid", value: cmd.UID}}, l.features, cmd)
|
|
if err != nil {
|
|
return model.LibraryElementDTO{}, err
|
|
}
|
|
if len(libraryElements) > 1 {
|
|
return model.LibraryElementDTO{}, fmt.Errorf("found %d elements, while expecting at most one", len(libraryElements))
|
|
}
|
|
|
|
return libraryElements[0], nil
|
|
}
|
|
|
|
// getLibraryElementByName gets a Library Element by name.
|
|
func (l *LibraryElementService) getLibraryElementsByName(c context.Context, signedInUser identity.Requester, name string) ([]model.LibraryElementDTO, error) {
|
|
return l.getLibraryElements(c, l.SQLStore, l.Cfg, signedInUser, []Pair{{"org_id", signedInUser.GetOrgID()}, {"name", name}}, l.features,
|
|
model.GetLibraryElementCommand{
|
|
FolderName: dashboards.RootFolderName,
|
|
})
|
|
}
|
|
|
|
// getAllLibraryElements gets all Library Elements.
|
|
func (l *LibraryElementService) getAllLibraryElements(c context.Context, signedInUser identity.Requester, query model.SearchLibraryElementsQuery) (model.LibraryElementSearchResult, error) {
|
|
elements := make([]model.LibraryElementWithMeta, 0)
|
|
result := model.LibraryElementSearchResult{}
|
|
|
|
recursiveQueriesAreSupported, err := l.SQLStore.RecursiveQueriesAreSupported()
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
|
|
if query.PerPage <= 0 {
|
|
query.PerPage = 100
|
|
}
|
|
if query.Page <= 0 {
|
|
query.Page = 1
|
|
}
|
|
var typeFilter []string
|
|
if len(strings.TrimSpace(query.TypeFilter)) > 0 {
|
|
typeFilter = strings.Split(query.TypeFilter, ",")
|
|
}
|
|
folderFilter := parseFolderFilter(query)
|
|
if folderFilter.parseError != nil {
|
|
return model.LibraryElementSearchResult{}, folderFilter.parseError
|
|
}
|
|
err = l.SQLStore.WithDbSession(c, func(session *db.Session) error {
|
|
builder := db.NewSqlBuilder(l.Cfg, l.features, l.SQLStore.GetDialect(), recursiveQueriesAreSupported)
|
|
if folderFilter.includeGeneralFolder {
|
|
builder.Write(selectLibraryElementDTOWithMeta)
|
|
builder.Write(", 'General' as folder_name ")
|
|
builder.Write(", '' as folder_uid ")
|
|
builder.Write(getFromLibraryElementDTOWithMeta(l.SQLStore.GetDialect()))
|
|
builder.Write(` WHERE le.org_id=? AND le.folder_id=0`, signedInUser.GetOrgID())
|
|
writeKindSQL(query, &builder)
|
|
writeSearchStringSQL(query, l.SQLStore, &builder)
|
|
writeExcludeSQL(query, &builder)
|
|
writeTypeFilterSQL(typeFilter, &builder)
|
|
builder.Write(" UNION ")
|
|
}
|
|
builder.Write(selectLibraryElementDTOWithMeta)
|
|
builder.Write(", dashboard.title as folder_name ")
|
|
builder.Write(", dashboard.uid as folder_uid ")
|
|
builder.Write(getFromLibraryElementDTOWithMeta(l.SQLStore.GetDialect()))
|
|
builder.Write(" INNER JOIN dashboard AS dashboard on le.folder_id = dashboard.id AND le.folder_id<>0")
|
|
builder.Write(` WHERE le.org_id=?`, signedInUser.GetOrgID())
|
|
writeKindSQL(query, &builder)
|
|
writeSearchStringSQL(query, l.SQLStore, &builder)
|
|
writeExcludeSQL(query, &builder)
|
|
writeTypeFilterSQL(typeFilter, &builder)
|
|
if err := folderFilter.writeFolderFilterSQL(false, &builder); err != nil {
|
|
return err
|
|
}
|
|
if !signedInUser.HasRole(org.RoleAdmin) {
|
|
builder.WriteDashboardPermissionFilter(signedInUser, dashboards.PERMISSION_VIEW, "")
|
|
}
|
|
if query.SortDirection == search.SortAlphaDesc.Name {
|
|
builder.Write(" ORDER BY 1 DESC")
|
|
} else {
|
|
builder.Write(" ORDER BY 1 ASC")
|
|
}
|
|
writePerPageSQL(query, l.SQLStore, &builder)
|
|
if err := session.SQL(builder.GetSQLString(), builder.GetParams()...).Find(&elements); err != nil {
|
|
return err
|
|
}
|
|
|
|
retDTOs := make([]model.LibraryElementDTO, 0)
|
|
for _, element := range elements {
|
|
retDTOs = append(retDTOs, model.LibraryElementDTO{
|
|
ID: element.ID,
|
|
OrgID: element.OrgID,
|
|
FolderID: element.FolderID, // nolint:staticcheck
|
|
FolderUID: element.FolderUID,
|
|
UID: element.UID,
|
|
Name: element.Name,
|
|
Kind: element.Kind,
|
|
Type: element.Type,
|
|
Description: element.Description,
|
|
Model: element.Model,
|
|
Version: element.Version,
|
|
Meta: model.LibraryElementDTOMeta{
|
|
FolderName: element.FolderName,
|
|
FolderUID: element.FolderUID,
|
|
ConnectedDashboards: element.ConnectedDashboards,
|
|
Created: element.Created,
|
|
Updated: element.Updated,
|
|
CreatedBy: librarypanel.LibraryElementDTOMetaUser{
|
|
Id: element.CreatedBy,
|
|
Name: element.CreatedByName,
|
|
AvatarUrl: dtos.GetGravatarUrl(element.CreatedByEmail),
|
|
},
|
|
UpdatedBy: librarypanel.LibraryElementDTOMetaUser{
|
|
Id: element.UpdatedBy,
|
|
Name: element.UpdatedByName,
|
|
AvatarUrl: dtos.GetGravatarUrl(element.UpdatedByEmail),
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
var libraryElements []model.LibraryElement
|
|
countBuilder := db.SQLBuilder{}
|
|
if folderFilter.includeGeneralFolder {
|
|
countBuilder.Write(selectLibraryElementDTOWithMeta)
|
|
countBuilder.Write(getFromLibraryElementDTOWithMeta(l.SQLStore.GetDialect()))
|
|
countBuilder.Write(` WHERE le.org_id=? AND le.folder_id=0`, signedInUser.GetOrgID())
|
|
writeKindSQL(query, &countBuilder)
|
|
writeSearchStringSQL(query, l.SQLStore, &countBuilder)
|
|
writeExcludeSQL(query, &countBuilder)
|
|
writeTypeFilterSQL(typeFilter, &countBuilder)
|
|
countBuilder.Write(" UNION ")
|
|
}
|
|
countBuilder.Write(selectLibraryElementDTOWithMeta)
|
|
countBuilder.Write(getFromLibraryElementDTOWithMeta(l.SQLStore.GetDialect()))
|
|
countBuilder.Write(" INNER JOIN dashboard AS dashboard on le.folder_id = dashboard.id and le.folder_id<>0")
|
|
countBuilder.Write(` WHERE le.org_id=?`, signedInUser.GetOrgID())
|
|
writeKindSQL(query, &countBuilder)
|
|
writeSearchStringSQL(query, l.SQLStore, &countBuilder)
|
|
writeExcludeSQL(query, &countBuilder)
|
|
writeTypeFilterSQL(typeFilter, &countBuilder)
|
|
if err := folderFilter.writeFolderFilterSQL(true, &countBuilder); err != nil {
|
|
return err
|
|
}
|
|
if err := session.SQL(countBuilder.GetSQLString(), countBuilder.GetParams()...).Find(&libraryElements); err != nil {
|
|
return err
|
|
}
|
|
|
|
result = model.LibraryElementSearchResult{
|
|
TotalCount: int64(len(libraryElements)),
|
|
Elements: retDTOs,
|
|
Page: query.Page,
|
|
PerPage: query.PerPage,
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
return result, err
|
|
}
|
|
|
|
func (l *LibraryElementService) handleFolderIDPatches(ctx context.Context, elementToPatch *model.LibraryElement,
|
|
fromFolderID int64, toFolderID int64, user identity.Requester) error {
|
|
// FolderID was not provided in the PATCH request
|
|
if toFolderID == -1 {
|
|
toFolderID = fromFolderID
|
|
}
|
|
|
|
// FolderID was provided in the PATCH request
|
|
if toFolderID != -1 && toFolderID != fromFolderID {
|
|
if err := l.requireEditPermissionsOnFolder(ctx, user, toFolderID); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Always check permissions for the folder where library element resides
|
|
if err := l.requireEditPermissionsOnFolder(ctx, user, fromFolderID); err != nil {
|
|
return err
|
|
}
|
|
// nolint:staticcheck
|
|
elementToPatch.FolderID = toFolderID
|
|
|
|
return nil
|
|
}
|
|
|
|
// patchLibraryElement updates a Library Element.
|
|
func (l *LibraryElementService) patchLibraryElement(c context.Context, signedInUser identity.Requester, cmd model.PatchLibraryElementCommand, uid string) (model.LibraryElementDTO, error) {
|
|
var dto model.LibraryElementDTO
|
|
if err := l.requireSupportedElementKind(cmd.Kind); err != nil {
|
|
return model.LibraryElementDTO{}, err
|
|
}
|
|
err := l.SQLStore.WithTransactionalDbSession(c, func(session *db.Session) error {
|
|
elementInDB, err := GetLibraryElement(l.SQLStore.GetDialect(), session, uid, signedInUser.GetOrgID())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if elementInDB.Version != cmd.Version {
|
|
return model.ErrLibraryElementVersionMismatch
|
|
}
|
|
updateUID := cmd.UID
|
|
if len(updateUID) == 0 {
|
|
updateUID = uid
|
|
} else if updateUID != uid {
|
|
if !util.IsValidShortUID(updateUID) {
|
|
return model.ErrLibraryElementInvalidUID
|
|
} else if util.IsShortUIDTooLong(updateUID) {
|
|
return model.ErrLibraryElementUIDTooLong
|
|
}
|
|
|
|
_, err := GetLibraryElement(l.SQLStore.GetDialect(), session, updateUID, signedInUser.GetOrgID())
|
|
if !errors.Is(err, model.ErrLibraryElementNotFound) {
|
|
return model.ErrLibraryElementAlreadyExists
|
|
}
|
|
}
|
|
|
|
var userID int64
|
|
namespaceID, identifier := signedInUser.GetNamespacedID()
|
|
switch namespaceID {
|
|
case identity.NamespaceUser, identity.NamespaceServiceAccount:
|
|
var errID error
|
|
userID, errID = identity.IntIdentifier(namespaceID, identifier)
|
|
if errID != nil {
|
|
l.log.Warn("Error while parsing userID", "namespaceID", namespaceID, "userID", identifier, "err", errID)
|
|
}
|
|
}
|
|
|
|
var libraryElement = model.LibraryElement{
|
|
ID: elementInDB.ID,
|
|
OrgID: signedInUser.GetOrgID(),
|
|
FolderID: cmd.FolderID, // nolint:staticcheck
|
|
UID: updateUID,
|
|
Name: cmd.Name,
|
|
Kind: elementInDB.Kind,
|
|
Type: elementInDB.Type,
|
|
Description: elementInDB.Description,
|
|
Model: cmd.Model,
|
|
Version: elementInDB.Version + 1,
|
|
Created: elementInDB.Created,
|
|
CreatedBy: elementInDB.CreatedBy,
|
|
Updated: time.Now(),
|
|
UpdatedBy: userID,
|
|
}
|
|
|
|
if cmd.Name == "" {
|
|
libraryElement.Name = elementInDB.Name
|
|
}
|
|
if cmd.Model == nil {
|
|
libraryElement.Model = elementInDB.Model
|
|
}
|
|
// nolint:staticcheck
|
|
if err := l.handleFolderIDPatches(c, &libraryElement, elementInDB.FolderID, cmd.FolderID, signedInUser); err != nil {
|
|
return err
|
|
}
|
|
if err := syncFieldsWithModel(&libraryElement); err != nil {
|
|
return err
|
|
}
|
|
if rowsAffected, err := session.ID(elementInDB.ID).Update(&libraryElement); err != nil {
|
|
if l.SQLStore.GetDialect().IsUniqueConstraintViolation(err) {
|
|
return model.ErrLibraryElementAlreadyExists
|
|
}
|
|
return err
|
|
} else if rowsAffected != 1 {
|
|
return model.ErrLibraryElementNotFound
|
|
}
|
|
|
|
dto = model.LibraryElementDTO{
|
|
ID: libraryElement.ID,
|
|
OrgID: libraryElement.OrgID,
|
|
FolderID: libraryElement.FolderID, // nolint:staticcheck
|
|
UID: libraryElement.UID,
|
|
Name: libraryElement.Name,
|
|
Kind: libraryElement.Kind,
|
|
Type: libraryElement.Type,
|
|
Description: libraryElement.Description,
|
|
Model: libraryElement.Model,
|
|
Version: libraryElement.Version,
|
|
Meta: model.LibraryElementDTOMeta{
|
|
ConnectedDashboards: elementInDB.ConnectedDashboards,
|
|
Created: libraryElement.Created,
|
|
Updated: libraryElement.Updated,
|
|
CreatedBy: librarypanel.LibraryElementDTOMetaUser{
|
|
Id: elementInDB.CreatedBy,
|
|
Name: elementInDB.CreatedByName,
|
|
AvatarUrl: dtos.GetGravatarUrl(elementInDB.CreatedByEmail),
|
|
},
|
|
UpdatedBy: librarypanel.LibraryElementDTOMetaUser{
|
|
Id: libraryElement.UpdatedBy,
|
|
Name: signedInUser.GetLogin(),
|
|
AvatarUrl: dtos.GetGravatarUrl(signedInUser.GetEmail()),
|
|
},
|
|
},
|
|
}
|
|
return nil
|
|
})
|
|
|
|
return dto, err
|
|
}
|
|
|
|
// getConnections gets all connections for a Library Element.
|
|
func (l *LibraryElementService) getConnections(c context.Context, signedInUser identity.Requester, uid string) ([]model.LibraryElementConnectionDTO, error) {
|
|
connections := make([]model.LibraryElementConnectionDTO, 0)
|
|
recursiveQueriesAreSupported, err := l.SQLStore.RecursiveQueriesAreSupported()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = l.SQLStore.WithDbSession(c, func(session *db.Session) error {
|
|
element, err := GetLibraryElement(l.SQLStore.GetDialect(), session, uid, signedInUser.GetOrgID())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var libraryElementConnections []model.LibraryElementConnectionWithMeta
|
|
builder := db.NewSqlBuilder(l.Cfg, l.features, l.SQLStore.GetDialect(), recursiveQueriesAreSupported)
|
|
builder.Write("SELECT lec.*, u1.login AS created_by_name, u1.email AS created_by_email, dashboard.uid AS connection_uid")
|
|
builder.Write(" FROM " + model.LibraryElementConnectionTableName + " AS lec")
|
|
builder.Write(" LEFT JOIN " + l.SQLStore.GetDialect().Quote("user") + " AS u1 ON lec.created_by = u1.id")
|
|
builder.Write(" INNER JOIN dashboard AS dashboard on lec.connection_id = dashboard.id")
|
|
builder.Write(` WHERE lec.element_id=?`, element.ID)
|
|
if signedInUser.GetOrgRole() != org.RoleAdmin {
|
|
builder.WriteDashboardPermissionFilter(signedInUser, dashboards.PERMISSION_VIEW, "")
|
|
}
|
|
if err := session.SQL(builder.GetSQLString(), builder.GetParams()...).Find(&libraryElementConnections); err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, connection := range libraryElementConnections {
|
|
connections = append(connections, model.LibraryElementConnectionDTO{
|
|
ID: connection.ID,
|
|
Kind: connection.Kind,
|
|
ElementID: connection.ElementID,
|
|
ConnectionID: connection.ConnectionID,
|
|
ConnectionUID: connection.ConnectionUID,
|
|
Created: connection.Created,
|
|
CreatedBy: librarypanel.LibraryElementDTOMetaUser{
|
|
Id: connection.CreatedBy,
|
|
Name: connection.CreatedByName,
|
|
AvatarUrl: dtos.GetGravatarUrl(connection.CreatedByEmail),
|
|
},
|
|
})
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
return connections, err
|
|
}
|
|
|
|
// getElementsForDashboardID gets all elements for a specific dashboard
|
|
func (l *LibraryElementService) getElementsForDashboardID(c context.Context, dashboardID int64) (map[string]model.LibraryElementDTO, error) {
|
|
libraryElementMap := make(map[string]model.LibraryElementDTO)
|
|
err := l.SQLStore.WithDbSession(c, func(session *db.Session) error {
|
|
var libraryElements []model.LibraryElementWithMeta
|
|
sql := selectLibraryElementDTOWithMeta +
|
|
", coalesce(dashboard.title, 'General') AS folder_name" +
|
|
", coalesce(dashboard.uid, '') AS folder_uid" +
|
|
getFromLibraryElementDTOWithMeta(l.SQLStore.GetDialect()) +
|
|
" LEFT JOIN dashboard AS dashboard ON dashboard.id = le.folder_id" +
|
|
" INNER JOIN " + model.LibraryElementConnectionTableName + " AS lce ON lce.element_id = le.id AND lce.kind=1 AND lce.connection_id=?"
|
|
sess := session.SQL(sql, dashboardID)
|
|
err := sess.Find(&libraryElements)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, element := range libraryElements {
|
|
libraryElementMap[element.UID] = model.LibraryElementDTO{
|
|
ID: element.ID,
|
|
OrgID: element.OrgID,
|
|
FolderID: element.FolderID, // nolint:staticcheck
|
|
UID: element.UID,
|
|
Name: element.Name,
|
|
Kind: element.Kind,
|
|
Type: element.Type,
|
|
Description: element.Description,
|
|
Model: element.Model,
|
|
Version: element.Version,
|
|
Meta: model.LibraryElementDTOMeta{
|
|
FolderName: element.FolderName,
|
|
FolderUID: element.FolderUID,
|
|
ConnectedDashboards: element.ConnectedDashboards,
|
|
Created: element.Created,
|
|
Updated: element.Updated,
|
|
CreatedBy: librarypanel.LibraryElementDTOMetaUser{
|
|
Id: element.CreatedBy,
|
|
Name: element.CreatedByName,
|
|
AvatarUrl: dtos.GetGravatarUrl(element.CreatedByEmail),
|
|
},
|
|
UpdatedBy: librarypanel.LibraryElementDTOMetaUser{
|
|
Id: element.UpdatedBy,
|
|
Name: element.UpdatedByName,
|
|
AvatarUrl: dtos.GetGravatarUrl(element.UpdatedByEmail),
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
return libraryElementMap, err
|
|
}
|
|
|
|
// connectElementsToDashboardID adds connections for all elements Library Elements in a Dashboard.
|
|
func (l *LibraryElementService) connectElementsToDashboardID(c context.Context, signedInUser identity.Requester, elementUIDs []string, dashboardID int64) error {
|
|
err := l.SQLStore.WithTransactionalDbSession(c, func(session *db.Session) error {
|
|
_, err := session.Exec("DELETE FROM "+model.LibraryElementConnectionTableName+" WHERE kind=1 AND connection_id=?", dashboardID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, elementUID := range elementUIDs {
|
|
element, err := GetLibraryElement(l.SQLStore.GetDialect(), session, elementUID, signedInUser.GetOrgID())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// nolint:staticcheck
|
|
if err := l.requireViewPermissionsOnFolder(c, signedInUser, element.FolderID); err != nil {
|
|
return err
|
|
}
|
|
|
|
namespaceID, identifier := signedInUser.GetNamespacedID()
|
|
userID := int64(0)
|
|
switch namespaceID {
|
|
case identity.NamespaceUser, identity.NamespaceServiceAccount:
|
|
userID, err = identity.IntIdentifier(namespaceID, identifier)
|
|
if err != nil {
|
|
l.log.Warn("Failed to parse user ID from namespace identifier", "namespace", namespaceID, "identifier", identifier, "error", err)
|
|
}
|
|
}
|
|
|
|
connection := model.LibraryElementConnection{
|
|
ElementID: element.ID,
|
|
Kind: 1,
|
|
ConnectionID: dashboardID,
|
|
Created: time.Now(),
|
|
CreatedBy: userID,
|
|
}
|
|
if _, err := session.Insert(&connection); err != nil {
|
|
if l.SQLStore.GetDialect().IsUniqueConstraintViolation(err) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
|
|
return err
|
|
}
|
|
|
|
// disconnectElementsFromDashboardID deletes connections for all Library Elements in a Dashboard.
|
|
func (l *LibraryElementService) disconnectElementsFromDashboardID(c context.Context, dashboardID int64) error {
|
|
return l.SQLStore.WithTransactionalDbSession(c, func(session *db.Session) error {
|
|
_, err := session.Exec("DELETE FROM "+model.LibraryElementConnectionTableName+" WHERE kind=1 AND connection_id=?", dashboardID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// deleteLibraryElementsInFolderUID deletes all Library Elements in a folder.
|
|
func (l *LibraryElementService) deleteLibraryElementsInFolderUID(c context.Context, signedInUser identity.Requester, folderUID string) error {
|
|
return l.SQLStore.WithTransactionalDbSession(c, func(session *db.Session) error {
|
|
var folderUIDs []struct {
|
|
ID int64 `xorm:"id"`
|
|
}
|
|
err := session.SQL("SELECT id from dashboard WHERE uid=? AND org_id=? AND is_folder=?", folderUID, signedInUser.GetOrgID(), l.SQLStore.GetDialect().BooleanStr(true)).Find(&folderUIDs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(folderUIDs) == 0 {
|
|
return dashboards.ErrFolderNotFound
|
|
}
|
|
|
|
if len(folderUIDs) != 1 {
|
|
return fmt.Errorf("found %d folders, while expecting at most one", len(folderUIDs))
|
|
}
|
|
|
|
folderID := folderUIDs[0].ID
|
|
|
|
if err := l.requireEditPermissionsOnFolder(c, signedInUser, folderID); err != nil {
|
|
return err
|
|
}
|
|
var connectionIDs []struct {
|
|
ConnectionID int64 `xorm:"connection_id"`
|
|
}
|
|
sql := "SELECT lec.connection_id FROM library_element AS le"
|
|
sql += " INNER JOIN " + model.LibraryElementConnectionTableName + " AS lec on le.id = lec.element_id"
|
|
sql += " WHERE le.folder_id=? AND le.org_id=?"
|
|
err = session.SQL(sql, folderID, signedInUser.GetOrgID()).Find(&connectionIDs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(connectionIDs) > 0 {
|
|
return model.ErrFolderHasConnectedLibraryElements
|
|
}
|
|
|
|
var elementIDs []struct {
|
|
ID int64 `xorm:"id"`
|
|
}
|
|
err = session.SQL("SELECT id from library_element WHERE folder_id=? AND org_id=?", folderID, signedInUser.GetOrgID()).Find(&elementIDs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, elementID := range elementIDs {
|
|
_, err := session.Exec("DELETE FROM "+model.LibraryElementConnectionTableName+" WHERE element_id=?", elementID.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if _, err := session.Exec("DELETE FROM library_element WHERE folder_id=? AND org_id=?", folderID, signedInUser.GetOrgID()); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|