diff --git a/system/commands/sink.go b/system/commands/sink.go new file mode 100644 index 000000000..6440922ac --- /dev/null +++ b/system/commands/sink.go @@ -0,0 +1,89 @@ +package commands + +import ( + "context" + "net/url" + "strings" + "time" + + "github.com/spf13/cobra" + + "github.com/cortezaproject/corteza-server/internal/auth" + "github.com/cortezaproject/corteza-server/pkg/cli" +) + +// Will perform OpenID connect auto-configuration +func Sink(ctx context.Context, c *cli.Config) *cobra.Command { + var ( + expires string + origin string + contentType string + method string + ) + + cmd := &cobra.Command{ + Use: "sink", + Short: "Sink", + } + + signatureCmd := &cobra.Command{ + Use: "signature", + Short: "Creates signature for sink HTTP endpoint", + RunE: func(cmd *cobra.Command, args []string) error { + c.InitServices(ctx, c) + + method = strings.ToUpper(method) + + if expires != "" { + // validate expiration date if set + if _, err := time.Parse("2006-01-02", expires); err != nil { + return err + } + } + + v := url.Values{} + v.Set("sign", auth.DefaultSigner.Sign(0, method, "/sink", contentType, origin, expires)) + v.Set("expires", expires) + v.Set("content-type", contentType) + v.Set("origin", origin) + v.Set("method", method) + + // @todo add host & schema + cmd.Println((&url.URL{ + Path: "/sink", + RawQuery: v.Encode()}).String()) + + return nil + }, + } + + signatureCmd.Flags().StringVar( + &origin, + "origin", + "", + "Origin of the request (arbitrary string, optional)") + + signatureCmd.Flags().StringVar( + &contentType, + "content-type", + "", + "Content type (optional)") + + signatureCmd.Flags().StringVar( + &expires, + "expires", + "", + "Date of expiration (YYYY-MM-DD, optional)") + + signatureCmd.Flags().StringVar( + &method, + "method", + "GET", + "HTTP method that will be used") + + cmd.AddCommand( + signatureCmd, + ) + + return cmd +} diff --git a/system/internal/service/automation_runner.go b/system/internal/service/automation_runner.go index 3211ca253..e56aee56a 100644 --- a/system/internal/service/automation_runner.go +++ b/system/internal/service/automation_runner.go @@ -80,8 +80,8 @@ func (svc automationRunner) findMailScripts(headers types.MailMessageHeader) aut ss, _ := svc.scriptFinder.FindRunnableScripts("system:mail", "onReceive", mailTrigger.MakeChecker(headers, uev)). Filter(func(script *automation.Script) (bool, error) { - // Filter out user-agent scripts - return !script.RunInUA, nil + // Filter out user-agent scripts && scripts w/o defined runner. + return !script.RunInUA && script.RunAsDefined(), nil }) return ss diff --git a/system/internal/service/sink.go b/system/internal/service/sink.go index e98b59db1..65c197654 100644 --- a/system/internal/service/sink.go +++ b/system/internal/service/sink.go @@ -30,10 +30,10 @@ const ( func Sink() *sink { return &sink{ + logger: DefaultLogger, proc: map[string]sinkContentProc{ SinkContentTypeMail: Mailproc(), }, - logger: DefaultLogger, } } diff --git a/system/rest/router.go b/system/rest/router.go index ebd28f40b..32f7a2799 100644 --- a/system/rest/router.go +++ b/system/rest/router.go @@ -14,18 +14,19 @@ func MountRoutes(r chi.Router) { r.Group(func(r chi.Router) { handlers.NewAuth((Auth{}).New()).MountRoutes(r) handlers.NewAuthInternal((AuthInternal{}).New()).MountRoutes(r) + + // A special case that, we do not add this through standard request, handlers & controllers + // combo but directly -- we need access to r.Body + r.Handle("/sink", &Sink{ + svc: service.DefaultSink, + sign: auth.DefaultSigner, + }) }) // Protect all _private_ routes r.Group(func(r chi.Router) { r.Use(auth.MiddlewareValidOnly) - // A special case that, we do not add this through standard request, handlers & controllers - // combo but directly -- we need access to r.Body - r.Handle("/sink", &Sink{ - svc: service.DefaultSink, - }) - handlers.NewUser(User{}.New()).MountRoutes(r) handlers.NewRole(Role{}.New()).MountRoutes(r) handlers.NewOrganisation(Organisation{}.New()).MountRoutes(r) diff --git a/system/rest/sink.go b/system/rest/sink.go index cbbeb04ac..f72be7352 100644 --- a/system/rest/sink.go +++ b/system/rest/sink.go @@ -5,9 +5,11 @@ import ( "io" "net/http" "strings" + "time" "github.com/pkg/errors" + "github.com/cortezaproject/corteza-server/internal/auth" "github.com/cortezaproject/corteza-server/system/internal/service" ) @@ -18,36 +20,66 @@ type Sink struct { svc interface { Process(context.Context, string, io.Reader) error } + + sign auth.Signer } func (ctrl *Sink) ServeHTTP(w http.ResponseWriter, r *http.Request) { var ( - ctx = r.Context() - cType = r.URL.Query().Get("content-type") + ctx = r.Context() + + // What are we getting + part of the signature + contentType = r.URL.Query().Get("content-type") + + // + sign = r.URL.Query().Get("sign") + origin = r.URL.Query().Get("origin") + expires = r.URL.Query().Get("expires") + method = strings.ToUpper(r.Method) unsupported = func() { http.Error(w, "unsupported content-type", http.StatusBadRequest) } ) - if cType == "" { - // If content-type not explicitly set (via QS), - // try to get it from the headers - cType = r.Header.Get("content-type") - if i := strings.Index(cType, ";"); i > 0 { - // intentionally > 0 - cType = cType[0 : i-1] + if sign == "" { + http.Error(w, "signature missing", http.StatusUnauthorized) + return + } + + if ctrl.sign.Verify(sign, 0, method, "/sink", contentType, origin, expires) { + http.Error(w, "invalid signature", http.StatusForbidden) + return + } + + if expires != "" { + if exp, err := time.Parse("2006-01-02", expires); err != nil { + http.Error(w, "could not process expiration date", http.StatusInternalServerError) + return + } else if exp.Before(time.Now()) { + http.Error(w, "signature expired", http.StatusGone) + return } } - if cType == "" { + if contentType == "" { + // If content-type not explicitly set (via QS), + // try to get it from the headers + contentType = r.Header.Get("content-type") + if i := strings.Index(contentType, ";"); i > 0 { + // intentionally > 0 + contentType = contentType[0 : i-1] + } + } + + if contentType == "" { unsupported() return } defer r.Body.Close() - switch ctrl.svc.Process(ctx, cType, r.Body) { + switch ctrl.svc.Process(ctx, contentType, r.Body) { case service.ErrSinkContentProcessingFailed: http.Error(w, "sink processing failed", http.StatusInternalServerError) diff --git a/system/system.go b/system/system.go index 9a2bfd4e0..069bcefa9 100644 --- a/system/system.go +++ b/system/system.go @@ -129,6 +129,9 @@ func Configure() *cli.Config { func(ctx context.Context, c *cli.Config) *cobra.Command { return commands.Roles(ctx, c) }, + func(ctx context.Context, c *cli.Config) *cobra.Command { + return commands.Sink(ctx, c) + }, }, ProvisionMigrateDatabase: cli.Runners{