Merge pull request #2298 from Molecule-AI/fix/issue-2290-required-env-no-force-bypass

fix(org-import): remove force=true bypass of required-env preflight (#2290)
This commit is contained in:
Hongming Wang 2026-04-29 16:22:32 +00:00 committed by GitHub
commit b733cf46c3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 59 additions and 24 deletions

View File

@ -504,13 +504,6 @@ func (h *OrgHandler) Import(c *gin.Context) {
var body struct {
Dir string `json:"dir"` // org template directory name
Template OrgTemplate `json:"template"` // or inline template
// Force skips the required-env preflight. Used by tooling
// that already computed the preflight client-side and wants
// to proceed despite missing creds (usually because the
// user explicitly acknowledged the tradeoff). Default behavior
// refuses the import with a 412 and the missing-key list so
// the canvas can surface them in its preflight modal.
Force bool `json:"force"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
@ -557,22 +550,17 @@ func (h *OrgHandler) Import(c *gin.Context) {
}
// Required-env preflight — refuses import when any required_env is
// missing from global_secrets (unless `force: true` overrides). The
// canvas runs the same check client-side against GET /org/templates
// output and shows a modal so users set keys before clicking Import;
// this server-side check is the authoritative guard in case a caller
// bypasses the UI (CLI, API clients, etc.). 412 Precondition Failed
// carries the missing-key list so tooling can render the same
// add-key flow.
// missing from global_secrets. No bypass: the prior `force: true`
// escape hatch was removed (issue #2290) because it was the silent
// failure mode that let an org import without ANTHROPIC_API_KEY and
// ship workspaces that 401'd on every LLM call. The canvas runs the
// same check client-side against GET /org/templates output and shows
// a modal so users set keys before clicking Import; this server-side
// check is the authoritative guard in case a caller bypasses the UI
// (CLI, API clients, etc.). 412 Precondition Failed carries the
// missing-key list so tooling can render the same add-key flow.
required, _ := collectOrgEnv(&tmpl)
if body.Force {
// Log the bypass so a post-incident search can find who
// imported an org with missing creds. The common audit flow
// treats log.Printf at INFO as the low-cost trail for
// explicit-override actions — keeps force as a supported
// knob but makes it investigable.
log.Printf("Org import: force=true bypass — template=%q, required_env=%v", tmpl.Name, required)
} else if len(required) > 0 {
if len(required) > 0 {
ctx := c.Request.Context()
configured, err := loadConfiguredGlobalSecretKeys(ctx)
if err != nil {
@ -583,7 +571,7 @@ func (h *OrgHandler) Import(c *gin.Context) {
// that will fail at container-start time on every node.
log.Printf("Org import preflight: global secrets lookup failed: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{
"error": "could not verify required environment variables; try again or pass force=true to override",
"error": "could not verify required environment variables; try again",
})
return
}
@ -603,7 +591,7 @@ func (h *OrgHandler) Import(c *gin.Context) {
"missing_env": missing,
"required_env": required,
"template": tmpl.Name,
"suggestion": "set these as global secrets (POST /settings/secrets) or pass force=true to override",
"suggestion": "set these as global secrets (POST /settings/secrets) before importing",
})
return
}

View File

@ -0,0 +1,47 @@
package handlers
import (
"encoding/json"
"reflect"
"testing"
)
// TestOrgImport_ForceFieldRemoved pins the post-#2290 contract: the
// org-import request body no longer carries a Force field, and external
// callers passing legacy `{"force": true}` must NOT be able to bypass
// the required-env preflight. The pre-#2290 force=true escape hatch is
// what allowed the ux-ab-lab org to import without ANTHROPIC_API_KEY
// and ship workspaces that 401'd on every LLM call.
//
// This test is intentionally cheap (no DB, no gin router) — it pins the
// shape that any future "what if we added a force flag for X?" change
// must consciously break in order to revert. The exact same struct
// definition lives in Import() in org.go; if that drifts, the
// reflect-FieldByName check fails.
func TestOrgImport_ForceFieldRemoved(t *testing.T) {
// Mirror the request-body struct from Import() exactly.
var body struct {
Dir string `json:"dir"`
Template OrgTemplate `json:"template"`
}
// Legacy clients still sending force=true must be silently tolerated
// (Go's json.Unmarshal drops unknown fields by default, so this
// passes regardless — the assertion is that we *want* this drop
// behavior, not strict-decoding that 400s).
raw := `{"dir":"some-org","force":true}`
if err := json.Unmarshal([]byte(raw), &body); err != nil {
t.Fatalf("json.Unmarshal: %v — request body must accept legacy force=true without erroring", err)
}
if body.Dir != "some-org" {
t.Fatalf("Dir = %q, want %q", body.Dir, "some-org")
}
// The struct must NOT carry a Force field. If a future change adds
// one back (intentional or not), this test fails and forces an
// explicit reckoning with the issue #2290 rationale.
bv := reflect.TypeOf(body)
if _, found := bv.FieldByName("Force"); found {
t.Fatal("import body must not carry a Force field — the required-env bypass was removed in #2290")
}
}