- viaMCP, includeBody and the material views (no body unless asked) - jz.bm.mcp-links: absolute url / *_url under MCP only - jz.bm.manage keyed per user for dispatched calls - bm_material and bm_materials in the pinned tool list Compile-only stubs; the tests fail on their assertions.
109 lines
3.3 KiB
Go
109 lines
3.3 KiB
Go
package bm
|
|
|
|
import (
|
|
"net/http"
|
|
"sync"
|
|
|
|
"git.golem15.com/golem15/summercms/modules/bouncer"
|
|
"git.golem15.com/golem15/summercms/modules/pact"
|
|
"git.golem15.com/golem15/summercms/modules/party"
|
|
"git.golem15.com/golem15/summercms/modules/wire"
|
|
"git.golem15.com/jakub/sm-bm-plugin/classes"
|
|
)
|
|
|
|
// MiddlewareRealtimeAccess guards the realtime connection-token route: a
|
|
// token is minted only for someone with a realtime consumer, a chat-group
|
|
// member or a presentation owner. It runs after jwt.auth.
|
|
const MiddlewareRealtimeAccess = "jz.bm.realtime-access"
|
|
|
|
// presentationsOwnerGuard is golem15.presentations' owner guard, named
|
|
// here without importing that plugin.
|
|
const presentationsOwnerGuard = "presentations.realtime-owner"
|
|
|
|
// Middlewares exports the realtime access guard and the MCP link rewriter.
|
|
func (p *Plugin) Middlewares() map[string]pact.Middleware {
|
|
return map[string]pact.Middleware{
|
|
MiddlewareRealtimeAccess: p.realtimeAccess,
|
|
MiddlewareMCPLinks: p.mcpLinks,
|
|
}
|
|
}
|
|
|
|
// realtimeAccess lets a chat-group member through, otherwise, when
|
|
// golem15.presentations is active, whoever its owner guard lets through
|
|
// (it answers its own refusals), and refuses everyone else. The owner guard
|
|
// is looked up once, by name, among the activated plugins. With BM's own
|
|
// Centrifugo instance the tokens it mints are valid for BM only.
|
|
func (p *Plugin) realtimeAccess(next http.Handler) http.Handler {
|
|
var once sync.Once
|
|
var owner http.Handler
|
|
ownerGuard := func() http.Handler {
|
|
once.Do(func() {
|
|
if mw := p.presentationsMiddleware(presentationsOwnerGuard); mw != nil {
|
|
owner = mw(next)
|
|
}
|
|
})
|
|
return owner
|
|
}
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
user, ok := bouncer.User(r.Context())
|
|
if !ok || user == nil || user.ID == 0 {
|
|
writeRealtimeGuard(w, http.StatusUnauthorized, "Unauthorized")
|
|
return
|
|
}
|
|
db := p.database()
|
|
if db == nil {
|
|
writeRealtimeGuard(w, http.StatusServiceUnavailable, "Service unavailable")
|
|
return
|
|
}
|
|
if p.app.HasPlugin("golem15.chat") {
|
|
isMember := p.chatMember
|
|
if isMember == nil {
|
|
isMember = classes.IsChatMember
|
|
}
|
|
member, err := isMember(r.Context(), db, user.ID, p.chatGroup())
|
|
if err != nil {
|
|
writeRealtimeGuard(w, http.StatusServiceUnavailable, "Service unavailable")
|
|
return
|
|
}
|
|
if member {
|
|
next.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
}
|
|
if p.app.HasPlugin("golem15.presentations") {
|
|
if h := ownerGuard(); h != nil {
|
|
h.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
}
|
|
writeRealtimeGuard(w, http.StatusForbidden, "Forbidden")
|
|
})
|
|
}
|
|
|
|
// presentationsMiddleware returns the named middleware of the activated
|
|
// golem15.presentations plugin, or nil.
|
|
func (p *Plugin) presentationsMiddleware(name string) pact.Middleware {
|
|
if p.app == nil {
|
|
return nil
|
|
}
|
|
activated, ok := p.app.Lookup[party.Activated]()
|
|
if !ok {
|
|
return nil
|
|
}
|
|
for _, pl := range activated {
|
|
if pl.ID() != "golem15.presentations" {
|
|
continue
|
|
}
|
|
if hm, ok := pl.(pact.HasMiddleware); ok {
|
|
return hm.Middlewares()[name]
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// writeRealtimeGuard answers in the token route's own error shape.
|
|
func writeRealtimeGuard(w http.ResponseWriter, status int, message string) {
|
|
w.Header().Set("Cache-Control", "no-cache, private")
|
|
wire.WriteJSON(w, status, map[string]string{"error": message})
|
|
}
|