release gate / check (push) Failing after 6m43s
High severity: - wire TRUSTED_PROXY through Compose/deploy/docs and warn at web startup - bucket public rate limits on effective client IP, add per-bookmark budget, and forward the signed browser identity from the share page - disable first-run bootstrap in server deployments and document seeding - serialize last-admin demotion in one advisory-locked transaction Medium/low: - cap and batch share recipient inserts - add a shared outbound budget for preview/CardDAV/IMAP/signal work - pin CardDAV dials to 443 and bound IMAP logout - fix rightward kanban column drags and normalize cards on column delete - classify backend outages in auth guards, add request timeouts and an error boundary, remove the dead login route, surface throttling - enforce pinned/unpinned page reorder grouping server-side - avoid preview work for stale bookmark versions; drop duplicate dialog fetch - sequence-guard collection/tag/calendar/quick-find loads - validate property config and fix MULTI_SELECT/DATE filter inputs - document index maintenance; apply same-origin check to sign-out - trim signup names, unique shared dates and kanban keys (migration 010), reset reference cache on identity changes, hide OIDC issuer, return 413 for oversized direct bodies, escape Markdown link destinations - remove dead template files and duplicate test helper Verified with go build/vet, full PostgreSQL race suite, db convergence, tsc, both ESLint configs and a Next production build.
144 lines
4.5 KiB
Go
144 lines
4.5 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"net/url"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
// DBURL returns the database connection string, preferring DATABASE_URL and
|
|
// falling back to DB_URL (used by the Makefile).
|
|
func DBURL() string {
|
|
if v := os.Getenv("DATABASE_URL"); v != "" {
|
|
return v
|
|
}
|
|
return os.Getenv("DB_URL")
|
|
}
|
|
|
|
// Config holds the runtime configuration, loaded from environment variables.
|
|
// See porting guide section 7.
|
|
type Config struct {
|
|
DatabaseURL string
|
|
NextAuthSecret string // == AUTH_SECRET; signs sessions and derives IMAP key
|
|
AuthSecret string
|
|
NextAuthURL string
|
|
OIDCIssuer string
|
|
OIDCClientID string
|
|
OIDCClientSecret string
|
|
AdminEmails []string
|
|
}
|
|
|
|
// Load reads configuration from the environment. Placeholders keep OIDC off.
|
|
func Load() *Config {
|
|
return &Config{
|
|
DatabaseURL: DBURL(),
|
|
NextAuthSecret: os.Getenv("NEXTAUTH_SECRET"),
|
|
AuthSecret: os.Getenv("AUTH_SECRET"),
|
|
NextAuthURL: os.Getenv("NEXTAUTH_URL"),
|
|
OIDCIssuer: os.Getenv("OIDC_ISSUER"),
|
|
OIDCClientID: os.Getenv("OIDC_CLIENT_ID"),
|
|
OIDCClientSecret: os.Getenv("OIDC_CLIENT_SECRET"),
|
|
AdminEmails: AdminEmails(),
|
|
}
|
|
}
|
|
|
|
// Secret returns the signing secret, preferring NEXTAUTH_SECRET then AUTH_SECRET.
|
|
// The original code falls back to a hard-coded string; a port must fail instead.
|
|
func (c *Config) Secret() string {
|
|
if c.NextAuthSecret != "" {
|
|
return c.NextAuthSecret
|
|
}
|
|
return c.AuthSecret
|
|
}
|
|
|
|
// EncryptionSecret returns the key material for at-rest encryption of
|
|
// integration credentials (review OPS-01). A dedicated ENCRYPTION_KEY lets
|
|
// operators rotate AUTH_SECRET (sessions) without invalidating stored IMAP
|
|
// passwords. Without it the auth secret remains the key, matching the
|
|
// original derivation, and stored rows keep decrypting via the legacy
|
|
// fallback in signals.LoadMailbox.
|
|
func (c *Config) EncryptionSecret() string {
|
|
if v := os.Getenv("ENCRYPTION_KEY"); v != "" {
|
|
return v
|
|
}
|
|
return c.Secret()
|
|
}
|
|
|
|
// CookieSecure reports whether session cookies must carry the Secure flag.
|
|
// An explicit COOKIE_SECURE setting wins; otherwise the NEXTAUTH_URL scheme
|
|
// decides, so TLS deployments are secure by default without knowing about
|
|
// the extra variable.
|
|
func (c *Config) CookieSecure() bool {
|
|
if v := os.Getenv("COOKIE_SECURE"); v != "" {
|
|
return v == "1"
|
|
}
|
|
u, err := url.Parse(strings.TrimSpace(c.NextAuthURL))
|
|
return err == nil && u.Scheme == "https"
|
|
}
|
|
|
|
// adminEmailsCache memoizes the parsed ADMIN_EMAILS value by its raw env
|
|
// string (review M-12: IsAdminEmail runs on the auth hot path and re-split
|
|
// and re-lowercased the list on every call). Keying on the raw value keeps
|
|
// t.Setenv-based tests correct without a process-global startup snapshot.
|
|
var adminEmailsCache struct {
|
|
mu sync.Mutex
|
|
raw string
|
|
out []string
|
|
}
|
|
|
|
// AdminEmails splits env ADMIN_EMAILS on commas, trims, lowercases.
|
|
func AdminEmails() []string {
|
|
s := os.Getenv("ADMIN_EMAILS")
|
|
adminEmailsCache.mu.Lock()
|
|
defer adminEmailsCache.mu.Unlock()
|
|
if adminEmailsCache.raw == s {
|
|
return adminEmailsCache.out
|
|
}
|
|
var out []string
|
|
for _, email := range strings.Split(s, ",") {
|
|
if email = strings.ToLower(strings.TrimSpace(email)); email != "" {
|
|
out = append(out, email)
|
|
}
|
|
}
|
|
adminEmailsCache.raw, adminEmailsCache.out = s, out
|
|
return out
|
|
}
|
|
|
|
// IsAdminEmail reports whether email is listed in ADMIN_EMAILS.
|
|
func IsAdminEmail(email string) bool {
|
|
e := strings.ToLower(strings.TrimSpace(email))
|
|
for _, a := range AdminEmails() {
|
|
if strings.ToLower(a) == e {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// AllowBootstrapSignup reports whether the first self-service signup on a
|
|
// fresh installation may claim administrator (review SEC-5). Default is on
|
|
// so the documented "sign up, then sign in" onboarding keeps working;
|
|
// exposed deployments set ALLOW_BOOTSTRAP_SIGNUP=0 and provision operators
|
|
// via ADMIN_EMAILS/seed instead, so an attacker scanning for new
|
|
// installations cannot claim the instance.
|
|
func AllowBootstrapSignup() bool {
|
|
return os.Getenv("ALLOW_BOOTSTRAP_SIGNUP") != "0"
|
|
}
|
|
|
|
// Validate fails fast on configuration the app cannot use, instead of
|
|
// discovering it on the first request (review M-12).
|
|
func (c *Config) Validate() error {
|
|
if strings.TrimSpace(c.Secret()) == "" {
|
|
return fmt.Errorf("AUTH_SECRET (or NEXTAUTH_SECRET) must be set")
|
|
}
|
|
if c.NextAuthURL != "" {
|
|
u, err := url.Parse(c.NextAuthURL)
|
|
if err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" {
|
|
return fmt.Errorf("NEXTAUTH_URL must be an absolute http(s) URL, got %q", c.NextAuthURL)
|
|
}
|
|
}
|
|
return nil
|
|
}
|