697 lines
18 KiB
Go
697 lines
18 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/joho/godotenv"
|
|
|
|
"reasonix/internal/fileutil"
|
|
fileencoding "reasonix/internal/fileutil/encoding"
|
|
)
|
|
|
|
const (
|
|
CredentialsStoreAuto = "auto"
|
|
CredentialsStoreKeyring = "keyring"
|
|
CredentialsStoreFile = "file"
|
|
|
|
credentialsKeyringService = "reasonix"
|
|
credentialClearedPrefix = "# reasonix-cleared "
|
|
)
|
|
|
|
const (
|
|
CredentialSourceEnvironment = "environment"
|
|
CredentialSourceProjectEnv = "project_env"
|
|
CredentialSourceCredentials = "credentials"
|
|
CredentialSourceHomeEnv = "home_env"
|
|
CredentialSourceLegacy = "legacy_credentials"
|
|
)
|
|
|
|
type CredentialSource struct {
|
|
Kind string `json:"kind"`
|
|
Path string `json:"path,omitempty"`
|
|
Label string `json:"label,omitempty"`
|
|
}
|
|
|
|
type CredentialResolution struct {
|
|
Name string `json:"name"`
|
|
Set bool `json:"set"`
|
|
Value string `json:"-"`
|
|
Source CredentialSource `json:"source,omitempty"`
|
|
Shadowed []CredentialSource `json:"shadowed,omitempty"`
|
|
}
|
|
|
|
type trackedCredentialSource struct {
|
|
source CredentialSource
|
|
value string
|
|
}
|
|
|
|
var credentialSourceTracker = struct {
|
|
sync.Mutex
|
|
byKey map[string]trackedCredentialSource
|
|
}{byKey: map[string]trackedCredentialSource{}}
|
|
|
|
var storedCredentialValueLookup = storedCredentialValue
|
|
var legacyKeyringCredentialValueLookup = legacyKeyringCredentialValue
|
|
|
|
// CredentialResolver resolves credentials repeatedly for one caller-owned view
|
|
// build. It keeps expensive global credential-store lookups bounded to one per
|
|
// key while preserving the same source/shadow reporting as the one-shot helpers.
|
|
type CredentialResolver struct {
|
|
root string
|
|
|
|
mu sync.Mutex
|
|
globalFirstCache map[string]CredentialResolution
|
|
}
|
|
|
|
// NewCredentialResolverForRoot returns a resolver scoped to a workspace root.
|
|
func NewCredentialResolverForRoot(root string) *CredentialResolver {
|
|
return &CredentialResolver{root: resolveRoot(root)}
|
|
}
|
|
|
|
// ResolveGlobalFirst resolves key from Reasonix's global .env only. Repeated
|
|
// calls for the same key reuse the first result so UI views with multiple
|
|
// provider entries sharing api_key_env stay consistent.
|
|
func (r *CredentialResolver) ResolveGlobalFirst(key string) CredentialResolution {
|
|
key = strings.TrimSpace(key)
|
|
if key == "" {
|
|
return CredentialResolution{Name: key}
|
|
}
|
|
if r == nil {
|
|
return resolveCredentialForRootGlobalFirst(".", key)
|
|
}
|
|
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
if r.globalFirstCache == nil {
|
|
r.globalFirstCache = map[string]CredentialResolution{}
|
|
}
|
|
if cached, ok := r.globalFirstCache[key]; ok {
|
|
return cloneCredentialResolution(cached)
|
|
}
|
|
res := resolveCredentialForRootGlobalFirst(r.root, key)
|
|
r.globalFirstCache[key] = cloneCredentialResolution(res)
|
|
return res
|
|
}
|
|
|
|
func cloneCredentialResolution(res CredentialResolution) CredentialResolution {
|
|
if len(res.Shadowed) > 0 {
|
|
res.Shadowed = append([]CredentialSource(nil), res.Shadowed...)
|
|
}
|
|
return res
|
|
}
|
|
|
|
func normalizeCredentialsStore(mode string) string {
|
|
switch strings.ToLower(strings.TrimSpace(mode)) {
|
|
case CredentialsStoreKeyring:
|
|
return CredentialsStoreKeyring
|
|
case CredentialsStoreFile:
|
|
return CredentialsStoreFile
|
|
default:
|
|
return CredentialsStoreAuto
|
|
}
|
|
}
|
|
|
|
func credentialsStoreMode() string {
|
|
if mode := strings.TrimSpace(os.Getenv("REASONIX_CREDENTIALS_STORE")); mode != "" {
|
|
return normalizeCredentialsStore(mode)
|
|
}
|
|
var partial struct {
|
|
CredentialsStore string `toml:"credentials_store"`
|
|
}
|
|
if path := userConfigLoadPath(); path != "" {
|
|
_, _ = decodeTOMLFile(path, &partial)
|
|
}
|
|
return normalizeCredentialsStore(partial.CredentialsStore)
|
|
}
|
|
|
|
func credentialEnvNamesForRoot(root string) []string {
|
|
root = resolveRoot(root)
|
|
cfg := Default()
|
|
|
|
projectTOML := "reasonix.toml"
|
|
if root != "." {
|
|
projectTOML = filepath.Join(root, "reasonix.toml")
|
|
}
|
|
if uc := userConfigLoadPath(); uc != "" {
|
|
_ = mergeFile(cfg, uc)
|
|
}
|
|
_ = mergeFile(cfg, projectTOML)
|
|
var tomlSources []string
|
|
if uc := userConfigLoadPath(); uc != "" {
|
|
tomlSources = append(tomlSources, uc)
|
|
}
|
|
tomlSources = append(tomlSources, projectTOML)
|
|
if providers, _, _, ok, err := mergeTOMLProviders(tomlSources); err == nil && ok {
|
|
cfg.Providers = providers
|
|
}
|
|
|
|
return credentialEnvNamesFromConfig(cfg)
|
|
}
|
|
|
|
func credentialEnvNamesFromConfig(cfg *Config) []string {
|
|
seen := map[string]bool{}
|
|
var out []string
|
|
add := func(name string) {
|
|
name = strings.TrimSpace(name)
|
|
if name == "" || seen[name] {
|
|
return
|
|
}
|
|
seen[name] = true
|
|
out = append(out, name)
|
|
}
|
|
for _, p := range cfg.Providers {
|
|
add(p.APIKeyEnv)
|
|
}
|
|
add(cfg.Bot.QQ.AppSecretEnv)
|
|
add(cfg.Bot.Feishu.AppSecretEnv)
|
|
add(cfg.Bot.Weixin.TokenEnv)
|
|
for _, conn := range cfg.Bot.Connections {
|
|
add(conn.Credential.AppSecretEnv)
|
|
add(conn.Credential.TokenEnv)
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|
|
|
|
func resolveProviderCredentialsForRoot(root string, cfg *Config) {
|
|
if cfg == nil || len(cfg.Providers) == 0 {
|
|
return
|
|
}
|
|
resolver := NewCredentialResolverForRoot(root)
|
|
for i := range cfg.Providers {
|
|
resolveProviderCredentialWithResolver(&cfg.Providers[i], resolver)
|
|
}
|
|
}
|
|
|
|
func resolveProviderCredentialWithResolver(entry *ProviderEntry, resolver *CredentialResolver) {
|
|
if entry == nil {
|
|
return
|
|
}
|
|
key := strings.TrimSpace(entry.APIKeyEnv)
|
|
if key == "" {
|
|
entry.resolvedAPIKey = ""
|
|
entry.resolvedSource = CredentialSource{}
|
|
return
|
|
}
|
|
if resolver == nil {
|
|
resolver = NewCredentialResolverForRoot(".")
|
|
}
|
|
res := resolver.ResolveGlobalFirst(key)
|
|
if !res.Set || res.Value == "" {
|
|
entry.resolvedAPIKey = ""
|
|
entry.resolvedSource = CredentialSource{}
|
|
return
|
|
}
|
|
entry.resolvedAPIKey = res.Value
|
|
entry.resolvedSource = res.Source
|
|
}
|
|
|
|
func (e *ProviderEntry) ResolveAPIKeyForRoot(root string) {
|
|
resolveProviderCredentialWithResolver(e, NewCredentialResolverForRoot(root))
|
|
}
|
|
|
|
func loadCredentialStoreForRoot(root string) {
|
|
names := credentialEnvNamesForRoot(root)
|
|
if len(names) == 0 {
|
|
return
|
|
}
|
|
if p := UserCredentialsPath(); p != "" {
|
|
loadDotEnvFileAs(p, CredentialSource{Kind: CredentialSourceCredentials, Path: p, Label: "Reasonix credentials (.env)"})
|
|
}
|
|
}
|
|
|
|
// StoreCredentialLines stores KEY=value assignments in Reasonix's global .env
|
|
// and pins them into the current process environment.
|
|
func StoreCredentialLines(lines []string) (string, error) {
|
|
assignments := parseCredentialLines(lines)
|
|
if len(assignments) == 0 {
|
|
return CredentialsTargetDescription(), nil
|
|
}
|
|
if err := storeCredentialsInFile(UserCredentialsPath(), assignments); err != nil {
|
|
return "", err
|
|
}
|
|
pinCredentialAssignments(assignments)
|
|
return UserCredentialsPath(), nil
|
|
}
|
|
|
|
func SetCredential(key, value string) (string, error) {
|
|
key = strings.TrimSpace(key)
|
|
if !isCredentialKey(key) {
|
|
return "", fmt.Errorf("invalid credential key %q", key)
|
|
}
|
|
if strings.ContainsAny(value, "\r\n") {
|
|
return "", fmt.Errorf("credential value for %s contains a newline", key)
|
|
}
|
|
return StoreCredentialLines([]string{key + "=" + value})
|
|
}
|
|
|
|
// IsValidCredentialKey reports whether key can be stored in Reasonix's dotenv
|
|
// credential file and exposed as an environment variable.
|
|
func IsValidCredentialKey(key string) bool {
|
|
return isCredentialKey(strings.TrimSpace(key))
|
|
}
|
|
|
|
func RemoveCredential(key string) error {
|
|
key = strings.TrimSpace(key)
|
|
if key == "" || !isCredentialKey(key) {
|
|
return nil
|
|
}
|
|
if path := UserCredentialsPath(); path != "" {
|
|
if err := removeCredentialFromFile(path, key); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return os.Unsetenv(key)
|
|
}
|
|
|
|
func CredentialIsSet(key string) bool {
|
|
key = strings.TrimSpace(key)
|
|
if key == "" {
|
|
return false
|
|
}
|
|
return CredentialStored(key)
|
|
}
|
|
|
|
func CredentialStored(key string) bool {
|
|
key = strings.TrimSpace(key)
|
|
if key == "" {
|
|
return false
|
|
}
|
|
return envFileHasValue(UserCredentialsPath(), key)
|
|
}
|
|
|
|
func credentialCurrentStoreHasKey(key string) bool {
|
|
key = strings.TrimSpace(key)
|
|
if key == "" {
|
|
return false
|
|
}
|
|
return envFileHasValue(UserCredentialsPath(), key)
|
|
}
|
|
|
|
func credentialCurrentStoreClearedKey(key string) bool {
|
|
key = strings.TrimSpace(key)
|
|
if key == "" {
|
|
return false
|
|
}
|
|
return envFileHasClearedKey(UserCredentialsPath(), key)
|
|
}
|
|
|
|
func CredentialsTargetDescription() string {
|
|
return UserCredentialsPath()
|
|
}
|
|
|
|
func parseCredentialLines(lines []string) map[string]string {
|
|
out := map[string]string{}
|
|
for _, raw := range lines {
|
|
if strings.ContainsAny(raw, "\r\n") {
|
|
continue
|
|
}
|
|
values, err := godotenv.Unmarshal(raw)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
for key, value := range values {
|
|
key = strings.TrimSpace(key)
|
|
if !isCredentialKey(key) || strings.ContainsAny(value, "\r\n") {
|
|
continue
|
|
}
|
|
out[key] = value
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func pinCredentialAssignments(assignments map[string]string) {
|
|
for key, value := range assignments {
|
|
_ = os.Setenv(key, value)
|
|
recordCredentialSource(key, value, CredentialSource{Kind: CredentialSourceCredentials, Path: UserCredentialsPath(), Label: "Reasonix credentials (.env)"})
|
|
}
|
|
}
|
|
|
|
func recordExistingCredentialSource(key string) {
|
|
key = strings.TrimSpace(key)
|
|
value := os.Getenv(key)
|
|
if key == "" || value == "" {
|
|
return
|
|
}
|
|
credentialSourceTracker.Lock()
|
|
defer credentialSourceTracker.Unlock()
|
|
if current, ok := credentialSourceTracker.byKey[key]; ok && current.value == value {
|
|
return
|
|
}
|
|
if _, ok := credentialSourceTracker.byKey[key]; ok {
|
|
return
|
|
}
|
|
credentialSourceTracker.byKey[key] = trackedCredentialSource{
|
|
source: CredentialSource{Kind: CredentialSourceEnvironment, Label: "environment variable"},
|
|
value: value,
|
|
}
|
|
}
|
|
|
|
func recordCredentialSource(key, value string, source CredentialSource) {
|
|
key = strings.TrimSpace(key)
|
|
if key == "" || value == "" {
|
|
return
|
|
}
|
|
source.Label = credentialSourceLabel(source)
|
|
credentialSourceTracker.Lock()
|
|
credentialSourceTracker.byKey[key] = trackedCredentialSource{source: source, value: value}
|
|
credentialSourceTracker.Unlock()
|
|
}
|
|
|
|
func trackedCredential(key, value string) (CredentialSource, bool) {
|
|
credentialSourceTracker.Lock()
|
|
defer credentialSourceTracker.Unlock()
|
|
current, ok := credentialSourceTracker.byKey[key]
|
|
if !ok || current.value != value {
|
|
return CredentialSource{}, false
|
|
}
|
|
return current.source, true
|
|
}
|
|
|
|
func credentialSourceLabel(source CredentialSource) string {
|
|
if strings.TrimSpace(source.Label) != "" {
|
|
return source.Label
|
|
}
|
|
switch source.Kind {
|
|
case CredentialSourceProjectEnv:
|
|
return "project .env"
|
|
case CredentialSourceCredentials:
|
|
return "Reasonix credentials"
|
|
case CredentialSourceHomeEnv:
|
|
return "home .env"
|
|
case CredentialSourceLegacy:
|
|
return "legacy Reasonix credentials"
|
|
case CredentialSourceEnvironment:
|
|
return "environment variable"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func ResolveCredential(key string) CredentialResolution {
|
|
return ResolveCredentialForRoot(".", key)
|
|
}
|
|
|
|
func ResolveCredentialForRoot(root, key string) CredentialResolution {
|
|
key = strings.TrimSpace(key)
|
|
res := CredentialResolution{Name: key}
|
|
if key == "" {
|
|
return res
|
|
}
|
|
value := os.Getenv(key)
|
|
if value == "" {
|
|
return res
|
|
}
|
|
res.Set = true
|
|
res.Value = value
|
|
if source, ok := trackedCredential(key, value); ok {
|
|
res.Source = source
|
|
} else if source, ok := inferCredentialSource(root, key, value); ok {
|
|
res.Source = source
|
|
} else {
|
|
res.Source = CredentialSource{Kind: CredentialSourceEnvironment, Label: credentialSourceLabel(CredentialSource{Kind: CredentialSourceEnvironment})}
|
|
}
|
|
res.Source.Label = credentialSourceLabel(res.Source)
|
|
res.Shadowed = shadowedCredentialSources(root, key, value, res.Source)
|
|
return res
|
|
}
|
|
|
|
func ResolveCredentialForRootGlobalFirst(root, key string) CredentialResolution {
|
|
key = strings.TrimSpace(key)
|
|
return NewCredentialResolverForRoot(root).ResolveGlobalFirst(key)
|
|
}
|
|
|
|
func resolveCredentialForRootGlobalFirst(root, key string) CredentialResolution {
|
|
root = resolveRoot(root)
|
|
res := CredentialResolution{Name: key}
|
|
if key == "" {
|
|
return res
|
|
}
|
|
if value, source, ok := storedCredentialValueLookup(key); ok {
|
|
res.Set = true
|
|
res.Value = value
|
|
res.Source = source
|
|
res.Source.Label = credentialSourceLabel(res.Source)
|
|
res.Shadowed = shadowedCredentialSources(root, key, value, res.Source)
|
|
return res
|
|
}
|
|
return res
|
|
}
|
|
|
|
func storedCredentialValue(key string) (string, CredentialSource, bool) {
|
|
if p := UserCredentialsPath(); p != "" {
|
|
if value, ok := envFileValue(p, key); ok && value != "" {
|
|
return value, CredentialSource{Kind: CredentialSourceCredentials, Path: p, Label: "Reasonix credentials (.env)"}, true
|
|
}
|
|
}
|
|
return "", CredentialSource{}, false
|
|
}
|
|
|
|
func inferCredentialSource(root, key, value string) (CredentialSource, bool) {
|
|
for _, candidate := range credentialSourceCandidates(root) {
|
|
if v, ok := envFileValue(candidate.Path, key); ok && v == value {
|
|
candidate.Label = credentialSourceLabel(candidate)
|
|
return candidate, true
|
|
}
|
|
}
|
|
return CredentialSource{}, false
|
|
}
|
|
|
|
func shadowedCredentialSources(root, key, activeValue string, active CredentialSource) []CredentialSource {
|
|
var out []CredentialSource
|
|
for _, candidate := range credentialSourceCandidates(root) {
|
|
if sameCredentialSource(candidate, active) {
|
|
continue
|
|
}
|
|
if v, ok := envFileValue(candidate.Path, key); ok && v != activeValue {
|
|
candidate.Label = credentialSourceLabel(candidate)
|
|
out = append(out, candidate)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func credentialSourceCandidates(root string) []CredentialSource {
|
|
root = resolveRoot(root)
|
|
var out []CredentialSource
|
|
dotEnvPath := ".env"
|
|
if root != "" && root != "." {
|
|
dotEnvPath = filepath.Join(root, ".env")
|
|
}
|
|
out = append(out, CredentialSource{Kind: CredentialSourceProjectEnv, Path: dotEnvPath})
|
|
if p := UserCredentialsPath(); p != "" {
|
|
out = append(out, CredentialSource{Kind: CredentialSourceCredentials, Path: p})
|
|
}
|
|
if IsolatedHomeDir() == "" {
|
|
if home, err := os.UserHomeDir(); err == nil {
|
|
out = append(out, CredentialSource{Kind: CredentialSourceHomeEnv, Path: filepath.Join(home, ".env")})
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func sameCredentialSource(a, b CredentialSource) bool {
|
|
if a.Kind != b.Kind {
|
|
return false
|
|
}
|
|
if a.Path == "" || b.Path == "" {
|
|
return a.Path == b.Path
|
|
}
|
|
return samePath(a.Path, b.Path)
|
|
}
|
|
|
|
func storeCredentialsInFile(path string, assignments map[string]string) error {
|
|
if strings.TrimSpace(path) == "" {
|
|
return fmt.Errorf("credentials store unavailable")
|
|
}
|
|
lines, err := readCredentialFileLines(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
filtered := make([]string, 0, len(lines))
|
|
for _, line := range lines {
|
|
if key, ok := credentialClearedLineKey(line); ok {
|
|
if _, hit := assignments[key]; hit {
|
|
continue
|
|
}
|
|
}
|
|
filtered = append(filtered, line)
|
|
}
|
|
lines = filtered
|
|
replaced := map[string]bool{}
|
|
for i, line := range lines {
|
|
key, ok := credentialLineKey(line)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if value, hit := assignments[key]; hit {
|
|
lines[i] = formatCredentialLine(key, value)
|
|
replaced[key] = true
|
|
}
|
|
}
|
|
keys := make([]string, 0, len(assignments))
|
|
for key := range assignments {
|
|
keys = append(keys, key)
|
|
}
|
|
sort.Strings(keys)
|
|
for _, key := range keys {
|
|
if !replaced[key] {
|
|
lines = append(lines, formatCredentialLine(key, assignments[key]))
|
|
}
|
|
}
|
|
return writeCredentialFileLines(path, lines)
|
|
}
|
|
|
|
func formatCredentialLine(key, value string) string {
|
|
if isBareDotEnvValue(value) {
|
|
return key + "=" + value
|
|
}
|
|
line, err := godotenv.Marshal(map[string]string{key: value})
|
|
if err != nil {
|
|
return key + "=" + value
|
|
}
|
|
return line
|
|
}
|
|
|
|
func isBareDotEnvValue(value string) bool {
|
|
if value == "" {
|
|
return true
|
|
}
|
|
return !strings.ContainsAny(value, " \t\r\n#'\"\\")
|
|
}
|
|
|
|
func removeCredentialFromFile(path, key string) error {
|
|
lines, err := readCredentialFileLines(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
out := make([]string, 0, len(lines))
|
|
for _, line := range lines {
|
|
if k, ok := credentialLineKey(line); ok && k == key {
|
|
continue
|
|
}
|
|
if k, ok := credentialClearedLineKey(line); ok && k == key {
|
|
continue
|
|
}
|
|
out = append(out, line)
|
|
}
|
|
out = append(out, credentialClearedPrefix+key)
|
|
return writeCredentialFileLines(path, out)
|
|
}
|
|
|
|
func readCredentialFileLines(path string) ([]string, error) {
|
|
data, err := fileencoding.ReadFileUTF8(path)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
text := strings.TrimRight(string(data), "\n")
|
|
if text == "" {
|
|
return nil, nil
|
|
}
|
|
return strings.Split(text, "\n"), nil
|
|
}
|
|
|
|
func writeCredentialFileLines(path string, lines []string) error {
|
|
if strings.TrimSpace(path) == "" {
|
|
return fmt.Errorf("credentials store unavailable")
|
|
}
|
|
out := ""
|
|
if len(lines) > 0 {
|
|
out = strings.Join(lines, "\n") + "\n"
|
|
}
|
|
dir := filepath.Dir(path)
|
|
if dir != "" && dir != "." {
|
|
if err := os.MkdirAll(dir, 0o700); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
tmp, err := os.CreateTemp(dir, "credentials.*.tmp")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tmpPath := tmp.Name()
|
|
if _, err := tmp.WriteString(out); err != nil {
|
|
tmp.Close()
|
|
os.Remove(tmpPath)
|
|
return err
|
|
}
|
|
if err := tmp.Close(); err != nil {
|
|
os.Remove(tmpPath)
|
|
return err
|
|
}
|
|
if err := os.Chmod(tmpPath, 0o600); err != nil {
|
|
os.Remove(tmpPath)
|
|
return err
|
|
}
|
|
if err := fileutil.ReplaceFile(tmpPath, path); err != nil {
|
|
os.Remove(tmpPath)
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func credentialLineKey(line string) (string, bool) {
|
|
trimmed := strings.TrimPrefix(strings.TrimSpace(line), "export ")
|
|
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
|
|
return "", false
|
|
}
|
|
key, _, ok := strings.Cut(trimmed, "=")
|
|
key = strings.TrimSpace(key)
|
|
return key, ok && isCredentialKey(key)
|
|
}
|
|
|
|
func credentialClearedLineKey(line string) (string, bool) {
|
|
trimmed := strings.TrimSpace(line)
|
|
if !strings.HasPrefix(trimmed, credentialClearedPrefix) {
|
|
return "", false
|
|
}
|
|
key := strings.TrimSpace(strings.TrimPrefix(trimmed, credentialClearedPrefix))
|
|
return key, isCredentialKey(key)
|
|
}
|
|
|
|
func isCredentialKey(key string) bool {
|
|
if key == "" {
|
|
return false
|
|
}
|
|
for i, r := range key {
|
|
if r == '_' || r >= 'A' && r <= 'Z' || r >= 'a' && r <= 'z' || i > 0 && r >= '0' && r <= '9' {
|
|
continue
|
|
}
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func envFileHasValue(path, key string) bool {
|
|
if strings.TrimSpace(path) == "" {
|
|
return false
|
|
}
|
|
value, ok := envFileValue(path, key)
|
|
return ok && strings.TrimSpace(value) != ""
|
|
}
|
|
|
|
func envFileHasClearedKey(path, key string) bool {
|
|
if strings.TrimSpace(path) == "" {
|
|
return false
|
|
}
|
|
lines, err := readCredentialFileLines(path)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
for _, line := range lines {
|
|
if k, ok := credentialClearedLineKey(line); ok && k == key {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|