package auth import ( "context" "time" "chat-switchboard/store" ) // EveryoneGroupID is the stable ID of the implicit "Everyone" group seeded in // migration 020. Every authenticated user receives its permissions without an // explicit membership row. const EveryoneGroupID = "00000000-0000-0000-0000-000000000001" // Permission constants — domain.action convention. const ( PermModelUse = "model.use" // use models for completion PermModelSelectAny = "model.select_any" // use any enabled model (vs. group allowlist) PermKBRead = "kb.read" // search KBs through personas PermKBWrite = "kb.write" // upload/delete KB documents PermKBCreate = "kb.create" // create new knowledge bases PermChannelCreate = "channel.create" // create group/channel conversations PermChannelInvite = "channel.invite" // invite users to channels PermPersonaCreate = "persona.create" // create new personas PermPersonaManage = "persona.manage" // edit/delete team personas PermWorkflowCreate = "workflow.create" // create workflow definitions (v0.25.0) PermAdminView = "admin.view" // read-only admin panel access PermTokenUnlimited = "token.unlimited" // bypass token budgets PermTaskCreate = "task.create" // create scheduled tasks (v0.27.2) PermTaskAdmin = "task.admin" // manage all tasks, set global task config (v0.27.2) PermTaskAction = "task.action" // create non-LLM action tasks (v0.28.0) PermTaskStarlark = "task.starlark" // create Starlark tasks (pre-positioned for v0.29.0) ) // AllPermissions is the complete set of valid permission strings. // Used for validation in handlers and rendering checkboxes in admin UI. var AllPermissions = []string{ PermModelUse, PermModelSelectAny, PermKBRead, PermKBWrite, PermKBCreate, PermChannelCreate, PermChannelInvite, PermPersonaCreate, PermPersonaManage, PermWorkflowCreate, PermAdminView, PermTokenUnlimited, PermTaskCreate, PermTaskAdmin, PermTaskAction, PermTaskStarlark, } // ── Resolution ────────────────────────────── // ResolvePermissions returns the effective permission set for a user. // Always includes the Everyone group regardless of membership. // Callers should short-circuit for role=admin before calling this. func ResolvePermissions(ctx context.Context, stores store.Stores, userID string) (map[string]bool, error) { perms := make(map[string]bool) // Always apply Everyone group — no membership row required. if everyone, err := stores.Groups.GetByID(ctx, EveryoneGroupID); err == nil { for _, p := range everyone.Permissions { perms[p] = true } } // Union all explicit group memberships. groups, err := stores.Groups.ListForUser(ctx, userID) if err != nil { return perms, err } for _, g := range groups { for _, p := range g.Permissions { perms[p] = true } } return perms, nil } // ── Token Budget ──────────────────────────── // TokenBudget holds the resolved ceiling for a user and the time window it covers. type TokenBudget struct { Limit int64 Period string // "daily" or "monthly" } // ResolveTokenBudget returns the most restrictive token budget across all of the // user's groups. Returns nil if no group has a budget set (unrestricted). // Callers should skip budget enforcement when providerScope == "personal" (BYOK). func ResolveTokenBudget(ctx context.Context, stores store.Stores, userID string) (*TokenBudget, error) { groups, err := stores.Groups.ListForUser(ctx, userID) if err != nil { return nil, err } now := time.Now() isNewDay := now.Hour() < 1 _ = isNewDay // used by callers for cache invalidation hints if needed var daily, monthly *int64 for _, g := range groups { if g.TokenBudgetDaily != nil { if daily == nil || *g.TokenBudgetDaily < *daily { daily = g.TokenBudgetDaily } } if g.TokenBudgetMonthly != nil { if monthly == nil || *g.TokenBudgetMonthly < *monthly { monthly = g.TokenBudgetMonthly } } } // Return the most restrictive window. Daily wins when both are set // and daily is the binding constraint. if daily != nil { return &TokenBudget{Limit: *daily, Period: "daily"}, nil } if monthly != nil { return &TokenBudget{Limit: *monthly, Period: "monthly"}, nil } return nil, nil } // ── Model Allowlist ───────────────────────── // ResolveModelAllowlist returns the union of allowed model IDs across all of the // user's groups. Returns nil if the user is unrestricted (any group has a nil // allowlist, which means "no restriction"). func ResolveModelAllowlist(ctx context.Context, stores store.Stores, userID string) (map[string]bool, error) { groups, err := stores.Groups.ListForUser(ctx, userID) if err != nil { return nil, err } // If any group has nil AllowedModels, the user is unrestricted. for _, g := range groups { if g.AllowedModels == nil { return nil, nil } } // All groups have explicit allowlists — union them. allowed := make(map[string]bool) for _, g := range groups { for _, m := range g.AllowedModels { allowed[m] = true } } // No groups at all → fall through to Everyone group check. if len(groups) == 0 { if everyone, err := stores.Groups.GetByID(ctx, EveryoneGroupID); err == nil { if everyone.AllowedModels == nil { return nil, nil // Everyone has no restriction } for _, m := range everyone.AllowedModels { allowed[m] = true } } } if len(allowed) == 0 { return nil, nil // empty allowlists across all groups = unrestricted } return allowed, nil }