diff --git a/system/commands/sink.go b/system/commands/sink.go index a9db288c1..cf1c0f010 100644 --- a/system/commands/sink.go +++ b/system/commands/sink.go @@ -31,12 +31,41 @@ func Sink() *cobra.Command { } } - cmd.Printf("%+v\n", srup) - - if su, err := service.DefaultSink.SignURL(srup); err != nil { + if su, srup, err := service.DefaultSink.SignURL(srup); err != nil { return err } else { cmd.Println(su) + + cmd.Println("Sink request constraints:") + if srup.SignatureInPath { + cmd.Println(" - signature should be part of path") + } else { + cmd.Println(" - signature should be part of query-string") + } + + if srup.Method != "" { + cmd.Printf(" - expecting request method %q\n", srup.Method) + + } + if srup.Expires != nil { + cmd.Printf(" - signature expires at: %s\n", srup.Expires) + + } + if srup.MaxBodySize > 0 { + cmd.Printf(" - max request body size is %d Kb\n", srup.MaxBodySize/1024) + + } else { + cmd.Println(" - body size is not limited") + + } + if srup.ContentType != "" { + cmd.Printf(" - expecting content type to be %q\n", srup.ContentType) + + } + if srup.Path != "" { + cmd.Printf(" - valid path under /sink: %q\n", srup.Path) + + } } return nil @@ -55,6 +84,12 @@ func Sink() *cobra.Command { "", "Content type (optional)") + signatureCmd.Flags().StringVar( + &srup.Path, + "path", + "", + "Full sink request path (do not include /sink prefix, add / for just root)") + signatureCmd.Flags().StringVar( &expires, "expires", @@ -64,7 +99,7 @@ func Sink() *cobra.Command { signatureCmd.Flags().StringVar( &srup.Method, "method", - "GET", + "", "HTTP method that will be used (optional)") signatureCmd.Flags().Int64Var( diff --git a/system/rest/router.go b/system/rest/router.go index 235526cd8..b81c1e840 100644 --- a/system/rest/router.go +++ b/system/rest/router.go @@ -18,7 +18,7 @@ func MountRoutes(r chi.Router) { // 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{ + r.Handle(service.SinkBaseURL+"*", &Sink{ svc: service.DefaultSink, sign: auth.DefaultSigner, }) diff --git a/system/service/sink.go b/system/service/sink.go index a5c3a6bae..69133edf6 100644 --- a/system/service/sink.go +++ b/system/service/sink.go @@ -11,6 +11,8 @@ import ( "strings" "time" + "github.com/davecgh/go-spew/spew" + "github.com/cortezaproject/corteza-server/pkg/actionlog" internalAuth "github.com/cortezaproject/corteza-server/pkg/auth" "github.com/cortezaproject/corteza-server/pkg/eventbus" @@ -42,9 +44,11 @@ type ( // Acceptable content type ContentType string `json:"ct,omitempty"` + Path string `json:"pt,omitempty"` + // Should we put signature in the path (true) // or in query string (false, default) - SignatureInPath bool `json:"-"` + SignatureInPath bool `json:"sip,omitempty"` } sinkEventDispatcher interface { @@ -55,7 +59,14 @@ type ( const ( SinkContentTypeMail = "message/rfc822" - SinkSignUrlParamName = "__sign" + // base url + // we're using this for router, signature... + SinkBaseURL = "/sink" + + // name of the parameter used for sink request signature + SinkSignUrlParamName = "__sign" + + // delimiter between signature and payload SinkSignUrlParamDelimiter = "_" ) @@ -72,27 +83,28 @@ func Sink() *sink { // // With signed URL, external systems can make requests to sink subsystem // and trigger scripts -func (svc sink) SignURL(surp SinkRequestUrlParams) (signedURL *url.URL, err error) { +func (svc sink) SignURL(srup SinkRequestUrlParams) (signedURL *url.URL, out SinkRequestUrlParams, err error) { var ( params []byte - sap = &sinkActionProps{sinkParams: &surp} - - path = svc.GetPath() - qs = url.Values{} + sap = &sinkActionProps{sinkParams: &srup} + qs = url.Values{} ) err = func() error { + // Append normalized path to the base URL + srup.Path = svc.pathCleanup(srup.Path) + path := svc.GetPath() + srup.Path - params, err = json.Marshal(surp) + srup.Method = strings.ToUpper(srup.Method) + + params, err = json.Marshal(srup) if err != nil { return SinkErrFailedToSign(sap).Wrap(err) } - surp.Method = strings.ToUpper(surp.Method) - signature := svc.signer.Sign(0, params) + SinkSignUrlParamDelimiter + base64.StdEncoding.EncodeToString(params) - if surp.SignatureInPath { + if srup.SignatureInPath { // Optional, use path for sink signature path = fmt.Sprintf("%s/%s=%s", path, SinkSignUrlParamName, signature) } else { @@ -104,7 +116,7 @@ func (svc sink) SignURL(surp SinkRequestUrlParams) (signedURL *url.URL, err erro return nil }() - return signedURL, svc.recordAction(context.Background(), sap, SinkActionSign, err) + return signedURL, srup, svc.recordAction(context.Background(), sap, SinkActionSign, err) } func (svc sink) GetPath() string { @@ -114,7 +126,20 @@ func (svc sink) GetPath() string { path = "/system" } - return path + "/sink" + return path + SinkBaseURL +} + +// pathCleanup removes base URL prefix and adds leading slash +func (svc sink) pathCleanup(p string) string { + if len(p) > 0 { + if strings.HasPrefix(p, SinkBaseURL) { + p = p[len(SinkBaseURL):] + } + + return "/" + strings.TrimLeft(p, "/") + } + + return "" } // ProcessRequest function is used directly in the HTTP controller @@ -161,6 +186,8 @@ func (svc sink) handleRequest(r *http.Request) (*SinkRequestUrlParams, error) { sap = &sinkActionProps{} qs = r.URL.Query() + signatureFoundInPath bool + param string ) @@ -174,7 +201,7 @@ func (svc sink) handleRequest(r *http.Request) (*SinkRequestUrlParams, error) { param = r.URL.Path[i+len(SinkSignUrlParamName)+1:] // this is more for consistency and cleaner tests - srup.SignatureInPath = true + signatureFoundInPath = true } if len(param) == 0 { @@ -203,6 +230,11 @@ func (svc sink) handleRequest(r *http.Request) (*SinkRequestUrlParams, error) { sap.setSinkParams(srup) + if srup.SignatureInPath != signatureFoundInPath { + spew.Dump(srup, r.URL, signatureFoundInPath) + return nil, SinkErrMisplacedSignature(sap) + } + if srup.Method != "" && srup.Method != r.Method { return nil, SinkErrInvalidHttpMethod(sap) } @@ -218,6 +250,12 @@ func (svc sink) handleRequest(r *http.Request) (*SinkRequestUrlParams, error) { } } + if srup.Path != "" { + if srup.Path != svc.pathCleanup(r.URL.Path) { + return nil, SinkErrInvalidPath(sap) + } + } + if srup.Expires != nil && srup.Expires.Before(time.Now()) { return nil, SinkErrSignatureExpired(sap) } @@ -277,13 +315,17 @@ func (svc *sink) process(contentType string, w http.ResponseWriter, r *http.Requ } ) - // Sanitize URL by removing sink sign url param + // Sanitize URL sanitizedURL := r.URL + + // Step 1: removing sink sign url param sanitizedQuery := r.URL.Query() sanitizedQuery.Del(SinkSignUrlParamName) sanitizedURL.RawQuery = sanitizedQuery.Encode() - if strings.HasPrefix(sanitizedURL.Path, svc.GetPath()) { - sanitizedURL.Path = sanitizedURL.Path[len(svc.GetPath()):] + + // Step 2: remove prefix + if i := strings.Index(sanitizedURL.Path, SinkBaseURL); i > -1 { + sanitizedURL.Path = sanitizedURL.Path[i+len(SinkBaseURL):] } r.URL = sanitizedURL diff --git a/system/service/sink_actions.gen.go b/system/service/sink_actions.gen.go index 750238078..2d228f0dc 100644 --- a/system/service/sink_actions.gen.go +++ b/system/service/sink_actions.gen.go @@ -766,6 +766,70 @@ func SinkErrInvalidContentType(props ...*sinkActionProps) *sinkError { } +// SinkErrInvalidPath returns "system:sink.invalidPath" audit event as actionlog.Alert +// +// +// This function is auto-generated. +// +func SinkErrInvalidPath(props ...*sinkActionProps) *sinkError { + var e = &sinkError{ + timestamp: time.Now(), + resource: "system:sink", + error: "invalidPath", + action: "error", + message: "invalid path", + log: "invalid path", + severity: actionlog.Alert, + props: func() *sinkActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + + httpStatusCode: http.StatusUnauthorized, + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// SinkErrMisplacedSignature returns "system:sink.misplacedSignature" audit event as actionlog.Alert +// +// +// This function is auto-generated. +// +func SinkErrMisplacedSignature(props ...*sinkActionProps) *sinkError { + var e = &sinkError{ + timestamp: time.Now(), + resource: "system:sink", + error: "misplacedSignature", + action: "error", + message: "signature misplaced", + log: "signature misplaced", + severity: actionlog.Alert, + props: func() *sinkActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + + httpStatusCode: http.StatusBadRequest, + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + // SinkErrSignatureExpired returns "system:sink.signatureExpired" audit event as actionlog.Alert // // diff --git a/system/service/sink_actions.yaml b/system/service/sink_actions.yaml index f44eba482..57758420f 100644 --- a/system/service/sink_actions.yaml +++ b/system/service/sink_actions.yaml @@ -79,6 +79,14 @@ errors: message: "invalid content-type header" httpStatus: StatusUnauthorized + - error: invalidPath + message: "invalid path" + httpStatus: StatusUnauthorized + + - error: misplacedSignature + message: "signature misplaced" + httpStatus: StatusBadRequest + - error: signatureExpired message: "signature expired" httpStatus: StatusGone diff --git a/system/service/sink_test.go b/system/service/sink_test.go index ea351412c..ad083b49d 100644 --- a/system/service/sink_test.go +++ b/system/service/sink_test.go @@ -35,7 +35,7 @@ func Test_sink_SignURL(t *testing.T) { wantSignedURL: "/sink?__sign=d8a8c5591acb0f5f6695ab6aa4a205a7066b3bf4_eyJtdGQiOiJQT1NUIiwib3JpZ2luIjoidGVzdCIsIm1icyI6MTAyNCwiY3QiOiJwbGFpbi90ZXh0In0%3D", }, { - name: "basic", + name: "signature in a path", surp: SinkRequestUrlParams{ Method: "POST", Origin: "test", @@ -44,7 +44,14 @@ func Test_sink_SignURL(t *testing.T) { ContentType: "plain/text", SignatureInPath: true, }, - wantSignedURL: "/sink/__sign=d8a8c5591acb0f5f6695ab6aa4a205a7066b3bf4_eyJtdGQiOiJQT1NUIiwib3JpZ2luIjoidGVzdCIsIm1icyI6MTAyNCwiY3QiOiJwbGFpbi90ZXh0In0=", + wantSignedURL: "/sink/__sign=a4a5652c66159ed0142c01a9aa5d90b3e9f76241_eyJtdGQiOiJQT1NUIiwib3JpZ2luIjoidGVzdCIsIm1icyI6MTAyNCwiY3QiOiJwbGFpbi90ZXh0Iiwic2lwIjp0cnVlfQ==", + }, + { + name: "required path", + surp: SinkRequestUrlParams{ + Path: "/foo/bar", + }, + wantSignedURL: "/sink/foo/bar?__sign=910436a3944ad1ee010db07b936209f198be481d_eyJwdCI6Ii9mb28vYmFyIn0%3D", }, } ) @@ -55,7 +62,7 @@ func Test_sink_SignURL(t *testing.T) { signer: signer, } - gotSignedURL, err := svc.SignURL(tt.surp) + gotSignedURL, _, err := svc.SignURL(tt.surp) if (err != nil) != tt.wantErr { t.Errorf("SignURL() \n"+ " error: %v, \n"+ @@ -83,16 +90,19 @@ func Test_sink_handleRequest(t *testing.T) { MaxBodySize: 1024, ContentType: "plain/text", } - signedUrl, _ = svc.SignURL(signParams) + signedUrl, _, _ = svc.SignURL(signParams) - signParamsNoPref = SinkRequestUrlParams{} - signedUrlNoPref, _ = svc.SignURL(signParamsNoPref) + signParamsNoPref = SinkRequestUrlParams{} + signedUrlNoPref, _, _ = svc.SignURL(signParamsNoPref) - signParamsExp = SinkRequestUrlParams{Expires: &time.Time{}} - signedUrlExp, _ = svc.SignURL(signParamsExp) + signParamsExp = SinkRequestUrlParams{Expires: &time.Time{}} + signedUrlExp, _, _ = svc.SignURL(signParamsExp) - signParamsPath = SinkRequestUrlParams{SignatureInPath: true} - signedUrlPath, _ = svc.SignURL(signParamsPath) + signParamsInPath = SinkRequestUrlParams{SignatureInPath: true} + signedUrlInPath, _, _ = svc.SignURL(signParamsInPath) + + signParamsFixedPath = SinkRequestUrlParams{Path: "/foo/bar"} + signedUrlFixedPath, _, _ = svc.SignURL(signParamsFixedPath) ) var ( @@ -169,15 +179,23 @@ func Test_sink_handleRequest(t *testing.T) { }, { name: "signature in a path", - withMethod: "POST", - withURL: signedUrlPath.String(), - wantParams: &signParamsPath, + withURL: signedUrlInPath.String(), + wantParams: &signParamsInPath, + }, + { + name: "signed fixed path", + withURL: signedUrlFixedPath.String(), + wantParams: &signParamsFixedPath, }, } ) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + if tt.withMethod == "" { + tt.withMethod = "GET" + } + req, err := http.NewRequest(tt.withMethod, tt.withURL, tt.withBody) if err != nil { panic(err) @@ -191,11 +209,15 @@ func Test_sink_handleRequest(t *testing.T) { got, err := svc.handleRequest(req) if !errors.Is(err, tt.wantErr) { - t.Errorf("handleRequest() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("handleRequest()\n"+ + " error: %v\n"+ + "wantErr: %v", err, tt.wantErr) return } if !reflect.DeepEqual(got, tt.wantParams) { - t.Errorf("handleRequest() params = %v, want %v", got, tt.wantParams) + t.Errorf("handleRequest()\n"+ + "params: %v\n"+ + " want: %v", got, tt.wantParams) } }) }