Files
larksuite-cli/internal/binding/audit_test.go
evandance ce80b3bc46 feat(config): add 'config bind' for per-Agent credential isolation (#515)
Give each AI Agent (OpenClaw, Hermes) its own lark-cli workspace so
its Feishu calls don't overwrite the developer's local config or
collide with other Agents.

    lark-cli config bind [--source openclaw|hermes] [--app-id <id>]
                         [--identity bot-only|user-default] [--force]

Key capabilities:

- Source auto-detected from OPENCLAW_* / HERMES_* env signals; config
  written to ~/.lark-cli/<agent>/, isolated per Agent.
- Two identity presets: 'bot-only' (flag-mode default) and
  'user-default'. Flag mode rejects silent bot→user escalation
  without --force; TUI prompts are exempt.
- Agent-friendly stdout JSON with 'identity' + 'message' for
  next-step branching.
- 'config show' and 'doctor' expose the bound 'workspace'.
- OpenClaw SecretRef resolution: plain / ${VAR} / file:+JSON Pointer
  / exec:.
2026-04-23 19:51:36 +08:00

364 lines
9.7 KiB
Go

// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package binding
import (
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
)
func TestAssertSecurePath_NonAbsolutePath(t *testing.T) {
_, err := AssertSecurePath(AuditParams{
TargetPath: "relative/path.txt",
Label: "test",
AllowInsecurePath: true,
})
if err == nil {
t.Fatal("expected error for non-absolute path, got nil")
}
want := fmt.Sprintf("test: path must be absolute, got %q", "relative/path.txt")
if err.Error() != want {
t.Errorf("error = %q, want %q", err.Error(), want)
}
}
func TestAssertSecurePath_FileDoesNotExist(t *testing.T) {
nonexistent := filepath.Join(t.TempDir(), "nonexistent.txt")
_, err := AssertSecurePath(AuditParams{
TargetPath: nonexistent,
Label: "test",
AllowInsecurePath: true,
})
if err == nil {
t.Fatal("expected error for non-existent file, got nil")
}
wantPrefix := fmt.Sprintf("test: cannot stat %q: ", nonexistent)
if !strings.HasPrefix(err.Error(), wantPrefix) {
t.Errorf("error = %q, want prefix %q", err.Error(), wantPrefix)
}
}
func TestAssertSecurePath_ValidAbsolutePath(t *testing.T) {
dir := t.TempDir()
p := filepath.Join(dir, "valid.txt")
if err := os.WriteFile(p, []byte("data"), 0o600); err != nil {
t.Fatalf("write temp file: %v", err)
}
got, err := AssertSecurePath(AuditParams{
TargetPath: p,
Label: "test",
AllowInsecurePath: true,
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got != p {
t.Errorf("got %q, want %q", got, p)
}
}
func TestAssertSecurePath_WorldWritable_Rejected(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("permission tests not applicable on Windows")
}
dir := t.TempDir()
p := filepath.Join(dir, "insecure.txt")
if err := os.WriteFile(p, []byte("data"), 0o600); err != nil {
t.Fatalf("write temp file: %v", err)
}
if err := os.Chmod(p, 0o666); err != nil {
t.Fatalf("chmod: %v", err)
}
_, err := AssertSecurePath(AuditParams{
TargetPath: p,
Label: "test",
AllowInsecurePath: false,
AllowReadableByOthers: true, // only test writable check
})
if err == nil {
t.Fatal("expected error for world-writable file, got nil")
}
want := fmt.Sprintf("test: path %q is world-writable (mode 0666)", p)
if err.Error() != want {
t.Errorf("error = %q, want %q", err.Error(), want)
}
}
func TestAssertSecurePath_AllowInsecurePath_Bypasses(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("permission tests not applicable on Windows")
}
dir := t.TempDir()
p := filepath.Join(dir, "insecure.txt")
if err := os.WriteFile(p, []byte("data"), 0o600); err != nil {
t.Fatalf("write temp file: %v", err)
}
if err := os.Chmod(p, 0o666); err != nil {
t.Fatalf("chmod: %v", err)
}
got, err := AssertSecurePath(AuditParams{
TargetPath: p,
Label: "test",
AllowInsecurePath: true,
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got != p {
t.Errorf("got %q, want %q", got, p)
}
}
func TestAssertSecurePath_DirectoryRejected(t *testing.T) {
dir := t.TempDir()
_, err := AssertSecurePath(AuditParams{
TargetPath: dir,
Label: "test",
AllowInsecurePath: true,
})
if err == nil {
t.Fatal("expected error for directory path, got nil")
}
want := fmt.Sprintf("test: path %q is a directory, not a file", dir)
if err.Error() != want {
t.Errorf("error = %q, want %q", err.Error(), want)
}
}
func TestAssertSecurePath_GroupWritable_Rejected(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("permission tests not applicable on Windows")
}
dir := t.TempDir()
p := filepath.Join(dir, "groupw.txt")
if err := os.WriteFile(p, []byte("data"), 0o600); err != nil {
t.Fatalf("write: %v", err)
}
if err := os.Chmod(p, 0o620); err != nil {
t.Fatalf("chmod: %v", err)
}
_, err := AssertSecurePath(AuditParams{
TargetPath: p,
Label: "test",
AllowInsecurePath: false,
AllowReadableByOthers: true,
})
if err == nil {
t.Fatal("expected error for group-writable file, got nil")
}
want := fmt.Sprintf("test: path %q is group-writable (mode 0620)", p)
if err.Error() != want {
t.Errorf("error = %q, want %q", err.Error(), want)
}
}
func TestAssertSecurePath_WorldReadable_Rejected(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("permission tests not applicable on Windows")
}
dir := t.TempDir()
p := filepath.Join(dir, "worldr.txt")
if err := os.WriteFile(p, []byte("data"), 0o600); err != nil {
t.Fatalf("write: %v", err)
}
if err := os.Chmod(p, 0o604); err != nil {
t.Fatalf("chmod: %v", err)
}
_, err := AssertSecurePath(AuditParams{
TargetPath: p,
Label: "test",
AllowInsecurePath: false,
AllowReadableByOthers: false,
})
if err == nil {
t.Fatal("expected error for world-readable file, got nil")
}
want := fmt.Sprintf("test: path %q is world-readable (mode 0604)", p)
if err.Error() != want {
t.Errorf("error = %q, want %q", err.Error(), want)
}
}
func TestAssertSecurePath_AllowReadableByOthers_Passes(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("permission tests not applicable on Windows")
}
dir := t.TempDir()
p := filepath.Join(dir, "readable.txt")
if err := os.WriteFile(p, []byte("data"), 0o600); err != nil {
t.Fatalf("write: %v", err)
}
if err := os.Chmod(p, 0o644); err != nil {
t.Fatalf("chmod: %v", err)
}
got, err := AssertSecurePath(AuditParams{
TargetPath: p,
Label: "test",
AllowInsecurePath: false,
AllowReadableByOthers: true,
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got != p {
t.Errorf("got %q, want %q", got, p)
}
}
func TestAssertSecurePath_OwnerUID_Valid(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("owner UID tests not applicable on Windows")
}
dir := t.TempDir()
p := filepath.Join(dir, "owned.txt")
if err := os.WriteFile(p, []byte("data"), 0o600); err != nil {
t.Fatalf("write: %v", err)
}
got, err := AssertSecurePath(AuditParams{
TargetPath: p,
Label: "test",
AllowInsecurePath: false,
AllowReadableByOthers: true,
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got != p {
t.Errorf("got %q, want %q", got, p)
}
}
func TestAssertSecurePath_Symlink_Rejected(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("symlink tests not applicable on Windows")
}
dir := t.TempDir()
target := filepath.Join(dir, "real.txt")
if err := os.WriteFile(target, []byte("data"), 0o600); err != nil {
t.Fatalf("write: %v", err)
}
link := filepath.Join(dir, "link.txt")
if err := os.Symlink(target, link); err != nil {
t.Fatalf("symlink: %v", err)
}
_, err := AssertSecurePath(AuditParams{
TargetPath: link,
Label: "test",
AllowSymlinkPath: false,
AllowInsecurePath: true,
})
if err == nil {
t.Fatal("expected error for symlink with AllowSymlinkPath=false, got nil")
}
want := fmt.Sprintf("test: path %q is a symlink (not allowed)", link)
if err.Error() != want {
t.Errorf("error = %q, want %q", err.Error(), want)
}
}
func TestAssertSecurePath_Symlink_Allowed(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("symlink tests not applicable on Windows")
}
dir := t.TempDir()
target := filepath.Join(dir, "real.txt")
if err := os.WriteFile(target, []byte("data"), 0o600); err != nil {
t.Fatalf("write: %v", err)
}
link := filepath.Join(dir, "link.txt")
if err := os.Symlink(target, link); err != nil {
t.Fatalf("symlink: %v", err)
}
got, err := AssertSecurePath(AuditParams{
TargetPath: link,
Label: "test",
AllowSymlinkPath: true,
AllowInsecurePath: true,
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// On macOS /var → /private/var, so compare resolved paths
wantResolved, err := filepath.EvalSymlinks(target)
if err != nil {
t.Fatalf("EvalSymlinks(target): %v", err)
}
if got != wantResolved {
t.Errorf("got %q, want resolved %q", got, wantResolved)
}
}
func TestAssertSecurePath_TrustedDirs_ExactMatch(t *testing.T) {
dir := t.TempDir()
p := filepath.Join(dir, "file.txt")
if err := os.WriteFile(p, []byte("data"), 0o600); err != nil {
t.Fatalf("write: %v", err)
}
got, err := AssertSecurePath(AuditParams{
TargetPath: p,
Label: "test",
TrustedDirs: []string{p},
AllowInsecurePath: true,
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got != p {
t.Errorf("got %q, want %q", got, p)
}
}
func TestAssertSecurePath_TrustedDirs(t *testing.T) {
trustedDir := t.TempDir()
untrustedDir := t.TempDir()
trustedFile := filepath.Join(trustedDir, "secret.txt")
if err := os.WriteFile(trustedFile, []byte("data"), 0o600); err != nil {
t.Fatalf("write temp file: %v", err)
}
untrustedFile := filepath.Join(untrustedDir, "secret.txt")
if err := os.WriteFile(untrustedFile, []byte("data"), 0o600); err != nil {
t.Fatalf("write temp file: %v", err)
}
// File outside trusted dir should fail
_, err := AssertSecurePath(AuditParams{
TargetPath: untrustedFile,
Label: "test",
TrustedDirs: []string{trustedDir},
AllowInsecurePath: true,
})
if err == nil {
t.Fatal("expected error for file outside trusted dir, got nil")
}
want := fmt.Sprintf("test: path %q is not inside any trusted directory", untrustedFile)
if err.Error() != want {
t.Errorf("error = %q, want %q", err.Error(), want)
}
// File inside trusted dir should pass
got, err := AssertSecurePath(AuditParams{
TargetPath: trustedFile,
Label: "test",
TrustedDirs: []string{trustedDir},
AllowInsecurePath: true,
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got != trustedFile {
t.Errorf("got %q, want %q", got, trustedFile)
}
}