diff --git a/pkg/report/frame.go b/pkg/report/frame.go index 66bed9370..3d021aabf 100644 --- a/pkg/report/frame.go +++ b/pkg/report/frame.go @@ -6,6 +6,7 @@ import ( "fmt" "strings" + "github.com/cortezaproject/corteza-server/pkg/errors" "github.com/cortezaproject/corteza-server/pkg/expr" "github.com/cortezaproject/corteza-server/pkg/filter" "github.com/cortezaproject/corteza-server/pkg/qlng" @@ -78,6 +79,7 @@ type ( // Filter is a qlng.ASTNode wrapper to get some unmarshal/marshal features Filter struct { *qlng.ASTNode + Error string `json:"error,omitempty"` } ) @@ -91,6 +93,7 @@ func (f *Filter) Clone() *Filter { return &Filter{ ASTNode: f.ASTNode.Clone(), + Error: f.Error, } } @@ -130,11 +133,12 @@ func (f *Filter) UnmarshalJSON(data []byte) (err error) { return } - if f.ASTNode, err = p.Parse(v); err != nil { - return - } + f.ASTNode, err = p.Parse(v) f.ASTNode.Raw = v - return + if err != nil { + f.Error = err.Error() + } + return nil } // special case for empty JSON @@ -144,16 +148,17 @@ func (f *Filter) UnmarshalJSON(data []byte) (err error) { // non-string is considered an AST and we parse that if err = json.Unmarshal(data, &f.ASTNode); err != nil { - return + f.Error = err.Error() + return nil } // traverse the AST to parse any raw exprs. if f.ASTNode == nil { - return + return nil } // A raw expression takes priority and replaces the original AST sub-tree - return f.ASTNode.Traverse(func(n *qlng.ASTNode) (bool, *qlng.ASTNode, error) { + err = f.ASTNode.Traverse(func(n *qlng.ASTNode) (bool, *qlng.ASTNode, error) { if n.Raw == "" { return true, n, nil } @@ -166,6 +171,14 @@ func (f *Filter) UnmarshalJSON(data []byte) (err error) { return false, aux, nil }) + + if err != nil { + f.Error = err.Error() + } else { + f.Error = "" + } + + return nil } // With guard element @@ -418,6 +431,13 @@ func (f *FrameDefinition) Clone() (out *FrameDefinition) { } } +func (d FrameDefinition) Validate() error { + if d.Filter != nil && d.Filter.Error != "" { + return errors.InvalidData(d.Filter.Error) + } + return nil +} + func (dd FrameDefinitionSet) Find(name string) *FrameDefinition { for _, d := range dd { if d.Name == name { diff --git a/pkg/report/model.go b/pkg/report/model.go index c2b2fba29..39839d013 100644 --- a/pkg/report/model.go +++ b/pkg/report/model.go @@ -2,9 +2,9 @@ package report import ( "context" - "errors" "fmt" + "github.com/cortezaproject/corteza-server/pkg/errors" "github.com/cortezaproject/corteza-server/pkg/filter" ) @@ -57,7 +57,7 @@ func Model(ctx context.Context, sources map[string]DatasourceProvider, dd ...*St switch { case d.Load != nil: if sources == nil { - return errors.New("no datasource providers defined") + return fmt.Errorf("no datasource providers defined") } steps = append(steps, &stepLoad{def: d.Load, dsp: sources[d.Load.Source]}) @@ -70,7 +70,7 @@ func Model(ctx context.Context, sources map[string]DatasourceProvider, dd ...*St // @todo Transform default: - return errors.New("malformed step definition: unsupported step kind") + return fmt.Errorf("malformed step definition: unsupported step kind") } } return nil @@ -85,6 +85,24 @@ func Model(ctx context.Context, sources map[string]DatasourceProvider, dd ...*St }, nil } +func (ss StepDefinitionSet) Validate() error { + var f *Filter + for _, s := range ss { + switch { + case s.Load != nil: + f = s.Load.Filter + case s.Join != nil: + f = s.Join.Filter + case s.Group != nil: + f = s.Group.Filter + } + if f != nil && f.Error != "" { + return errors.InvalidData(f.Error) + } + } + return nil +} + // Add adds additional steps to the model func (m *model) Add(ss ...step) M { m.steps = append(m.steps, ss...) @@ -101,11 +119,11 @@ func (m *model) Run(ctx context.Context) (err error) { // initial validation err = func() (err error) { if m.ran { - return errors.New("model already ran") + return fmt.Errorf("model already ran") } if len(m.steps) == 0 { - return errors.New("no model steps defined") + return fmt.Errorf("no model steps defined") } return nil diff --git a/pkg/report/step_join_strategies.go b/pkg/report/step_join_strategies.go index 1ee2a0355..0160afa44 100644 --- a/pkg/report/step_join_strategies.go +++ b/pkg/report/step_join_strategies.go @@ -408,9 +408,9 @@ func (d *joinedDataset) strategizePaging(local, foreign *FrameDefinition, invert // local otherwise. switch pp[0].ref { case local.Ref, "": - local.Filter = merger(&Filter{pp[0].filterCut}, local.Filter, "and") + local.Filter = merger(&Filter{pp[0].filterCut, ""}, local.Filter, "and") case foreign.Ref: - foreign.Filter = merger(&Filter{pp[0].filterCut}, foreign.Filter, "and") + foreign.Filter = merger(&Filter{pp[0].filterCut, ""}, foreign.Filter, "and") } return diff --git a/system/service/report.go b/system/service/report.go index 14586a650..dc539ced1 100644 --- a/system/service/report.go +++ b/system/service/report.go @@ -293,6 +293,10 @@ func (svc *report) DescribeFresh(ctx context.Context, src types.ReportDataSource out = make(rep.FrameDescriptionSet, 0, len(sources)*2) err = func() (err error) { + if !svc.ac.CanCreateReport(ctx) { + return ReportErrNotAllowedToCreate() + } + ss := src.ModelSteps() ss = append(ss, st...) @@ -352,6 +356,16 @@ func (svc *report) Run(ctx context.Context, reportID uint64, dd rep.FrameDefinit ss := r.Sources.ModelSteps() ss = append(ss, r.Blocks.ModelSteps()...) + if err = ss.Validate(); err != nil { + return ReportErrInvalidConfiguration().Wrap(err) + } + + for _, d := range dd { + if err = d.Validate(); err != nil { + return ReportErrInvalidConfiguration().Wrap(err) + } + } + // Model the report model, err := rep.Model(ctx, reporters, ss...) if err != nil { diff --git a/system/service/report_actions.gen.go b/system/service/report_actions.gen.go index d8efd968a..c45dae6a9 100644 --- a/system/service/report_actions.gen.go +++ b/system/service/report_actions.gen.go @@ -779,6 +779,42 @@ func ReportErrNotAllowedToRun(mm ...*reportActionProps) *errors.Error { return e } +// ReportErrInvalidConfiguration returns "system:report.invalidConfiguration" as *errors.Error +// +// +// This function is auto-generated. +// +func ReportErrInvalidConfiguration(mm ...*reportActionProps) *errors.Error { + var p = &reportActionProps{} + if len(mm) > 0 { + p = mm[0] + } + + var e = errors.New( + errors.KindInternal, + + p.Format("invalid report configuration", nil), + + errors.Meta("type", "invalidConfiguration"), + errors.Meta("resource", "system:report"), + + // action log entry; no formatting, it will be applied inside recordAction fn. + errors.Meta(reportLogMetaKey{}, "failed to run {{report}}; invalid configuration"), + errors.Meta(reportPropsMetaKey{}, p), + + // translation namespace & key + errors.Meta(locale.ErrorMetaNamespace{}, "system"), + errors.Meta(locale.ErrorMetaKey{}, "report.errors.invalidConfiguration"), + + errors.StackSkip(1), + ) + + if len(mm) > 0 { + } + + return e +} + // ********************************************************************************************************************* // ********************************************************************************************************************* diff --git a/system/service/report_actions.yaml b/system/service/report_actions.yaml index b8d6bb2d6..5dd9475bd 100644 --- a/system/service/report_actions.yaml +++ b/system/service/report_actions.yaml @@ -90,3 +90,7 @@ errors: - error: notAllowedToRun message: "not allowed to run this report" log: "failed to run {{report}}; insufficient permissions" + + - error: invalidConfiguration + message: "invalid report configuration" + log: "failed to run {{report}}; invalid configuration"