- jz.bm.realtime-access admits holders of jz.bm.access_materials
(Assumption A5), so every editor can follow materials:{id}
- README documents the materials channel, event and authorizer
126 lines
3.8 KiB
Go
126 lines
3.8 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"
|
|
"git.golem15.com/jakub/sm-bm-plugin/controllers/api"
|
|
)
|
|
|
|
// MiddlewareRealtimeAccess guards the realtime connection-token route: a
|
|
// token is minted only for someone with a realtime consumer, a chat-group
|
|
// member, a material manager (jz.bm.access_materials, for the live revision
|
|
// heads-up of the materials they edit) or a presentation owner. It runs
|
|
// after jwt.auth. The token only opens the socket: every channel is still
|
|
// authorized by its namespace's authorizer.
|
|
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 or a material manager 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
|
|
}
|
|
}
|
|
isManager := p.materialManager
|
|
if isManager == nil {
|
|
isManager = api.ManagesMaterials
|
|
}
|
|
manager, err := isManager(r.Context(), db, user.ID)
|
|
if err != nil {
|
|
writeRealtimeGuard(w, http.StatusServiceUnavailable, "Service unavailable")
|
|
return
|
|
}
|
|
if manager {
|
|
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})
|
|
}
|