From a7debaa6edecfc16eb6cb8f5c1fd00a8aef58dda Mon Sep 17 00:00:00 2001 From: Naman Verma Date: Fri, 15 May 2026 13:12:04 +0530 Subject: [PATCH] feat: lock, unlock, create public, update public v2 dashboard APIs (#11167) * feat: lock, unlock, create public, update public v2 dashboard APIs * chore: update api specs * fix: use new pattern of checking for admin permission * fix: remove soft delete reference * chore: revert all frontend changes * fix: fix build errors and remove v2 create/update public apis * chore: use v1 methods wherever possible * fix: use update v2 store method --- docs/api/openapi.yml | 205 ++++++++++++++++++ ee/modules/dashboard/impldashboard/module.go | 4 + pkg/apiserver/signozapiserver/dashboard.go | 34 +++ pkg/modules/dashboard/dashboard.go | 6 + .../dashboard/impldashboard/v2_handler.go | 61 ++++++ .../dashboard/impldashboard/v2_module.go | 15 ++ pkg/types/dashboardtypes/perses_dashboard.go | 16 ++ 7 files changed, 341 insertions(+) diff --git a/docs/api/openapi.yml b/docs/api/openapi.yml index eac1f639e4..81f849527d 100644 --- a/docs/api/openapi.yml +++ b/docs/api/openapi.yml @@ -12736,6 +12736,211 @@ paths: summary: Update dashboard (v2) tags: - dashboard + /api/v2/dashboards/{id}/lock: + delete: + deprecated: false + description: This endpoint unlocks a v2-shape dashboard. Only the dashboard's + creator or an org admin may lock or unlock. + operationId: UnlockDashboardV2 + parameters: + - in: path + name: id + required: true + schema: + type: string + responses: + "204": + content: + application/json: + schema: + type: string + description: No Content + "401": + content: + application/json: + schema: + $ref: '#/components/schemas/RenderErrorResponse' + description: Unauthorized + "403": + content: + application/json: + schema: + $ref: '#/components/schemas/RenderErrorResponse' + description: Forbidden + "500": + content: + application/json: + schema: + $ref: '#/components/schemas/RenderErrorResponse' + description: Internal Server Error + security: + - api_key: + - EDITOR + - tokenizer: + - EDITOR + summary: Unlock dashboard (v2) + tags: + - dashboard + put: + deprecated: false + description: This endpoint locks a v2-shape dashboard. Only the dashboard's + creator or an org admin may lock or unlock. + operationId: LockDashboardV2 + parameters: + - in: path + name: id + required: true + schema: + type: string + responses: + "204": + content: + application/json: + schema: + type: string + description: No Content + "401": + content: + application/json: + schema: + $ref: '#/components/schemas/RenderErrorResponse' + description: Unauthorized + "403": + content: + application/json: + schema: + $ref: '#/components/schemas/RenderErrorResponse' + description: Forbidden + "500": + content: + application/json: + schema: + $ref: '#/components/schemas/RenderErrorResponse' + description: Internal Server Error + security: + - api_key: + - EDITOR + - tokenizer: + - EDITOR + summary: Lock dashboard (v2) + tags: + - dashboard + /api/v2/dashboards/{id}/public: + patch: + deprecated: false + description: This endpoint creates the public sharing config for a v2 dashboard + and returns the dashboard with the new public config attached. Lock state + does not gate this endpoint. + operationId: CreatePublicDashboardV2 + parameters: + - in: path + name: id + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/DashboardtypesPostablePublicDashboard' + responses: + "200": + content: + application/json: + schema: + properties: + data: + $ref: '#/components/schemas/DashboardtypesGettableDashboardV2' + status: + type: string + required: + - status + - data + type: object + description: OK + "401": + content: + application/json: + schema: + $ref: '#/components/schemas/RenderErrorResponse' + description: Unauthorized + "403": + content: + application/json: + schema: + $ref: '#/components/schemas/RenderErrorResponse' + description: Forbidden + "500": + content: + application/json: + schema: + $ref: '#/components/schemas/RenderErrorResponse' + description: Internal Server Error + security: + - api_key: + - ADMIN + - tokenizer: + - ADMIN + summary: Make a dashboard v2 public + tags: + - dashboard + put: + deprecated: false + description: This endpoint updates the public sharing config (time range settings) + of an already-public v2 dashboard. Lock state does not gate this endpoint. + operationId: UpdatePublicDashboardV2 + parameters: + - in: path + name: id + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/DashboardtypesUpdatablePublicDashboard' + responses: + "200": + content: + application/json: + schema: + properties: + data: + $ref: '#/components/schemas/DashboardtypesGettableDashboardV2' + status: + type: string + required: + - status + - data + type: object + description: OK + "401": + content: + application/json: + schema: + $ref: '#/components/schemas/RenderErrorResponse' + description: Unauthorized + "403": + content: + application/json: + schema: + $ref: '#/components/schemas/RenderErrorResponse' + description: Forbidden + "500": + content: + application/json: + schema: + $ref: '#/components/schemas/RenderErrorResponse' + description: Internal Server Error + security: + - api_key: + - ADMIN + - tokenizer: + - ADMIN + summary: Update public sharing config for a dashboard v2 + tags: + - dashboard /api/v2/factor_password/forgot: post: deprecated: false diff --git a/ee/modules/dashboard/impldashboard/module.go b/ee/modules/dashboard/impldashboard/module.go index 7eabfdf16c..caa3575364 100644 --- a/ee/modules/dashboard/impldashboard/module.go +++ b/ee/modules/dashboard/impldashboard/module.go @@ -211,6 +211,10 @@ func (module *module) UpdateV2(ctx context.Context, orgID valuer.UUID, id valuer return module.pkgDashboardModule.UpdateV2(ctx, orgID, id, updatedBy, updateable) } +func (module *module) LockUnlockV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, isAdmin bool, lock bool) error { + return module.pkgDashboardModule.LockUnlockV2(ctx, orgID, id, updatedBy, isAdmin, lock) +} + func (module *module) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*dashboardtypes.Dashboard, error) { return module.pkgDashboardModule.Get(ctx, orgID, id) } diff --git a/pkg/apiserver/signozapiserver/dashboard.go b/pkg/apiserver/signozapiserver/dashboard.go index c1970dd663..8676152f9b 100644 --- a/pkg/apiserver/signozapiserver/dashboard.go +++ b/pkg/apiserver/signozapiserver/dashboard.go @@ -65,6 +65,40 @@ func (provider *provider) addDashboardRoutes(router *mux.Router) error { return err } + if err := router.Handle("/api/v2/dashboards/{id}/lock", handler.New(provider.authzMiddleware.EditAccess(provider.dashboardHandler.LockV2), handler.OpenAPIDef{ + ID: "LockDashboardV2", + Tags: []string{"dashboard"}, + Summary: "Lock dashboard (v2)", + Description: "This endpoint locks a v2-shape dashboard. Only the dashboard's creator or an org admin may lock or unlock.", + Request: nil, + RequestContentType: "", + Response: nil, + ResponseContentType: "application/json", + SuccessStatusCode: http.StatusNoContent, + ErrorStatusCodes: []int{}, + Deprecated: false, + SecuritySchemes: newSecuritySchemes(types.RoleEditor), + })).Methods(http.MethodPut).GetError(); err != nil { + return err + } + + if err := router.Handle("/api/v2/dashboards/{id}/lock", handler.New(provider.authzMiddleware.EditAccess(provider.dashboardHandler.UnlockV2), handler.OpenAPIDef{ + ID: "UnlockDashboardV2", + Tags: []string{"dashboard"}, + Summary: "Unlock dashboard (v2)", + Description: "This endpoint unlocks a v2-shape dashboard. Only the dashboard's creator or an org admin may lock or unlock.", + Request: nil, + RequestContentType: "", + Response: nil, + ResponseContentType: "application/json", + SuccessStatusCode: http.StatusNoContent, + ErrorStatusCodes: []int{}, + Deprecated: false, + SecuritySchemes: newSecuritySchemes(types.RoleEditor), + })).Methods(http.MethodDelete).GetError(); err != nil { + return err + } + if err := router.Handle("/api/v1/dashboards/{id}/public", handler.New(provider.authzMiddleware.AdminAccess(provider.dashboardHandler.CreatePublic), handler.OpenAPIDef{ ID: "CreatePublicDashboard", Tags: []string{"dashboard"}, diff --git a/pkg/modules/dashboard/dashboard.go b/pkg/modules/dashboard/dashboard.go index b61bc855af..76928e6ce7 100644 --- a/pkg/modules/dashboard/dashboard.go +++ b/pkg/modules/dashboard/dashboard.go @@ -59,6 +59,8 @@ type Module interface { GetV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*dashboardtypes.DashboardV2, error) UpdateV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, updateable dashboardtypes.UpdateableDashboardV2) (*dashboardtypes.DashboardV2, error) + + LockUnlockV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, isAdmin bool, lock bool) error } type Handler interface { @@ -90,4 +92,8 @@ type Handler interface { GetV2(http.ResponseWriter, *http.Request) UpdateV2(http.ResponseWriter, *http.Request) + + LockV2(http.ResponseWriter, *http.Request) + + UnlockV2(http.ResponseWriter, *http.Request) } diff --git a/pkg/modules/dashboard/impldashboard/v2_handler.go b/pkg/modules/dashboard/impldashboard/v2_handler.go index a67227f0a2..c7c48178d6 100644 --- a/pkg/modules/dashboard/impldashboard/v2_handler.go +++ b/pkg/modules/dashboard/impldashboard/v2_handler.go @@ -10,6 +10,7 @@ import ( "github.com/SigNoz/signoz/pkg/http/binding" "github.com/SigNoz/signoz/pkg/http/render" "github.com/SigNoz/signoz/pkg/types/authtypes" + "github.com/SigNoz/signoz/pkg/types/coretypes" "github.com/SigNoz/signoz/pkg/types/dashboardtypes" "github.com/SigNoz/signoz/pkg/valuer" "github.com/gorilla/mux" @@ -82,6 +83,66 @@ func (handler *handler) GetV2(rw http.ResponseWriter, r *http.Request) { render.Success(rw, http.StatusOK, dashboard.ToGettableDashboardV2()) } +func (handler *handler) LockV2(rw http.ResponseWriter, r *http.Request) { + handler.lockUnlockV2(rw, r, true) +} + +func (handler *handler) UnlockV2(rw http.ResponseWriter, r *http.Request) { + handler.lockUnlockV2(rw, r, false) +} + +func (handler *handler) lockUnlockV2(rw http.ResponseWriter, r *http.Request, lock bool) { + ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second) + defer cancel() + + claims, err := authtypes.ClaimsFromContext(ctx) + if err != nil { + render.Error(rw, err) + return + } + + orgID, err := valuer.NewUUID(claims.OrgID) + if err != nil { + render.Error(rw, err) + return + } + + id := mux.Vars(r)["id"] + if id == "" { + render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is missing in the path")) + return + } + dashboardID, err := valuer.NewUUID(id) + if err != nil { + render.Error(rw, err) + return + } + + isAdmin := false + selectors := []coretypes.Selector{ + coretypes.TypeRole.MustSelector(authtypes.SigNozAdminRoleName), + } + err = handler.authz.CheckWithTupleCreation( + ctx, + claims, + valuer.MustNewUUID(claims.OrgID), + authtypes.Relation{Verb: coretypes.VerbAssignee}, + coretypes.NewResourceRole(), + selectors, + selectors, + ) + if err == nil { + isAdmin = true + } + + if err := handler.module.LockUnlockV2(ctx, orgID, dashboardID, claims.Email, isAdmin, lock); err != nil { + render.Error(rw, err) + return + } + + render.Success(rw, http.StatusNoContent, nil) +} + func (handler *handler) UpdateV2(rw http.ResponseWriter, r *http.Request) { ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second) defer cancel() diff --git a/pkg/modules/dashboard/impldashboard/v2_module.go b/pkg/modules/dashboard/impldashboard/v2_module.go index 035293c740..e8ab845ccd 100644 --- a/pkg/modules/dashboard/impldashboard/v2_module.go +++ b/pkg/modules/dashboard/impldashboard/v2_module.go @@ -90,3 +90,18 @@ func (module *module) UpdateV2(ctx context.Context, orgID valuer.UUID, id valuer return existing, nil } + +func (module *module) LockUnlockV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, isAdmin bool, lock bool) error { + existing, err := module.GetV2(ctx, orgID, id) + if err != nil { + return err + } + if err := existing.LockUnlock(lock, isAdmin, updatedBy); err != nil { + return err + } + storable, err := existing.ToStorableDashboard() + if err != nil { + return err + } + return module.store.UpdateV2(ctx, orgID, id, updatedBy, storable.Data) +} diff --git a/pkg/types/dashboardtypes/perses_dashboard.go b/pkg/types/dashboardtypes/perses_dashboard.go index 3d18ed4d75..3cf43ed3e7 100644 --- a/pkg/types/dashboardtypes/perses_dashboard.go +++ b/pkg/types/dashboardtypes/perses_dashboard.go @@ -70,6 +70,22 @@ func (d *DashboardV2) Update(updateable UpdateableDashboardV2, updatedBy string, d.UpdatedAt = time.Now() return nil } +func (d *DashboardV2) CanLockUnlock(isAdmin bool, updatedBy string) error { + if d.CreatedBy != updatedBy && !isAdmin { + return errors.Newf(errors.TypeForbidden, errors.CodeForbidden, "you are not authorized to lock/unlock this dashboard") + } + return nil +} + +func (d *DashboardV2) LockUnlock(lock bool, isAdmin bool, updatedBy string) error { + if err := d.CanLockUnlock(isAdmin, updatedBy); err != nil { + return err + } + d.Locked = lock + d.UpdatedBy = updatedBy + d.UpdatedAt = time.Now() + return nil +} type DashboardV2Data struct { Metadata DashboardV2Metadata `json:"metadata"`