Phase 1.5 — auth layer (Redis sessions, allowlist, requireAuth)
Some checks failed
Docker Build / build-and-push-image (push) Failing after 18s
Some checks failed
Docker Build / build-and-push-image (push) Failing after 18s
Adds an authentication layer in front of the bot handlers : - Auth handler on the principal bot (@arcodange_factory_bot, slug factory) parses /start, /auth <code>, /whoami, /logout. On a successful /auth, the message containing the code is best-effort deleted from the user's chat (replay defense). - Redis-backed sessions (key tg-gw:auth:<from.id>, TTL 24h, configurable via AUTH_SESSION_TTL). Constant-time secret compare via crypto/subtle. - ALLOWED_USERS env (CSV of Telegram user IDs) — silent-drops anyone not in the list before the auth gate runs. - New per-bot field 'requireAuth' (pointer-bool). Default = true (secure by default). Auto-forced to false for handler=auth (chicken-and-egg). - Server gates: allowlist first, then requireAuth before handler dispatch. - Fail-at-startup if a bot is configured with handler=auth or requireAuth: true while AUTH_SECRET is unset. Design: factory/docs/adr/20260509-telegram-gateway-auth.md (in factory PR). User docs: AUTH.md (new), HOWTO_ADD_BOT.md (Cas 2 updated for default true and gated flow). New deps: github.com/redis/go-redis/v9. Refs ~/.claude/plans/pour-les-notifications-on-inherited-seal.md § Phase 1.5.
This commit is contained in:
55
allowlist.go
Normal file
55
allowlist.go
Normal file
@@ -0,0 +1,55 @@
|
||||
// Voir factory/docs/adr/20260509-telegram-gateway-auth.md
|
||||
package main
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Allowlist filters incoming Telegram updates by `from.id`. Empty raw value
|
||||
// means "open to all" (back-compat with Phase 1). When non-empty, only IDs
|
||||
// in the set are allowed through ; everything else is silently dropped
|
||||
// upstream of the auth gate (so unknown users don't even know the bot exists).
|
||||
type Allowlist struct {
|
||||
ids map[int64]struct{}
|
||||
open bool
|
||||
}
|
||||
|
||||
// NewAllowlist parses a comma-separated list of int64 user IDs. Whitespace
|
||||
// around items and empty items are ignored. Invalid items are skipped (with
|
||||
// the caller responsible for logging if it cares).
|
||||
func NewAllowlist(raw string) Allowlist {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return Allowlist{open: true}
|
||||
}
|
||||
ids := make(map[int64]struct{})
|
||||
for _, s := range strings.Split(raw, ",") {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
continue
|
||||
}
|
||||
id, err := strconv.ParseInt(s, 10, 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
ids[id] = struct{}{}
|
||||
}
|
||||
if len(ids) == 0 {
|
||||
// CSV provided but every entry was unparseable → treat as closed for safety.
|
||||
return Allowlist{ids: ids, open: false}
|
||||
}
|
||||
return Allowlist{ids: ids, open: false}
|
||||
}
|
||||
|
||||
func (a Allowlist) IsAllowed(userID int64) bool {
|
||||
if a.open {
|
||||
return true
|
||||
}
|
||||
_, ok := a.ids[userID]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (a Allowlist) Open() bool { return a.open }
|
||||
|
||||
func (a Allowlist) Size() int { return len(a.ids) }
|
||||
Reference in New Issue
Block a user