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
This commit is contained in:
111
modules/lighthouse/registry.go
Normal file
111
modules/lighthouse/registry.go
Normal file
@@ -0,0 +1,111 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user