Add path constraint, improve CLI
This commit is contained in:
+39
-4
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
+59
-17
@@ -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
|
||||
|
||||
@@ -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
|
||||
//
|
||||
//
|
||||
|
||||
@@ -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
|
||||
|
||||
+37
-15
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user