The connection-token route is guarded by jz.bm.realtime-access instead of presentations.realtime-owner: chat-group members get a token, and when golem15.presentations is active so does everyone its owner guard lets through (looked up by name among the activated plugins, no import). Everyone else gets 401, 403 or 503 in the guard's error shape. The realtime routes mount when golem15.chat or golem15.presentations is active. /_chat, /_chatchannels and /_chatbots are reserved prefixes of the student app.
106 lines
3.2 KiB
Go
106 lines
3.2 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.
|
|
func (p *Plugin) Middlewares() map[string]pact.Middleware {
|
|
return map[string]pact.Middleware{MiddlewareRealtimeAccess: p.realtimeAccess}
|
|
}
|
|
|
|
// 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})
|
|
}
|