diff --git a/cmd/community/server.go b/cmd/community/server.go index 9d7909f4b7..6a059e2688 100644 --- a/cmd/community/server.go +++ b/cmd/community/server.go @@ -5,9 +5,12 @@ import ( "log/slog" "github.com/SigNoz/signoz/cmd" + "github.com/SigNoz/signoz/ee/authz/openfgaauthz" + "github.com/SigNoz/signoz/ee/authz/openfgaschema" "github.com/SigNoz/signoz/ee/sqlstore/postgressqlstore" "github.com/SigNoz/signoz/pkg/analytics" "github.com/SigNoz/signoz/pkg/authn" + "github.com/SigNoz/signoz/pkg/authz" "github.com/SigNoz/signoz/pkg/factory" "github.com/SigNoz/signoz/pkg/licensing" "github.com/SigNoz/signoz/pkg/licensing/nooplicensing" @@ -76,6 +79,9 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e func(ctx context.Context, providerSettings factory.ProviderSettings, store authtypes.AuthNStore, licensing licensing.Licensing) (map[authtypes.AuthNProvider]authn.AuthN, error) { return signoz.NewAuthNs(ctx, providerSettings, store, licensing) }, + func(ctx context.Context, sqlstore sqlstore.SQLStore) factory.ProviderFactory[authz.AuthZ, authz.Config] { + return openfgaauthz.NewProviderFactory(sqlstore, openfgaschema.NewSchema().Get(ctx)) + }, ) if err != nil { logger.ErrorContext(ctx, "failed to create signoz", "error", err) diff --git a/cmd/enterprise/server.go b/cmd/enterprise/server.go index 87d3354d16..aac32981c1 100644 --- a/cmd/enterprise/server.go +++ b/cmd/enterprise/server.go @@ -8,6 +8,8 @@ import ( "github.com/SigNoz/signoz/cmd" "github.com/SigNoz/signoz/ee/authn/callbackauthn/oidccallbackauthn" "github.com/SigNoz/signoz/ee/authn/callbackauthn/samlcallbackauthn" + "github.com/SigNoz/signoz/ee/authz/openfgaauthz" + "github.com/SigNoz/signoz/ee/authz/openfgaschema" enterpriselicensing "github.com/SigNoz/signoz/ee/licensing" "github.com/SigNoz/signoz/ee/licensing/httplicensing" enterpriseapp "github.com/SigNoz/signoz/ee/query-service/app" @@ -17,6 +19,7 @@ import ( "github.com/SigNoz/signoz/ee/zeus/httpzeus" "github.com/SigNoz/signoz/pkg/analytics" "github.com/SigNoz/signoz/pkg/authn" + "github.com/SigNoz/signoz/pkg/authz" "github.com/SigNoz/signoz/pkg/factory" "github.com/SigNoz/signoz/pkg/licensing" "github.com/SigNoz/signoz/pkg/modules/organization" @@ -105,6 +108,9 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e return authNs, nil }, + func(ctx context.Context, sqlstore sqlstore.SQLStore) factory.ProviderFactory[authz.AuthZ, authz.Config] { + return openfgaauthz.NewProviderFactory(sqlstore, openfgaschema.NewSchema().Get(ctx)) + }, ) if err != nil { logger.ErrorContext(ctx, "failed to create signoz", "error", err) diff --git a/ee/authz/openfgaauthz/provider.go b/ee/authz/openfgaauthz/provider.go index 7623b047a4..6034314b4a 100644 --- a/ee/authz/openfgaauthz/provider.go +++ b/ee/authz/openfgaauthz/provider.go @@ -48,7 +48,26 @@ func (provider *provider) Check(ctx context.Context, tuple *openfgav1.TupleKey) } func (provider *provider) CheckWithTupleCreation(ctx context.Context, claims authtypes.Claims, orgID valuer.UUID, relation authtypes.Relation, _ authtypes.Relation, typeable authtypes.Typeable, selectors []authtypes.Selector) error { - subject, err := authtypes.NewSubject(authtypes.TypeUser, claims.UserID, authtypes.Relation{}) + subject, err := authtypes.NewSubject(authtypes.TypeableUser, claims.UserID, orgID, nil) + if err != nil { + return err + } + + tuples, err := typeable.Tuples(subject, relation, selectors, orgID) + if err != nil { + return err + } + + err = provider.BatchCheck(ctx, tuples) + if err != nil { + return err + } + + return nil +} + +func (provider *provider) CheckWithTupleCreationWithoutClaims(ctx context.Context, orgID valuer.UUID, relation authtypes.Relation, _ authtypes.Relation, typeable authtypes.Typeable, selectors []authtypes.Selector) error { + subject, err := authtypes.NewSubject(authtypes.TypeableAnonymous, authtypes.AnonymousUser.String(), orgID, nil) if err != nil { return err } diff --git a/ee/authz/openfgaschema/base.fga b/ee/authz/openfgaschema/base.fga index fc4d077a4e..c28377cee6 100644 --- a/ee/authz/openfgaschema/base.fga +++ b/ee/authz/openfgaschema/base.fga @@ -15,7 +15,7 @@ type anonymous type role relations - define assignee: [user] + define assignee: [user, anonymous] define read: [user, role#assignee] define update: [user, role#assignee] @@ -37,4 +37,4 @@ type metaresource type telemetryresource relations - define read: [user, anonymous, role#assignee] + define read: [user, role#assignee] diff --git a/ee/query-service/app/api/api.go b/ee/query-service/app/api/api.go index d3663c9108..4b3b382103 100644 --- a/ee/query-service/app/api/api.go +++ b/ee/query-service/app/api/api.go @@ -20,6 +20,10 @@ import ( basemodel "github.com/SigNoz/signoz/pkg/query-service/model" rules "github.com/SigNoz/signoz/pkg/query-service/rules" "github.com/SigNoz/signoz/pkg/signoz" + "github.com/SigNoz/signoz/pkg/types" + "github.com/SigNoz/signoz/pkg/types/authtypes" + "github.com/SigNoz/signoz/pkg/types/dashboardtypes" + "github.com/SigNoz/signoz/pkg/valuer" "github.com/SigNoz/signoz/pkg/version" "github.com/gorilla/mux" ) @@ -99,6 +103,39 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) { router.HandleFunc("/api/v1/billing", am.AdminAccess(ah.getBilling)).Methods(http.MethodGet) router.HandleFunc("/api/v1/portal", am.AdminAccess(ah.LicensingAPI.Portal)).Methods(http.MethodPost) + // dashboards + router.HandleFunc("/api/v1/dashboards/{id}/public", am.AdminAccess(ah.Signoz.Handlers.Dashboard.CreatePublic)).Methods(http.MethodPost) + router.HandleFunc("/api/v1/dashboards/{id}/public", am.AdminAccess(ah.Signoz.Handlers.Dashboard.GetPublic)).Methods(http.MethodGet) + router.HandleFunc("/api/v1/dashboards/{id}/public", am.AdminAccess(ah.Signoz.Handlers.Dashboard.UpdatePublic)).Methods(http.MethodPut) + router.HandleFunc("/api/v1/dashboards/{id}/public", am.AdminAccess(ah.Signoz.Handlers.Dashboard.DeletePublic)).Methods(http.MethodDelete) + + // public access for dashboards + router.HandleFunc("/api/v1/public/dashboards/{id}", am.CheckWithoutClaims( + ah.Signoz.Handlers.Dashboard.GetPublicData, + authtypes.RelationRead, authtypes.RelationRead, + dashboardtypes.TypeableMetaResourcePublicDashboard, + func(req *http.Request, orgs []*types.Organization) ([]authtypes.Selector, valuer.UUID, error) { + id, err := valuer.NewUUID(mux.Vars(req)["id"]) + if err != nil { + return nil, valuer.UUID{}, err + } + + return ah.Signoz.Modules.Dashboard.GetPublicDashboardOrgAndSelectors(req.Context(), id, orgs) + })).Methods(http.MethodGet) + + router.HandleFunc("/api/v1/public/dashboards/{id}/widgets/{index}/query_range", am.CheckWithoutClaims( + ah.Signoz.Handlers.Dashboard.GetPublicWidgetQueryRange, + authtypes.RelationRead, authtypes.RelationRead, + dashboardtypes.TypeableMetaResourcePublicDashboard, + func(req *http.Request, orgs []*types.Organization) ([]authtypes.Selector, valuer.UUID, error) { + id, err := valuer.NewUUID(mux.Vars(req)["id"]) + if err != nil { + return nil, valuer.UUID{}, err + } + + return ah.Signoz.Modules.Dashboard.GetPublicDashboardOrgAndSelectors(req.Context(), id, orgs) + })).Methods(http.MethodGet) + // v3 router.HandleFunc("/api/v3/licenses", am.AdminAccess(ah.LicensingAPI.Activate)).Methods(http.MethodPost) router.HandleFunc("/api/v3/licenses", am.AdminAccess(ah.LicensingAPI.Refresh)).Methods(http.MethodPut) diff --git a/ee/query-service/app/server.go b/ee/query-service/app/server.go index c9c6205c38..769c96d627 100644 --- a/ee/query-service/app/server.go +++ b/ee/query-service/app/server.go @@ -192,7 +192,7 @@ func (s Server) HealthCheckStatus() chan healthcheck.Status { func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*http.Server, error) { r := baseapp.NewRouter() - am := middleware.NewAuthZ(s.signoz.Instrumentation.Logger()) + am := middleware.NewAuthZ(s.signoz.Instrumentation.Logger(), s.signoz.Modules.OrgGetter, s.signoz.Authz) r.Use(otelmux.Middleware( "apiserver", diff --git a/pkg/authz/authz.go b/pkg/authz/authz.go index 4b0bbc05d7..87c8bbc475 100644 --- a/pkg/authz/authz.go +++ b/pkg/authz/authz.go @@ -18,6 +18,8 @@ type AuthZ interface { // CheckWithTupleCreation takes upon the responsibility for generating the tuples alongside everything Check does. CheckWithTupleCreation(context.Context, authtypes.Claims, valuer.UUID, authtypes.Relation, authtypes.Relation, authtypes.Typeable, []authtypes.Selector) error + CheckWithTupleCreationWithoutClaims(context.Context, valuer.UUID, authtypes.Relation, authtypes.Relation, authtypes.Typeable, []authtypes.Selector) error + // Batch Check returns error when the upstream authorization server is unavailable or for all the tuples of subject (s) doesn't have relation (r) on object (o). BatchCheck(context.Context, []*openfgav1.TupleKey) error diff --git a/pkg/authz/openfgaauthz/provider.go b/pkg/authz/openfgaauthz/provider.go index fa0ddd57b8..d4a1c59e5b 100644 --- a/pkg/authz/openfgaauthz/provider.go +++ b/pkg/authz/openfgaauthz/provider.go @@ -2,6 +2,7 @@ package openfgaauthz import ( "context" + "strconv" "sync" authz "github.com/SigNoz/signoz/pkg/authz" @@ -121,13 +122,15 @@ func (provider *provider) Check(ctx context.Context, tupleReq *openfgav1.TupleKe func (provider *provider) BatchCheck(ctx context.Context, tupleReq []*openfgav1.TupleKey) error { storeID, modelID := provider.getStoreIDandModelID() batchCheckItems := make([]*openfgav1.BatchCheckItem, 0) - for _, tuple := range tupleReq { + for idx, tuple := range tupleReq { batchCheckItems = append(batchCheckItems, &openfgav1.BatchCheckItem{ TupleKey: &openfgav1.CheckRequestTupleKey{ User: tuple.User, Relation: tuple.Relation, Object: tuple.Object, }, + // the batch check response is map[string] keyed by correlationID. + CorrelationId: strconv.Itoa(idx), }) } @@ -153,7 +156,26 @@ func (provider *provider) BatchCheck(ctx context.Context, tupleReq []*openfgav1. } func (provider *provider) CheckWithTupleCreation(ctx context.Context, claims authtypes.Claims, orgID valuer.UUID, _ authtypes.Relation, translation authtypes.Relation, _ authtypes.Typeable, _ []authtypes.Selector) error { - subject, err := authtypes.NewSubject(authtypes.TypeUser, claims.UserID, authtypes.Relation{}) + subject, err := authtypes.NewSubject(authtypes.TypeableUser, claims.UserID, orgID, nil) + if err != nil { + return err + } + + tuples, err := authtypes.TypeableOrganization.Tuples(subject, translation, []authtypes.Selector{authtypes.MustNewSelector(authtypes.TypeOrganization, orgID.StringValue())}, orgID) + if err != nil { + return err + } + + err = provider.BatchCheck(ctx, tuples) + if err != nil { + return err + } + + return nil +} + +func (provider *provider) CheckWithTupleCreationWithoutClaims(ctx context.Context, orgID valuer.UUID, _ authtypes.Relation, translation authtypes.Relation, _ authtypes.Typeable, _ []authtypes.Selector) error { + subject, err := authtypes.NewSubject(authtypes.TypeableAnonymous, authtypes.AnonymousUser.String(), orgID, nil) if err != nil { return err } @@ -186,7 +208,8 @@ func (provider *provider) Write(ctx context.Context, additions []*openfgav1.Tupl return nil } return &openfgav1.WriteRequestWrites{ - TupleKeys: additions, + TupleKeys: additions, + OnDuplicate: "ignore", } }(), Deletes: func() *openfgav1.WriteRequestDeletes { @@ -195,6 +218,7 @@ func (provider *provider) Write(ctx context.Context, additions []*openfgav1.Tupl } return &openfgav1.WriteRequestDeletes{ TupleKeys: deletionTuplesWithoutCondition, + OnMissing: "ignore", } }(), }) diff --git a/pkg/http/middleware/authz.go b/pkg/http/middleware/authz.go index 737840ad2f..164df1f38c 100644 --- a/pkg/http/middleware/authz.go +++ b/pkg/http/middleware/authz.go @@ -6,6 +6,7 @@ import ( "github.com/SigNoz/signoz/pkg/authz" "github.com/SigNoz/signoz/pkg/http/render" + "github.com/SigNoz/signoz/pkg/modules/organization" "github.com/SigNoz/signoz/pkg/types/authtypes" "github.com/SigNoz/signoz/pkg/valuer" "github.com/gorilla/mux" @@ -17,15 +18,16 @@ const ( type AuthZ struct { logger *slog.Logger + orgGetter organization.Getter authzService authz.AuthZ } -func NewAuthZ(logger *slog.Logger) *AuthZ { +func NewAuthZ(logger *slog.Logger, orgGetter organization.Getter, authzService authz.AuthZ) *AuthZ { if logger == nil { panic("cannot build authz middleware, logger is empty") } - return &AuthZ{logger: logger} + return &AuthZ{logger: logger, orgGetter: orgGetter, authzService: authzService} } func (middleware *AuthZ) ViewAccess(next http.HandlerFunc) http.HandlerFunc { @@ -107,7 +109,7 @@ func (middleware *AuthZ) OpenAccess(next http.HandlerFunc) http.HandlerFunc { }) } -func (middleware *AuthZ) Check(next http.HandlerFunc, relation authtypes.Relation, translation authtypes.Relation, typeable authtypes.Typeable, cb authtypes.SelectorCallbackFn) http.HandlerFunc { +func (middleware *AuthZ) Check(next http.HandlerFunc, relation authtypes.Relation, translation authtypes.Relation, typeable authtypes.Typeable, cb authtypes.SelectorCallbackWithClaimsFn) http.HandlerFunc { return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { claims, err := authtypes.ClaimsFromContext(req.Context()) if err != nil { @@ -121,7 +123,7 @@ func (middleware *AuthZ) Check(next http.HandlerFunc, relation authtypes.Relatio return } - selectors, err := cb(req.Context(), claims) + selectors, err := cb(req, claims) if err != nil { render.Error(rw, err) return @@ -136,3 +138,28 @@ func (middleware *AuthZ) Check(next http.HandlerFunc, relation authtypes.Relatio next(rw, req) }) } + +func (middleware *AuthZ) CheckWithoutClaims(next http.HandlerFunc, relation authtypes.Relation, translation authtypes.Relation, typeable authtypes.Typeable, cb authtypes.SelectorCallbackWithoutClaimsFn) http.HandlerFunc { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + ctx := req.Context() + orgs, err := middleware.orgGetter.ListByOwnedKeyRange(ctx) + if err != nil { + render.Error(rw, err) + return + } + + selectors, orgID, err := cb(req, orgs) + if err != nil { + render.Error(rw, err) + return + } + + err = middleware.authzService.CheckWithTupleCreationWithoutClaims(ctx, orgID, relation, translation, typeable, selectors) + if err != nil { + render.Error(rw, err) + return + } + + next(rw, req) + }) +} diff --git a/pkg/modules/dashboard/dashboard.go b/pkg/modules/dashboard/dashboard.go index c91ef3d5ab..54a29af137 100644 --- a/pkg/modules/dashboard/dashboard.go +++ b/pkg/modules/dashboard/dashboard.go @@ -7,11 +7,30 @@ import ( "github.com/SigNoz/signoz/pkg/modules/role" "github.com/SigNoz/signoz/pkg/statsreporter" "github.com/SigNoz/signoz/pkg/types" + "github.com/SigNoz/signoz/pkg/types/authtypes" "github.com/SigNoz/signoz/pkg/types/dashboardtypes" "github.com/SigNoz/signoz/pkg/valuer" ) type Module interface { + // enables public sharing for dashboard. + CreatePublic(context.Context, valuer.UUID, *dashboardtypes.PublicDashboard) error + + // gets the config for public sharing by org_id and dashboard_id. + GetPublic(context.Context, valuer.UUID, valuer.UUID) (*dashboardtypes.PublicDashboard, error) + + // get the dashboard data by public dashboard id + GetDashboardByPublicID(context.Context, valuer.UUID) (*dashboardtypes.Dashboard, error) + + // gets the org for the given public dashboard + GetPublicDashboardOrgAndSelectors(ctx context.Context, id valuer.UUID, orgs []*types.Organization) ([]authtypes.Selector, valuer.UUID, error) + + // updates the config for public sharing. + UpdatePublic(context.Context, *dashboardtypes.PublicDashboard) error + + // disables the public sharing for the dashboard. + DeletePublic(context.Context, valuer.UUID, valuer.UUID) error + Create(ctx context.Context, orgID valuer.UUID, createdBy string, creator valuer.UUID, data dashboardtypes.PostableDashboard) (*dashboardtypes.Dashboard, error) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*dashboardtypes.Dashboard, error) @@ -32,6 +51,18 @@ type Module interface { } type Handler interface { + CreatePublic(http.ResponseWriter, *http.Request) + + GetPublic(http.ResponseWriter, *http.Request) + + GetPublicData(http.ResponseWriter, *http.Request) + + GetPublicWidgetQueryRange(http.ResponseWriter, *http.Request) + + UpdatePublic(http.ResponseWriter, *http.Request) + + DeletePublic(http.ResponseWriter, *http.Request) + Create(http.ResponseWriter, *http.Request) Update(http.ResponseWriter, *http.Request) diff --git a/pkg/modules/dashboard/impldashboard/handler.go b/pkg/modules/dashboard/impldashboard/handler.go index 21b600a04a..fffffeadf7 100644 --- a/pkg/modules/dashboard/impldashboard/handler.go +++ b/pkg/modules/dashboard/impldashboard/handler.go @@ -4,12 +4,16 @@ import ( "context" "encoding/json" "net/http" + "strconv" "time" "github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/factory" + "github.com/SigNoz/signoz/pkg/http/binding" "github.com/SigNoz/signoz/pkg/http/render" + "github.com/SigNoz/signoz/pkg/licensing" "github.com/SigNoz/signoz/pkg/modules/dashboard" + "github.com/SigNoz/signoz/pkg/querier" "github.com/SigNoz/signoz/pkg/transition" "github.com/SigNoz/signoz/pkg/types/authtypes" "github.com/SigNoz/signoz/pkg/types/ctxtypes" @@ -21,10 +25,12 @@ import ( type handler struct { module dashboard.Module providerSettings factory.ProviderSettings + querier querier.Querier + licensing licensing.Licensing } -func NewHandler(module dashboard.Module, providerSettings factory.ProviderSettings) dashboard.Handler { - return &handler{module: module, providerSettings: providerSettings} +func NewHandler(module dashboard.Module, providerSettings factory.ProviderSettings, querier querier.Querier, licensing licensing.Licensing) dashboard.Handler { + return &handler{module: module, providerSettings: providerSettings, querier: querier, licensing: licensing} } func (handler *handler) Create(rw http.ResponseWriter, r *http.Request) { @@ -196,3 +202,278 @@ func (handler *handler) Delete(rw http.ResponseWriter, r *http.Request) { render.Success(rw, http.StatusNoContent, nil) } + +func (handler *handler) CreatePublic(rw http.ResponseWriter, r *http.Request) { + ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second) + defer cancel() + + claims, err := authtypes.ClaimsFromContext(ctx) + if err != nil { + render.Error(rw, err) + return + } + + _, err = handler.licensing.GetActive(ctx, valuer.MustNewUUID(claims.OrgID)) + if err != nil { + render.Error(rw, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())) + return + } + + id, err := valuer.NewUUID(mux.Vars(r)["id"]) + if err != nil { + render.Error(rw, err) + return + } + + _, err = handler.module.Get(ctx, valuer.MustNewUUID(claims.OrgID), id) + if err != nil { + render.Error(rw, err) + return + } + + req := new(dashboardtypes.PostablePublicDashboard) + if err := binding.JSON.BindBody(r.Body, req); err != nil { + render.Error(rw, err) + return + } + + publicDashboard := dashboardtypes.NewPublicDashboard(req.TimeRangeEnabled, req.DefaultTimeRange, id) + err = handler.module.CreatePublic(ctx, valuer.MustNewUUID(claims.OrgID), publicDashboard) + if err != nil { + render.Error(rw, err) + return + } + + render.Success(rw, http.StatusCreated, nil) +} + +func (handler *handler) GetPublic(rw http.ResponseWriter, r *http.Request) { + ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second) + defer cancel() + + claims, err := authtypes.ClaimsFromContext(ctx) + if err != nil { + render.Error(rw, err) + return + } + + _, err = handler.licensing.GetActive(ctx, valuer.MustNewUUID(claims.OrgID)) + if err != nil { + render.Error(rw, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())) + return + } + + id, err := valuer.NewUUID(mux.Vars(r)["id"]) + if err != nil { + render.Error(rw, err) + return + } + + _, err = handler.module.Get(ctx, valuer.MustNewUUID(claims.OrgID), id) + if err != nil { + render.Error(rw, err) + return + } + + publicDashboard, err := handler.module.GetPublic(ctx, valuer.MustNewUUID(claims.OrgID), id) + if err != nil { + render.Error(rw, err) + return + } + + render.Success(rw, http.StatusOK, dashboardtypes.NewGettablePublicDashboard(publicDashboard)) +} + +func (handler *handler) GetPublicData(rw http.ResponseWriter, r *http.Request) { + ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second) + defer cancel() + + id, err := valuer.NewUUID(mux.Vars(r)["id"]) + if err != nil { + render.Error(rw, err) + return + } + + dashboard, err := handler.module.GetDashboardByPublicID(ctx, id) + if err != nil { + render.Error(rw, err) + return + } + + publicDashboard, err := handler.module.GetPublic(ctx, dashboard.OrgID, valuer.MustNewUUID(dashboard.ID)) + if err != nil { + render.Error(rw, err) + return + } + + gettablePublicDashboardData, err := dashboardtypes.NewPublicDashboardDataFromDashboard(dashboard, publicDashboard) + if err != nil { + render.Error(rw, err) + return + } + + render.Success(rw, http.StatusOK, gettablePublicDashboardData) +} + +func (handler *handler) GetPublicWidgetQueryRange(rw http.ResponseWriter, r *http.Request) { + ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second) + defer cancel() + + id, err := valuer.NewUUID(mux.Vars(r)["id"]) + if err != nil { + render.Error(rw, err) + return + } + + widgetIndex, ok := mux.Vars(r)["index"] + if !ok { + render.Error(rw, errors.New(errors.TypeInvalidInput, dashboardtypes.ErrCodePublicDashboardInvalidInput, "widget index is missing from the path")) + return + } + + dashboard, err := handler.module.GetDashboardByPublicID(ctx, id) + if err != nil { + render.Error(rw, err) + return + } + + publicDashboard, err := handler.module.GetPublic(ctx, dashboard.OrgID, valuer.MustNewUUID(dashboard.ID)) + if err != nil { + render.Error(rw, err) + return + } + + widgetIdxInt, err := strconv.ParseInt(widgetIndex, 10, 64) + if err != nil { + render.Error(rw, errors.New(errors.TypeInvalidInput, dashboardtypes.ErrCodePublicDashboardInvalidInput, "invalid widget index")) + return + } + + var startTime, endTime uint64 + if publicDashboard.TimeRangeEnabled { + startTimeUint, err := strconv.ParseUint(r.URL.Query().Get("startTime"), 10, 64) + if err != nil { + render.Error(rw, errors.New(errors.TypeInvalidInput, dashboardtypes.ErrCodePublicDashboardInvalidInput, "invalid startTime")) + return + } + + endTimeUint, err := strconv.ParseUint(r.URL.Query().Get("endTime"), 10, 64) + if err != nil { + render.Error(rw, errors.New(errors.TypeInvalidInput, dashboardtypes.ErrCodePublicDashboardInvalidInput, "invalid endTime")) + return + } + + startTime = startTimeUint + endTime = endTimeUint + } else { + timeRange, err := time.ParseDuration(publicDashboard.DefaultTimeRange) + if err != nil { + // this should't happen as we shouldn't let such values in DB + panic(err) + } + + startTime = uint64(time.Now().Add(-timeRange).UnixMilli()) + endTime = uint64(time.Now().UnixMilli()) + } + + query, err := dashboard.GetWidgetQuery(startTime, endTime, widgetIdxInt, handler.providerSettings.Logger) + if err != nil { + render.Error(rw, err) + return + } + + queryRangeResults, err := handler.querier.QueryRange(ctx, dashboard.OrgID, query) + if err != nil { + render.Error(rw, err) + return + } + + render.Success(rw, http.StatusOK, queryRangeResults) +} + +func (handler *handler) UpdatePublic(rw http.ResponseWriter, r *http.Request) { + ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second) + defer cancel() + + claims, err := authtypes.ClaimsFromContext(ctx) + if err != nil { + render.Error(rw, err) + return + } + + _, err = handler.licensing.GetActive(ctx, valuer.MustNewUUID(claims.OrgID)) + if err != nil { + render.Error(rw, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())) + return + } + + id, err := valuer.NewUUID(mux.Vars(r)["id"]) + if err != nil { + render.Error(rw, err) + return + } + + _, err = handler.module.Get(ctx, valuer.MustNewUUID(claims.OrgID), id) + if err != nil { + render.Error(rw, err) + return + } + + req := new(dashboardtypes.UpdatablePublicDashboard) + if err := binding.JSON.BindBody(r.Body, req); err != nil { + render.Error(rw, err) + return + } + + publicDashboard, err := handler.module.GetPublic(ctx, valuer.MustNewUUID(claims.OrgID), id) + if err != nil { + render.Error(rw, err) + return + } + + publicDashboard.Update(req.TimeRangeEnabled, req.DefaultTimeRange) + err = handler.module.UpdatePublic(ctx, publicDashboard) + if err != nil { + render.Error(rw, err) + return + } + + render.Success(rw, http.StatusNoContent, nil) +} + +func (handler *handler) DeletePublic(rw http.ResponseWriter, r *http.Request) { + ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second) + defer cancel() + + claims, err := authtypes.ClaimsFromContext(ctx) + if err != nil { + render.Error(rw, err) + return + } + + _, err = handler.licensing.GetActive(ctx, valuer.MustNewUUID(claims.OrgID)) + if err != nil { + render.Error(rw, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())) + return + } + + id, err := valuer.NewUUID(mux.Vars(r)["id"]) + if err != nil { + render.Error(rw, err) + return + } + + _, err = handler.module.Get(ctx, valuer.MustNewUUID(claims.OrgID), id) + if err != nil { + render.Error(rw, err) + return + } + + err = handler.module.DeletePublic(ctx, valuer.MustNewUUID(claims.OrgID), id) + if err != nil { + render.Error(rw, err) + return + } + + render.Success(rw, http.StatusNoContent, nil) +} diff --git a/pkg/modules/dashboard/impldashboard/module.go b/pkg/modules/dashboard/impldashboard/module.go index 78727c537e..40850c965f 100644 --- a/pkg/modules/dashboard/impldashboard/module.go +++ b/pkg/modules/dashboard/impldashboard/module.go @@ -8,10 +8,13 @@ import ( "github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/factory" "github.com/SigNoz/signoz/pkg/modules/dashboard" + "github.com/SigNoz/signoz/pkg/modules/organization" + "github.com/SigNoz/signoz/pkg/modules/role" "github.com/SigNoz/signoz/pkg/sqlstore" "github.com/SigNoz/signoz/pkg/types" "github.com/SigNoz/signoz/pkg/types/authtypes" "github.com/SigNoz/signoz/pkg/types/dashboardtypes" + "github.com/SigNoz/signoz/pkg/types/roletypes" "github.com/SigNoz/signoz/pkg/valuer" ) @@ -19,14 +22,18 @@ type module struct { store dashboardtypes.Store settings factory.ScopedProviderSettings analytics analytics.Analytics + orgGetter organization.Getter + role role.Module } -func NewModule(sqlstore sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics) dashboard.Module { +func NewModule(sqlstore sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, role role.Module) dashboard.Module { scopedProviderSettings := factory.NewScopedProviderSettings(settings, "github.com/SigNoz/signoz/pkg/modules/impldashboard") return &module{ store: NewStore(sqlstore), settings: scopedProviderSettings, analytics: analytics, + orgGetter: orgGetter, + role: role, } } @@ -50,17 +57,79 @@ func (module *module) Create(ctx context.Context, orgID valuer.UUID, createdBy s return dashboard, nil } +func (module *module) CreatePublic(ctx context.Context, orgID valuer.UUID, publicDashboard *dashboardtypes.PublicDashboard) error { + role, err := module.role.GetOrCreate(ctx, roletypes.NewRole(roletypes.AnonymousUserRoleName, roletypes.AnonymousUserRoleDescription, roletypes.RoleTypeManaged.StringValue(), orgID)) + if err != nil { + return err + } + + err = module.role.Assign(ctx, role.ID, orgID, authtypes.MustNewSubject(authtypes.TypeableAnonymous, authtypes.AnonymousUser.StringValue(), orgID, nil)) + if err != nil { + return err + } + + additionObject := authtypes.MustNewObject( + authtypes.Resource{ + Name: dashboardtypes.TypeableMetaResourcePublicDashboard.Name(), + Type: authtypes.TypeMetaResource, + }, + authtypes.MustNewSelector(authtypes.TypeMetaResource, publicDashboard.ID.String()), + ) + + err = module.role.PatchObjects(ctx, orgID, role.ID, authtypes.RelationRead, []*authtypes.Object{additionObject}, nil) + if err != nil { + return err + } + + err = module.store.CreatePublic(ctx, dashboardtypes.NewStorablePublicDashboardFromPublicDashboard(publicDashboard)) + if err != nil { + return err + } + + return nil +} + func (module *module) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*dashboardtypes.Dashboard, error) { storableDashboard, err := module.store.Get(ctx, orgID, id) if err != nil { return nil, err } - dashboard, err := dashboardtypes.NewDashboardFromStorableDashboard(storableDashboard) + return dashboardtypes.NewDashboardFromStorableDashboard(storableDashboard), nil +} + +func (module *module) GetPublic(ctx context.Context, orgID valuer.UUID, dashboardID valuer.UUID) (*dashboardtypes.PublicDashboard, error) { + storablePublicDashboard, err := module.store.GetPublic(ctx, dashboardID.StringValue()) if err != nil { return nil, err } - return dashboard, nil + + return dashboardtypes.NewPublicDashboardFromStorablePublicDashboard(storablePublicDashboard), nil +} + +func (module *module) GetDashboardByPublicID(ctx context.Context, id valuer.UUID) (*dashboardtypes.Dashboard, error) { + storableDashboard, err := module.store.GetDashboardByPublicID(ctx, id.StringValue()) + if err != nil { + return nil, err + } + + return dashboardtypes.NewDashboardFromStorableDashboard(storableDashboard), nil +} + +func (module *module) GetPublicDashboardOrgAndSelectors(ctx context.Context, id valuer.UUID, orgs []*types.Organization) ([]authtypes.Selector, valuer.UUID, error) { + orgIDs := make([]string, len(orgs)) + for idx, org := range orgs { + orgIDs[idx] = org.ID.StringValue() + } + + storableDashboard, err := module.store.GetDashboardByOrgsAndPublicID(ctx, orgIDs, id.StringValue()) + if err != nil { + return nil, valuer.UUID{}, err + } + + return []authtypes.Selector{ + authtypes.MustNewSelector(authtypes.TypeMetaResource, id.StringValue()), + }, storableDashboard.OrgID, nil } func (module *module) List(ctx context.Context, orgID valuer.UUID) ([]*dashboardtypes.Dashboard, error) { @@ -69,12 +138,7 @@ func (module *module) List(ctx context.Context, orgID valuer.UUID) ([]*dashboard return nil, err } - dashboards, err := dashboardtypes.NewDashboardsFromStorableDashboards(storableDashboards) - if err != nil { - return nil, err - } - - return dashboards, nil + return dashboardtypes.NewDashboardsFromStorableDashboards(storableDashboards), nil } func (module *module) Update(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, updatableDashboard dashboardtypes.UpdatableDashboard, diff int) (*dashboardtypes.Dashboard, error) { @@ -101,6 +165,10 @@ func (module *module) Update(ctx context.Context, orgID valuer.UUID, id valuer.U return dashboard, nil } +func (module *module) UpdatePublic(ctx context.Context, publicDashboard *dashboardtypes.PublicDashboard) error { + return module.store.UpdatePublic(ctx, dashboardtypes.NewStorablePublicDashboardFromPublicDashboard(publicDashboard)) +} + func (module *module) LockUnlock(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, role types.Role, lock bool) error { dashboard, err := module.Get(ctx, orgID, id) if err != nil { @@ -134,7 +202,61 @@ func (module *module) Delete(ctx context.Context, orgID valuer.UUID, id valuer.U return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "dashboard is locked, please unlock the dashboard to be delete it") } - return module.store.Delete(ctx, orgID, id) + err = module.store.RunInTx(ctx, func(ctx context.Context) error { + err := module.store.Delete(ctx, orgID, id) + if err != nil { + return err + } + + err = module.store.DeletePublic(ctx, id.StringValue()) + if err != nil { + // do not do anything if no public config exists. + if !errors.Ast(err, errors.TypeNotFound) { + return err + } + + return nil + } + + return nil + }) + if err != nil { + return err + } + + return nil +} + +func (module *module) DeletePublic(ctx context.Context, orgID valuer.UUID, dashboardID valuer.UUID) error { + publicDashboard, err := module.GetPublic(ctx, orgID, dashboardID) + if err != nil { + return err + } + + role, err := module.role.GetOrCreate(ctx, roletypes.NewRole(roletypes.AnonymousUserRoleName, roletypes.AnonymousUserRoleDescription, roletypes.RoleTypeManaged.StringValue(), orgID)) + if err != nil { + return err + } + + deletionObject := authtypes.MustNewObject( + authtypes.Resource{ + Name: dashboardtypes.TypeableMetaResourcePublicDashboard.Name(), + Type: authtypes.TypeMetaResource, + }, + authtypes.MustNewSelector(authtypes.TypeMetaResource, publicDashboard.ID.String()), + ) + + err = module.role.PatchObjects(ctx, orgID, role.ID, authtypes.RelationRead, nil, []*authtypes.Object{deletionObject}) + if err != nil { + return err + } + + err = module.store.DeletePublic(ctx, dashboardID.StringValue()) + if err != nil { + return err + } + + return nil } func (module *module) GetByMetricNames(ctx context.Context, orgID valuer.UUID, metricNames []string) (map[string][]map[string]string, error) { @@ -225,5 +347,5 @@ func (module *module) Collect(ctx context.Context, orgID valuer.UUID) (map[strin } func (module *module) MustGetTypeables() []authtypes.Typeable { - return []authtypes.Typeable{dashboardtypes.TypeableResourceDashboard, dashboardtypes.TypeableResourcesDashboards} + return []authtypes.Typeable{dashboardtypes.TypeableMetaResourceDashboard, dashboardtypes.TypeableMetaResourcesDashboards} } diff --git a/pkg/modules/dashboard/impldashboard/store.go b/pkg/modules/dashboard/impldashboard/store.go index 1b449d0cc7..028c3ee5ae 100644 --- a/pkg/modules/dashboard/impldashboard/store.go +++ b/pkg/modules/dashboard/impldashboard/store.go @@ -7,6 +7,7 @@ import ( "github.com/SigNoz/signoz/pkg/sqlstore" "github.com/SigNoz/signoz/pkg/types/dashboardtypes" "github.com/SigNoz/signoz/pkg/valuer" + "github.com/uptrace/bun" ) type store struct { @@ -31,9 +32,22 @@ func (store *store) Create(ctx context.Context, storabledashboard *dashboardtype return nil } +func (store *store) CreatePublic(ctx context.Context, storable *dashboardtypes.StorablePublicDashboard) error { + _, err := store. + sqlstore. + BunDBCtx(ctx). + NewInsert(). + Model(storable). + Exec(ctx) + if err != nil { + return store.sqlstore.WrapAlreadyExistsErrf(err, dashboardtypes.ErrCodePublicDashboardAlreadyExists, "dashboard with id %s is already public", storable.DashboardID) + } + + return nil +} + func (store *store) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*dashboardtypes.StorableDashboard, error) { storableDashboard := new(dashboardtypes.StorableDashboard) - err := store. sqlstore. BunDB(). @@ -49,9 +63,61 @@ func (store *store) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) return storableDashboard, nil } +func (store *store) GetPublic(ctx context.Context, dashboardID string) (*dashboardtypes.StorablePublicDashboard, error) { + storable := new(dashboardtypes.StorablePublicDashboard) + err := store. + sqlstore. + BunDB(). + NewSelect(). + Model(storable). + Where("dashboard_id = ?", dashboardID). + Scan(ctx) + if err != nil { + return nil, store.sqlstore.WrapNotFoundErrf(err, dashboardtypes.ErrCodePublicDashboardNotFound, "dashboard with id %s isn't public", dashboardID) + } + + return storable, nil +} + +func (store *store) GetDashboardByOrgsAndPublicID(ctx context.Context, orgIDs []string, id string) (*dashboardtypes.StorableDashboard, error) { + storable := new(dashboardtypes.StorableDashboard) + err := store. + sqlstore. + BunDB(). + NewSelect(). + Model(storable). + Join("JOIN public_dashboard"). + JoinOn("public_dashboard.dashboard_id = dashboard.id"). + Where("public_dashboard.id = ?", id). + Where("org_id IN (?)", bun.In(orgIDs)). + Scan(ctx) + if err != nil { + return nil, store.sqlstore.WrapNotFoundErrf(err, dashboardtypes.ErrCodePublicDashboardNotFound, "couldn't find dashboard with id %s ", id) + } + + return storable, nil +} + +func (store *store) GetDashboardByPublicID(ctx context.Context, id string) (*dashboardtypes.StorableDashboard, error) { + storable := new(dashboardtypes.StorableDashboard) + err := store. + sqlstore. + BunDB(). + NewSelect(). + Model(storable). + Join("JOIN public_dashboard"). + JoinOn("public_dashboard.dashboard_id = dashboard.id"). + Where("public_dashboard.id = ?", id). + Scan(ctx) + if err != nil { + return nil, store.sqlstore.WrapNotFoundErrf(err, dashboardtypes.ErrCodePublicDashboardNotFound, "couldn't find dashboard with id %s ", id) + } + + return storable, nil +} + func (store *store) List(ctx context.Context, orgID valuer.UUID) ([]*dashboardtypes.StorableDashboard, error) { storableDashboards := make([]*dashboardtypes.StorableDashboard, 0) - err := store. sqlstore. BunDB(). @@ -60,7 +126,7 @@ func (store *store) List(ctx context.Context, orgID valuer.UUID) ([]*dashboardty Where("org_id = ?", orgID). Scan(ctx) if err != nil { - return nil, store.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "no dashboards found in orgID %s", orgID) + return nil, err } return storableDashboards, nil @@ -76,7 +142,22 @@ func (store *store) Update(ctx context.Context, orgID valuer.UUID, storableDashb Where("org_id = ?", orgID). Exec(ctx) if err != nil { - return store.sqlstore.WrapNotFoundErrf(err, errors.CodeAlreadyExists, "dashboard with id %s doesn't exist", storableDashboard.ID) + return store.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "dashboard with id %s doesn't exist", storableDashboard.ID) + } + + return nil +} + +func (store *store) UpdatePublic(ctx context.Context, storable *dashboardtypes.StorablePublicDashboard) error { + _, err := store. + sqlstore. + BunDB(). + NewUpdate(). + Model(storable). + WherePK(). + Exec(ctx) + if err != nil { + return store.sqlstore.WrapNotFoundErrf(err, dashboardtypes.ErrCodePublicDashboardNotFound, "dashboard with id %s isn't public", storable.DashboardID) } return nil @@ -97,3 +178,24 @@ func (store *store) Delete(ctx context.Context, orgID valuer.UUID, id valuer.UUI return nil } + +func (store *store) DeletePublic(ctx context.Context, dashboardID string) error { + _, err := store. + sqlstore. + BunDB(). + NewDelete(). + Model(new(dashboardtypes.StorablePublicDashboard)). + Where("dashboard_id = ?", dashboardID). + Exec(ctx) + if err != nil { + return store.sqlstore.WrapNotFoundErrf(err, dashboardtypes.ErrCodePublicDashboardNotFound, "dashboard with id %s isn't public", dashboardID) + } + + return nil +} + +func (store *store) RunInTx(ctx context.Context, cb func(ctx context.Context) error) error { + return store.sqlstore.RunInTxCtx(ctx, nil, func(ctx context.Context) error { + return cb(ctx) + }) +} diff --git a/pkg/modules/role/implrole/handler.go b/pkg/modules/role/implrole/handler.go index c0a557f1fa..38cea6241c 100644 --- a/pkg/modules/role/implrole/handler.go +++ b/pkg/modules/role/implrole/handler.go @@ -35,13 +35,13 @@ func (handler *handler) Create(rw http.ResponseWriter, r *http.Request) { return } - role, err := handler.module.Create(ctx, valuer.MustNewUUID(claims.OrgID), req.DisplayName, req.Description) + err = handler.module.Create(ctx, roletypes.NewRole(req.Name, req.Description, roletypes.RoleTypeCustom.StringValue(), valuer.MustNewUUID(claims.OrgID))) if err != nil { render.Error(rw, err) return } - render.Success(rw, http.StatusCreated, role.ID.StringValue()) + render.Success(rw, http.StatusCreated, nil) } func (handler *handler) Get(rw http.ResponseWriter, r *http.Request) { @@ -150,12 +150,7 @@ func (handler *handler) Patch(rw http.ResponseWriter, r *http.Request) { return } - id, ok := mux.Vars(r)["id"] - if !ok { - render.Error(rw, errors.New(errors.TypeInvalidInput, roletypes.ErrCodeRoleInvalidInput, "id is missing from the request")) - return - } - roleID, err := valuer.NewUUID(id) + id, err := valuer.NewUUID(mux.Vars(r)["id"]) if err != nil { render.Error(rw, err) return @@ -167,7 +162,14 @@ func (handler *handler) Patch(rw http.ResponseWriter, r *http.Request) { return } - err = handler.module.Patch(ctx, valuer.MustNewUUID(claims.OrgID), roleID, req.DisplayName, req.Description) + role, err := handler.module.Get(ctx, valuer.MustNewUUID(claims.OrgID), id) + if err != nil { + render.Error(rw, err) + return + } + + role.PatchMetadata(req.Name, req.Description) + err = handler.module.Patch(ctx, valuer.MustNewUUID(claims.OrgID), role) if err != nil { render.Error(rw, err) return @@ -184,23 +186,13 @@ func (handler *handler) PatchObjects(rw http.ResponseWriter, r *http.Request) { return } - id, ok := mux.Vars(r)["id"] - if !ok { - render.Error(rw, errors.New(errors.TypeInvalidInput, roletypes.ErrCodeRoleInvalidInput, "id is missing from the request")) - return - } - roleID, err := valuer.NewUUID(id) + id, err := valuer.NewUUID(mux.Vars(r)["id"]) if err != nil { render.Error(rw, err) return } - relationStr, ok := mux.Vars(r)["relation"] - if !ok { - render.Error(rw, errors.New(errors.TypeInvalidInput, roletypes.ErrCodeRoleInvalidInput, "relation is missing from the request")) - return - } - relation, err := authtypes.NewRelation(relationStr) + relation, err := authtypes.NewRelation(mux.Vars(r)["relation"]) if err != nil { render.Error(rw, err) return @@ -218,7 +210,7 @@ func (handler *handler) PatchObjects(rw http.ResponseWriter, r *http.Request) { return } - err = handler.module.PatchObjects(ctx, valuer.MustNewUUID(claims.OrgID), roleID, relation, patchableObjects.Additions, patchableObjects.Deletions) + err = handler.module.PatchObjects(ctx, valuer.MustNewUUID(claims.OrgID), id, relation, patchableObjects.Additions, patchableObjects.Deletions) if err != nil { render.Error(rw, err) return @@ -235,18 +227,13 @@ func (handler *handler) Delete(rw http.ResponseWriter, r *http.Request) { return } - id, ok := mux.Vars(r)["id"] - if !ok { - render.Error(rw, errors.New(errors.TypeInvalidInput, roletypes.ErrCodeRoleInvalidInput, "id is missing from the request")) - return - } - roleID, err := valuer.NewUUID(id) + id, err := valuer.NewUUID(mux.Vars(r)["id"]) if err != nil { render.Error(rw, err) return } - err = handler.module.Delete(ctx, valuer.MustNewUUID(claims.OrgID), roleID) + err = handler.module.Delete(ctx, valuer.MustNewUUID(claims.OrgID), id) if err != nil { render.Error(rw, err) return diff --git a/pkg/modules/role/implrole/module.go b/pkg/modules/role/implrole/module.go index e58cbe2c8b..e100269d5a 100644 --- a/pkg/modules/role/implrole/module.go +++ b/pkg/modules/role/implrole/module.go @@ -5,6 +5,7 @@ import ( "slices" "github.com/SigNoz/signoz/pkg/authz" + "github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/modules/role" "github.com/SigNoz/signoz/pkg/types/authtypes" "github.com/SigNoz/signoz/pkg/types/roletypes" @@ -25,15 +26,23 @@ func NewModule(store roletypes.Store, authz authz.AuthZ, registry []role.Registe } } -func (module *module) Create(ctx context.Context, orgID valuer.UUID, displayName, description string) (*roletypes.Role, error) { - role := roletypes.NewRole(displayName, description, orgID) +func (module *module) Create(ctx context.Context, role *roletypes.Role) error { + return module.store.Create(ctx, roletypes.NewStorableRoleFromRole(role)) +} - storableRole, err := roletypes.NewStorableRoleFromRole(role) +func (module *module) GetOrCreate(ctx context.Context, role *roletypes.Role) (*roletypes.Role, error) { + existingRole, err := module.store.GetByNameAndOrgID(ctx, role.Name, role.OrgID) if err != nil { - return nil, err + if !errors.Ast(err, errors.TypeNotFound) { + return nil, err + } } - err = module.store.Create(ctx, storableRole) + if existingRole != nil { + return roletypes.NewRoleFromStorableRole(existingRole), nil + } + + err = module.store.Create(ctx, roletypes.NewStorableRoleFromRole(role)) if err != nil { return nil, err } @@ -63,12 +72,7 @@ func (module *module) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID return nil, err } - role, err := roletypes.NewRoleFromStorableRole(storableRole) - if err != nil { - return nil, err - } - - return role, nil + return roletypes.NewRoleFromStorableRole(storableRole), nil } func (module *module) GetObjects(ctx context.Context, orgID valuer.UUID, id valuer.UUID, relation authtypes.Relation) ([]*authtypes.Object, error) { @@ -84,7 +88,7 @@ func (module *module) GetObjects(ctx context.Context, orgID valuer.UUID, id valu authz. ListObjects( ctx, - authtypes.MustNewSubject(authtypes.TypeRole, storableRole.ID.String(), authtypes.RelationAssignee), + authtypes.MustNewSubject(authtypes.TypeableRole, storableRole.ID.String(), orgID, &authtypes.RelationAssignee), relation, authtypes.MustNewTypeableFromType(resource.Type, resource.Name), ) @@ -107,39 +111,14 @@ func (module *module) List(ctx context.Context, orgID valuer.UUID) ([]*roletypes roles := make([]*roletypes.Role, len(storableRoles)) for idx, storableRole := range storableRoles { - role, err := roletypes.NewRoleFromStorableRole(storableRole) - if err != nil { - return nil, err - } - roles[idx] = role + roles[idx] = roletypes.NewRoleFromStorableRole(storableRole) } return roles, nil } -func (module *module) Patch(ctx context.Context, orgID valuer.UUID, id valuer.UUID, displayName, description *string) error { - storableRole, err := module.store.Get(ctx, orgID, id) - if err != nil { - return err - } - - role, err := roletypes.NewRoleFromStorableRole(storableRole) - if err != nil { - return err - } - - role.PatchMetadata(displayName, description) - updatedRole, err := roletypes.NewStorableRoleFromRole(role) - if err != nil { - return err - } - - err = module.store.Update(ctx, orgID, updatedRole) - if err != nil { - return err - } - - return nil +func (module *module) Patch(ctx context.Context, orgID valuer.UUID, role *roletypes.Role) error { + return module.store.Update(ctx, orgID, roletypes.NewStorableRoleFromRole(role)) } func (module *module) PatchObjects(ctx context.Context, orgID valuer.UUID, id valuer.UUID, relation authtypes.Relation, additions, deletions []*authtypes.Object) error { @@ -161,6 +140,21 @@ func (module *module) PatchObjects(ctx context.Context, orgID valuer.UUID, id va return nil } +func (module *module) Assign(ctx context.Context, id valuer.UUID, orgID valuer.UUID, subject string) error { + tuples, err := authtypes.TypeableRole.Tuples( + subject, + authtypes.RelationAssignee, + []authtypes.Selector{ + authtypes.MustNewSelector(authtypes.TypeRole, id.StringValue()), + }, + orgID, + ) + if err != nil { + return err + } + return module.authz.Write(ctx, tuples, nil) +} + func (module *module) Delete(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error { return module.store.Delete(ctx, orgID, id) } diff --git a/pkg/modules/role/implrole/store.go b/pkg/modules/role/implrole/store.go index 01f18a7440..f704617dc3 100644 --- a/pkg/modules/role/implrole/store.go +++ b/pkg/modules/role/implrole/store.go @@ -48,6 +48,23 @@ func (store *store) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) return role, nil } +func (store *store) GetByNameAndOrgID(ctx context.Context, name string, orgID valuer.UUID) (*roletypes.StorableRole, error) { + role := new(roletypes.StorableRole) + err := store. + sqlstore. + BunDB(). + NewSelect(). + Model(role). + Where("org_id = ?", orgID). + Where("name = ?", name). + Scan(ctx) + if err != nil { + return nil, store.sqlstore.WrapNotFoundErrf(err, roletypes.ErrCodeRoleNotFound, "role with name: %s doesn't exist", name) + } + + return role, nil +} + func (store *store) List(ctx context.Context, orgID valuer.UUID) ([]*roletypes.StorableRole, error) { roles := make([]*roletypes.StorableRole, 0) err := store. diff --git a/pkg/modules/role/role.go b/pkg/modules/role/role.go index 9154d51806..87ec4c501f 100644 --- a/pkg/modules/role/role.go +++ b/pkg/modules/role/role.go @@ -10,30 +10,36 @@ import ( ) type Module interface { - // Creates the role metadata - Create(context.Context, valuer.UUID, string, string) (*roletypes.Role, error) + // Creates the role. + Create(context.Context, *roletypes.Role) error - // Gets the role metadata + // Gets the role if it exists or creates one. + GetOrCreate(context.Context, *roletypes.Role) (*roletypes.Role, error) + + // Gets the role Get(context.Context, valuer.UUID, valuer.UUID) (*roletypes.Role, error) - // Gets the objects associated with the given role and relation + // Gets the objects associated with the given role and relation. GetObjects(context.Context, valuer.UUID, valuer.UUID, authtypes.Relation) ([]*authtypes.Object, error) - // Lists all the roles metadata for the organization + // Lists all the roles for the organization. List(context.Context, valuer.UUID) ([]*roletypes.Role, error) - // Gets all the typeable resources registered from role registry + // Gets all the typeable resources registered from role registry. GetResources(context.Context) []*authtypes.Resource - // Patches the roles metadata - Patch(context.Context, valuer.UUID, valuer.UUID, *string, *string) error + // Patches the role. + Patch(context.Context, valuer.UUID, *roletypes.Role) error // Patches the objects in authorization server associated with the given role and relation PatchObjects(context.Context, valuer.UUID, valuer.UUID, authtypes.Relation, []*authtypes.Object, []*authtypes.Object) error - // Deletes the role metadata and tuples in authorization server + // Deletes the role and tuples in authorization server. Delete(context.Context, valuer.UUID, valuer.UUID) error + // Assigns role to the given subject. + Assign(context.Context, valuer.UUID, valuer.UUID, string) error + RegisterTypeable } diff --git a/pkg/query-service/app/server.go b/pkg/query-service/app/server.go index 5b8455be15..1df8b8efdd 100644 --- a/pkg/query-service/app/server.go +++ b/pkg/query-service/app/server.go @@ -190,7 +190,7 @@ func (s *Server) createPublicServer(api *APIHandler, web web.Web) (*http.Server, r.Use(middleware.NewLogging(s.signoz.Instrumentation.Logger(), s.config.APIServer.Logging.ExcludedRoutes).Wrap) r.Use(middleware.NewComment().Wrap) - am := middleware.NewAuthZ(s.signoz.Instrumentation.Logger()) + am := middleware.NewAuthZ(s.signoz.Instrumentation.Logger(), s.signoz.Modules.OrgGetter, s.signoz.Authz) api.RegisterRoutes(r, am) api.RegisterLogsRoutes(r, am) diff --git a/pkg/signoz/handler.go b/pkg/signoz/handler.go index 891a4e41c2..e11a143ff8 100644 --- a/pkg/signoz/handler.go +++ b/pkg/signoz/handler.go @@ -2,6 +2,7 @@ package signoz import ( "github.com/SigNoz/signoz/pkg/factory" + "github.com/SigNoz/signoz/pkg/licensing" "github.com/SigNoz/signoz/pkg/modules/apdex" "github.com/SigNoz/signoz/pkg/modules/apdex/implapdex" "github.com/SigNoz/signoz/pkg/modules/authdomain" @@ -28,6 +29,7 @@ import ( "github.com/SigNoz/signoz/pkg/modules/tracefunnel/impltracefunnel" "github.com/SigNoz/signoz/pkg/modules/user" "github.com/SigNoz/signoz/pkg/modules/user/impluser" + "github.com/SigNoz/signoz/pkg/querier" ) type Handlers struct { @@ -46,14 +48,14 @@ type Handlers struct { Services services.Handler } -func NewHandlers(modules Modules, providerSettings factory.ProviderSettings) Handlers { +func NewHandlers(modules Modules, providerSettings factory.ProviderSettings, querier querier.Querier, licensing licensing.Licensing) Handlers { return Handlers{ Organization: implorganization.NewHandler(modules.OrgGetter, modules.OrgSetter), Preference: implpreference.NewHandler(modules.Preference), User: impluser.NewHandler(modules.User, modules.UserGetter), SavedView: implsavedview.NewHandler(modules.SavedView), Apdex: implapdex.NewHandler(modules.Apdex), - Dashboard: impldashboard.NewHandler(modules.Dashboard, providerSettings), + Dashboard: impldashboard.NewHandler(modules.Dashboard, providerSettings, querier, licensing), QuickFilter: implquickfilter.NewHandler(modules.QuickFilter), TraceFunnel: impltracefunnel.NewHandler(modules.TraceFunnel), RawDataExport: implrawdataexport.NewHandler(modules.RawDataExport), diff --git a/pkg/signoz/handler_test.go b/pkg/signoz/handler_test.go index afe0ccdc83..a6a9cf8e3f 100644 --- a/pkg/signoz/handler_test.go +++ b/pkg/signoz/handler_test.go @@ -35,9 +35,9 @@ func TestNewHandlers(t *testing.T) { require.NoError(t, err) tokenizer := tokenizertest.New() emailing := emailingtest.New() - modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil) + modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil) - handlers := NewHandlers(modules, providerSettings) + handlers := NewHandlers(modules, providerSettings, nil, nil) reflectVal := reflect.ValueOf(handlers) for i := 0; i < reflectVal.NumField(); i++ { diff --git a/pkg/signoz/module.go b/pkg/signoz/module.go index 4ea45d2fc0..77d518d413 100644 --- a/pkg/signoz/module.go +++ b/pkg/signoz/module.go @@ -4,6 +4,7 @@ import ( "github.com/SigNoz/signoz/pkg/alertmanager" "github.com/SigNoz/signoz/pkg/analytics" "github.com/SigNoz/signoz/pkg/authn" + "github.com/SigNoz/signoz/pkg/authz" "github.com/SigNoz/signoz/pkg/emailing" "github.com/SigNoz/signoz/pkg/factory" "github.com/SigNoz/signoz/pkg/modules/apdex" @@ -20,6 +21,7 @@ import ( "github.com/SigNoz/signoz/pkg/modules/quickfilter/implquickfilter" "github.com/SigNoz/signoz/pkg/modules/rawdataexport" "github.com/SigNoz/signoz/pkg/modules/rawdataexport/implrawdataexport" + "github.com/SigNoz/signoz/pkg/modules/role/implrole" "github.com/SigNoz/signoz/pkg/modules/savedview" "github.com/SigNoz/signoz/pkg/modules/savedview/implsavedview" "github.com/SigNoz/signoz/pkg/modules/services" @@ -69,6 +71,7 @@ func NewModules( querier querier.Querier, telemetryStore telemetrystore.TelemetryStore, authNs map[authtypes.AuthNProvider]authn.AuthN, + authz authz.AuthZ, ) Modules { quickfilter := implquickfilter.NewModule(implquickfilter.NewStore(sqlstore)) orgSetter := implorganization.NewSetter(implorganization.NewStore(sqlstore), alertmanager, quickfilter) @@ -81,7 +84,7 @@ func NewModules( Preference: implpreference.NewModule(implpreference.NewStore(sqlstore), preferencetypes.NewAvailablePreference()), SavedView: implsavedview.NewModule(sqlstore), Apdex: implapdex.NewModule(sqlstore), - Dashboard: impldashboard.NewModule(sqlstore, providerSettings, analytics), + Dashboard: impldashboard.NewModule(sqlstore, providerSettings, analytics, orgGetter, implrole.NewModule(implrole.NewStore(sqlstore), authz, nil)), User: user, UserGetter: userGetter, QuickFilter: quickfilter, diff --git a/pkg/signoz/module_test.go b/pkg/signoz/module_test.go index 6298ebcc7c..0799bf8445 100644 --- a/pkg/signoz/module_test.go +++ b/pkg/signoz/module_test.go @@ -35,7 +35,7 @@ func TestNewModules(t *testing.T) { require.NoError(t, err) tokenizer := tokenizertest.New() emailing := emailingtest.New() - modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil) + modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil) reflectVal := reflect.ValueOf(modules) for i := 0; i < reflectVal.NumField(); i++ { diff --git a/pkg/signoz/provider.go b/pkg/signoz/provider.go index a42de0411f..2a825de249 100644 --- a/pkg/signoz/provider.go +++ b/pkg/signoz/provider.go @@ -139,6 +139,8 @@ func NewSQLMigrationProviderFactories( sqlmigration.NewAddRoutePolicyFactory(sqlstore, sqlschema), sqlmigration.NewAddAuthTokenFactory(sqlstore, sqlschema), sqlmigration.NewAddAuthzFactory(sqlstore, sqlschema), + sqlmigration.NewAddPublicDashboardsFactory(sqlstore, sqlschema), + sqlmigration.NewAddRoleFactory(sqlstore, sqlschema), ) } diff --git a/pkg/signoz/signoz.go b/pkg/signoz/signoz.go index c8b16b3f5f..dea9373806 100644 --- a/pkg/signoz/signoz.go +++ b/pkg/signoz/signoz.go @@ -9,6 +9,7 @@ import ( "github.com/SigNoz/signoz/pkg/analytics" "github.com/SigNoz/signoz/pkg/authn" "github.com/SigNoz/signoz/pkg/authn/authnstore/sqlauthnstore" + "github.com/SigNoz/signoz/pkg/authz" "github.com/SigNoz/signoz/pkg/cache" "github.com/SigNoz/signoz/pkg/emailing" "github.com/SigNoz/signoz/pkg/factory" @@ -51,6 +52,7 @@ type SigNoz struct { Sharder sharder.Sharder StatsReporter statsreporter.StatsReporter Tokenizer pkgtokenizer.Tokenizer + Authz authz.AuthZ Modules Modules Handlers Handlers } @@ -69,6 +71,7 @@ func New( sqlstoreProviderFactories factory.NamedMap[factory.ProviderFactory[sqlstore.SQLStore, sqlstore.Config]], telemetrystoreProviderFactories factory.NamedMap[factory.ProviderFactory[telemetrystore.TelemetryStore, telemetrystore.Config]], authNsCallback func(ctx context.Context, providerSettings factory.ProviderSettings, store authtypes.AuthNStore, licensing licensing.Licensing) (map[authtypes.AuthNProvider]authn.AuthN, error), + authzCallback func(context.Context, sqlstore.SQLStore) factory.ProviderFactory[authz.AuthZ, authz.Config], ) (*SigNoz, error) { // Initialize instrumentation instrumentation, err := instrumentation.New(ctx, config.Instrumentation, version.Info, "signoz") @@ -243,6 +246,13 @@ func New( return nil, err } + // Initialize authz + authzProviderFactory := authzCallback(ctx, sqlstore) + authz, err := authzProviderFactory.New(ctx, providerSettings, authz.Config{}) + if err != nil { + return nil, err + } + // Initialize user getter userGetter := impluser.NewGetter(impluser.NewStore(sqlstore, providerSettings)) @@ -300,10 +310,10 @@ func New( } // Initialize all modules - modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, analytics, querier, telemetrystore, authNs) + modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, analytics, querier, telemetrystore, authNs, authz) // Initialize all handlers for the modules - handlers := NewHandlers(modules, providerSettings) + handlers := NewHandlers(modules, providerSettings, querier, licensing) // Create a list of all stats collectors statsCollectors := []statsreporter.StatsCollector{ @@ -337,6 +347,7 @@ func New( factory.NewNamedService(factory.MustNewName("licensing"), licensing), factory.NewNamedService(factory.MustNewName("statsreporter"), statsReporter), factory.NewNamedService(factory.MustNewName("tokenizer"), tokenizer), + factory.NewNamedService(factory.MustNewName("authz"), authz), ) if err != nil { return nil, err @@ -358,6 +369,7 @@ func New( Emailing: emailing, Sharder: sharder, Tokenizer: tokenizer, + Authz: authz, Modules: modules, Handlers: handlers, }, nil diff --git a/pkg/sqlmigration/052_add_public_dashboards.go b/pkg/sqlmigration/052_add_public_dashboards.go new file mode 100644 index 0000000000..28ae5fa841 --- /dev/null +++ b/pkg/sqlmigration/052_add_public_dashboards.go @@ -0,0 +1,82 @@ +package sqlmigration + +import ( + "context" + + "github.com/SigNoz/signoz/pkg/factory" + "github.com/SigNoz/signoz/pkg/sqlschema" + "github.com/SigNoz/signoz/pkg/sqlstore" + "github.com/uptrace/bun" + "github.com/uptrace/bun/migrate" +) + +type addPublicDashboards struct { + sqlstore sqlstore.SQLStore + sqlschema sqlschema.SQLSchema +} + +func NewAddPublicDashboardsFactory(sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) factory.ProviderFactory[SQLMigration, Config] { + return factory.NewProviderFactory(factory.MustNewName("add_public_dashboards"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) { + return newAddPublicDashboards(ctx, ps, c, sqlstore, sqlschema) + }) +} + +func newAddPublicDashboards(_ context.Context, _ factory.ProviderSettings, _ Config, sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) (SQLMigration, error) { + return &addPublicDashboards{sqlstore: sqlstore, sqlschema: sqlschema}, nil +} + +func (migration *addPublicDashboards) Register(migrations *migrate.Migrations) error { + return migrations.Register(migration.Up, migration.Down) +} + +func (migration *addPublicDashboards) Up(ctx context.Context, db *bun.DB) error { + tx, err := db.BeginTx(ctx, nil) + if err != nil { + return err + } + + defer func() { + _ = tx.Rollback() + }() + + sqls := [][]byte{} + tableSQL := migration.sqlschema.Operator().CreateTable(&sqlschema.Table{ + Name: "public_dashboard", + Columns: []*sqlschema.Column{ + {Name: "id", DataType: sqlschema.DataTypeText, Nullable: false}, + {Name: "time_range_enabled", DataType: sqlschema.DataTypeBoolean, Nullable: false}, + {Name: "default_time_range", DataType: sqlschema.DataTypeText, Nullable: false}, + {Name: "created_at", DataType: sqlschema.DataTypeTimestamp, Nullable: false}, + {Name: "updated_at", DataType: sqlschema.DataTypeTimestamp, Nullable: false}, + {Name: "dashboard_id", DataType: sqlschema.DataTypeText, Nullable: false}, + }, + PrimaryKeyConstraint: &sqlschema.PrimaryKeyConstraint{ColumnNames: []sqlschema.ColumnName{"id"}}, + ForeignKeyConstraints: []*sqlschema.ForeignKeyConstraint{ + {ReferencingColumnName: sqlschema.ColumnName("dashboard_id"), ReferencedTableName: sqlschema.TableName("dashboard"), ReferencedColumnName: sqlschema.ColumnName("id")}, + }, + }) + sqls = append(sqls, tableSQL...) + + indexSQL := migration.sqlschema.Operator().CreateIndex(&sqlschema.UniqueIndex{ + TableName: "public_dashboard", + ColumnNames: []sqlschema.ColumnName{"dashboard_id"}, + }) + sqls = append(sqls, indexSQL...) + + for _, sql := range sqls { + if _, err := tx.ExecContext(ctx, string(sql)); err != nil { + return err + } + } + + err = tx.Commit() + if err != nil { + return err + } + + return nil +} + +func (migration *addPublicDashboards) Down(_ context.Context, _ *bun.DB) error { + return nil +} diff --git a/pkg/sqlmigration/53_add_role.go b/pkg/sqlmigration/53_add_role.go new file mode 100644 index 0000000000..3047e54b1a --- /dev/null +++ b/pkg/sqlmigration/53_add_role.go @@ -0,0 +1,91 @@ +package sqlmigration + +import ( + "context" + + "github.com/SigNoz/signoz/pkg/factory" + "github.com/SigNoz/signoz/pkg/sqlschema" + "github.com/SigNoz/signoz/pkg/sqlstore" + "github.com/uptrace/bun" + "github.com/uptrace/bun/migrate" +) + +type addRole struct { + sqlstore sqlstore.SQLStore + sqlschema sqlschema.SQLSchema +} + +func NewAddRoleFactory(sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) factory.ProviderFactory[SQLMigration, Config] { + return factory.NewProviderFactory(factory.MustNewName("add_role"), func(ctx context.Context, providerSettings factory.ProviderSettings, config Config) (SQLMigration, error) { + return newAddRole(ctx, providerSettings, config, sqlstore, sqlschema) + }) +} + +func newAddRole(_ context.Context, _ factory.ProviderSettings, _ Config, sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) (SQLMigration, error) { + return &addRole{ + sqlstore: sqlstore, + sqlschema: sqlschema, + }, nil +} + +func (migration *addRole) Register(migrations *migrate.Migrations) error { + if err := migrations.Register(migration.Up, migration.Down); err != nil { + return err + } + return nil +} + +func (migration *addRole) Up(ctx context.Context, db *bun.DB) error { + tx, err := db.BeginTx(ctx, nil) + if err != nil { + return err + } + + defer func() { + _ = tx.Rollback() + }() + + sqls := [][]byte{} + tableSQLs := migration.sqlschema.Operator().CreateTable(&sqlschema.Table{ + Name: "role", + Columns: []*sqlschema.Column{ + {Name: "id", DataType: sqlschema.DataTypeText, Nullable: false}, + {Name: "created_at", DataType: sqlschema.DataTypeTimestamp, Nullable: false}, + {Name: "updated_at", DataType: sqlschema.DataTypeTimestamp, Nullable: false}, + {Name: "name", DataType: sqlschema.DataTypeText, Nullable: false}, + {Name: "description", DataType: sqlschema.DataTypeText, Nullable: true}, + {Name: "type", DataType: sqlschema.DataTypeText, Nullable: false}, + {Name: "org_id", DataType: sqlschema.DataTypeText, Nullable: false}, + }, + PrimaryKeyConstraint: &sqlschema.PrimaryKeyConstraint{ + ColumnNames: []sqlschema.ColumnName{"id"}, + }, + ForeignKeyConstraints: []*sqlschema.ForeignKeyConstraint{ + { + ReferencingColumnName: sqlschema.ColumnName("org_id"), + ReferencedTableName: sqlschema.TableName("organizations"), + ReferencedColumnName: sqlschema.ColumnName("id"), + }, + }, + }) + sqls = append(sqls, tableSQLs...) + + indexSQLs := migration.sqlschema.Operator().CreateIndex(&sqlschema.UniqueIndex{TableName: "role", ColumnNames: []sqlschema.ColumnName{"name", "org_id"}}) + sqls = append(sqls, indexSQLs...) + + for _, sqlStmt := range sqls { + if _, err := tx.ExecContext(ctx, string(sqlStmt)); err != nil { + return err + } + } + + if err := tx.Commit(); err != nil { + return err + } + + return nil +} + +func (migration *addRole) Down(ctx context.Context, db *bun.DB) error { + return nil +} diff --git a/pkg/transition/migrate_alert.go b/pkg/transition/migrate_alert.go index 9a7b9603b7..c500ef4d54 100644 --- a/pkg/transition/migrate_alert.go +++ b/pkg/transition/migrate_alert.go @@ -79,7 +79,7 @@ func (m *alertMigrateV5) Migrate(ctx context.Context, ruleData map[string]any) b m.logger.InfoContext(ctx, "migrated querymap") // wrap it in the v5 envelope - envelope := m.wrapInV5Envelope(name, queryMap, "builder_query") + envelope := m.WrapInV5Envelope(name, queryMap, "builder_query") m.logger.InfoContext(ctx, "envelope after wrap", "envelope", envelope) compositeQuery["queries"] = append(compositeQuery["queries"].([]any), envelope) } diff --git a/pkg/transition/migrate_common.go b/pkg/transition/migrate_common.go index 1572f71a80..518e48417f 100644 --- a/pkg/transition/migrate_common.go +++ b/pkg/transition/migrate_common.go @@ -17,7 +17,13 @@ type migrateCommon struct { logger *slog.Logger } -func (migration *migrateCommon) wrapInV5Envelope(name string, queryMap map[string]any, queryType string) map[string]any { +func NewMigrateCommon(logger *slog.Logger) *migrateCommon { + return &migrateCommon{ + logger: logger, + } +} + +func (migration *migrateCommon) WrapInV5Envelope(name string, queryMap map[string]any, queryType string) map[string]any { // Create a properly structured v5 query v5Query := map[string]any{ "name": name, diff --git a/pkg/transition/migrate_saved_view.go b/pkg/transition/migrate_saved_view.go index 87349a52b9..c0e7d3a5e9 100644 --- a/pkg/transition/migrate_saved_view.go +++ b/pkg/transition/migrate_saved_view.go @@ -46,7 +46,7 @@ func (m *savedViewMigrateV5) Migrate(ctx context.Context, data map[string]any) b m.logger.InfoContext(ctx, "migrated querymap") // wrap it in the v5 envelope - envelope := m.wrapInV5Envelope(name, queryMap, "builder_query") + envelope := m.WrapInV5Envelope(name, queryMap, "builder_query") m.logger.InfoContext(ctx, "envelope after wrap", "envelope", envelope) data["queries"] = append(data["queries"].([]any), envelope) } diff --git a/pkg/types/authtypes/name.go b/pkg/types/authtypes/name.go index aa256e208c..6585664a7d 100644 --- a/pkg/types/authtypes/name.go +++ b/pkg/types/authtypes/name.go @@ -8,7 +8,7 @@ import ( ) var ( - nameRegex = regexp.MustCompile("^[a-z]{1,35}$") + nameRegex = regexp.MustCompile("^[a-z-]{1,50}$") _ json.Marshaler = new(Name) _ json.Unmarshaler = new(Name) diff --git a/pkg/types/authtypes/selector.go b/pkg/types/authtypes/selector.go index e374b8928e..e9e13aceb2 100644 --- a/pkg/types/authtypes/selector.go +++ b/pkg/types/authtypes/selector.go @@ -1,11 +1,13 @@ package authtypes import ( - "context" "encoding/json" + "net/http" "regexp" "github.com/SigNoz/signoz/pkg/errors" + "github.com/SigNoz/signoz/pkg/types" + "github.com/SigNoz/signoz/pkg/valuer" ) var ( @@ -20,13 +22,15 @@ var ( var ( typeUserSelectorRegex = regexp.MustCompile(`^[0-9a-f]{8}(?:\-[0-9a-f]{4}){3}-[0-9a-f]{12}$`) typeRoleSelectorRegex = regexp.MustCompile(`^[0-9a-f]{8}(?:\-[0-9a-f]{4}){3}-[0-9a-f]{12}$`) + typeAnonymousSelectorRegex = regexp.MustCompile(`^\*$`) typeOrganizationSelectorRegex = regexp.MustCompile(`^[0-9a-f]{8}(?:\-[0-9a-f]{4}){3}-[0-9a-f]{12}$`) typeMetaResourceSelectorRegex = regexp.MustCompile(`^[0-9a-f]{8}(?:\-[0-9a-f]{4}){3}-[0-9a-f]{12}$`) // metaresources selectors are used to select either all or none typeMetaResourcesSelectorRegex = regexp.MustCompile(`^\*$`) ) -type SelectorCallbackFn func(context.Context, Claims) ([]Selector, error) +type SelectorCallbackWithClaimsFn func(*http.Request, Claims) ([]Selector, error) +type SelectorCallbackWithoutClaimsFn func(*http.Request, []*types.Organization) ([]Selector, valuer.UUID, error) type Selector struct { val string @@ -83,6 +87,11 @@ func IsValidSelector(typed Type, selector string) error { return errors.Newf(errors.TypeInvalidInput, ErrCodeAuthZInvalidSelector, "selector must conform to regex %s", typeRoleSelectorRegex.String()) } return nil + case TypeAnonymous: + if !typeAnonymousSelectorRegex.MatchString(selector) { + return errors.Newf(errors.TypeInvalidInput, ErrCodeAuthZInvalidSelector, "selector must conform to regex %s", typeAnonymousSelectorRegex.String()) + } + return nil case TypeOrganization: if !typeOrganizationSelectorRegex.MatchString(selector) { return errors.Newf(errors.TypeInvalidInput, ErrCodeAuthZInvalidSelector, "selector must conform to regex %s", typeOrganizationSelectorRegex.String()) diff --git a/pkg/types/authtypes/subject.go b/pkg/types/authtypes/subject.go index 79ace13075..f7aea0b75d 100644 --- a/pkg/types/authtypes/subject.go +++ b/pkg/types/authtypes/subject.go @@ -1,15 +1,17 @@ package authtypes -func NewSubject(subjectType Type, selector string, relation Relation) (string, error) { - if relation.IsZero() { - return subjectType.StringValue() + ":" + selector, nil +import "github.com/SigNoz/signoz/pkg/valuer" + +func NewSubject(subjectType Typeable, selector string, orgID valuer.UUID, relation *Relation) (string, error) { + if relation == nil { + return subjectType.Prefix(orgID) + "/" + selector, nil } - return subjectType.StringValue() + ":" + selector + "#" + relation.StringValue(), nil + return subjectType.Prefix(orgID) + "/" + selector + "#" + relation.StringValue(), nil } -func MustNewSubject(subjectType Type, selector string, relation Relation) string { - subject, err := NewSubject(subjectType, selector, relation) +func MustNewSubject(subjectType Typeable, selector string, orgID valuer.UUID, relation *Relation) string { + subject, err := NewSubject(subjectType, selector, orgID, relation) if err != nil { panic(err) } diff --git a/pkg/types/authtypes/transaction.go b/pkg/types/authtypes/transaction.go index 73d45dc9bf..ee01888ea6 100644 --- a/pkg/types/authtypes/transaction.go +++ b/pkg/types/authtypes/transaction.go @@ -32,6 +32,15 @@ func NewObject(resource Resource, selector Selector) (*Object, error) { return &Object{Resource: resource, Selector: selector}, nil } +func MustNewObject(resource Resource, selector Selector) *Object { + object, err := NewObject(resource, selector) + if err != nil { + panic(err) + } + + return object +} + func MustNewObjectFromString(input string) *Object { parts := strings.Split(input, "/") if len(parts) != 4 { diff --git a/pkg/types/authtypes/typeable.go b/pkg/types/authtypes/typeable.go index f23bc2b154..6b00e7cbdf 100644 --- a/pkg/types/authtypes/typeable.go +++ b/pkg/types/authtypes/typeable.go @@ -16,6 +16,7 @@ var ( var ( TypeUser = Type{valuer.NewString("user")} + TypeAnonymous = Type{valuer.NewString("anonymous")} TypeRole = Type{valuer.NewString("role")} TypeOrganization = Type{valuer.NewString("organization")} TypeMetaResource = Type{valuer.NewString("metaresource")} @@ -24,6 +25,7 @@ var ( var ( TypeableUser = &typeableUser{} + TypeableAnonymous = &typeableAnonymous{} TypeableRole = &typeableRole{} TypeableOrganization = &typeableOrganization{} ) diff --git a/pkg/types/authtypes/typeable_anonymous.go b/pkg/types/authtypes/typeable_anonymous.go new file mode 100644 index 0000000000..0950a40085 --- /dev/null +++ b/pkg/types/authtypes/typeable_anonymous.go @@ -0,0 +1,37 @@ +package authtypes + +import ( + "github.com/SigNoz/signoz/pkg/valuer" + openfgav1 "github.com/openfga/api/proto/openfga/v1" +) + +var _ Typeable = new(typeableAnonymous) + +var ( + AnonymousUser = valuer.UUID{} +) + +type typeableAnonymous struct{} + +func (typeableAnonymous *typeableAnonymous) Tuples(subject string, relation Relation, selector []Selector, orgID valuer.UUID) ([]*openfgav1.TupleKey, error) { + tuples := make([]*openfgav1.TupleKey, 0) + for _, selector := range selector { + object := typeableAnonymous.Prefix(orgID) + "/" + selector.String() + tuples = append(tuples, &openfgav1.TupleKey{User: subject, Relation: relation.StringValue(), Object: object}) + } + + return tuples, nil +} + +func (typeableAnonymous *typeableAnonymous) Type() Type { + return TypeAnonymous +} + +func (typeableAnonymous *typeableAnonymous) Name() Name { + return MustNewName("anonymous") +} + +// example: anonymous:organization/0199c47d-f61b-7833-bc5f-c0730f12f046/anonymous +func (typeableAnonymous *typeableAnonymous) Prefix(orgID valuer.UUID) string { + return typeableAnonymous.Type().StringValue() + ":" + "organization" + "/" + orgID.StringValue() + "/" + typeableAnonymous.Name().String() +} diff --git a/pkg/types/dashboardtypes/dashboard.go b/pkg/types/dashboardtypes/dashboard.go index 2ccea1d65e..871620eaf5 100644 --- a/pkg/types/dashboardtypes/dashboard.go +++ b/pkg/types/dashboardtypes/dashboard.go @@ -3,22 +3,33 @@ package dashboardtypes import ( "context" "encoding/json" + "log/slog" "time" "github.com/SigNoz/signoz/pkg/errors" + "github.com/SigNoz/signoz/pkg/transition" "github.com/SigNoz/signoz/pkg/types" "github.com/SigNoz/signoz/pkg/types/authtypes" + "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5" "github.com/SigNoz/signoz/pkg/valuer" "github.com/uptrace/bun" ) var ( - TypeableResourceDashboard = authtypes.MustNewTypeableMetaResource(authtypes.MustNewName("dashboard")) - TypeableResourcesDashboards = authtypes.MustNewTypeableMetaResources(authtypes.MustNewName("dashboards")) + TypeableMetaResourceDashboard = authtypes.MustNewTypeableMetaResource(authtypes.MustNewName("dashboard")) + TypeableMetaResourcePublicDashboard = authtypes.MustNewTypeableMetaResource(authtypes.MustNewName("public-dashboard")) + TypeableMetaResourcesDashboards = authtypes.MustNewTypeableMetaResources(authtypes.MustNewName("dashboards")) +) + +var ( + ErrCodeDashboardInvalidInput = errors.MustNewCode("dashboard_invalid_input") + ErrCodeDashboardNotFound = errors.MustNewCode("dashboard_not_found") + ErrCodeDashboardInvalidData = errors.MustNewCode("dashboard_invalid_data") + ErrCodeDashboardInvalidWidgetQuery = errors.MustNewCode("dashboard_invalid_widget_query") ) type StorableDashboard struct { - bun.BaseModel `bun:"table:dashboard"` + bun.BaseModel `bun:"table:dashboard,alias:dashboard"` types.Identifiable types.TimeAuditable @@ -97,7 +108,7 @@ func NewDashboard(orgID valuer.UUID, createdBy string, storableDashboardData Sto }, nil } -func NewDashboardFromStorableDashboard(storableDashboard *StorableDashboard) (*Dashboard, error) { +func NewDashboardFromStorableDashboard(storableDashboard *StorableDashboard) *Dashboard { return &Dashboard{ ID: storableDashboard.ID.StringValue(), TimeAuditable: types.TimeAuditable{ @@ -111,20 +122,16 @@ func NewDashboardFromStorableDashboard(storableDashboard *StorableDashboard) (*D OrgID: storableDashboard.OrgID, Data: storableDashboard.Data, Locked: storableDashboard.Locked, - }, nil + } } -func NewDashboardsFromStorableDashboards(storableDashboards []*StorableDashboard) ([]*Dashboard, error) { +func NewDashboardsFromStorableDashboards(storableDashboards []*StorableDashboard) []*Dashboard { dashboards := make([]*Dashboard, len(storableDashboards)) for idx, storableDashboard := range storableDashboards { - dashboard, err := NewDashboardFromStorableDashboard(storableDashboard) - if err != nil { - return nil, err - } - dashboards[idx] = dashboard + dashboards[idx] = NewDashboardFromStorableDashboard(storableDashboard) } - return dashboards, nil + return dashboards } func NewGettableDashboardsFromDashboards(dashboards []*Dashboard) ([]*GettableDashboard, error) { @@ -313,14 +320,129 @@ func (lockUnlockDashboard *LockUnlockDashboard) UnmarshalJSON(src []byte) error return nil } -type Store interface { - Create(context.Context, *StorableDashboard) error +func (dashboard *Dashboard) GetWidgetQuery(startTime, endTime uint64, widgetIndex int64, logger *slog.Logger) (*querybuildertypesv5.QueryRangeRequest, error) { + type dashboardData struct { + Widgets []struct { + PanelTypes string `json:"panelTypes"` + Query struct { + Builder struct { + QueryData []map[string]any `json:"queryData"` + QueryFormulas []map[string]any `json:"queryFormulas"` + QueryTraceOperator []map[string]any `json:"queryTraceOperator"` + } `json:"builder"` + ClickhouseSQL []map[string]any `json:"clickhouse_sql"` + PromQL []map[string]any `json:"promql"` + QueryType string `json:"queryType"` + } `json:"query"` + } `json:"widgets"` + } - Get(context.Context, valuer.UUID, valuer.UUID) (*StorableDashboard, error) + dataJSON, err := json.Marshal(dashboard.Data) + if err != nil { + return nil, errors.Wrapf(err, errors.TypeInvalidInput, ErrCodeDashboardInvalidData, "invalid dashboard data") + } - List(context.Context, valuer.UUID) ([]*StorableDashboard, error) + var data dashboardData + err = json.Unmarshal(dataJSON, &data) + if err != nil { + return nil, errors.Wrapf(err, errors.TypeInvalidInput, ErrCodeDashboardInvalidData, "invalid dashboard data") + } - Update(context.Context, valuer.UUID, *StorableDashboard) error + if len(data.Widgets) < int(widgetIndex)+1 { + return nil, errors.Newf(errors.TypeInvalidInput, ErrCodeDashboardInvalidInput, "widget with index %v doesn't exist", widgetIndex) + } - Delete(context.Context, valuer.UUID, valuer.UUID) error + compositeQueries := []any{} + widgetData := data.Widgets[widgetIndex] + switch widgetData.Query.QueryType { + case "builder": + migrate := transition.NewMigrateCommon(logger) + for _, query := range widgetData.Query.Builder.QueryData { + queryName, ok := query["queryName"].(string) + if !ok { + return nil, errors.New(errors.TypeInvalidInput, ErrCodeDashboardInvalidWidgetQuery, "cannot type cast query name as string") + } + compositeQueries = append(compositeQueries, migrate.WrapInV5Envelope(queryName, query, "builder_query")) + } + for _, query := range widgetData.Query.Builder.QueryFormulas { + queryName, ok := query["queryName"].(string) + if !ok { + return nil, errors.New(errors.TypeInvalidInput, ErrCodeDashboardInvalidWidgetQuery, "cannot type cast query name as string") + } + compositeQueries = append(compositeQueries, migrate.WrapInV5Envelope(queryName, query, "builder_formula")) + } + for _, query := range widgetData.Query.Builder.QueryTraceOperator { + queryName, ok := query["queryName"].(string) + if !ok { + return nil, errors.New(errors.TypeInvalidInput, ErrCodeDashboardInvalidWidgetQuery, "cannot type cast query name as string") + } + compositeQueries = append(compositeQueries, migrate.WrapInV5Envelope(queryName, query, "builder_trace_operator")) + } + case "clickhouse_sql": + for _, query := range widgetData.Query.ClickhouseSQL { + envelope := map[string]any{ + "type": "clickhouse_sql", + "spec": map[string]any{ + "name": query["name"], + "query": query["query"], + "disabled": query["disabled"], + "legend": query["legend"], + }, + } + compositeQueries = append(compositeQueries, envelope) + } + case "promql": + for _, query := range widgetData.Query.PromQL { + envelope := map[string]any{ + "type": "promql", + "spec": map[string]any{ + "name": query["name"], + "query": query["query"], + "disabled": query["disabled"], + "legend": query["legend"], + }, + } + compositeQueries = append(compositeQueries, envelope) + } + } + + queryRangeReq := map[string]any{ + "schemaVersion": "v1", + "start": startTime, + "end": endTime, + "requestType": dashboard.getQueryRequestTypeFromPanelType(widgetData.PanelTypes), + "compositeQuery": map[string]any{ + "queries": compositeQueries, + }, + } + + req, err := json.Marshal(queryRangeReq) + if err != nil { + return nil, errors.Wrapf(err, errors.TypeInvalidInput, ErrCodeDashboardInvalidWidgetQuery, "invalid query request") + } + + queryRangeRequest := new(querybuildertypesv5.QueryRangeRequest) + err = json.Unmarshal(req, queryRangeRequest) + if err != nil { + return nil, errors.Wrapf(err, errors.TypeInvalidInput, ErrCodeDashboardInvalidWidgetQuery, "invalid query request") + } + + return queryRangeRequest, nil +} + +func (dashboard *Dashboard) getQueryRequestTypeFromPanelType(panelType string) querybuildertypesv5.RequestType { + switch panelType { + case "graph", "bar": + return querybuildertypesv5.RequestTypeTimeSeries + case "table", "pie", "value": + return querybuildertypesv5.RequestTypeScalar + case "trace": + return querybuildertypesv5.RequestTypeTrace + case "list": + return querybuildertypesv5.RequestTypeRaw + case "histogram": + return querybuildertypesv5.RequestTypeDistribution + } + + return querybuildertypesv5.RequestTypeUnknown } diff --git a/pkg/types/dashboardtypes/public_dashboard.go b/pkg/types/dashboardtypes/public_dashboard.go new file mode 100644 index 0000000000..0f726541df --- /dev/null +++ b/pkg/types/dashboardtypes/public_dashboard.go @@ -0,0 +1,265 @@ +package dashboardtypes + +import ( + "encoding/json" + "time" + + "github.com/SigNoz/signoz/pkg/errors" + "github.com/SigNoz/signoz/pkg/types" + "github.com/SigNoz/signoz/pkg/valuer" + "github.com/uptrace/bun" +) + +var ( + ErrCodePublicDashboardInvalidInput = errors.MustNewCode("public_dashboard_invalid_input") + ErrCodePublicDashboardNotFound = errors.MustNewCode("public_dashboard_not_found") + ErrCodePublicDashboardAlreadyExists = errors.MustNewCode("public_dashboard_already_exists") +) + +type StorablePublicDashboard struct { + bun.BaseModel `bun:"table:public_dashboard"` + + types.Identifiable + types.TimeAuditable + TimeRangeEnabled bool `bun:"time_range_enabled,type:boolean,notnull"` + DefaultTimeRange string `bun:"default_time_range,type:text,notnull"` + DashboardID string `bun:"dashboard_id,type:text,notnull"` +} + +type PublicDashboard struct { + types.Identifiable + types.TimeAuditable + + TimeRangeEnabled bool `json:"timeRangeEnabled"` + DefaultTimeRange string `json:"defaultTimeRange"` + DashboardID valuer.UUID `json:"dashboardId"` +} + +type GettablePublicDasbhboard struct { + TimeRangeEnabled bool `json:"timeRangeEnabled"` + DefaultTimeRange string `json:"defaultTimeRange"` + PublicPath string `json:"publicPath"` +} + +type PostablePublicDashboard struct { + TimeRangeEnabled bool `json:"timeRangeEnabled"` + DefaultTimeRange string `json:"defaultTimeRange"` +} + +type UpdatablePublicDashboard struct { + TimeRangeEnabled bool `json:"timeRangeEnabled"` + DefaultTimeRange string `json:"defaultTimeRange"` +} + +type GettablePublicDashboardData struct { + Dashboard *Dashboard `json:"dashboard"` + PublicDashboard *GettablePublicDasbhboard `json:"publicDashboard"` +} + +func NewPublicDashboard(timeRangeEnabled bool, defaultTimeRange string, dashboardID valuer.UUID) *PublicDashboard { + return &PublicDashboard{ + Identifiable: types.Identifiable{ + ID: valuer.GenerateUUID(), + }, + TimeAuditable: types.TimeAuditable{ + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }, + TimeRangeEnabled: timeRangeEnabled, + DefaultTimeRange: defaultTimeRange, + DashboardID: dashboardID, + } +} + +func NewStorablePublicDashboardFromPublicDashboard(publicDashboard *PublicDashboard) *StorablePublicDashboard { + return &StorablePublicDashboard{ + Identifiable: publicDashboard.Identifiable, + TimeAuditable: publicDashboard.TimeAuditable, + TimeRangeEnabled: publicDashboard.TimeRangeEnabled, + DefaultTimeRange: publicDashboard.DefaultTimeRange, + DashboardID: publicDashboard.DashboardID.StringValue(), + } +} + +func NewPublicDashboardFromStorablePublicDashboard(storable *StorablePublicDashboard) *PublicDashboard { + return &PublicDashboard{ + Identifiable: storable.Identifiable, + TimeAuditable: storable.TimeAuditable, + TimeRangeEnabled: storable.TimeRangeEnabled, + DefaultTimeRange: storable.DefaultTimeRange, + DashboardID: valuer.MustNewUUID(storable.DashboardID), + } +} + +func NewGettablePublicDashboard(publicDashboard *PublicDashboard) *GettablePublicDasbhboard { + return &GettablePublicDasbhboard{ + TimeRangeEnabled: publicDashboard.TimeRangeEnabled, + DefaultTimeRange: publicDashboard.DefaultTimeRange, + PublicPath: publicDashboard.PublicPath(), + } +} + +func NewPublicDashboardDataFromDashboard(dashboard *Dashboard, publicDashboard *PublicDashboard) (*GettablePublicDashboardData, error) { + type dashboardData struct { + Widgets []struct { + PanelTypes string `json:"panelTypes"` + Query struct { + Builder struct { + QueryData []map[string]any `json:"queryData"` + QueryFormulas []map[string]any `json:"queryFormulas"` + QueryTraceOperator []map[string]any `json:"queryTraceOperator"` + } `json:"builder"` + ClickhouseSQL []map[string]any `json:"clickhouse_sql"` + PromQL []map[string]any `json:"promql"` + QueryType string `json:"queryType"` + } `json:"query"` + } `json:"widgets"` + } + + dataJSON, err := json.Marshal(dashboard.Data) + if err != nil { + return nil, errors.Wrapf(err, errors.TypeInvalidInput, ErrCodeDashboardInvalidData, "invalid dashboard data") + } + + var data dashboardData + err = json.Unmarshal(dataJSON, &data) + if err != nil { + return nil, errors.Wrapf(err, errors.TypeInvalidInput, ErrCodeDashboardInvalidData, "invalid dashboard data") + } + + for idx, widget := range data.Widgets { + switch widget.Query.QueryType { + case "builder": + widget.Query.ClickhouseSQL = []map[string]any{} + widget.Query.PromQL = []map[string]any{} + + updatedQueryData := []map[string]any{} + for _, queryData := range widget.Query.Builder.QueryData { + updatedQueryMap := map[string]any{} + updatedQueryMap["aggregations"] = queryData["aggregations"] + updatedQueryMap["legend"] = queryData["legend"] + updatedQueryMap["queryName"] = queryData["queryName"] + updatedQueryMap["expression"] = queryData["expression"] + updatedQueryData = append(updatedQueryData, updatedQueryMap) + } + widget.Query.Builder.QueryData = updatedQueryData + + updatedQueryFormulas := []map[string]any{} + for _, queryFormula := range widget.Query.Builder.QueryFormulas { + updatedQueryFormulaMap := map[string]any{} + updatedQueryFormulaMap["legend"] = queryFormula["legend"] + updatedQueryFormulaMap["queryName"] = queryFormula["queryName"] + updatedQueryFormulaMap["expression"] = queryFormula["expression"] + updatedQueryFormulas = append(updatedQueryFormulas, updatedQueryFormulaMap) + } + widget.Query.Builder.QueryFormulas = updatedQueryFormulas + + updatedQueryTraceOperator := []map[string]any{} + for _, queryTraceOperator := range widget.Query.Builder.QueryTraceOperator { + updatedQueryTraceOperatorMap := map[string]any{} + updatedQueryTraceOperatorMap["aggregations"] = queryTraceOperator["aggregations"] + updatedQueryTraceOperatorMap["legend"] = queryTraceOperator["legend"] + updatedQueryTraceOperatorMap["queryName"] = queryTraceOperator["queryName"] + updatedQueryTraceOperatorMap["expression"] = queryTraceOperator["expression"] + updatedQueryTraceOperator = append(updatedQueryTraceOperator, updatedQueryTraceOperatorMap) + } + widget.Query.Builder.QueryTraceOperator = updatedQueryTraceOperator + + case "clickhouse_sql": + widget.Query.Builder = struct { + QueryData []map[string]any `json:"queryData"` + QueryFormulas []map[string]any `json:"queryFormulas"` + QueryTraceOperator []map[string]any `json:"queryTraceOperator"` + }{} + widget.Query.PromQL = []map[string]any{} + + updatedClickhouseSQLQuery := []map[string]any{} + for _, clickhouseSQLQuery := range widget.Query.ClickhouseSQL { + updatedClickhouseSQLQueryMap := make(map[string]any) + updatedClickhouseSQLQueryMap["legend"] = clickhouseSQLQuery["legend"] + updatedClickhouseSQLQueryMap["name"] = clickhouseSQLQuery["name"] + updatedClickhouseSQLQuery = append(updatedClickhouseSQLQuery, updatedClickhouseSQLQueryMap) + } + widget.Query.ClickhouseSQL = updatedClickhouseSQLQuery + case "promql": + widget.Query.Builder = struct { + QueryData []map[string]any `json:"queryData"` + QueryFormulas []map[string]any `json:"queryFormulas"` + QueryTraceOperator []map[string]any `json:"queryTraceOperator"` + }{} + widget.Query.ClickhouseSQL = []map[string]any{} + + updatedPromQLQuery := []map[string]any{} + for _, promQLQuery := range widget.Query.PromQL { + updatedPromQLQueryMap := make(map[string]any) + updatedPromQLQueryMap["legend"] = promQLQuery["legend"] + updatedPromQLQueryMap["name"] = promQLQuery["name"] + updatedPromQLQuery = append(updatedPromQLQuery, updatedPromQLQueryMap) + } + widget.Query.PromQL = updatedPromQLQuery + default: + return nil, errors.Newf(errors.TypeInvalidInput, ErrCodeDashboardInvalidWidgetQuery, "invalid query type: %s", widget.Query.QueryType) + } + + if widgets, ok := dashboard.Data["widgets"].([]any); ok { + if widgetMap, ok := widgets[idx].(map[string]any); ok { + widgetMap["query"] = widget.Query + } + } + } + + return &GettablePublicDashboardData{ + Dashboard: &Dashboard{ + Data: dashboard.Data, + }, + PublicDashboard: &GettablePublicDasbhboard{ + TimeRangeEnabled: publicDashboard.TimeRangeEnabled, + DefaultTimeRange: publicDashboard.DefaultTimeRange, + PublicPath: publicDashboard.PublicPath(), + }, + }, nil +} + +func (typ *PublicDashboard) Update(timeRangeEnabled bool, defaultTimeRange string) { + typ.TimeRangeEnabled = timeRangeEnabled + typ.DefaultTimeRange = defaultTimeRange + typ.UpdatedAt = time.Now() +} + +func (typ *PublicDashboard) PublicPath() string { + return "/public/dashboard/" + typ.ID.StringValue() +} + +func (typ *PostablePublicDashboard) UnmarshalJSON(data []byte) error { + type alias PostablePublicDashboard + var temp alias + + if err := json.Unmarshal(data, &temp); err != nil { + return err + } + + _, err := time.ParseDuration(temp.DefaultTimeRange) + if err != nil { + return errors.Wrapf(err, errors.TypeInvalidInput, ErrCodePublicDashboardInvalidInput, "unable to parse defaultTimeRange %s", temp.DefaultTimeRange) + } + + *typ = PostablePublicDashboard(temp) + return nil +} + +func (typ *UpdatablePublicDashboard) UnmarshalJSON(data []byte) error { + type alias UpdatablePublicDashboard + var temp alias + + if err := json.Unmarshal(data, &temp); err != nil { + return err + } + + _, err := time.ParseDuration(temp.DefaultTimeRange) + if err != nil { + return errors.Wrapf(err, errors.TypeInvalidInput, ErrCodePublicDashboardInvalidInput, "unable to parse defaultTimeRange %s", temp.DefaultTimeRange) + } + + *typ = UpdatablePublicDashboard(temp) + return nil +} diff --git a/pkg/types/dashboardtypes/store.go b/pkg/types/dashboardtypes/store.go new file mode 100644 index 0000000000..27e5845a02 --- /dev/null +++ b/pkg/types/dashboardtypes/store.go @@ -0,0 +1,33 @@ +package dashboardtypes + +import ( + "context" + + "github.com/SigNoz/signoz/pkg/valuer" +) + +type Store interface { + Create(context.Context, *StorableDashboard) error + + CreatePublic(context.Context, *StorablePublicDashboard) error + + Get(context.Context, valuer.UUID, valuer.UUID) (*StorableDashboard, error) + + GetPublic(context.Context, string) (*StorablePublicDashboard, error) + + GetDashboardByOrgsAndPublicID(context.Context, []string, string) (*StorableDashboard, error) + + GetDashboardByPublicID(context.Context, string) (*StorableDashboard, error) + + List(context.Context, valuer.UUID) ([]*StorableDashboard, error) + + Update(context.Context, valuer.UUID, *StorableDashboard) error + + UpdatePublic(context.Context, *StorablePublicDashboard) error + + Delete(context.Context, valuer.UUID, valuer.UUID) error + + DeletePublic(context.Context, string) error + + RunInTx(context.Context, func(context.Context) error) error +} diff --git a/pkg/types/roletypes/role.go b/pkg/types/roletypes/role.go index b8905fbf83..2f4e170985 100644 --- a/pkg/types/roletypes/role.go +++ b/pkg/types/roletypes/role.go @@ -2,6 +2,7 @@ package roletypes import ( "encoding/json" + "regexp" "slices" "time" @@ -21,6 +22,20 @@ var ( ErrCodeRoleFailedTransactionsFromString = errors.MustNewCode("role_failed_transactions_from_string") ) +var ( + RoleNameRegex = regexp.MustCompile("^[a-z-]{1,50}$") +) + +var ( + RoleTypeCustom = valuer.NewString("custom") + RoleTypeManaged = valuer.NewString("managed") +) + +var ( + AnonymousUserRoleName = "signoz-anonymous" + AnonymousUserRoleDescription = "Role assigned to anonymous users for access to public resources." +) + var ( TypeableResourcesRoles = authtypes.MustNewTypeableMetaResources(authtypes.MustNewName("roles")) ) @@ -30,26 +45,28 @@ type StorableRole struct { types.Identifiable types.TimeAuditable - DisplayName string `bun:"display_name,type:string"` + Name string `bun:"name,type:string"` Description string `bun:"description,type:string"` + Type string `bun:"type,type:string"` OrgID string `bun:"org_id,type:string"` } type Role struct { types.Identifiable types.TimeAuditable - DisplayName string `json:"displayName"` + Name string `json:"name"` Description string `json:"description"` + Type string `json:"type"` OrgID valuer.UUID `json:"org_id"` } type PostableRole struct { - DisplayName string `json:"displayName"` + Name string `json:"name"` Description string `json:"description"` } type PatchableRole struct { - DisplayName *string `json:"displayName"` + Name *string `json:"name"` Description *string `json:"description"` } @@ -58,32 +75,29 @@ type PatchableObjects struct { Deletions []*authtypes.Object `json:"deletions"` } -func NewStorableRoleFromRole(role *Role) (*StorableRole, error) { +func NewStorableRoleFromRole(role *Role) *StorableRole { return &StorableRole{ Identifiable: role.Identifiable, TimeAuditable: role.TimeAuditable, - DisplayName: role.DisplayName, + Name: role.Name, Description: role.Description, + Type: role.Type, OrgID: role.OrgID.StringValue(), - }, nil + } } -func NewRoleFromStorableRole(storableRole *StorableRole) (*Role, error) { - orgID, err := valuer.NewUUID(storableRole.OrgID) - if err != nil { - return nil, err - } - +func NewRoleFromStorableRole(storableRole *StorableRole) *Role { return &Role{ Identifiable: storableRole.Identifiable, TimeAuditable: storableRole.TimeAuditable, - DisplayName: storableRole.DisplayName, + Name: storableRole.Name, Description: storableRole.Description, - OrgID: orgID, - }, nil + Type: storableRole.Type, + OrgID: valuer.MustNewUUID(storableRole.OrgID), + } } -func NewRole(displayName, description string, orgID valuer.UUID) *Role { +func NewRole(name, description string, roleType string, orgID valuer.UUID) *Role { return &Role{ Identifiable: types.Identifiable{ ID: valuer.GenerateUUID(), @@ -92,8 +106,9 @@ func NewRole(displayName, description string, orgID valuer.UUID) *Role { CreatedAt: time.Now(), UpdatedAt: time.Now(), }, - DisplayName: displayName, + Name: name, Description: description, + Type: roleType, OrgID: orgID, } } @@ -118,9 +133,9 @@ func NewPatchableObjects(additions []*authtypes.Object, deletions []*authtypes.O return &PatchableObjects{Additions: additions, Deletions: deletions}, nil } -func (role *Role) PatchMetadata(displayName, description *string) { - if displayName != nil { - role.DisplayName = *displayName +func (role *Role) PatchMetadata(name, description *string) { + if name != nil { + role.Name = *name } if description != nil { role.Description = *description @@ -130,7 +145,7 @@ func (role *Role) PatchMetadata(displayName, description *string) { func (role *PostableRole) UnmarshalJSON(data []byte) error { type shadowPostableRole struct { - DisplayName string `json:"displayName"` + Name string `json:"name"` Description string `json:"description"` } @@ -139,11 +154,15 @@ func (role *PostableRole) UnmarshalJSON(data []byte) error { return err } - if shadowRole.DisplayName == "" { - return errors.New(errors.TypeInvalidInput, ErrCodeRoleInvalidInput, "displayName is missing from the request") + if shadowRole.Name == "" { + return errors.New(errors.TypeInvalidInput, ErrCodeRoleInvalidInput, "name is missing from the request") } - role.DisplayName = shadowRole.DisplayName + if match := RoleNameRegex.MatchString(shadowRole.Name); !match { + return errors.Newf(errors.TypeInvalidInput, ErrCodeRoleInvalidInput, "name must conform to the regex: %s", RoleNameRegex.String()) + } + + role.Name = shadowRole.Name role.Description = shadowRole.Description return nil @@ -151,7 +170,7 @@ func (role *PostableRole) UnmarshalJSON(data []byte) error { func (role *PatchableRole) UnmarshalJSON(data []byte) error { type shadowPatchableRole struct { - DisplayName *string `json:"displayName"` + Name *string `json:"name"` Description *string `json:"description"` } @@ -160,11 +179,17 @@ func (role *PatchableRole) UnmarshalJSON(data []byte) error { return err } - if shadowRole.DisplayName == nil && shadowRole.Description == nil { - return errors.New(errors.TypeInvalidInput, ErrCodeRoleEmptyPatch, "empty role patch request received, at least one of displayName or description must be present") + if shadowRole.Name == nil && shadowRole.Description == nil { + return errors.New(errors.TypeInvalidInput, ErrCodeRoleEmptyPatch, "empty role patch request received, at least one of name or description must be present") } - role.DisplayName = shadowRole.DisplayName + if shadowRole.Name != nil { + if match := RoleNameRegex.MatchString(*shadowRole.Name); !match { + return errors.Newf(errors.TypeInvalidInput, ErrCodeRoleInvalidInput, "name must conform to the regex: %s", RoleNameRegex.String()) + } + } + + role.Name = shadowRole.Name role.Description = shadowRole.Description return nil @@ -177,9 +202,10 @@ func GetAdditionTuples(id valuer.UUID, orgID valuer.UUID, relation authtypes.Rel typeable := authtypes.MustNewTypeableFromType(object.Resource.Type, object.Resource.Name) transactionTuples, err := typeable.Tuples( authtypes.MustNewSubject( - authtypes.TypeRole, + authtypes.TypeableRole, id.String(), - authtypes.RelationAssignee, + orgID, + &authtypes.RelationAssignee, ), relation, []authtypes.Selector{object.Selector}, @@ -202,9 +228,10 @@ func GetDeletionTuples(id valuer.UUID, orgID valuer.UUID, relation authtypes.Rel typeable := authtypes.MustNewTypeableFromType(object.Resource.Type, object.Resource.Name) transactionTuples, err := typeable.Tuples( authtypes.MustNewSubject( - authtypes.TypeRole, + authtypes.TypeableRole, id.String(), - authtypes.RelationAssignee, + orgID, + &authtypes.RelationAssignee, ), relation, []authtypes.Selector{object.Selector}, diff --git a/pkg/types/roletypes/store.go b/pkg/types/roletypes/store.go index bb7d1f942c..5fbbff3032 100644 --- a/pkg/types/roletypes/store.go +++ b/pkg/types/roletypes/store.go @@ -9,6 +9,7 @@ import ( type Store interface { Create(context.Context, *StorableRole) error Get(context.Context, valuer.UUID, valuer.UUID) (*StorableRole, error) + GetByNameAndOrgID(context.Context, string, valuer.UUID) (*StorableRole, error) List(context.Context, valuer.UUID) ([]*StorableRole, error) Update(context.Context, valuer.UUID, *StorableRole) error Delete(context.Context, valuer.UUID, valuer.UUID) error