filer: hide write controls in the UI on the read-only port (#12429)

* filer: hide write controls in the UI on the read-only port

The read-only port accepts only GET and HEAD, but its directory page
offered New Folder, Upload, drag-and-drop, rename and delete, each of
which failed with 405. Requests that come through the read-only handler
now render the page without those controls or the upload script.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* filer: test the directory page controls on each port

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* filer: match the rendered row buttons in the port test

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Chris LuandClaude Opus 5.5 authored and GitHub committed 2026-11-05 10:11:52 -08:00
1 parent e44c16802d
commit 878979af70
4 files changed
+84 -1

No files matched your search

+1 -1
View File
@@ -194,7 +194,7 @@ func (fs *FilerServer) readonlyFilerHandler(w http.ResponseWriter, r *http.Reque
switch r.Method {
case http.MethodGet, http.MethodHead:
fs.GetOrHeadHandler(w, r)
fs.GetOrHeadHandler(w, r.WithContext(context.WithValue(r.Context(), readOnlyListingKey{}, true)))
default:
requestMethod = "INVALID"
w.WriteHeader(http.StatusMethodNotAllowed)
@@ -13,6 +13,10 @@ import (
"github.com/seaweedfs/seaweedfs/weed/util/version"
)
// readOnlyListingKey marks a request served on the read-only port, whose
// listing page must not offer write operations.
type readOnlyListingKey struct{}
// listDirectoryHandler lists directories and folders under a directory
// files are sorted by name and paginated via "lastFileName" and "limit".
// sub directories are listed on the first page, when "lastFileName"
@@ -91,6 +95,7 @@ func (fs *FilerServer) listDirectoryHandler(w http.ResponseWriter, r *http.Reque
ShouldDisplayLoadMore bool
EmptyFolder bool
ShowDirectoryDelete bool
ReadOnly bool
}{
version.Version(),
path,
@@ -101,6 +106,7 @@ func (fs *FilerServer) listDirectoryHandler(w http.ResponseWriter, r *http.Reque
shouldDisplayLoadMore,
emptyFolder,
fs.option.ShowUIDirectoryDelete,
ctx.Value(readOnlyListingKey{}) != nil,
})
if err != nil {
glog.V(0).InfofCtx(ctx, "Template Execute Error: %v", err)
@@ -0,0 +1,69 @@
package weed_server
import (
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"time"
"github.com/seaweedfs/seaweedfs/weed/filer"
"github.com/seaweedfs/seaweedfs/weed/security"
)
func TestListDirectoryWriteControlsFollowPort(t *testing.T) {
mtime := time.Unix(1700000000, 0)
store := newRenameTestStore()
store.entries["/docs"] = &filer.Entry{FullPath: "/docs", Attr: filer.Attr{Mtime: mtime, Mode: os.ModeDir | 0755}}
store.entries["/docs/sub"] = &filer.Entry{FullPath: "/docs/sub", Attr: filer.Attr{Mtime: mtime, Mode: os.ModeDir | 0755}}
store.entries["/docs/a.txt"] = &filer.Entry{FullPath: "/docs/a.txt", Attr: filer.Attr{Mtime: mtime, Mode: 0644}}
store.entries["/empty"] = &filer.Entry{FullPath: "/empty", Attr: filer.Attr{Mtime: mtime, Mode: os.ModeDir | 0755}}
fs := &FilerServer{
filer: newRenameTestFiler(t, store),
option: &FilerOption{ExposeDirectoryData: true, DirListingLimit: 100, ShowUIDirectoryDelete: true},
filerGuard: security.NewGuard(nil, "", 0, "", 0),
}
writeControls := []string{
`onclick="handleCreateDir()"`,
`for="fileElem"`,
`for="dirElem"`,
`onclick="handleRename(`,
`onclick="handleDelete(`,
`class="row add-files"`,
"function uploadFile(",
}
render := func(t *testing.T, handler http.HandlerFunc, path string) string {
t.Helper()
rec := httptest.NewRecorder()
handler(rec, httptest.NewRequest(http.MethodGet, path, nil))
if rec.Code != http.StatusOK {
t.Fatalf("GET %s: status %d, body %s", path, rec.Code, rec.Body.String())
}
return rec.Body.String()
}
for _, path := range []string{"/docs/", "/empty/"} {
readOnly := render(t, fs.readonlyFilerHandler, path)
if !strings.Contains(readOnly, "<ol class=\"breadcrumb\">") {
t.Fatalf("read-only listing of %s is not the directory page:\n%s", path, readOnly)
}
for _, control := range writeControls {
if strings.Contains(readOnly, control) {
t.Errorf("read-only listing of %s offers %q", path, control)
}
}
}
if readOnly := render(t, fs.readonlyFilerHandler, "/docs/"); !strings.Contains(readOnly, "a.txt") || !strings.Contains(readOnly, "sub") {
t.Errorf("read-only listing lost its entries:\n%s", readOnly)
}
writable := render(t, fs.GetOrHeadHandler, "/docs/") + render(t, fs.GetOrHeadHandler, "/empty/")
for _, control := range writeControls {
if !strings.Contains(writable, control) {
t.Errorf("listing on the main port is missing %q", control)
}
}
}
+8
View File
@@ -87,6 +87,7 @@
</div>
<div class="row">
<div>
{{ if not .ReadOnly }}
<div class="btn-group btn-group-sm pull-right" role="group" style="margin-top:3px;">
<label class="btn btn-default" onclick="handleCreateDir()">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> New Folder
@@ -98,6 +99,7 @@
<span class="glyphicon glyphicon-folder-close" aria-hidden="true"></span> Upload Folder
</label>
</div>
{{ end }}
<ol class="breadcrumb">
{{ range $entry := .Breadcrumbs }}
<li><a href="{{ printpath $entry.Link }}">
@@ -114,9 +116,11 @@
<input type="file" id="dirElem" webkitdirectory multiple onchange="handleFiles(this.files)">
{{ if .EmptyFolder }}
{{ if not .ReadOnly }}
<div class="row add-files">
+
</div>
{{ end }}
{{ else }}
<table width="100%" class="table table-hover">
{{ $path := .Path }}
@@ -148,6 +152,7 @@
<td align="right" nowrap>
{{ $entry.Timestamp.Format "2006-01-02 15:04" }}
</td>
{{ if not $.ReadOnly }}
<td style="width:75px">
<div class="btn-group btn-group-xs pull-right operations" role="group">
<label class="btn" onclick="handleRename('{{ $entry.Name }}', '{{ printpath $path "/" }}')">
@@ -165,6 +170,7 @@
{{ end }}
</div>
</td>
{{ end }}
</tr>
{{ end }}
</table>
@@ -186,6 +192,7 @@
</div>
</div>
</body>
{{ if not .ReadOnly }}
<script type="text/javascript">
// ************************ Drag and drop ***************** //
let dropArea = document.getElementById("drop-area");
@@ -442,4 +449,5 @@
reloadPage();
}
</script>
{{ end }}
</html>