diff --git a/protogen/gen/opencloud/messages/policies/v0/policies.pb.go b/protogen/gen/opencloud/messages/policies/v0/policies.pb.go index 2d95ac1574..95021478e4 100644 --- a/protogen/gen/opencloud/messages/policies/v0/policies.pb.go +++ b/protogen/gen/opencloud/messages/policies/v0/policies.pb.go @@ -223,6 +223,10 @@ type Request struct { Method string `protobuf:"bytes,1,opt,name=method,proto3" json:"method,omitempty"` Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"` + // Content-Length of the request body, 0 if unknown + ContentLength uint64 `protobuf:"varint,3,opt,name=content_length,json=contentLength,proto3" json:"content_length,omitempty"` + // TUS Upload-Length header, the declared total size of an upload, 0 if absent + UploadLength uint64 `protobuf:"varint,4,opt,name=upload_length,json=uploadLength,proto3" json:"upload_length,omitempty"` } func (x *Request) Reset() { @@ -271,6 +275,20 @@ func (x *Request) GetPath() string { return "" } +func (x *Request) GetContentLength() uint64 { + if x != nil { + return x.ContentLength + } + return 0 +} + +func (x *Request) GetUploadLength() uint64 { + if x != nil { + return x.UploadLength + } + return 0 +} + type Environment struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -487,35 +505,40 @@ var file_opencloud_messages_policies_v0_policies_proto_rawDesc = []byte{ 0x75, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6f, 0x70, 0x61, 0x71, 0x75, 0x65, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x70, 0x61, 0x63, 0x65, 0x49, 0x64, - 0x22, 0x35, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x6d, - 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6d, 0x65, 0x74, - 0x68, 0x6f, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x22, 0x8d, 0x02, 0x0a, 0x0b, 0x45, 0x6e, 0x76, 0x69, - 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x3b, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x67, 0x65, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x25, 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, - 0x75, 0x64, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x70, 0x6f, 0x6c, 0x69, - 0x63, 0x69, 0x65, 0x73, 0x2e, 0x76, 0x30, 0x2e, 0x53, 0x74, 0x61, 0x67, 0x65, 0x52, 0x05, 0x73, - 0x74, 0x61, 0x67, 0x65, 0x12, 0x38, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x6d, - 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x69, 0x65, 0x73, - 0x2e, 0x76, 0x30, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x41, - 0x0a, 0x07, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x27, 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x6d, 0x65, 0x73, 0x73, - 0x61, 0x67, 0x65, 0x73, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x69, 0x65, 0x73, 0x2e, 0x76, 0x30, - 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x07, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x44, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, + 0x22, 0x81, 0x01, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, + 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6d, 0x65, + 0x74, 0x68, 0x6f, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x25, 0x0a, 0x0e, 0x63, 0x6f, 0x6e, 0x74, + 0x65, 0x6e, 0x74, 0x5f, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, + 0x52, 0x0d, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x4c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x12, + 0x23, 0x0a, 0x0d, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x5f, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0c, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x4c, 0x65, + 0x6e, 0x67, 0x74, 0x68, 0x22, 0x8d, 0x02, 0x0a, 0x0b, 0x45, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, + 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x3b, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0e, 0x32, 0x25, 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x69, 0x65, - 0x73, 0x2e, 0x76, 0x30, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x08, 0x72, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2a, 0x25, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x67, 0x65, - 0x12, 0x0c, 0x0a, 0x08, 0x53, 0x54, 0x41, 0x47, 0x45, 0x5f, 0x50, 0x50, 0x10, 0x00, 0x12, 0x0e, - 0x0a, 0x0a, 0x53, 0x54, 0x41, 0x47, 0x45, 0x5f, 0x48, 0x54, 0x54, 0x50, 0x10, 0x01, 0x42, 0x4f, - 0x5a, 0x4d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, - 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2d, 0x65, 0x75, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, - 0x6f, 0x75, 0x64, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x65, 0x6e, - 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x6d, 0x65, 0x73, 0x73, 0x61, - 0x67, 0x65, 0x73, 0x2f, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x69, 0x65, 0x73, 0x2f, 0x76, 0x30, 0x62, - 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x73, 0x2e, 0x76, 0x30, 0x2e, 0x53, 0x74, 0x61, 0x67, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, 0x67, + 0x65, 0x12, 0x38, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x24, 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x6d, 0x65, 0x73, 0x73, + 0x61, 0x67, 0x65, 0x73, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x69, 0x65, 0x73, 0x2e, 0x76, 0x30, + 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x41, 0x0a, 0x07, 0x72, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x6f, + 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + 0x73, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x69, 0x65, 0x73, 0x2e, 0x76, 0x30, 0x2e, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x07, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x44, + 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x28, 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x6d, 0x65, 0x73, + 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x69, 0x65, 0x73, 0x2e, 0x76, + 0x30, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x2a, 0x25, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x67, 0x65, 0x12, 0x0c, 0x0a, + 0x08, 0x53, 0x54, 0x41, 0x47, 0x45, 0x5f, 0x50, 0x50, 0x10, 0x00, 0x12, 0x0e, 0x0a, 0x0a, 0x53, + 0x54, 0x41, 0x47, 0x45, 0x5f, 0x48, 0x54, 0x54, 0x50, 0x10, 0x01, 0x42, 0x4f, 0x5a, 0x4d, 0x67, + 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, + 0x6f, 0x75, 0x64, 0x2d, 0x65, 0x75, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, + 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x6f, 0x70, + 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, + 0x2f, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x69, 0x65, 0x73, 0x2f, 0x76, 0x30, 0x62, 0x06, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/protogen/gen/opencloud/services/policies/v0/policies.swagger.json b/protogen/gen/opencloud/services/policies/v0/policies.swagger.json index 7b514d5346..6e4f432931 100644 --- a/protogen/gen/opencloud/services/policies/v0/policies.swagger.json +++ b/protogen/gen/opencloud/services/policies/v0/policies.swagger.json @@ -134,6 +134,16 @@ }, "path": { "type": "string" + }, + "contentLength": { + "type": "string", + "format": "uint64", + "title": "Content-Length of the request body, 0 if unknown" + }, + "uploadLength": { + "type": "string", + "format": "uint64", + "title": "TUS Upload-Length header, the declared total size of an upload, 0 if absent" } } }, diff --git a/protogen/proto/opencloud/messages/policies/v0/policies.proto b/protogen/proto/opencloud/messages/policies/v0/policies.proto index 2d49a02d3c..2da19ac1f6 100644 --- a/protogen/proto/opencloud/messages/policies/v0/policies.proto +++ b/protogen/proto/opencloud/messages/policies/v0/policies.proto @@ -32,6 +32,10 @@ message Resource { message Request { string method = 1; string path = 2; + // Content-Length of the request body, 0 if unknown + uint64 content_length = 3; + // TUS Upload-Length header, the declared total size of an upload, 0 if absent + uint64 upload_length = 4; } enum Stage { diff --git a/services/policies/README.md b/services/policies/README.md index 8d210022f9..bcc3cafd45 100644 --- a/services/policies/README.md +++ b/services/policies/README.md @@ -185,6 +185,31 @@ To identify available keys for OPA, you need to look at [`engine.go`](https://gi Note that which keys are available depends on the module it is used in. +### Request Lengths + +In the proxy module, `input.request.content_length` contains the `Content-Length` of the request body and `input.request.upload_length` the total size of an upload as declared by the TUS `Upload-Length` header. Both are `0` if unknown. They allow rejecting uploads that are too big before any data is transferred: + +```rego +package proxy + +import future.keywords.if + +default granted := true + +# TUS creation declares the total size of the upload +granted = false if { + input.request.upload_length > 10737418240 +} + +# a plain PUT carries the whole file, a PATCH a chunk +granted = false if { + input.request.method == ["PUT", "PATCH"][_] + input.request.content_length > 10737418240 +} +``` + +This is only an early check. The TUS concatenation extension can merge several smaller uploads into one file, so the actual file size has to be checked in postprocessing using `input.resource.size`. + ## Rego Extensions Besides the standard rego built-in functions, the following functions are added on top: diff --git a/services/policies/pkg/engine/engine.go b/services/policies/pkg/engine/engine.go index c40a81ac97..923afa049f 100644 --- a/services/policies/pkg/engine/engine.go +++ b/services/policies/pkg/engine/engine.go @@ -40,6 +40,10 @@ type Resource struct { type Request struct { Method string `json:"method"` Path string `json:"path"` + // ContentLength is the length of the request body, 0 if unknown. + ContentLength uint64 `json:"content_length"` + // UploadLength is the total size of an upload as declared by the TUS Upload-Length header, 0 if absent. + UploadLength uint64 `json:"upload_length"` } // Environment contains every data that is needed to decide if the request should pass or not @@ -63,6 +67,10 @@ func NewEnvironmentFromPB(pEnv *v0.Environment) (Environment, error) { return env, err } + // protojson encodes uint64 as strings with camelCase keys, so copy the lengths explicitly + env.Request.ContentLength = pEnv.GetRequest().GetContentLength() + env.Request.UploadLength = pEnv.GetRequest().GetUploadLength() + switch pEnv.Stage { case v0.Stage_STAGE_HTTP: env.Stage = StageHTTP diff --git a/services/policies/pkg/engine/engine_test.go b/services/policies/pkg/engine/engine_test.go index ea3095d418..dab6caf9c0 100644 --- a/services/policies/pkg/engine/engine_test.go +++ b/services/policies/pkg/engine/engine_test.go @@ -22,4 +22,23 @@ var _ = Describe("Engine", func() { Entry("http stage", pMessage.Stage_STAGE_HTTP, engine.StageHTTP), Entry("pp stage", pMessage.Stage_STAGE_PP, engine.StagePP), ) + + It("NewEnvironmentFromPB keeps the request lengths", func() { + pEnv := &pMessage.Environment{ + Stage: pMessage.Stage_STAGE_HTTP, + Request: &pMessage.Request{ + Method: "POST", + Path: "/remote.php/dav/spaces/some-space-id", + ContentLength: 1024, + UploadLength: 20000000000, + }, + } + + env, err := engine.NewEnvironmentFromPB(pEnv) + Expect(err).ToNot(HaveOccurred()) + + Expect(env.Request.Method).To(Equal("POST")) + Expect(env.Request.ContentLength).To(Equal(uint64(1024))) + Expect(env.Request.UploadLength).To(Equal(uint64(20000000000))) + }) }) diff --git a/services/policies/pkg/engine/opa/engine_test.go b/services/policies/pkg/engine/opa/engine_test.go index c7e33d330e..2d731acc56 100644 --- a/services/policies/pkg/engine/opa/engine_test.go +++ b/services/policies/pkg/engine/opa/engine_test.go @@ -150,5 +150,28 @@ var _ = Describe("engine", func() { Expect(granted).To(Equal(tc.want), "for %s", tc.name) } }) + + It("compares the request lengths as numbers", func() { + e, _ := newEngine("package size\n\nimport future.keywords.if\n\ndefault granted := true\n\ngranted = false if {\n input.request.upload_length > 10737418240\n}\n\ngranted = false if {\n input.request.method == [\"PUT\", \"PATCH\"][_]\n input.request.content_length > 10737418240\n}\n") + + for _, tc := range []struct { + name string + request engine.Request + want bool + }{ + {"no lengths", engine.Request{Method: "POST"}, true}, + {"tus creation below limit", engine.Request{Method: "POST", UploadLength: 10737418240}, true}, + {"tus creation above limit", engine.Request{Method: "POST", UploadLength: 20000000000}, false}, + {"put above limit", engine.Request{Method: "PUT", ContentLength: 20000000000}, false}, + {"patch above limit", engine.Request{Method: "PATCH", ContentLength: 20000000000}, false}, + {"patch below limit", engine.Request{Method: "PATCH", ContentLength: 104857600}, true}, + } { + granted, err := e.Evaluate(context.Background(), "data.size.granted", engine.Environment{ + Request: tc.request, + }) + Expect(err).ToNot(HaveOccurred()) + Expect(granted).To(Equal(tc.want), "for %s", tc.name) + } + }) }) }) diff --git a/services/proxy/pkg/middleware/policies.go b/services/proxy/pkg/middleware/policies.go index 04aec89714..8e06c60f3a 100644 --- a/services/proxy/pkg/middleware/policies.go +++ b/services/proxy/pkg/middleware/policies.go @@ -5,6 +5,7 @@ import ( "net/http" "path" "path/filepath" + "strconv" "strings" "time" @@ -72,6 +73,14 @@ func Policies(qs string, opts ...Option) func(next http.Handler) http.Handler { }, } + // the declared lengths allow policies to reject uploads before the data is transferred + if r.ContentLength > 0 { + req.Environment.Request.ContentLength = uint64(r.ContentLength) + } + if uploadLength, err := strconv.ParseUint(r.Header.Get(net.HeaderUploadLength), 10, 64); err == nil { + req.Environment.Request.UploadLength = uploadLength + } + resource := &pMessage.Resource{} // tus diff --git a/services/proxy/pkg/middleware/policies_test.go b/services/proxy/pkg/middleware/policies_test.go index 363d7ff9d1..176b127408 100644 --- a/services/proxy/pkg/middleware/policies_test.go +++ b/services/proxy/pkg/middleware/policies_test.go @@ -8,6 +8,7 @@ import ( "io" "net/http" "net/http/httptest" + "strings" "testing" gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" @@ -109,6 +110,51 @@ func TestPolicies_EvaluationEnvironment_Request(t *testing.T) { policiesMiddleware.ServeHTTP(responseRecorder, httptest.NewRequest(http.MethodDelete, "/whatever", nil)) } +func TestPolicies_EvaluationEnvironment_RequestLengths(t *testing.T) { + tests := []struct { + name string + method string + body string + uploadLength string + expectedContentLength uint64 + expectedUploadLength uint64 + }{ + {name: "tus creation", method: http.MethodPost, uploadLength: "20000000000", expectedUploadLength: 20000000000}, + {name: "put", method: http.MethodPut, body: "some content", expectedContentLength: 12}, + {name: "patch", method: http.MethodPatch, body: "a chunk", expectedContentLength: 7}, + {name: "invalid upload length", method: http.MethodPost, uploadLength: "-1"}, + {name: "no lengths", method: http.MethodGet}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var g = NewWithT(t) + + policiesMiddleware, policiesProviderService, _ := prepare("any") + policiesProviderService.On("Evaluate", mock.Anything, mock.Anything, mock.Anything).Return( + func(_ context.Context, in *policiesPG.EvaluateRequest, _ ...client.CallOption) (*policiesPG.EvaluateResponse, error) { + g.Expect(in.Environment.Request.ContentLength).To(Equal(tt.expectedContentLength)) + g.Expect(in.Environment.Request.UploadLength).To(Equal(tt.expectedUploadLength)) + + return &policiesPG.EvaluateResponse{Result: false}, nil + }, + ).Once() + + var body io.Reader + if tt.body != "" { + body = strings.NewReader(tt.body) + } + request := httptest.NewRequest(tt.method, "/remote.php/dav/spaces/some-space-id/file.txt", body) + if tt.uploadLength != "" { + request.Header.Set(net.HeaderUploadLength, tt.uploadLength) + } + + policiesMiddleware.ServeHTTP(httptest.NewRecorder(), request) + policiesProviderService.AssertExpectations(t) + }) + } +} + func TestPolicies_EvaluationEnvironment_Resource(t *testing.T) { var g = NewWithT(t)