feat: support custom data dir and log directories (#302)

* feat: linux support custom data dir via environment variable

* feat(keychain): support custom log directory via LARKSUITE_CLI_LOG_DIR

* feat(security): validate env dir paths for security

Add validation for environment variable directory paths to ensure they are absolute and safe. This prevents potential security issues from malformed paths. Also add corresponding tests to verify the validation behavior.

* docs(validate): add function and test documentation comments

Add missing documentation comments for SafeEnvDirPath function and related test cases to improve code clarity and maintainability

* refactor(keychain): remove warning logs for invalid env vars
This commit is contained in:
JackZhao10086
2026-04-08 11:06:58 +08:00
committed by GitHub
parent 2e345a4fdd
commit f3c3a4c49f
6 changed files with 135 additions and 0 deletions

View File

@@ -9,6 +9,7 @@ import (
"sync"
"time"
"github.com/larksuite/cli/internal/validate"
"github.com/larksuite/cli/internal/vfs"
)
@@ -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
}
}
if dir := os.Getenv("LARKSUITE_CLI_CONFIG_DIR"); dir != "" {
return filepath.Join(dir, "logs")
}

View File

@@ -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)
}
}

View File

@@ -16,6 +16,7 @@ import (
"regexp"
"github.com/google/uuid"
"github.com/larksuite/cli/internal/validate"
"github.com/larksuite/cli/internal/vfs"
)
@@ -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)
}
}
home, err := vfs.UserHomeDir()
if err != nil || home == "" {
// If home is missing, fallback to relative path and print warning.

View File

@@ -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)
}
}

View File

@@ -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.

View File

@@ -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)
}
}