Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions internal/keychain/auth_log.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"sync"
"time"

"github.com/larksuite/cli/internal/validate"
"github.com/larksuite/cli/internal/vfs"
)

Expand All @@ -21,6 +22,13 @@ var (
)

func authLogDir() string {
if dir := os.Getenv("LARKSUITE_CLI_LOG_DIR"); dir != "" {
safeDir, err := validate.SafeEnvDirPath(dir, "LARKSUITE_CLI_LOG_DIR")
if err == nil {
return safeDir
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

if dir := os.Getenv("LARKSUITE_CLI_CONFIG_DIR"); dir != "" {
return filepath.Join(dir, "logs")
}
Expand Down
35 changes: 35 additions & 0 deletions internal/keychain/auth_log_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package keychain

import (
"path/filepath"
"testing"
)

// TestAuthLogDir_UsesValidatedLogDirEnv verifies that a valid absolute
// LARKSUITE_CLI_LOG_DIR is normalized and used as the auth log directory.
func TestAuthLogDir_UsesValidatedLogDirEnv(t *testing.T) {
base := t.TempDir()
base, _ = filepath.EvalSymlinks(base)
t.Setenv("LARKSUITE_CLI_LOG_DIR", filepath.Join(base, "logs", "..", "auth"))
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", "")

got := authLogDir()
want := filepath.Join(base, "auth")
if got != want {
t.Fatalf("authLogDir() = %q, want %q", got, want)
}
}

// TestAuthLogDir_InvalidLogDirFallsBackToConfigDir verifies that an invalid
// LARKSUITE_CLI_LOG_DIR falls back to LARKSUITE_CLI_CONFIG_DIR/logs.
func TestAuthLogDir_InvalidLogDirFallsBackToConfigDir(t *testing.T) {
t.Setenv("LARKSUITE_CLI_LOG_DIR", "relative-logs")
configDir := t.TempDir()
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", configDir)

got := authLogDir()
want := filepath.Join(configDir, "logs")
if got != want {
t.Fatalf("authLogDir() = %q, want %q", got, want)
}
}
7 changes: 7 additions & 0 deletions internal/keychain/keychain_other.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"regexp"

"github.com/google/uuid"
"github.com/larksuite/cli/internal/validate"
"github.com/larksuite/cli/internal/vfs"
)

Expand All @@ -25,6 +26,12 @@ const tagBytes = 16

// StorageDir returns the directory where encrypted files are stored.
func StorageDir(service string) string {
if dir := os.Getenv("LARKSUITE_CLI_DATA_DIR"); dir != "" {
safeDir, err := validate.SafeEnvDirPath(dir, "LARKSUITE_CLI_DATA_DIR")
if err == nil {
return filepath.Join(safeDir, service)
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
greptile-apps[bot] marked this conversation as resolved.
home, err := vfs.UserHomeDir()
if err != nil || home == "" {
// If home is missing, fallback to relative path and print warning.
Expand Down
37 changes: 37 additions & 0 deletions internal/keychain/keychain_other_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//go:build linux

package keychain

import (
"path/filepath"
"testing"
)

// TestStorageDir_UsesValidatedDataDirEnv verifies that a valid absolute
// LARKSUITE_CLI_DATA_DIR is normalized and still preserves service isolation.
func TestStorageDir_UsesValidatedDataDirEnv(t *testing.T) {
base := t.TempDir()
base, _ = filepath.EvalSymlinks(base)
t.Setenv("LARKSUITE_CLI_DATA_DIR", filepath.Join(base, "data", "..", "store"))

got := StorageDir("svc")
want := filepath.Join(base, "store", "svc")
if got != want {
t.Fatalf("StorageDir() = %q, want %q", got, want)
}
}

// TestStorageDir_InvalidDataDirFallsBackToDefault verifies that an invalid
// LARKSUITE_CLI_DATA_DIR falls back to the default per-service storage path.
func TestStorageDir_InvalidDataDirFallsBackToDefault(t *testing.T) {
home := t.TempDir()
home, _ = filepath.EvalSymlinks(home)
t.Setenv("LARKSUITE_CLI_DATA_DIR", "relative-data")
t.Setenv("HOME", home)

got := StorageDir("svc")
want := filepath.Join(home, ".local", "share", "svc")
if got != want {
t.Fatalf("StorageDir() = %q, want %q", got, want)
}
}
21 changes: 21 additions & 0 deletions internal/validate/path.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,27 @@ func SafeInputPath(path string) (string, error) {
return safePath(path, "--file")
}

// SafeEnvDirPath validates an environment-provided application directory path.
// It requires an absolute path, rejects control characters, normalizes the
// input, and resolves symlinks through the nearest existing ancestor so callers
// receive a canonical path for subsequent filesystem operations.
func SafeEnvDirPath(path, envName string) (string, error) {
if err := RejectControlChars(path, envName); err != nil {
return "", err
}

path = filepath.Clean(path)
if !filepath.IsAbs(path) {
return "", fmt.Errorf("%s must be an absolute path, got %q", envName, path)
}

resolved, err := resolveNearestAncestor(path)
if err != nil {
return "", fmt.Errorf("cannot resolve symlinks: %w", err)
}
return resolved, nil
}

// SafeLocalFlagPath validates a flag value as a local file path.
// Empty values and http/https URLs are returned unchanged without validation,
// allowing the caller to handle non-path inputs (e.g. API keys, URLs) upstream.
Expand Down
27 changes: 27 additions & 0 deletions internal/validate/path_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -283,3 +283,30 @@ func TestSafeInputPath_ErrorMessageContainsCorrectFlagName(t *testing.T) {
t.Errorf("error should mention --output, got: %s", err.Error())
}
}

// TestSafeEnvDirPath_RequiresAbsolutePath verifies that environment-provided
// directory paths must be absolute.
func TestSafeEnvDirPath_RequiresAbsolutePath(t *testing.T) {
_, err := SafeEnvDirPath("logs", "LARKSUITE_CLI_LOG_DIR")
if err == nil {
t.Fatal("expected error for relative path")
}
if !strings.Contains(err.Error(), "LARKSUITE_CLI_LOG_DIR") {
t.Fatalf("error should mention env name, got %v", err)
}
}

// TestSafeEnvDirPath_ReturnsNormalizedAbsolutePath verifies that a valid
// absolute environment directory is cleaned and resolved to its canonical path.
func TestSafeEnvDirPath_ReturnsNormalizedAbsolutePath(t *testing.T) {
base := t.TempDir()
base, _ = filepath.EvalSymlinks(base)
got, err := SafeEnvDirPath(filepath.Join(base, "logs", "..", "auth"), "LARKSUITE_CLI_LOG_DIR")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
want := filepath.Join(base, "auth")
if got != want {
t.Fatalf("SafeEnvDirPath() = %q, want %q", got, want)
}
}
Loading