diff --git a/coderd/usersecrets.go b/coderd/usersecrets.go index 16eb947824..e613d03044 100644 --- a/coderd/usersecrets.go +++ b/coderd/usersecrets.go @@ -53,10 +53,7 @@ func (api *API) postUserSecret(rw http.ResponseWriter, r *http.Request) { }) return } - envOpts := codersdk.UserSecretEnvValidationOptions{ - AIGatewayEnabled: api.DeploymentValues.AI.BridgeConfig.Enabled.Value(), - } - if err := codersdk.UserSecretEnvNameValid(req.EnvName, envOpts); err != nil { + if err := codersdk.UserSecretEnvNameValid(req.EnvName); err != nil { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ Message: "Invalid environment variable name.", Detail: err.Error(), @@ -184,10 +181,7 @@ func (api *API) patchUserSecret(rw http.ResponseWriter, r *http.Request) { return } if req.EnvName != nil { - envOpts := codersdk.UserSecretEnvValidationOptions{ - AIGatewayEnabled: api.DeploymentValues.AI.BridgeConfig.Enabled.Value(), - } - if err := codersdk.UserSecretEnvNameValid(*req.EnvName, envOpts); err != nil { + if err := codersdk.UserSecretEnvNameValid(*req.EnvName); err != nil { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ Message: "Invalid environment variable name.", Detail: err.Error(), diff --git a/codersdk/usersecretvalidation.go b/codersdk/usersecretvalidation.go index da7bd303ee..7702f95085 100644 --- a/codersdk/usersecretvalidation.go +++ b/codersdk/usersecretvalidation.go @@ -28,15 +28,6 @@ const ( maxFilePathLength = 4096 ) -// UserSecretEnvValidationOptions controls deployment-aware behavior -// in environment variable name validation. -type UserSecretEnvValidationOptions struct { - // AIGatewayEnabled indicates that the deployment has AI Gateway - // configured. When true, AI Gateway environment variables - // (OPENAI_API_KEY, etc.) are reserved to prevent conflicts. - AIGatewayEnabled bool -} - var ( // posixEnvNameRegex matches valid POSIX environment variable names: // must start with a letter or underscore, followed by letters, @@ -116,17 +107,6 @@ var ( "XDG_STATE_HOME": {}, } - // aiGatewayReservedEnvNames are reserved only when AI Gateway - // is enabled on the deployment. When AI Gateway is disabled, - // users may legitimately want to inject their own API keys - // via secrets. - aiGatewayReservedEnvNames = map[string]struct{}{ - "OPENAI_API_KEY": {}, - "OPENAI_BASE_URL": {}, - "ANTHROPIC_AUTH_TOKEN": {}, - "ANTHROPIC_BASE_URL": {}, - } - // reservedEnvPrefixes are namespace prefixes where every // variable in the family is reserved. Checked after the // exact-name map. The CODER / CODER_* namespace is handled @@ -154,9 +134,7 @@ var ( // UserSecretEnvNameValid validates an environment variable name for // a user secret. Empty string is allowed (means no env injection). -// The opts parameter controls deployment-aware checks such as AI -// bridge variable reservation. -func UserSecretEnvNameValid(s string, opts UserSecretEnvValidationOptions) error { +func UserSecretEnvNameValid(s string) error { if s == "" { return nil } @@ -181,12 +159,6 @@ func UserSecretEnvNameValid(s string, opts UserSecretEnvValidationOptions) error } } - if opts.AIGatewayEnabled { - if _, ok := aiGatewayReservedEnvNames[upper]; ok { - return xerrors.Errorf("%s is reserved when AI Gateway is enabled", upper) - } - } - return nil } diff --git a/codersdk/usersecretvalidation_test.go b/codersdk/usersecretvalidation_test.go index 8ccd2fac5a..07c23eda93 100644 --- a/codersdk/usersecretvalidation_test.go +++ b/codersdk/usersecretvalidation_test.go @@ -12,126 +12,109 @@ import ( func TestUserSecretEnvNameValid(t *testing.T) { t.Parallel() - // noAIGateway is the default for most tests — AI Gateway disabled. - noAIGateway := codersdk.UserSecretEnvValidationOptions{} - withAIGateway := codersdk.UserSecretEnvValidationOptions{AIGatewayEnabled: true} - tests := []struct { name string input string - opts codersdk.UserSecretEnvValidationOptions wantErr bool errMsg string }{ // Valid names. - {name: "SimpleUpper", input: "GITHUB_TOKEN", opts: noAIGateway}, - {name: "SimpleLower", input: "github_token", opts: noAIGateway}, - {name: "StartsWithUnderscore", input: "_FOO", opts: noAIGateway}, - {name: "SingleChar", input: "A", opts: noAIGateway}, - {name: "WithDigits", input: "A1B2", opts: noAIGateway}, - {name: "Empty", input: "", opts: noAIGateway}, + {name: "SimpleUpper", input: "GITHUB_TOKEN"}, + {name: "SimpleLower", input: "github_token"}, + {name: "StartsWithUnderscore", input: "_FOO"}, + {name: "SingleChar", input: "A"}, + {name: "WithDigits", input: "A1B2"}, + {name: "Empty", input: ""}, // Invalid POSIX names. - {name: "StartsWithDigit", input: "1FOO", opts: noAIGateway, wantErr: true, errMsg: "must start with"}, - {name: "ContainsHyphen", input: "FOO-BAR", opts: noAIGateway, wantErr: true, errMsg: "must start with"}, - {name: "ContainsDot", input: "FOO.BAR", opts: noAIGateway, wantErr: true, errMsg: "must start with"}, - {name: "ContainsSpace", input: "FOO BAR", opts: noAIGateway, wantErr: true, errMsg: "must start with"}, + {name: "StartsWithDigit", input: "1FOO", wantErr: true, errMsg: "must start with"}, + {name: "ContainsHyphen", input: "FOO-BAR", wantErr: true, errMsg: "must start with"}, + {name: "ContainsDot", input: "FOO.BAR", wantErr: true, errMsg: "must start with"}, + {name: "ContainsSpace", input: "FOO BAR", wantErr: true, errMsg: "must start with"}, // Reserved system names — core POSIX/login. - {name: "ReservedPATH", input: "PATH", opts: noAIGateway, wantErr: true, errMsg: "reserved"}, - {name: "ReservedHOME", input: "HOME", opts: noAIGateway, wantErr: true, errMsg: "reserved"}, - {name: "ReservedSHELL", input: "SHELL", opts: noAIGateway, wantErr: true, errMsg: "reserved"}, - {name: "ReservedUSER", input: "USER", opts: noAIGateway, wantErr: true, errMsg: "reserved"}, - {name: "ReservedLOGNAME", input: "LOGNAME", opts: noAIGateway, wantErr: true, errMsg: "reserved"}, - {name: "ReservedPWD", input: "PWD", opts: noAIGateway, wantErr: true, errMsg: "reserved"}, - {name: "ReservedOLDPWD", input: "OLDPWD", opts: noAIGateway, wantErr: true, errMsg: "reserved"}, + {name: "ReservedPATH", input: "PATH", wantErr: true, errMsg: "reserved"}, + {name: "ReservedHOME", input: "HOME", wantErr: true, errMsg: "reserved"}, + {name: "ReservedSHELL", input: "SHELL", wantErr: true, errMsg: "reserved"}, + {name: "ReservedUSER", input: "USER", wantErr: true, errMsg: "reserved"}, + {name: "ReservedLOGNAME", input: "LOGNAME", wantErr: true, errMsg: "reserved"}, + {name: "ReservedPWD", input: "PWD", wantErr: true, errMsg: "reserved"}, + {name: "ReservedOLDPWD", input: "OLDPWD", wantErr: true, errMsg: "reserved"}, // Reserved system names — locale/terminal. - {name: "ReservedLANG", input: "LANG", opts: noAIGateway, wantErr: true, errMsg: "reserved"}, - {name: "ReservedTERM", input: "TERM", opts: noAIGateway, wantErr: true, errMsg: "reserved"}, + {name: "ReservedLANG", input: "LANG", wantErr: true, errMsg: "reserved"}, + {name: "ReservedTERM", input: "TERM", wantErr: true, errMsg: "reserved"}, // Reserved system names — shell behavior. - {name: "ReservedIFS", input: "IFS", opts: noAIGateway, wantErr: true, errMsg: "reserved"}, - {name: "ReservedCDPATH", input: "CDPATH", opts: noAIGateway, wantErr: true, errMsg: "reserved"}, + {name: "ReservedIFS", input: "IFS", wantErr: true, errMsg: "reserved"}, + {name: "ReservedCDPATH", input: "CDPATH", wantErr: true, errMsg: "reserved"}, // Reserved system names — shell startup files. - {name: "ReservedENV", input: "ENV", opts: noAIGateway, wantErr: true, errMsg: "reserved"}, - {name: "ReservedBASH_ENV", input: "BASH_ENV", opts: noAIGateway, wantErr: true, errMsg: "reserved"}, + {name: "ReservedENV", input: "ENV", wantErr: true, errMsg: "reserved"}, + {name: "ReservedBASH_ENV", input: "BASH_ENV", wantErr: true, errMsg: "reserved"}, // Reserved system names — temp directories. - {name: "ReservedTMPDIR", input: "TMPDIR", opts: noAIGateway, wantErr: true, errMsg: "reserved"}, - {name: "ReservedTMP", input: "TMP", opts: noAIGateway, wantErr: true, errMsg: "reserved"}, - {name: "ReservedTEMP", input: "TEMP", opts: noAIGateway, wantErr: true, errMsg: "reserved"}, + {name: "ReservedTMPDIR", input: "TMPDIR", wantErr: true, errMsg: "reserved"}, + {name: "ReservedTMP", input: "TMP", wantErr: true, errMsg: "reserved"}, + {name: "ReservedTEMP", input: "TEMP", wantErr: true, errMsg: "reserved"}, // Reserved system names — host identity. - {name: "ReservedHOSTNAME", input: "HOSTNAME", opts: noAIGateway, wantErr: true, errMsg: "reserved"}, + {name: "ReservedHOSTNAME", input: "HOSTNAME", wantErr: true, errMsg: "reserved"}, // Reserved system names — SSH. - {name: "ReservedSSH_AUTH_SOCK", input: "SSH_AUTH_SOCK", opts: noAIGateway, wantErr: true, errMsg: "reserved"}, - {name: "ReservedSSH_CLIENT", input: "SSH_CLIENT", opts: noAIGateway, wantErr: true, errMsg: "reserved"}, - {name: "ReservedSSH_CONNECTION", input: "SSH_CONNECTION", opts: noAIGateway, wantErr: true, errMsg: "reserved"}, - {name: "ReservedSSH_TTY", input: "SSH_TTY", opts: noAIGateway, wantErr: true, errMsg: "reserved"}, + {name: "ReservedSSH_AUTH_SOCK", input: "SSH_AUTH_SOCK", wantErr: true, errMsg: "reserved"}, + {name: "ReservedSSH_CLIENT", input: "SSH_CLIENT", wantErr: true, errMsg: "reserved"}, + {name: "ReservedSSH_CONNECTION", input: "SSH_CONNECTION", wantErr: true, errMsg: "reserved"}, + {name: "ReservedSSH_TTY", input: "SSH_TTY", wantErr: true, errMsg: "reserved"}, // Reserved system names — editor/pager. - {name: "ReservedEDITOR", input: "EDITOR", opts: noAIGateway, wantErr: true, errMsg: "reserved"}, - {name: "ReservedVISUAL", input: "VISUAL", opts: noAIGateway, wantErr: true, errMsg: "reserved"}, - {name: "ReservedPAGER", input: "PAGER", opts: noAIGateway, wantErr: true, errMsg: "reserved"}, + {name: "ReservedEDITOR", input: "EDITOR", wantErr: true, errMsg: "reserved"}, + {name: "ReservedVISUAL", input: "VISUAL", wantErr: true, errMsg: "reserved"}, + {name: "ReservedPAGER", input: "PAGER", wantErr: true, errMsg: "reserved"}, // Reserved system names — IDE integration. - {name: "ReservedVSCODE_PROXY_URI", input: "VSCODE_PROXY_URI", opts: noAIGateway, wantErr: true, errMsg: "reserved"}, - {name: "ReservedCS_DISABLE", input: "CS_DISABLE_GETTING_STARTED_OVERRIDE", opts: noAIGateway, wantErr: true, errMsg: "reserved"}, + {name: "ReservedVSCODE_PROXY_URI", input: "VSCODE_PROXY_URI", wantErr: true, errMsg: "reserved"}, + {name: "ReservedCS_DISABLE", input: "CS_DISABLE_GETTING_STARTED_OVERRIDE", wantErr: true, errMsg: "reserved"}, // Reserved system names — XDG. - {name: "ReservedXDG_RUNTIME_DIR", input: "XDG_RUNTIME_DIR", opts: noAIGateway, wantErr: true, errMsg: "reserved"}, - {name: "ReservedXDG_CONFIG_HOME", input: "XDG_CONFIG_HOME", opts: noAIGateway, wantErr: true, errMsg: "reserved"}, - {name: "ReservedXDG_DATA_HOME", input: "XDG_DATA_HOME", opts: noAIGateway, wantErr: true, errMsg: "reserved"}, - {name: "ReservedXDG_CACHE_HOME", input: "XDG_CACHE_HOME", opts: noAIGateway, wantErr: true, errMsg: "reserved"}, - {name: "ReservedXDG_STATE_HOME", input: "XDG_STATE_HOME", opts: noAIGateway, wantErr: true, errMsg: "reserved"}, - - // AI Gateway vars — blocked when AI Gateway is enabled. - {name: "AIGateway/OPENAI_API_KEY/Enabled", input: "OPENAI_API_KEY", opts: withAIGateway, wantErr: true, errMsg: "AI Gateway"}, - {name: "AIGateway/OPENAI_BASE_URL/Enabled", input: "OPENAI_BASE_URL", opts: withAIGateway, wantErr: true, errMsg: "AI Gateway"}, - {name: "AIGateway/ANTHROPIC_AUTH_TOKEN/Enabled", input: "ANTHROPIC_AUTH_TOKEN", opts: withAIGateway, wantErr: true, errMsg: "AI Gateway"}, - {name: "AIGateway/ANTHROPIC_BASE_URL/Enabled", input: "ANTHROPIC_BASE_URL", opts: withAIGateway, wantErr: true, errMsg: "AI Gateway"}, - - // AI Gateway vars — allowed when AI Gateway is disabled. - {name: "AIGateway/OPENAI_API_KEY/Disabled", input: "OPENAI_API_KEY", opts: noAIGateway}, - {name: "AIGateway/OPENAI_BASE_URL/Disabled", input: "OPENAI_BASE_URL", opts: noAIGateway}, - {name: "AIGateway/ANTHROPIC_AUTH_TOKEN/Disabled", input: "ANTHROPIC_AUTH_TOKEN", opts: noAIGateway}, - {name: "AIGateway/ANTHROPIC_BASE_URL/Disabled", input: "ANTHROPIC_BASE_URL", opts: noAIGateway}, + {name: "ReservedXDG_RUNTIME_DIR", input: "XDG_RUNTIME_DIR", wantErr: true, errMsg: "reserved"}, + {name: "ReservedXDG_CONFIG_HOME", input: "XDG_CONFIG_HOME", wantErr: true, errMsg: "reserved"}, + {name: "ReservedXDG_DATA_HOME", input: "XDG_DATA_HOME", wantErr: true, errMsg: "reserved"}, + {name: "ReservedXDG_CACHE_HOME", input: "XDG_CACHE_HOME", wantErr: true, errMsg: "reserved"}, + {name: "ReservedXDG_STATE_HOME", input: "XDG_STATE_HOME", wantErr: true, errMsg: "reserved"}, // Case insensitivity. - {name: "ReservedCaseInsensitive", input: "path", opts: noAIGateway, wantErr: true, errMsg: "reserved"}, + {name: "ReservedCaseInsensitive", input: "path", wantErr: true, errMsg: "reserved"}, // CODER_ prefix. - {name: "CoderExact", input: "CODER", opts: noAIGateway, wantErr: true, errMsg: "CODER_"}, - {name: "CoderPrefix", input: "CODER_WORKSPACE_NAME", opts: noAIGateway, wantErr: true, errMsg: "CODER_"}, - {name: "CoderAgentToken", input: "CODER_AGENT_TOKEN", opts: noAIGateway, wantErr: true, errMsg: "CODER_"}, - {name: "CoderLowerCase", input: "coder_foo", opts: noAIGateway, wantErr: true, errMsg: "CODER_"}, + {name: "CoderExact", input: "CODER", wantErr: true, errMsg: "CODER_"}, + {name: "CoderPrefix", input: "CODER_WORKSPACE_NAME", wantErr: true, errMsg: "CODER_"}, + {name: "CoderAgentToken", input: "CODER_AGENT_TOKEN", wantErr: true, errMsg: "CODER_"}, + {name: "CoderLowerCase", input: "coder_foo", wantErr: true, errMsg: "CODER_"}, // GIT_* prefix. - {name: "GitSSHCommand", input: "GIT_SSH_COMMAND", opts: noAIGateway, wantErr: true, errMsg: "GIT_"}, - {name: "GitAskpass", input: "GIT_ASKPASS", opts: noAIGateway, wantErr: true, errMsg: "GIT_"}, - {name: "GitAuthorName", input: "GIT_AUTHOR_NAME", opts: noAIGateway, wantErr: true, errMsg: "GIT_"}, - {name: "GitLowerCase", input: "git_editor", opts: noAIGateway, wantErr: true, errMsg: "GIT_"}, + {name: "GitSSHCommand", input: "GIT_SSH_COMMAND", wantErr: true, errMsg: "GIT_"}, + {name: "GitAskpass", input: "GIT_ASKPASS", wantErr: true, errMsg: "GIT_"}, + {name: "GitAuthorName", input: "GIT_AUTHOR_NAME", wantErr: true, errMsg: "GIT_"}, + {name: "GitLowerCase", input: "git_editor", wantErr: true, errMsg: "GIT_"}, // LC_* prefix (locale). - {name: "LcAll", input: "LC_ALL", opts: noAIGateway, wantErr: true, errMsg: "LC_"}, - {name: "LcCtype", input: "LC_CTYPE", opts: noAIGateway, wantErr: true, errMsg: "LC_"}, + {name: "LcAll", input: "LC_ALL", wantErr: true, errMsg: "LC_"}, + {name: "LcCtype", input: "LC_CTYPE", wantErr: true, errMsg: "LC_"}, // LD_* prefix (dynamic linker). - {name: "LdPreload", input: "LD_PRELOAD", opts: noAIGateway, wantErr: true, errMsg: "LD_"}, - {name: "LdLibraryPath", input: "LD_LIBRARY_PATH", opts: noAIGateway, wantErr: true, errMsg: "LD_"}, + {name: "LdPreload", input: "LD_PRELOAD", wantErr: true, errMsg: "LD_"}, + {name: "LdLibraryPath", input: "LD_LIBRARY_PATH", wantErr: true, errMsg: "LD_"}, // DYLD_* prefix (macOS dynamic linker). - {name: "DyldInsert", input: "DYLD_INSERT_LIBRARIES", opts: noAIGateway, wantErr: true, errMsg: "DYLD_"}, - {name: "DyldLibraryPath", input: "DYLD_LIBRARY_PATH", opts: noAIGateway, wantErr: true, errMsg: "DYLD_"}, + {name: "DyldInsert", input: "DYLD_INSERT_LIBRARIES", wantErr: true, errMsg: "DYLD_"}, + {name: "DyldLibraryPath", input: "DYLD_LIBRARY_PATH", wantErr: true, errMsg: "DYLD_"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() - err := codersdk.UserSecretEnvNameValid(tt.input, tt.opts) + err := codersdk.UserSecretEnvNameValid(tt.input) if tt.wantErr { assert.Error(t, err) if tt.errMsg != "" { diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index b1dc175d4f..cb9f56561e 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -8249,20 +8249,6 @@ export interface UserSecret { readonly updated_at: string; } -// From codersdk/usersecretvalidation.go -/** - * UserSecretEnvValidationOptions controls deployment-aware behavior - * in environment variable name validation. - */ -export interface UserSecretEnvValidationOptions { - /** - * AIGatewayEnabled indicates that the deployment has AI Gateway - * configured. When true, AI Gateway environment variables - * (OPENAI_API_KEY, etc.) are reserved to prevent conflicts. - */ - readonly AIGatewayEnabled: boolean; -} - // From codersdk/users.go export type UserStatus = "active" | "dormant" | "suspended";