fix(authlog): stop trusting argument position, and speak up on a rejected log dir

Two behaviours carried over from internal/keychain, both left as they were when
the package moved.

FormatAuthCmdline kept the first three arguments. That protected secrets only
while no sensitive flag appeared early: a global flag in front of the
subcommand put its value straight into a file that is world-readable to the
user, kept for seven days. Today's CLI cannot reach that state — the only
persistent flag is --profile and secrets arrive through --app-secret-stdin — so
this is about the shape, not a live leak. Drop everything from the first flag
onward instead. A denylist of sensitive names would need extending whenever one
is added; the command path is what the log is for, and it lives entirely in the
leading non-flag arguments. args[0] is reduced to its base name so an absolute
install path stays out too.

logDir swallowed the error when LARKSUITE_CLI_LOG_DIR failed validation and
wrote elsewhere while the caller kept watching the path they configured. Warn
instead. This fires only on a rejected override, not on every run, and logDir
resolves once per process.

Tests cover a flag ahead of the subcommand, the absolute-path case, and that a
usable override still prints nothing.
This commit is contained in:
shanglei
2026-07-27 15:43:46 +08:00
parent 52a1187c20
commit 475f04a8dd
2 changed files with 145 additions and 3 deletions

View File

@@ -137,6 +137,18 @@ func (l *Logger) logDir() string {
if err == nil {
return safeDir
}
// The caller asked for a specific directory and it was rejected. Staying
// quiet would send the logs elsewhere while they keep watching the path
// they configured. This fires only on a rejected override, and logDir
// resolves once per process.
//
//nolint:forbidigo // leaf package with no IOStreams in scope; keychain
// surfaces its own directory warning the same way.
fmt.Fprintf(
os.Stderr,
"[lark-cli] [WARN] LARKSUITE_CLI_LOG_DIR is unusable (%v); writing auth logs under the default directory instead\n",
err,
)
}
return filepath.Join(l.runtimeDir(), "logs")
@@ -194,16 +206,38 @@ func (l *Logger) close() error {
return file.Close()
}
// FormatAuthCmdline renders the command path for the log and stops at the first
// flag.
//
// Everything from the first flag onward is dropped rather than filtered by name.
// A denylist of sensitive flags has to be extended whenever one is added, and
// the rule it replaced — keep the first three arguments — only held while no
// sensitive flag happened to appear early. Neither survives contact with a flag
// nobody remembered to classify. What the log needs is which command ran, and
// that is entirely in the leading non-flag arguments.
//
// args[0] is reduced to its base name so an absolute install path does not end
// up in the file either.
func FormatAuthCmdline(args []string) string {
if len(args) == 0 {
return ""
}
if len(args) <= 3 {
return strings.Join(args, " ")
path := []string{filepath.Base(args[0])}
dropped := false
for _, arg := range args[1:] {
if strings.HasPrefix(arg, "-") {
dropped = true
break
}
path = append(path, arg)
}
return strings.Join(args[:3], " ") + " ..."
line := strings.Join(path, " ")
if dropped {
line += " ..."
}
return line
}
// LogResponse records one authentication HTTP response.

View File

@@ -5,6 +5,8 @@ package authlog
import (
"errors"
"io"
"os"
"path/filepath"
"strings"
"testing"
@@ -197,6 +199,112 @@ func TestSetShared_ReleasesTheSupersededFallback(t *testing.T) {
}
}
// TestFormatAuthCmdline_DropsEverythingFromTheFirstFlag is the regression guard
// for the rule this replaced. Keeping the first three arguments was safe only
// while no sensitive flag appeared early; a global flag in front of the
// subcommand put its value straight into the file.
func TestFormatAuthCmdline_DropsEverythingFromTheFirstFlag(t *testing.T) {
cases := []struct {
name string
args []string
want string
}{
{
name: "flag before the subcommand",
args: []string{"lark-cli", "--token=super-secret", "auth", "login"},
want: "lark-cli ...",
},
{
name: "separated flag value",
args: []string{"lark-cli", "auth", "login", "--device-code", "device-secret"},
want: "lark-cli auth login ...",
},
{
name: "absolute install path is reduced to the binary name",
args: []string{"/opt/internal-tools/build-42/lark-cli", "auth", "status"},
want: "lark-cli auth status",
},
{
name: "no flags at all",
args: []string{"lark-cli", "auth", "status"},
want: "lark-cli auth status",
},
{
name: "empty",
args: nil,
want: "",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := FormatAuthCmdline(tc.args)
if got != tc.want {
t.Fatalf("FormatAuthCmdline(%q) = %q, want %q", tc.args, got, tc.want)
}
for _, arg := range tc.args {
if strings.Contains(arg, "secret") && strings.Contains(got, "secret") {
t.Fatalf("FormatAuthCmdline leaked %q into %q", arg, got)
}
}
})
}
}
// TestAuthLogDir_RejectedOverrideWarns covers the one case where staying silent
// misleads: the caller set the directory explicitly and it was refused.
func TestAuthLogDir_RejectedOverrideWarns(t *testing.T) {
t.Setenv("LARKSUITE_CLI_LOG_DIR", "relative-logs")
configDir := t.TempDir()
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", configDir)
logger := New(Options{RuntimeDir: func() string { return configDir }})
warning := captureStderr(t, func() { _ = logger.logDir() })
for _, want := range []string{"LARKSUITE_CLI_LOG_DIR", "default directory"} {
if !strings.Contains(warning, want) {
t.Errorf("warning %q does not mention %q", warning, want)
}
}
}
// TestAuthLogDir_AcceptedOverrideStaysQuiet keeps the warning scoped to the
// failure: a usable override must not print anything.
func TestAuthLogDir_AcceptedOverrideStaysQuiet(t *testing.T) {
base := t.TempDir()
base, _ = filepath.EvalSymlinks(base)
t.Setenv("LARKSUITE_CLI_LOG_DIR", filepath.Join(base, "auth"))
logger := New(Options{RuntimeDir: func() string { return base }})
if warning := captureStderr(t, func() { _ = logger.logDir() }); warning != "" {
t.Fatalf("a usable override printed %q", warning)
}
}
// captureStderr redirects os.Stderr for the duration of fn and returns what was
// written to it.
func captureStderr(t *testing.T, fn func()) string {
t.Helper()
reader, writer, err := os.Pipe()
if err != nil {
t.Fatalf("pipe: %v", err)
}
previous := os.Stderr
os.Stderr = writer
defer func() { os.Stderr = previous }()
fn()
if err := writer.Close(); err != nil {
t.Fatalf("close writer: %v", err)
}
out, err := io.ReadAll(reader)
if err != nil {
t.Fatalf("read captured stderr: %v", err)
}
return string(out)
}
// restoreShared isolates each test from the process-wide logger.
func restoreShared(t *testing.T) {
t.Helper()