test: expect signed one-time material uploads
- TestUploadTicket: HMAC under the jz.bm.upload.v1: label, expiry, malformed and foreign tickets, single use through the cache - TestSignedMaterialUploadRefusesBadTickets, TestCreateMaterialUploadNeedsCaller - TestRoutes, TestBMMCP and TestManageBucketKey expect the upload-url route, the uploads route with its two buckets and the upload tool - compile-only stubs
This commit is contained in:
@@ -14,6 +14,7 @@ import (
|
||||
chatclasses "git.golem15.com/golem15/sm-chat-plugin/classes"
|
||||
chanclasses "git.golem15.com/golem15/sm-chatchannels-plugin/classes"
|
||||
"git.golem15.com/golem15/summercms/modules/bouncer"
|
||||
"git.golem15.com/golem15/summercms/modules/cooler"
|
||||
"git.golem15.com/golem15/summercms/modules/phrasebook"
|
||||
"git.golem15.com/golem15/summercms/modules/wire"
|
||||
"gocloud.dev/blob"
|
||||
@@ -66,6 +67,12 @@ type Handlers struct {
|
||||
// ChatBots is whether golem15.chatbots is active, so its is_bot flags
|
||||
// can be read.
|
||||
ChatBots bool
|
||||
// UploadKey returns the key the signed material upload URLs are signed
|
||||
// with (app.key), or nil when none is set: then no URL is issued.
|
||||
UploadKey func() []byte
|
||||
// Cache returns the application cache that holds the issued upload
|
||||
// tickets until they are used, or nil while none is available.
|
||||
Cache func() cooler.Cache
|
||||
}
|
||||
|
||||
func (h *Handlers) now() time.Time {
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"git.golem15.com/golem15/summercms/modules/lagoon"
|
||||
"git.golem15.com/golem15/summercms/modules/lagoon/attach"
|
||||
"git.golem15.com/golem15/summercms/modules/wire"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -140,3 +141,16 @@ func (h *Handlers) AttachMaterialInline(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
h.writeMaterialDetailWith(w, r, db, m.ID, http.StatusCreated, map[string]any{"warning": MaterialFileWarning})
|
||||
}
|
||||
|
||||
// UploadPath is the path of the signed upload route.
|
||||
const UploadPath = "/_bm/api/v1/uploads/"
|
||||
|
||||
// CreateMaterialUpload is not implemented yet.
|
||||
func (h *Handlers) CreateMaterialUpload(w http.ResponseWriter, r *http.Request) {
|
||||
wire.WriteJSON(w, http.StatusNotImplemented, map[string]any{"error": true})
|
||||
}
|
||||
|
||||
// SignedMaterialUpload is not implemented yet.
|
||||
func (h *Handlers) SignedMaterialUpload(w http.ResponseWriter, r *http.Request) {
|
||||
wire.WriteJSON(w, http.StatusNotImplemented, map[string]any{"error": true})
|
||||
}
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
package api_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.golem15.com/golem15/summercms/modules/bouncer"
|
||||
"git.golem15.com/golem15/summercms/modules/cooler"
|
||||
"git.golem15.com/jakub/sm-bm-plugin/classes"
|
||||
"git.golem15.com/jakub/sm-bm-plugin/controllers/api"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@@ -88,3 +92,78 @@ func TestAttachMaterialInlineNoDatabase(t *testing.T) {
|
||||
t.Error("the database was looked up for an invalid body")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSignedMaterialUploadRefusesBadTickets (D-12): a malformed, forged,
|
||||
// never issued, expired, wrong-kind or used ticket gets the same 403 before
|
||||
// the database is looked up.
|
||||
func TestSignedMaterialUploadRefusesBadTickets(t *testing.T) {
|
||||
key := []byte("app-key")
|
||||
cache, err := cooler.NewMemoryCache("test:", 100)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
dbLooked := false
|
||||
h := &api.Handlers{
|
||||
DB: func() *gorm.DB { dbLooked = true; return nil },
|
||||
UploadKey: func() []byte { return key },
|
||||
Cache: func() cooler.Cache { return cache },
|
||||
}
|
||||
now := time.Now()
|
||||
forged, _, err := classes.IssueUpload(context.Background(), cache, []byte("other-key"), "material", 1, 7, now)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
unissued, err := classes.SignUpload(key, classes.UploadTicket{Kind: "material", Target: 1, User: 7, Expires: now.Add(time.Minute), Nonce: strings.Repeat("0f", 16)})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
expired, _, err := classes.IssueUpload(context.Background(), cache, key, "material", 1, 7, now.Add(-classes.UploadTTL-time.Second))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
deck, _, err := classes.IssueUpload(context.Background(), cache, key, "deck", 1, 7, now)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
refused := map[string]string{
|
||||
"empty": "",
|
||||
"garbage": "not-a-ticket",
|
||||
"two parts": "abc.def",
|
||||
"forged": forged,
|
||||
"unissued": unissued,
|
||||
"expired": expired,
|
||||
"wrong kind": deck,
|
||||
}
|
||||
for name, ticket := range refused {
|
||||
code, body := serve(t, h.SignedMaterialUpload, http.MethodPost, "/", nil, map[string]string{"ticket": ticket})
|
||||
if code != http.StatusForbidden || body["error"] != true || body["message"] != "Invalid or expired upload URL." {
|
||||
t.Errorf("%s: %d %v, want 403 Invalid or expired upload URL.", name, code, body)
|
||||
}
|
||||
}
|
||||
if dbLooked {
|
||||
t.Error("the database was looked up for a refused ticket")
|
||||
}
|
||||
|
||||
valid, _, err := classes.IssueUpload(context.Background(), cache, key, "material", 1, 7, now)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if code, body := serve(t, h.SignedMaterialUpload, http.MethodPost, "/", nil, map[string]string{"ticket": valid}); code != http.StatusServiceUnavailable {
|
||||
t.Errorf("valid ticket without a database = %d %v, want 503", code, body)
|
||||
}
|
||||
if code, body := serve(t, h.SignedMaterialUpload, http.MethodPost, "/", nil, map[string]string{"ticket": valid}); code != http.StatusForbidden {
|
||||
t.Errorf("used ticket = %d %v, want 403", code, body)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCreateMaterialUploadNeedsCaller: no signed-in user is a 401; a caller
|
||||
// without a database is a 503.
|
||||
func TestCreateMaterialUploadNeedsCaller(t *testing.T) {
|
||||
h := noDB()
|
||||
if code, body := serve(t, h.CreateMaterialUpload, http.MethodPost, "/", nil, map[string]string{"id": "1"}); code != http.StatusUnauthorized || body["error"] != true {
|
||||
t.Errorf("without a caller = %d %v, want 401", code, body)
|
||||
}
|
||||
if code, body := serve(t, h.CreateMaterialUpload, http.MethodPost, "/", &bouncer.Principal{ID: 7}, map[string]string{"id": "1"}); code != http.StatusServiceUnavailable {
|
||||
t.Errorf("without a database = %d %v, want 503", code, body)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user