Files
summercms/modules/lighthouse/registry.go
Jakub Zych 79fd705680 feat(11-03): re-authorize every Centrifugo subscribe through a namespace registry
- lighthouse: Registry of namespace authorizers (Result, Allowed, Denied),
  ParseChannel, ChannelID with PHP (int)-cast semantics (PHPInt, pinned by
  a php -r table test), FormatChannels, WithClientID/ClientID
- centrifugo: ProxyHandler (constant-time X-Centrifugo-Secret, HTTP 200
  generic deny, info [] on allow, presence allow/override merge, 64 KiB
  body cap) mounted as the ServerToServer subscribe route
- README: proxy contract, registry and channel rules
2026-09-30 12:29:09 +02:00

112 lines
3.2 KiB
Go

package lighthouse
import (
"context"
"fmt"
"sort"
"strings"
"sync"
)
// Result is an authorizer's subscribe decision. Info, Capabilities and
// Overrides are passed to the realtime server on an allow; nil means "use
// the driver default". The deny reason is for logs only and never reaches
// the client.
type Result struct {
Allowed bool
Info map[string]any
Capabilities []string
Overrides map[string]any
reason string
}
// Reason returns the internal deny reason ("" for an allow).
func (r Result) Reason() string { return r.reason }
// Allowed returns an allow with info (nil for none).
func Allowed(info map[string]any) Result {
return Result{Allowed: true, Info: info}
}
// Denied returns a deny with an internal reason for the logs.
func Denied(reason string) Result {
return Result{reason: reason}
}
// Authorizer decides whether userID may subscribe to channel. It is called
// on every subscribe with the full original channel, including any
// "presence:" prefix, and must check current state (no caching). The
// driver's client id is available through ClientID(ctx).
type Authorizer interface {
Authorize(ctx context.Context, userID uint, channel string) Result
}
// AuthorizerFunc adapts a function to Authorizer.
type AuthorizerFunc func(ctx context.Context, userID uint, channel string) Result
// Authorize calls f.
func (f AuthorizerFunc) Authorize(ctx context.Context, userID uint, channel string) Result {
return f(ctx, userID, channel)
}
// Registry maps channel namespaces to authorizers. It is safe for
// concurrent use.
type Registry struct {
mu sync.RWMutex
authorizers map[string]Authorizer
}
// NewRegistry returns an empty registry.
func NewRegistry() *Registry {
return &Registry{authorizers: map[string]Authorizer{}}
}
// Register adds the authorizer of namespace. An empty namespace, one that
// contains ":", a nil authorizer, or a namespace registered twice is an
// error (unlike the WinterCMS registry, a second registration does not
// silently replace the first).
func (r *Registry) Register(namespace string, a Authorizer) error {
if namespace == "" {
return fmt.Errorf("lighthouse: authorizer namespace is empty")
}
if strings.Contains(namespace, ":") {
return fmt.Errorf("lighthouse: authorizer namespace %q contains \":\"", namespace)
}
if a == nil {
return fmt.Errorf("lighthouse: authorizer for namespace %q is nil", namespace)
}
r.mu.Lock()
defer r.mu.Unlock()
if _, dup := r.authorizers[namespace]; dup {
return fmt.Errorf("lighthouse: authorizer namespace %q is already registered", namespace)
}
r.authorizers[namespace] = a
return nil
}
// Get returns the authorizer of namespace. The lookup is byte-exact.
func (r *Registry) Get(namespace string) (Authorizer, bool) {
if r == nil {
return nil, false
}
r.mu.RLock()
defer r.mu.RUnlock()
a, ok := r.authorizers[namespace]
return a, ok
}
// Namespaces returns the registered namespaces, sorted.
func (r *Registry) Namespaces() []string {
if r == nil {
return nil
}
r.mu.RLock()
defer r.mu.RUnlock()
out := make([]string, 0, len(r.authorizers))
for ns := range r.authorizers {
out = append(out, ns)
}
sort.Strings(out)
return out
}