bf9395e022
CI / results (push) Blocked by required conditions
CI / license-header (push) Has been skipped
CI / e2e-dry-run (push) Has been skipped
CI / e2e-live (push) Waiting to run
CI / fast-gate (push) Failing after 0s
Test PR Label Logic / test-pr-labels (push) Failing after 1s
Skill Format Check / check-format (push) Failing after 2s
CI / security (push) Failing after 5s
CI / unit-test (push) Has been skipped
CI / lint (push) Has been skipped
CI / script-test (push) Has been skipped
CI / deterministic-gate (push) Has been skipped
CI / coverage (push) Has been skipped
CI / deadcode (push) Waiting to run
80 lines
2.7 KiB
Go
80 lines
2.7 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package core
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/larksuite/cli/internal/keychain"
|
|
"github.com/larksuite/cli/internal/vfs"
|
|
)
|
|
|
|
const secretKeyPrefix = "appsecret:"
|
|
|
|
func secretAccountKey(appId string) string {
|
|
return secretKeyPrefix + appId
|
|
}
|
|
|
|
// ResolveSecretInput resolves a SecretInput to a plain string.
|
|
// SecretRef objects are resolved by source (file / keychain).
|
|
func ResolveSecretInput(s SecretInput, kc keychain.KeychainAccess) (string, error) {
|
|
if s.Ref == nil {
|
|
return s.Plain, nil
|
|
}
|
|
switch s.Ref.Source {
|
|
case "file":
|
|
data, err := vfs.ReadFile(s.Ref.ID)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to read secret file %s: %w", s.Ref.ID, err)
|
|
}
|
|
return strings.TrimSpace(string(data)), nil
|
|
case "keychain":
|
|
return kc.Get(keychain.LarkCliService, s.Ref.ID)
|
|
default:
|
|
return "", fmt.Errorf("unknown secret source: %s", s.Ref.Source)
|
|
}
|
|
}
|
|
|
|
// ForStorage determines how to store a secret in config.json.
|
|
// - SecretRef → preserved as-is
|
|
// - Plain text → stored in keychain, returns keychain SecretRef
|
|
// Returns error if keychain is unavailable (no silent plaintext fallback).
|
|
func ForStorage(appId string, input SecretInput, kc keychain.KeychainAccess) (SecretInput, error) {
|
|
if !input.IsPlain() {
|
|
return input, nil // SecretRef → keep as-is
|
|
}
|
|
key := secretAccountKey(appId)
|
|
if err := kc.Set(keychain.LarkCliService, key, input.Plain); err != nil {
|
|
return SecretInput{}, fmt.Errorf("keychain unavailable: %w\nhint: use file: reference in config to bypass keychain", err)
|
|
}
|
|
return SecretInput{Ref: &SecretRef{Source: "keychain", ID: key}}, nil
|
|
}
|
|
|
|
// ValidateSecretKeyMatch checks that the appSecret keychain key references the
|
|
// expected appId. This prevents silent mismatches when config.json is edited by
|
|
// hand (e.g. appId changed but appSecret.id still points to the old app).
|
|
// Only applicable when appSecret is a keychain SecretRef; other forms are skipped.
|
|
func ValidateSecretKeyMatch(appId string, secret SecretInput) error {
|
|
if secret.Ref == nil || secret.Ref.Source != "keychain" {
|
|
return nil
|
|
}
|
|
expected := secretAccountKey(appId)
|
|
if secret.Ref.ID != expected {
|
|
return fmt.Errorf(
|
|
"appSecret keychain key %q does not match appId %q (expected %q); %s",
|
|
secret.Ref.ID, appId, expected, reconfigureHint(),
|
|
)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// RemoveSecretStore cleans up keychain entries when an app is removed.
|
|
// Errors are intentionally ignored — cleanup is best-effort.
|
|
func RemoveSecretStore(input SecretInput, kc keychain.KeychainAccess) {
|
|
if input.IsSecretRef() && input.Ref.Source == "keychain" {
|
|
_ = kc.Remove(keychain.LarkCliService, input.Ref.ID)
|
|
}
|
|
}
|