mirror of
https://github.com/coder/coder.git
synced 2026-09-21 20:51:01 +08:00
feat: make sure creds are always masked (#24241)
## Summary Adds a `sanitizeCredentialHint` safety check in the db-to-SDK conversion layer to ensure credential hints are always masked before being exposed in the API. Also adds `credential_kind` and `credential_hint` assertions to the session threads API test.
This commit is contained in:
@@ -1241,7 +1241,7 @@ func buildAIBridgeThread(
|
||||
thread.Model = rootIntc.Model
|
||||
thread.Provider = rootIntc.Provider
|
||||
thread.CredentialKind = string(rootIntc.CredentialKind)
|
||||
thread.CredentialHint = rootIntc.CredentialHint
|
||||
thread.CredentialHint = sanitizeCredentialHint(rootIntc.CredentialHint)
|
||||
// Get first user prompt from root interception.
|
||||
// A thread can only have one prompt, by definition, since we currently
|
||||
// only store the last prompt observed in an interception.
|
||||
@@ -1407,6 +1407,25 @@ func InvalidatedPresets(invalidatedPresets []database.UpdatePresetsLastInvalidat
|
||||
return presets
|
||||
}
|
||||
|
||||
// sanitizeCredentialHint ensures the hint looks masked before exposing
|
||||
// it in the API. The aibridge library uses "..." as the masking
|
||||
// delimiter (e.g. "sk-a...efgh"), so we check for its presence. If
|
||||
// the hint doesn't contain "..." or exceeds the max length, it's
|
||||
// replaced with "..." to prevent leaking raw secrets.
|
||||
func sanitizeCredentialHint(hint string) string {
|
||||
// Matches the VARCHAR(15) DB constraint.
|
||||
const maxCredentialHintLength = 15
|
||||
|
||||
if hint == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
if len(hint) > maxCredentialHintLength || !strings.Contains(hint, "...") {
|
||||
return "..."
|
||||
}
|
||||
return hint
|
||||
}
|
||||
|
||||
func jsonOrEmptyMap(rawMessage pqtype.NullRawMessage) map[string]any {
|
||||
var m map[string]any
|
||||
if !rawMessage.Valid {
|
||||
|
||||
@@ -306,3 +306,29 @@ func TestAggregateTokenUsage(t *testing.T) {
|
||||
require.Empty(t, result.Metadata)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSanitizeCredentialHint(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"valid_short", "s...t", "s...t"},
|
||||
{"valid_long", "sk-a...efgh", "sk-a...efgh"},
|
||||
{"valid_only_dots", "...", "..."},
|
||||
{"empty", "", ""},
|
||||
{"short_unmasked_secret", "abc12", "..."},
|
||||
{"missing_dots", "sk-abcdefgh", "..."},
|
||||
{"too_long", "sk-a...efghijklmn", "..."},
|
||||
{"raw_secret", "sk-proj-abc123xyz789", "..."},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
require.Equal(t, tc.expected, sanitizeCredentialHint(tc.input))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1836,10 +1836,12 @@ func TestAIBridgeGetSessionThreads(t *testing.T) {
|
||||
now := dbtime.Now()
|
||||
endedAt := now.Add(time.Minute)
|
||||
i1 := dbgen.AIBridgeInterception(t, db, database.InsertAIBridgeInterceptionParams{
|
||||
InitiatorID: firstUser.UserID,
|
||||
Provider: "openai",
|
||||
Model: "gpt-4",
|
||||
StartedAt: now,
|
||||
InitiatorID: firstUser.UserID,
|
||||
Provider: "openai",
|
||||
Model: "gpt-4",
|
||||
StartedAt: now,
|
||||
CredentialKind: database.CredentialKindByok,
|
||||
CredentialHint: "sk-a...efgh",
|
||||
}, &endedAt)
|
||||
|
||||
// When no client session ID is set, the interception ID becomes the session identifier.
|
||||
@@ -1847,6 +1849,8 @@ func TestAIBridgeGetSessionThreads(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, i1.ID.String(), res.ID)
|
||||
require.Len(t, res.Threads, 1)
|
||||
require.Equal(t, "byok", res.Threads[0].CredentialKind)
|
||||
require.Equal(t, "sk-a...efgh", res.Threads[0].CredentialHint)
|
||||
})
|
||||
|
||||
t.Run("ThreadsWithAgenticActions", func(t *testing.T) {
|
||||
|
||||
@@ -496,7 +496,7 @@ require (
|
||||
github.com/anthropics/anthropic-sdk-go v1.19.0
|
||||
github.com/brianvoe/gofakeit/v7 v7.14.0
|
||||
github.com/coder/agentapi-sdk-go v0.0.0-20250505131810-560d1d88d225
|
||||
github.com/coder/aibridge v1.1.1-0.20260408143328-f72a795f1e77
|
||||
github.com/coder/aibridge v1.1.1-0.20260410135158-9ac7525ed947
|
||||
github.com/coder/aisdk-go v0.0.9
|
||||
github.com/coder/boundary v0.8.4-0.20260304164748-566aeea939ab
|
||||
github.com/coder/preview v1.0.8
|
||||
|
||||
@@ -312,8 +312,8 @@ github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os
|
||||
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4=
|
||||
github.com/coder/agentapi-sdk-go v0.0.0-20250505131810-560d1d88d225 h1:tRIViZ5JRmzdOEo5wUWngaGEFBG8OaE1o2GIHN5ujJ8=
|
||||
github.com/coder/agentapi-sdk-go v0.0.0-20250505131810-560d1d88d225/go.mod h1:rNLVpYgEVeu1Zk29K64z6Od8RBP9DwqCu9OfCzh8MR4=
|
||||
github.com/coder/aibridge v1.1.1-0.20260408143328-f72a795f1e77 h1:VNN+JBL4/IiC/sb9eTQmAkRv4ALwSnmMvW9iI2o+G6o=
|
||||
github.com/coder/aibridge v1.1.1-0.20260408143328-f72a795f1e77/go.mod h1:u6WvGLMQQbk3ByeOw+LBdVgDNc/v/ujAtUc6MfvzQb4=
|
||||
github.com/coder/aibridge v1.1.1-0.20260410135158-9ac7525ed947 h1:5asZktST8evXjs9vaS9ujSew/LNyp/lBLfJk8Rd8+pk=
|
||||
github.com/coder/aibridge v1.1.1-0.20260410135158-9ac7525ed947/go.mod h1:u6WvGLMQQbk3ByeOw+LBdVgDNc/v/ujAtUc6MfvzQb4=
|
||||
github.com/coder/aisdk-go v0.0.9 h1:Vzo/k2qwVGLTR10ESDeP2Ecek1SdPfZlEjtTfMveiVo=
|
||||
github.com/coder/aisdk-go v0.0.9/go.mod h1:KF6/Vkono0FJJOtWtveh5j7yfNrSctVTpwgweYWSp5M=
|
||||
github.com/coder/anthropic-sdk-go v0.0.0-20260409105508-5711db120546 h1:dYAA1uo93n9r/B4Gvx76pjaeS0kBTX1lO0WX0p0uqo8=
|
||||
|
||||
Reference in New Issue
Block a user