| 1 | package config |
| 2 | |
| 3 | import ( |
| 4 | "errors" |
| 5 | "fmt" |
| 6 | "os" |
| 7 | "runtime" |
| 8 | "strings" |
| 9 | ) |
| 10 | |
| 11 | // ErrCredentialKeyInUse marks a named credential variable that a connection |
| 12 | // may not claim; CredentialKeyInUseError says who holds it. |
| 13 | var ErrCredentialKeyInUse = errors.New("credential variable already in use") |
| 14 | |
| 15 | // CredentialKeyHolder names why a variable is unavailable. |
| 16 | type CredentialKeyHolder uint8 |
| 17 | |
| 18 | const ( |
| 19 | // CredentialKeyHeldByProvider: another provider entry references the variable. |
| 20 | CredentialKeyHeldByProvider CredentialKeyHolder = iota + 1 |
| 21 | // CredentialKeyHeldBySetting: a bot or remote-host setting reads the variable. |
| 22 | CredentialKeyHeldBySetting |
| 23 | // CredentialKeyHeldByStore: the credential store already has a value or tombstone for it. |
| 24 | CredentialKeyHeldByStore |
| 25 | // CredentialKeyHeldByEnvironment: the process environment sets it from outside the store. |
| 26 | CredentialKeyHeldByEnvironment |
| 27 | ) |
| 28 | |
| 29 | type CredentialKeyInUseError struct { |
| 30 | Key string |
| 31 | Holder CredentialKeyHolder |
| 32 | Provider string // set when Holder is CredentialKeyHeldByProvider |
| 33 | } |
| 34 | |
| 35 | func (e *CredentialKeyInUseError) Error() string { |
| 36 | switch e.Holder { |
| 37 | case CredentialKeyHeldByProvider: |
| 38 | return fmt.Sprintf("%s is referenced by provider %q", e.Key, e.Provider) |
| 39 | case CredentialKeyHeldBySetting: |
| 40 | return fmt.Sprintf("%s is read by a bot or remote-host setting", e.Key) |
| 41 | case CredentialKeyHeldByEnvironment: |
| 42 | return fmt.Sprintf("%s is already set in the environment", e.Key) |
| 43 | default: |
| 44 | return fmt.Sprintf("%s already has a stored credential", e.Key) |
| 45 | } |
| 46 | } |
| 47 | |
| 48 | func (e *CredentialKeyInUseError) Unwrap() error { return ErrCredentialKeyInUse } |
| 49 | |
| 50 | // sameCredentialName compares variable names the way the OS environment does. |
| 51 | func sameCredentialName(a, b string) bool { |
| 52 | if runtime.GOOS == "windows" { |
| 53 | return strings.EqualFold(a, b) |
| 54 | } |
| 55 | return a == b |
| 56 | } |
| 57 | |
| 58 | func environmentHasName(key string) bool { |
| 59 | for _, kv := range os.Environ() { |
| 60 | name, _, _ := strings.Cut(kv, "=") |
| 61 | if name != "" && sameCredentialName(name, key) { |
| 62 | return true |
| 63 | } |
| 64 | } |
| 65 | return false |
| 66 | } |
| 67 | |
| 68 | // CredentialKeyClaimable reports whether provider may write its key under key |
| 69 | // without changing what any other reader of that variable resolves. A stored |
| 70 | // value or tombstone may belong to a config this one cannot see. |
| 71 | func (c *Config) CredentialKeyClaimable(key, provider string) error { |
| 72 | key = strings.TrimSpace(key) |
| 73 | if !isCredentialKey(key) { |
| 74 | return fmt.Errorf("invalid credential key %q", key) |
| 75 | } |
| 76 | for _, p := range c.Providers { |
| 77 | if p.Name != provider && sameCredentialName(strings.TrimSpace(p.APIKeyEnv), key) { |
| 78 | return &CredentialKeyInUseError{Key: key, Holder: CredentialKeyHeldByProvider, Provider: p.Name} |
| 79 | } |
| 80 | } |
| 81 | others := *c |
| 82 | others.Providers = nil |
| 83 | for _, name := range credentialEnvNamesFromConfig(&others) { |
| 84 | if sameCredentialName(name, key) { |
| 85 | return &CredentialKeyInUseError{Key: key, Holder: CredentialKeyHeldBySetting} |
| 86 | } |
| 87 | } |
| 88 | if credentialCurrentStoreHasKey(key) || credentialCurrentStoreClearedKey(key) { |
| 89 | return &CredentialKeyInUseError{Key: key, Holder: CredentialKeyHeldByStore} |
| 90 | } |
| 91 | if environmentHasName(key) { |
| 92 | return &CredentialKeyInUseError{Key: key, Holder: CredentialKeyHeldByEnvironment} |
| 93 | } |
| 94 | return nil |
| 95 | } |
| 96 | |
| 97 | // StageNamedModelCredentialLocked is StageModelCredentialLocked for a name the |
| 98 | // user chose. It refuses a name CredentialKeyClaimable rejects, so the no- |
| 99 | // overwrite and rollback guarantees of a fresh slot still hold. |
| 100 | func (c *Config) StageNamedModelCredentialLocked(key, provider, value string) (string, error) { |
| 101 | if err := c.CredentialKeyClaimable(key, provider); err != nil { |
| 102 | return "", err |
| 103 | } |
| 104 | return c.stageModelCredentialLocked(strings.TrimSpace(key), value) |
| 105 | } |
| 106 | |
| 107 | // configReferencesCredential reports whether the config at path reads key. |
| 108 | // A config that cannot be parsed counts as referencing it: removal needs proof. |
| 109 | func configReferencesCredential(path, key string) bool { |
| 110 | if _, err := os.Stat(path); os.IsNotExist(err) { |
| 111 | return false |
| 112 | } |
| 113 | cfg, err := LoadForEditWithoutCredentialsReadOnlyStrict(path) |
| 114 | if err != nil { |
| 115 | return true |
| 116 | } |
| 117 | for _, name := range credentialEnvNamesFromConfig(cfg) { |
| 118 | if sameCredentialName(name, key) { |
| 119 | return true |
| 120 | } |
| 121 | } |
| 122 | return false |
| 123 | } |
| 124 | |
| 125 | func stagedCredentialDigest(value string) (string, error) { |
| 126 | return ModelSettingsRequestDigest([]byte(strings.TrimSpace(value))) |
| 127 | } |
| 128 | |
| 129 | // stagedValueUnchanged reports whether slot still holds what this edit staged. |
| 130 | // No recorded digest means a journal from before digests existed. |
| 131 | func stagedValueUnchanged(slot, digest string) bool { |
| 132 | if digest == "" { |
| 133 | return true |
| 134 | } |
| 135 | value, ok := envFileValue(UserCredentialsPath(), slot) |
| 136 | if !ok { |
| 137 | return !envFileHasClearedKey(UserCredentialsPath(), slot) |
| 138 | } |
| 139 | got, err := stagedCredentialDigest(value) |
| 140 | return err == nil && got == digest |
| 141 | } |
| 142 |