bf9395e022
CI / license-header (push) Has been skipped
CI / e2e-dry-run (push) Has been skipped
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 / results (push) Has been cancelled
CI / deadcode (push) Has been cancelled
CI / e2e-live (push) Has been cancelled
95 lines
3.4 KiB
Go
95 lines
3.4 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package cmdpolicy_test
|
|
|
|
import (
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"github.com/larksuite/cli/errs"
|
|
"github.com/larksuite/cli/extension/platform"
|
|
"github.com/larksuite/cli/internal/cmdpolicy"
|
|
)
|
|
|
|
// The envelope's policy_source must never leak the absolute home path.
|
|
// "yaml:/Users/alice/.lark-cli/policy.yml" would expose Alice's username
|
|
// to any agent or log consumer; the contract is to emit just "yaml" and
|
|
// rely on rule_name (from the yaml's "name:" field) for disambiguation.
|
|
func TestEnvelope_yamlPolicySourceDoesNotLeakHomePath(t *testing.T) {
|
|
root := &cobra.Command{Use: "lark-cli"}
|
|
docs := &cobra.Command{Use: "docs"}
|
|
root.AddCommand(docs)
|
|
leaf := &cobra.Command{Use: "+write", RunE: func(*cobra.Command, []string) error { return nil }}
|
|
docs.AddCommand(leaf)
|
|
|
|
e := cmdpolicy.New(&platform.Rule{
|
|
Name: "my-readonly-rule",
|
|
Allow: []string{"contact/**"}, // docs/* falls outside, denied
|
|
})
|
|
denied := cmdpolicy.BuildDeniedByPath(root, e.EvaluateAll(root),
|
|
cmdpolicy.ResolveSource{
|
|
Kind: cmdpolicy.SourceYAML,
|
|
Name: "/Users/alice/.lark-cli/policy.yml", // simulate an absolute path
|
|
}, "my-readonly-rule")
|
|
|
|
cmdpolicy.Apply(root, denied)
|
|
err := leaf.RunE(leaf, nil)
|
|
|
|
var ve *errs.ValidationError
|
|
if !errors.As(err, &ve) {
|
|
t.Fatalf("expected denial *errs.ValidationError, got %T %v", err, err)
|
|
}
|
|
// The policy source is folded into the Hint as "yaml" -- the bare
|
|
// kind, never the absolute path.
|
|
if !strings.Contains(ve.Hint, "source yaml") {
|
|
t.Errorf("hint must carry policy_source %q (no path leak), got %q", "yaml", ve.Hint)
|
|
}
|
|
// rule_name carries the disambiguating identifier.
|
|
if !strings.Contains(ve.Hint, "my-readonly-rule") {
|
|
t.Errorf("hint must carry rule_name my-readonly-rule, got %q", ve.Hint)
|
|
}
|
|
// Direct privacy probe: the absolute home path must not appear
|
|
// anywhere in the user-facing message OR hint text.
|
|
if strings.Contains(ve.Message, "/Users/alice") {
|
|
t.Errorf("error message must not leak '/Users/alice', got %q", ve.Message)
|
|
}
|
|
if strings.Contains(ve.Hint, "/Users/alice") {
|
|
t.Errorf("error hint must not leak '/Users/alice', got %q", ve.Hint)
|
|
}
|
|
}
|
|
|
|
// Plugin name IS allowed in policy_source because plugins are in-binary
|
|
// and their names are part of the contract (an integrator debugging a
|
|
// denial wants to know which plugin fired). This test pins that intent
|
|
// so a future change does not silently strip the plugin name too.
|
|
func TestEnvelope_pluginPolicySourceCarriesName(t *testing.T) {
|
|
root := &cobra.Command{Use: "lark-cli"}
|
|
leaf := &cobra.Command{Use: "+block", RunE: func(*cobra.Command, []string) error { return nil }}
|
|
root.AddCommand(leaf)
|
|
|
|
e := cmdpolicy.New(&platform.Rule{
|
|
Name: "secaudit-policy",
|
|
Deny: []string{"+block"},
|
|
})
|
|
denied := cmdpolicy.BuildDeniedByPath(root, e.EvaluateAll(root),
|
|
cmdpolicy.ResolveSource{Kind: cmdpolicy.SourcePlugin, Name: "secaudit"},
|
|
"secaudit-policy")
|
|
cmdpolicy.Apply(root, denied)
|
|
|
|
err := leaf.RunE(leaf, nil)
|
|
var ve *errs.ValidationError
|
|
if !errors.As(err, &ve) {
|
|
t.Fatalf("expected *errs.ValidationError, got %T", err)
|
|
}
|
|
// The plugin name IS surfaced (in-binary, part of the contract): it
|
|
// must appear in the Hint so an integrator debugging a denial knows
|
|
// which plugin fired.
|
|
if !strings.Contains(ve.Hint, "plugin:secaudit") {
|
|
t.Errorf("hint must carry policy_source plugin:secaudit, got %q", ve.Hint)
|
|
}
|
|
}
|