jz.bm.realtime-access admits chat-group members and, through golem15.presentations' owner guard, presentation owners; everyone else gets 401, 403 or 503 with Cache-Control no-cache, private. /_chat, /_chatchannels and /_chatbots are reserved for the APIs.
126 lines
4.6 KiB
Go
126 lines
4.6 KiB
Go
package bm
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"git.golem15.com/golem15/summercms/modules/backpack"
|
|
"git.golem15.com/golem15/summercms/modules/bouncer"
|
|
"git.golem15.com/golem15/summercms/modules/compass"
|
|
"git.golem15.com/golem15/summercms/modules/pact"
|
|
"git.golem15.com/golem15/summercms/modules/party"
|
|
"git.golem15.com/golem15/summercms/modules/wire"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// fakePresentations stands in for golem15.presentations: its owner guard
|
|
// lets user 5 through and refuses everyone else.
|
|
type fakePresentations struct{}
|
|
|
|
func (fakePresentations) ID() string { return "golem15.presentations" }
|
|
func (fakePresentations) Requires() []string { return nil }
|
|
func (fakePresentations) Register(*backpack.App) error { return nil }
|
|
func (fakePresentations) Boot(*backpack.App) error { return nil }
|
|
func (fakePresentations) Middlewares() map[string]pact.Middleware {
|
|
return map[string]pact.Middleware{
|
|
"presentations.realtime-owner": func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if u, ok := bouncer.User(r.Context()); !ok || u.ID != 5 {
|
|
w.Header().Set("Cache-Control", "no-cache, private")
|
|
wire.WriteJSON(w, http.StatusForbidden, map[string]string{"error": "Forbidden"})
|
|
return
|
|
}
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
},
|
|
}
|
|
}
|
|
|
|
// chatGroupMember is the injected group check: user 7 is in the chat
|
|
// group, user 13 makes the check fail.
|
|
func chatGroupMember(_ context.Context, _ *gorm.DB, userID uint, group string) (bool, error) {
|
|
if userID == 13 {
|
|
return false, errors.New("database down")
|
|
}
|
|
return group == "studenci" && userID == 7, nil
|
|
}
|
|
|
|
// accessPlugin builds jz.bm on an app with the given active plugin ids, a
|
|
// published (fake) database unless noDB, and golem15.presentations in the
|
|
// activated list when withOwnerGuard.
|
|
func accessPlugin(t *testing.T, ids []string, noDB, withOwnerGuard bool) *Plugin {
|
|
t.Helper()
|
|
cfg, err := compass.Open(compass.Options{Dir: t.TempDir(), Environ: []string{"SUMMER_ENV=testing"}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
app := backpack.New(cfg)
|
|
app.SetPlugins(ids)
|
|
if !noDB {
|
|
if err := app.Publish[*gorm.DB](&gorm.DB{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
activated := party.Activated{}
|
|
if withOwnerGuard {
|
|
activated = append(activated, fakePresentations{})
|
|
}
|
|
if err := app.Publish(activated); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return &Plugin{app: app, chatMember: chatGroupMember}
|
|
}
|
|
|
|
func TestRealtimeAccess(t *testing.T) {
|
|
chatOnly := []string{"golem15.user", "golem15.chat", "jz.bm"}
|
|
both := []string{"golem15.user", "golem15.chat", "jz.bm", "golem15.presentations"}
|
|
presOnly := []string{"golem15.user", "jz.bm", "golem15.presentations"}
|
|
for _, c := range []struct {
|
|
name string
|
|
ids []string
|
|
noDB, guard bool
|
|
userID uint
|
|
want int
|
|
}{
|
|
{"no user", both, false, true, 0, http.StatusUnauthorized},
|
|
{"chat member", chatOnly, false, false, 7, http.StatusOK},
|
|
{"chat member with presentations", both, false, true, 7, http.StatusOK},
|
|
{"non-member without presentations", chatOnly, false, false, 8, http.StatusForbidden},
|
|
{"presentation owner", both, false, true, 5, http.StatusOK},
|
|
{"presentation owner without chat", presOnly, false, true, 5, http.StatusOK},
|
|
{"neither member nor owner", both, false, true, 8, http.StatusForbidden},
|
|
{"presentations guard not found", both, false, false, 5, http.StatusForbidden},
|
|
{"no database", both, true, true, 7, http.StatusServiceUnavailable},
|
|
{"membership check fails", both, false, true, 13, http.StatusServiceUnavailable},
|
|
{"no consumer", []string{"golem15.user", "jz.bm"}, false, false, 7, http.StatusForbidden},
|
|
} {
|
|
p := accessPlugin(t, c.ids, c.noDB, c.guard)
|
|
mw, ok := p.Middlewares()[MiddlewareRealtimeAccess]
|
|
if !ok {
|
|
t.Fatalf("%s is not exported", MiddlewareRealtimeAccess)
|
|
}
|
|
h := mw(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) }))
|
|
req := httptest.NewRequest(http.MethodGet, "/api/realtime/token", nil)
|
|
if c.userID != 0 {
|
|
req = req.WithContext(bouncer.WithUser(req.Context(), &bouncer.Principal{ID: c.userID}))
|
|
}
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, req)
|
|
if rec.Code != c.want {
|
|
t.Errorf("%s: %d %s, want %d", c.name, rec.Code, rec.Body.String(), c.want)
|
|
continue
|
|
}
|
|
if c.want != http.StatusOK {
|
|
if got := rec.Header().Get("Cache-Control"); got != "no-cache, private" {
|
|
t.Errorf("%s: Cache-Control %q, want no-cache, private", c.name, got)
|
|
}
|
|
if body := rec.Body.String(); body == "" || body[0] != '{' {
|
|
t.Errorf("%s: body %q is not an error object", c.name, body)
|
|
}
|
|
}
|
|
}
|
|
}
|