From 5e349d8294588fd6f8c0928bd7c16b08ed7a740d Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Thu, 2 Nov 2023 22:52:50 +0530 Subject: [PATCH] chore: dashboard locking ee query-service (#3856) * chore: dashboard locking ee query-service * chore: remove print statements * chore: remove unused imports * chore: no one is allowed to edit/delete the locked dashboard --------- Co-authored-by: Srikanth Chekuri --- ee/query-service/app/api/api.go | 3 ++ ee/query-service/app/api/dashboard.go | 51 +++++++++++++++++++++++ pkg/query-service/app/dashboards/model.go | 43 +++++++++++++++++-- pkg/query-service/app/parser/metrics.go | 1 - pkg/query-service/dao/sqlite/apdex.go | 2 - 5 files changed, 93 insertions(+), 7 deletions(-) create mode 100644 ee/query-service/app/api/dashboard.go diff --git a/ee/query-service/app/api/api.go b/ee/query-service/app/api/api.go index a17eb7b79a..ddb617188f 100644 --- a/ee/query-service/app/api/api.go +++ b/ee/query-service/app/api/api.go @@ -160,6 +160,9 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *baseapp.AuthMiddlew router.HandleFunc("/api/v1/billing", am.AdminAccess(ah.getBilling)).Methods(http.MethodGet) router.HandleFunc("/api/v1/portal", am.AdminAccess(ah.portalSession)).Methods(http.MethodPost) + router.HandleFunc("/api/v1/dashboards/{uuid}/lock", am.EditAccess(ah.lockDashboard)).Methods(http.MethodPut) + router.HandleFunc("/api/v1/dashboards/{uuid}/unlock", am.EditAccess(ah.unlockDashboard)).Methods(http.MethodPut) + router.HandleFunc("/api/v2/licenses", am.ViewAccess(ah.listLicensesV2)). Methods(http.MethodGet) diff --git a/ee/query-service/app/api/dashboard.go b/ee/query-service/app/api/dashboard.go new file mode 100644 index 0000000000..abd1aed3f4 --- /dev/null +++ b/ee/query-service/app/api/dashboard.go @@ -0,0 +1,51 @@ +package api + +import ( + "github.com/gorilla/mux" + "go.signoz.io/signoz/pkg/query-service/app/dashboards" + "go.signoz.io/signoz/pkg/query-service/auth" + "go.signoz.io/signoz/pkg/query-service/common" + "go.signoz.io/signoz/pkg/query-service/model" + "net/http" +) + +func (ah *APIHandler) lockDashboard(w http.ResponseWriter, r *http.Request) { + ah.lockUnlockDashboard(w, r, true) +} + +func (ah *APIHandler) unlockDashboard(w http.ResponseWriter, r *http.Request) { + ah.lockUnlockDashboard(w, r, false) +} + +func (ah *APIHandler) lockUnlockDashboard(w http.ResponseWriter, r *http.Request, lock bool) { + // Locking can only be done by the owner of the dashboard + // or an admin + + // - Fetch the dashboard + // - Check if the user is the owner or an admin + // - If yes, lock/unlock the dashboard + // - If no, return 403 + + // Get the dashboard UUID from the request + uuid := mux.Vars(r)["uuid"] + dashboard, err := dashboards.GetDashboard(r.Context(), uuid) + if err != nil { + RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, err.Error()) + return + } + + user := common.GetUserFromContext(r.Context()) + if !auth.IsAdmin(user) || (dashboard.CreateBy != nil && *dashboard.CreateBy != user.Email) { + RespondError(w, &model.ApiError{Typ: model.ErrorForbidden, Err: err}, "You are not authorized to lock/unlock this dashboard") + return + } + + // Lock/Unlock the dashboard + err = dashboards.LockUnlockDashboard(r.Context(), uuid, lock) + if err != nil { + RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, err.Error()) + return + } + + ah.Respond(w, "Dashboard updated successfully") +} diff --git a/pkg/query-service/app/dashboards/model.go b/pkg/query-service/app/dashboards/model.go index fa51864d4c..698b697279 100644 --- a/pkg/query-service/app/dashboards/model.go +++ b/pkg/query-service/app/dashboards/model.go @@ -128,6 +128,12 @@ func InitDB(dataSourceName string) (*sqlx.DB, error) { return nil, fmt.Errorf("error in adding column updated_by to dashboards table: %s", err.Error()) } + locked := `ALTER TABLE dashboards ADD COLUMN locked INTEGER DEFAULT 0;` + _, err = db.Exec(locked) + if err != nil && !strings.Contains(err.Error(), "duplicate column name") { + return nil, fmt.Errorf("error in adding column locked to dashboards table: %s", err.Error()) + } + return db, nil } @@ -141,6 +147,7 @@ type Dashboard struct { UpdateBy *string `json:"updated_by" db:"updated_by"` Title string `json:"-" db:"-"` Data Data `json:"data" db:"data"` + Locked *int `json:"isLocked" db:"locked"` } type Data map[string]interface{} @@ -239,6 +246,12 @@ func DeleteDashboard(ctx context.Context, uuid string, fm interfaces.FeatureLook return dErr } + if user := common.GetUserFromContext(ctx); user != nil { + if dashboard.Locked != nil && *dashboard.Locked == 1 { + return model.BadRequest(fmt.Errorf("dashboard is locked, please unlock the dashboard to be able to delete it")) + } + } + query := `DELETE FROM dashboards WHERE uuid=?` result, err := db.Exec(query, uuid) @@ -289,6 +302,14 @@ func UpdateDashboard(ctx context.Context, uuid string, data map[string]interface return nil, apiErr } + var userEmail string + if user := common.GetUserFromContext(ctx); user != nil { + userEmail = user.Email + if dashboard.Locked != nil && *dashboard.Locked == 1 { + return nil, model.BadRequest(fmt.Errorf("dashboard is locked, please unlock the dashboard to be able to edit it")) + } + } + // check if the count of trace and logs QB panel has changed, if yes, then check feature flag count existingCount, existingTotal := countTraceAndLogsPanel(dashboard.Data) newCount, newTotal := countTraceAndLogsPanel(data) @@ -306,10 +327,6 @@ func UpdateDashboard(ctx context.Context, uuid string, data map[string]interface } dashboard.UpdatedAt = time.Now() - var userEmail string - if user := common.GetUserFromContext(ctx); user != nil { - userEmail = user.Email - } dashboard.UpdateBy = &userEmail dashboard.Data = data @@ -327,6 +344,24 @@ func UpdateDashboard(ctx context.Context, uuid string, data map[string]interface return dashboard, nil } +func LockUnlockDashboard(ctx context.Context, uuid string, lock bool) *model.ApiError { + var query string + if lock { + query = `UPDATE dashboards SET locked=1 WHERE uuid=?;` + } else { + query = `UPDATE dashboards SET locked=0 WHERE uuid=?;` + } + + _, err := db.Exec(query, uuid) + + if err != nil { + zap.S().Errorf("Error in updating dashboard: ", uuid, err) + return &model.ApiError{Typ: model.ErrorExec, Err: err} + } + + return nil +} + func updateFeatureUsage(fm interfaces.FeatureLookup, usage int64) *model.ApiError { feature, err := fm.GetFeatureFlag(model.QueryBuilderPanels) if err != nil { diff --git a/pkg/query-service/app/parser/metrics.go b/pkg/query-service/app/parser/metrics.go index b13ff6d534..3391be78c7 100644 --- a/pkg/query-service/app/parser/metrics.go +++ b/pkg/query-service/app/parser/metrics.go @@ -91,7 +91,6 @@ func ParseMetricAutocompleteTagParams(r *http.Request) (*model.MetricAutocomplet } tagsStr := r.URL.Query().Get("tags") - // fmt.Println(tagsStr) // parsing tags var tags map[string]string diff --git a/pkg/query-service/dao/sqlite/apdex.go b/pkg/query-service/dao/sqlite/apdex.go index 65c1eae350..63280d8fb6 100644 --- a/pkg/query-service/dao/sqlite/apdex.go +++ b/pkg/query-service/dao/sqlite/apdex.go @@ -2,7 +2,6 @@ package sqlite import ( "context" - "fmt" "github.com/jmoiron/sqlx" "go.signoz.io/signoz/pkg/query-service/model" @@ -51,7 +50,6 @@ func (mds *ModelDaoSqlite) GetApdexSettings(ctx context.Context, services []stri func (mds *ModelDaoSqlite) SetApdexSettings(ctx context.Context, apdexSettings *model.ApdexSettings) *model.ApiError { - fmt.Println("apdexSettings:", apdexSettings) _, err := mds.db.NamedExec(` INSERT OR REPLACE INTO apdex_settings ( service_name,