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 }