tinyrelayfile
tinyrelay/internal/daemon/member_file_access_test.go/

internal/daemon/member_file_access_test.go raw
1package daemon
2
3import (
4 "context"
5 "encoding/json"
6 "net/http"
7 "net/http/httptest"
8 "strings"
9 "testing"
10)
11
12func TestMemberFileAccessAcrossHTTPAndBrowseSurfaces(t *testing.T) {
13 h := newRoomHarness(t)
14 ctx := context.Background()
15 const body = "members-only file bytes"
16 request := httptest.NewRequest(http.MethodPut, "http://relay.test/upload?access=members&filename=private.txt", strings.NewReader(body))
17 request.Header.Set("Content-Type", "text/plain")
18 signRequestWithSecret(t, request, body, h.secrets["alice"])
19 uploaded := httptest.NewRecorder()
20 h.tenant.ServeHTTP(uploaded, request)
21 if uploaded.Code != http.StatusOK {
22 t.Fatalf("member upload = %d: %s", uploaded.Code, uploaded.Body.String())
23 }
24 var descriptor struct {
25 Hash string `json:"sha256"`
26 }
27 if err := json.Unmarshal(uploaded.Body.Bytes(), &descriptor); err != nil || len(descriptor.Hash) != 64 {
28 t.Fatalf("upload descriptor = %s: %v", uploaded.Body.String(), err)
29 }
30
31 if rows, err := h.browse("bob", "browsefiles", map[string]any{"view": "library", "limit": 50}); err != nil {
32 t.Fatalf("member library: %v", err)
33 } else if !libraryContainsHash(rows, descriptor.Hash, "private.txt") {
34 t.Fatalf("member library omitted private file: %v", rows)
35 }
36 if rows, err := h.browse("stranger", "browsefiles", map[string]any{"view": "library", "limit": 50}); err != nil {
37 t.Fatalf("outsider library: %v", err)
38 } else if libraryContainsHash(rows, descriptor.Hash, "private.txt") {
39 t.Fatalf("outsider library exposed private file: %v", rows)
40 }
41
42 for _, route := range []string{"/" + descriptor.Hash, "/nip96/" + descriptor.Hash, "/files/raw?hash=" + descriptor.Hash, "/media/" + descriptor.Hash} {
43 for _, method := range []string{http.MethodGet, http.MethodHead} {
44 for _, actor := range []string{"owner", "bob"} {
45 t.Run(method+" "+actor+" "+route, func(t *testing.T) {
46 response := memberFileRequest(t, h, method, route, actor)
47 if response.Code != http.StatusOK {
48 t.Fatalf("%s response = %d: %s", actor, response.Code, response.Body.String())
49 }
50 if method == http.MethodGet && response.Body.String() != body {
51 t.Fatalf("%s body = %q", actor, response.Body.String())
52 }
53 })
54 }
55 t.Run(method+" guest "+route, func(t *testing.T) {
56 response := memberFileRequest(t, h, method, route, "")
57 if response.Code != http.StatusUnauthorized && response.Code != http.StatusForbidden {
58 t.Fatalf("guest response = %d: %s", response.Code, response.Body.String())
59 }
60 })
61 t.Run(method+" outsider "+route, func(t *testing.T) {
62 response := memberFileRequest(t, h, method, route, "stranger")
63 if response.Code != http.StatusForbidden {
64 t.Fatalf("outsider response = %d: %s", response.Code, response.Body.String())
65 }
66 })
67 }
68 }
69
70 for _, actor := range []string{"bob", "stranger"} {
71 request := httptest.NewRequest(http.MethodGet, "http://relay.test/file?hash="+descriptor.Hash, nil)
72 if actor != "" {
73 signRequestWithSecret(t, request, "", h.secrets[actor])
74 }
75 response := httptest.NewRecorder()
76 h.tenant.ServeHTTP(response, request)
77 page := response.Body.String()
78 if actor == "bob" {
79 if response.Code != http.StatusOK || !strings.Contains(page, "private.txt") {
80 t.Fatalf("member file page = %d: %s", response.Code, page)
81 }
82 } else if strings.Contains(page, "private.txt") || strings.Contains(page, body) {
83 t.Fatalf("outsider file page leaked private data: %d: %s", response.Code, page)
84 }
85 }
86
87 result, err := h.tenant.Execute(ctx, h.keys["bob"], "browsefile", []json.RawMessage{rawJSON(map[string]any{"hash": descriptor.Hash})})
88 if err != nil || !strings.Contains(string(rawJSON(result)), "private.txt") {
89 t.Fatalf("member browsefile = %v, %v", result, err)
90 }
91 if _, err := h.tenant.Execute(ctx, h.keys["stranger"], "browsefile", []json.RawMessage{rawJSON(map[string]any{"hash": descriptor.Hash})}); err == nil {
92 t.Fatal("outsider browsefile exposed private file")
93 }
94
95 if _, err := h.tenant.Execute(ctx, h.keys["owner"], "removemember", []json.RawMessage{rawJSON(h.keys["bob"])}); err != nil {
96 t.Fatalf("remove member: %v", err)
97 }
98 if response := memberFileRequest(t, h, http.MethodGet, "/files/raw?hash="+descriptor.Hash+"&check=revoked", "bob"); response.Code != http.StatusForbidden {
99 t.Fatalf("revoked member response = %d: %s", response.Code, response.Body.String())
100 }
101 if _, err := h.tenant.Execute(ctx, h.keys["owner"], "setmember", []json.RawMessage{rawJSON(h.keys["bob"]), rawJSON(map[string]any{"role": "member"})}); err != nil {
102 t.Fatalf("restore member: %v", err)
103 }
104 if _, err := h.tenant.Execute(ctx, h.keys["owner"], "banpubkey", []json.RawMessage{rawJSON(h.keys["bob"]), rawJSON("test")}); err != nil {
105 t.Fatalf("ban member: %v", err)
106 }
107 if response := memberFileRequest(t, h, http.MethodGet, "/files/raw?hash="+descriptor.Hash+"&check=banned", "bob"); response.Code != http.StatusForbidden {
108 t.Fatalf("banned member response = %d: %s", response.Code, response.Body.String())
109 }
110}
111
112func memberFileRequest(t *testing.T, h *roomHarness, method, route, actor string) *httptest.ResponseRecorder {
113 t.Helper()
114 request := httptest.NewRequest(method, "http://relay.test"+route, nil)
115 if actor != "" {
116 signRequestWithSecret(t, request, "", h.secrets[actor])
117 }
118 response := httptest.NewRecorder()
119 h.tenant.ServeHTTP(response, request)
120 return response
121}
122
123func libraryContainsHash(rows map[string]any, hash, name string) bool {
124 items, ok := rows["items"].([]any)
125 if !ok {
126 return false
127 }
128 for _, raw := range items {
129 item, ok := raw.(map[string]any)
130 if ok && item["sha256"] == hash && item["name"] == name {
131 return true
132 }
133 }
134 return false
135}

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

  • https://012.run/npub1cq2s86qrtadf36eqq3epm38wmhdynrrcwxf84eldk3f94dkv7ttsrulrz5/tinyrelay.git

Refs