Files
Enterprise-Knowledge-Base/cmd/server/bootstrap.go
T
wizardchen d2ffc1b973 refactor(api-keys): simplify authorization to a single-role gate
Redesign tenant API key authorization around one dimension (role) plus a
central default-deny gate, replacing the previous role + KB-scoped flag model.

- Add APIKeyGate as the single authority for API-key authorization: routes
  declare one policy (MinRole) via apiKeyGroup/apiKeyRoute helpers; undeclared
  routes are denied by default. JWT principals short-circuit the gate and the
  JWT-centric RBAC guards short-circuit for API-key principals.
- Roles: viewer/contributor/admin are knowledge-base data-plane only (KB scope
  is a pure downstream data filter); owner is a full-access tenant tier that
  unlocks tenant-level infrastructure routes and is required for every method.
- Drop the KBScoped policy flag and the KB-restricted-vs-tenant branching.
- Security hardening: never disclose HMAC secret (return presence only), abort
  writes on API-key encryption failure, deny KB-restricted keys on /files, and
  backfill legacy key hashes at startup instead of O(N) plaintext scans.
- Frontend: two-level create dialog (access type -> permission level), rename
  role labels to reflect actual scope, sync en/zh/ko/ru locales.
- Fix startup self-check false-positive on trailing-slash routes and normalize
  gin paths before comparison; add regression tests.
2026-07-07 12:00:13 +08:00

127 lines
4.8 KiB
Go

// Bootstrap-time hooks that run after the DI container is built but
// before the HTTP server starts listening. These are deliberately
// best-effort: any failure here only warns and does NOT abort startup.
// The reasoning is that an operator running with a misconfigured env
// var should still be able to bring the server up (and fix the issue
// from the running instance) rather than have a typo brick the deploy.
package main
import (
"context"
"os"
"strings"
"go.uber.org/dig"
"github.com/Tencent/WeKnora/internal/logger"
"github.com/Tencent/WeKnora/internal/types/interfaces"
)
// bootstrapEnvVar is the env var that names the email of the user who
// may be promoted to system administrator when the deployment has no
// existing system administrators.
//
// Why an env var (vs a CLI subcommand)?
// - Zero-friction in docker-compose / k8s deploys: set it once in the
// manifest and the very first user account that signs up with that
// email is auto-promoted, with no extra ops step.
// - Idempotent: if the user is already a system admin, bootstrapping is
// a no-op.
// - Safe to leave set: once at least one system admin exists, the env
// var stops granting privileges. That prevents a UI revoke from being
// silently undone on the next restart.
const bootstrapEnvVar = "WEKNORA_BOOTSTRAP_SYSTEM_ADMIN_EMAIL"
// runStartupBootstrap consults the env and applies any one-shot
// bootstrap actions. Currently it only handles system-admin promotion;
// future bootstrap steps (default model seeding, etc.) can be added
// here as additional dig.Invoke calls.
func runStartupBootstrap(c *dig.Container) {
ctx := context.Background()
// One-shot: backfill SHA-256 key_hash for legacy tenant API keys that
// migration 000065 seeded with a placeholder. Without this, the first
// authentication of each migrated key falls back to an O(n) plaintext
// scan of the tenant's keys. Best-effort: warn, never abort startup.
if err := c.Invoke(func(apiKeySvc interfaces.TenantAPIKeyService) {
if n, err := apiKeySvc.BackfillMissingKeyHashes(ctx); err != nil {
logger.Warnf(ctx, "[bootstrap] tenant api key hash backfill failed: %v", err)
} else if n > 0 {
logger.Infof(ctx, "[bootstrap] backfilled %d legacy tenant api key hash(es)", n)
}
}); err != nil {
logger.Warnf(ctx, "[bootstrap] failed to resolve TenantAPIKeyService: %v", err)
}
email := strings.TrimSpace(os.Getenv(bootstrapEnvVar))
if email == "" {
return
}
// dig.Invoke resolves UserService from the container; if user
// service registration is broken we want to know loudly, but still
// not abort startup — bootstrap is best-effort.
if err := c.Invoke(func(userSvc interfaces.UserService) {
bootstrapSystemAdmin(ctx, userSvc, email)
}); err != nil {
logger.Warnf(ctx, "[bootstrap] failed to resolve UserService: %v", err)
}
}
// bootstrapSystemAdmin promotes the user identified by `email` to system
// administrator only when the deployment currently has no system admins.
// The function is idempotent and non-fatal — it warns and returns on
// every error path.
//
// The bootstrap intentionally does NOT create a user when the email is
// not yet registered: account creation is a workflow with side effects
// (password hashing, tenant assignment, audit) that we don't want to
// short-circuit. Operators should sign up normally first, then set the
// env var on the next restart.
func bootstrapSystemAdmin(ctx context.Context, userSvc interfaces.UserService, email string) {
user, err := userSvc.GetUserByEmail(ctx, email)
if err != nil {
// "not found" surfaces as an error in this codebase; treat it
// gently — operators commonly set the var before the user has
// signed up. The next restart after registration will succeed.
logger.Warnf(ctx,
"[bootstrap] %s=%s: user lookup failed (have they signed up yet?): %v",
bootstrapEnvVar, email, err)
return
}
if user == nil {
logger.Warnf(ctx,
"[bootstrap] %s=%s: no matching user (will retry on next restart)",
bootstrapEnvVar, email)
return
}
if user.IsSystemAdmin {
logger.Infof(ctx,
"[bootstrap] %s=%s: user %s is already a system admin (no-op)",
bootstrapEnvVar, email, user.ID)
return
}
_, total, err := userSvc.ListSystemAdmins(ctx, 0, 1)
if err != nil {
logger.Warnf(ctx,
"[bootstrap] %s=%s: cannot verify existing system admins, skipping promotion: %v",
bootstrapEnvVar, email, err)
return
}
if total > 0 {
logger.Infof(ctx,
"[bootstrap] %s=%s: %d system admin(s) already exist; not promoting user %s",
bootstrapEnvVar, email, total, user.ID)
return
}
user.IsSystemAdmin = true
if err := userSvc.UpdateUser(ctx, user); err != nil {
logger.Warnf(ctx,
"[bootstrap] %s=%s: failed to promote user %s: %v",
bootstrapEnvVar, email, user.ID, err)
return
}
logger.Infof(ctx,
"[bootstrap] promoted user %s (%s) to system admin via %s",
user.ID, email, bootstrapEnvVar)
}