3
0

Add path constraint, improve CLI

This commit is contained in:
Denis Arh
2020-05-27 16:26:57 +02:00
parent f69ffe01d3
commit f4213e5a7f
6 changed files with 208 additions and 37 deletions
+39 -4
View File
@@ -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(
+1 -1
View File
@@ -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
View File
@@ -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
+64
View File
@@ -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
//
//
+8
View File
@@ -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
View File
@@ -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)
}
})
}