Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 51 additions & 28 deletions protogen/gen/opencloud/messages/policies/v0/policies.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions protogen/gen/opencloud/services/policies/v0/policies.swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
},
Expand Down
4 changes: 4 additions & 0 deletions protogen/proto/opencloud/messages/policies/v0/policies.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
25 changes: 25 additions & 0 deletions services/policies/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
8 changes: 8 additions & 0 deletions services/policies/pkg/engine/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
19 changes: 19 additions & 0 deletions services/policies/pkg/engine/engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)))
})
})
23 changes: 23 additions & 0 deletions services/policies/pkg/engine/opa/engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
})
})
})
9 changes: 9 additions & 0 deletions services/proxy/pkg/middleware/policies.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"net/http"
"path"
"path/filepath"
"strconv"
"strings"
"time"

Expand Down Expand Up @@ -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
Expand Down
46 changes: 46 additions & 0 deletions services/proxy/pkg/middleware/policies_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"

gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
Expand Down Expand Up @@ -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)

Expand Down
Loading