test: lock down HTTP and WebDAV permission matrix
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"chfs-ng/internal/auth"
|
||||
"chfs-ng/internal/storage"
|
||||
)
|
||||
|
||||
func permissionTestServer(t *testing.T, mask auth.PermMask) (*Server, string) {
|
||||
t.Helper()
|
||||
root := t.TempDir()
|
||||
if err := os.MkdirAll(filepath.Join(root, "dir"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(root, "file.txt"), []byte("hello"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(root, "dir", "nested.txt"), []byte("nested"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s := &Server{
|
||||
Store: storage.Local{Root: root},
|
||||
Auth: &auth.Manager{
|
||||
Accounts: map[string]*auth.Account{},
|
||||
Guest: &auth.Account{Mask: mask},
|
||||
},
|
||||
RemoveMode: 3,
|
||||
ImagePreview: true,
|
||||
}
|
||||
return s, root
|
||||
}
|
||||
|
||||
func doRequest(h http.Handler, method, target string, body io.Reader, contentType string) *httptest.ResponseRecorder {
|
||||
req := httptest.NewRequest(method, target, body)
|
||||
if contentType != "" {
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
}
|
||||
rr := httptest.NewRecorder()
|
||||
h.ServeHTTP(rr, req)
|
||||
return rr
|
||||
}
|
||||
|
||||
func multipartField(t *testing.T, field, value string) (*bytes.Buffer, string) {
|
||||
t.Helper()
|
||||
var b bytes.Buffer
|
||||
mw := multipart.NewWriter(&b)
|
||||
if err := mw.WriteField(field, value); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := mw.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return &b, mw.FormDataContentType()
|
||||
}
|
||||
|
||||
func multipartUpload(t *testing.T, name, data string) (*bytes.Buffer, string) {
|
||||
t.Helper()
|
||||
var b bytes.Buffer
|
||||
mw := multipart.NewWriter(&b)
|
||||
fw, err := mw.CreateFormFile("file", name)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := io.WriteString(fw, data); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := mw.WriteField("folder", ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := mw.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return &b, mw.FormDataContentType()
|
||||
}
|
||||
|
||||
func TestHTTPPermissionMatrix(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
mask auth.PermMask
|
||||
read int
|
||||
write int
|
||||
del int
|
||||
}{
|
||||
{"none", auth.None, http.StatusForbidden, http.StatusForbidden, http.StatusForbidden},
|
||||
{"read", auth.Read, http.StatusOK, http.StatusForbidden, http.StatusForbidden},
|
||||
{"read-write", auth.Read | auth.Write, http.StatusOK, http.StatusCreated, http.StatusForbidden},
|
||||
{"read-write-delete", auth.Read | auth.Write | auth.Delete, http.StatusOK, http.StatusCreated, http.StatusNoContent},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
s, root := permissionTestServer(t, tc.mask)
|
||||
h := s.Handler()
|
||||
|
||||
files := doRequest(h, http.MethodGet, "/chfs/files?filepath=", nil, "")
|
||||
if files.Code != http.StatusOK {
|
||||
t.Fatalf("files: got %d", files.Code)
|
||||
}
|
||||
|
||||
body, ct := multipartUpload(t, "uploaded.txt", "upload")
|
||||
upload := doRequest(h, http.MethodPost, "/chfs/upload", body, ct)
|
||||
if upload.Code != tc.write {
|
||||
t.Errorf("upload: got %d want %d", upload.Code, tc.write)
|
||||
}
|
||||
_, _ = os.Stat(filepath.Join(root, "uploaded.txt"))
|
||||
|
||||
body, ct = multipartField(t, "filepath", "newdir")
|
||||
newdir := doRequest(h, http.MethodPost, "/chfs/newdir", body, ct)
|
||||
if newdir.Code != tc.write {
|
||||
t.Errorf("newdir: got %d want %d", newdir.Code, tc.write)
|
||||
}
|
||||
|
||||
body, ct = multipartField(t, "old", "file.txt")
|
||||
body2, _ := multipartField(t, "new", "renamed.txt")
|
||||
var renameBody bytes.Buffer
|
||||
renameBody.Write(body.Bytes())
|
||||
renameBody.Write(body2.Bytes())
|
||||
// Rebuild the combined multipart request because each helper owns its boundary.
|
||||
var rb bytes.Buffer
|
||||
mw := multipart.NewWriter(&rb)
|
||||
_ = mw.WriteField("old", "file.txt")
|
||||
_ = mw.WriteField("new", "renamed.txt")
|
||||
_ = mw.Close()
|
||||
_ = body
|
||||
_ = ct
|
||||
rename := doRequest(h, http.MethodPost, "/chfs/rename", &rb, mw.FormDataContentType())
|
||||
if rename.Code != tc.write {
|
||||
t.Errorf("rename: got %d want %d", rename.Code, tc.write)
|
||||
}
|
||||
|
||||
deleteBody, deleteCT := multipartField(t, "filepath", "dir/nested.txt")
|
||||
del := doRequest(h, http.MethodDelete, "/chfs/rmfiles", deleteBody, deleteCT)
|
||||
if del.Code != tc.del {
|
||||
t.Errorf("delete: got %d want %d", del.Code, tc.del)
|
||||
}
|
||||
|
||||
zip := doRequest(h, http.MethodGet, "/chfs/downloaddir/dir", nil, "")
|
||||
if zip.Code != tc.read {
|
||||
t.Errorf("zip: got %d want %d", zip.Code, tc.read)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"chfs-ng/internal/auth"
|
||||
"chfs-ng/internal/storage"
|
||||
)
|
||||
|
||||
func webDAVMatrixServer(t *testing.T, mask auth.PermMask) (*Server, string) {
|
||||
t.Helper()
|
||||
root := t.TempDir()
|
||||
for _, name := range []string{"src-copy.txt", "src-move.txt", "src-delete.txt"} {
|
||||
if err := os.WriteFile(filepath.Join(root, name), []byte("src"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
return &Server{
|
||||
Store: storage.Local{Root: root},
|
||||
Auth: &auth.Manager{
|
||||
Accounts: map[string]*auth.Account{},
|
||||
Guest: &auth.Account{Mask: mask},
|
||||
},
|
||||
RemoveMode: 3,
|
||||
WebDAVRoot: root,
|
||||
}, root
|
||||
}
|
||||
|
||||
func webDAVRequest(h http.Handler, method, target string, body []byte, headers map[string]string) int {
|
||||
req := httptest.NewRequest(method, target, bytes.NewReader(body))
|
||||
for k, v := range headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
rr := httptest.NewRecorder()
|
||||
h.ServeHTTP(rr, req)
|
||||
return rr.Code
|
||||
}
|
||||
|
||||
func TestWebDAVPermissionMatrix(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
mask auth.PermMask
|
||||
read int
|
||||
write int
|
||||
del int
|
||||
copy int
|
||||
move int
|
||||
}{
|
||||
{"none", auth.None, http.StatusForbidden, http.StatusForbidden, http.StatusForbidden, http.StatusForbidden, http.StatusForbidden},
|
||||
{"read", auth.Read, http.StatusMultiStatus, http.StatusForbidden, http.StatusForbidden, http.StatusForbidden, http.StatusForbidden},
|
||||
{"read-write", auth.Read | auth.Write, http.StatusMultiStatus, http.StatusCreated, http.StatusForbidden, http.StatusCreated, http.StatusCreated},
|
||||
{"read-write-delete", auth.Read | auth.Write | auth.Delete, http.StatusMultiStatus, http.StatusCreated, http.StatusNoContent, http.StatusCreated, http.StatusCreated},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
s, root := webDAVMatrixServer(t, tc.mask)
|
||||
h := s.Handler()
|
||||
|
||||
propf := webDAVRequest(h, "PROPFIND", "/webdav/", nil, map[string]string{"Depth": "1"})
|
||||
if propf != tc.read {
|
||||
t.Errorf("PROPFIND: got %d want %d", propf, tc.read)
|
||||
}
|
||||
get := webDAVRequest(h, http.MethodGet, "/webdav/src-copy.txt", nil, nil)
|
||||
if get != func() int {
|
||||
if tc.mask.HasRead() {
|
||||
return http.StatusOK
|
||||
}
|
||||
return http.StatusForbidden
|
||||
}() {
|
||||
want := http.StatusForbidden
|
||||
if tc.mask.HasRead() {
|
||||
want = http.StatusOK
|
||||
}
|
||||
t.Errorf("GET: got %d want %d", get, want)
|
||||
}
|
||||
|
||||
put := webDAVRequest(h, http.MethodPut, "/webdav/new.txt", []byte("new"), nil)
|
||||
if put != tc.write {
|
||||
t.Errorf("PUT: got %d want %d", put, tc.write)
|
||||
}
|
||||
|
||||
mkcol := webDAVRequest(h, "MKCOL", "/webdav/newdir", nil, nil)
|
||||
if mkcol != tc.write {
|
||||
t.Errorf("MKCOL: got %d want %d", mkcol, tc.write)
|
||||
}
|
||||
|
||||
del := webDAVRequest(h, http.MethodDelete, "/webdav/src-delete.txt", nil, nil)
|
||||
if del != tc.del {
|
||||
t.Errorf("DELETE: got %d want %d", del, tc.del)
|
||||
}
|
||||
|
||||
copy := webDAVRequest(h, "COPY", "/webdav/src-copy.txt", nil, map[string]string{"Destination": "/webdav/copied.txt"})
|
||||
if copy != tc.copy {
|
||||
t.Errorf("COPY: got %d want %d", copy, tc.copy)
|
||||
}
|
||||
|
||||
move := webDAVRequest(h, "MOVE", "/webdav/src-move.txt", nil, map[string]string{"Destination": "/webdav/moved.txt"})
|
||||
if move != tc.move {
|
||||
t.Errorf("MOVE: got %d want %d", move, tc.move)
|
||||
}
|
||||
|
||||
if tc.write == http.StatusCreated {
|
||||
if _, err := os.Stat(filepath.Join(root, "new.txt")); tc.name != "none" && err != nil {
|
||||
t.Errorf("PUT file missing: %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user