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}) }