feat(authz): publicly shareable dashboards (#9584)

* feat(authz): base setup for public shareable dashboards

* feat(authz): add support for public masking

* feat(authz): added public path for gettable public dashboard

* feat(authz): checkpoint-1 for widget query to query range conversion

* feat(authz): checkpoint-2 for widget query to query range conversion

* feat(authz): fix widget index issue

* feat(authz): better handling for dashboard json and query

* feat(authz): use the default time range if timerange is disabled

* feat(authz): use the default time range if timerange is disabled

* feat(authz): add authz changes

* feat(authz): integrate role with dashboard anonymous access

* feat(authz): integrate the new middleware

* feat(authz): integrate the new middleware

* feat(authz): add back licensing

* feat(authz): renaming selector callback

* feat(authz): self review

* feat(authz): self review

* feat(authz): change to promql
This commit is contained in:
Vikrant Gupta
2025-11-18 00:21:46 +05:30
committed by GitHub
parent a48455b2b3
commit 7bd3e1c453
40 changed files with 1544 additions and 178 deletions

View File

@@ -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)

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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]

View File

@@ -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)

View File

@@ -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",

View File

@@ -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

View File

@@ -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",
}
}(),
})

View File

@@ -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)
})
}

View File

@@ -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)

View File

@@ -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)
}

View File

@@ -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}
}

View File

@@ -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)
})
}

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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.

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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),

View File

@@ -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++ {

View File

@@ -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,

View File

@@ -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++ {

View File

@@ -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),
)
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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,

View File

@@ -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)
}

View File

@@ -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)

View File

@@ -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())

View File

@@ -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)
}

View File

@@ -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 {

View File

@@ -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{}
)

View File

@@ -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()
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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},

View File

@@ -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