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
209 lines
8.1 KiB
Go
209 lines
8.1 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package cmdpolicy
|
|
|
|
import (
|
|
"github.com/spf13/cobra"
|
|
|
|
"github.com/larksuite/cli/errs"
|
|
"github.com/larksuite/cli/extension/platform"
|
|
)
|
|
|
|
// Apply walks the command tree and installs denyStubs for every path in
|
|
// deniedByPath whose Denial.Layer == "policy". It is the user-layer
|
|
// counterpart to applyStrictModeDenials in cmd/prune.go; both consume the
|
|
// same deniedByPath map produced by the bootstrap pipeline, neither
|
|
// re-evaluates rules.
|
|
//
|
|
// Three things must happen for every denied command (hard-constraints 1-4
|
|
// in the tech doc):
|
|
//
|
|
// 1. cmd.Hidden = true -- removes from help / completion
|
|
// 2. cmd.DisableFlagParsing = true -- denial-wins invariant; otherwise
|
|
// cobra would intercept the call
|
|
// with "missing required flag"
|
|
// before we can return our error
|
|
// 3. cmd.RunE = denyStub(denial) -- returns a typed
|
|
// *errs.ValidationError so
|
|
// cmd/root.go's envelope writer
|
|
// emits structured JSON; the
|
|
// wrapped error chain still
|
|
// exposes *platform.CommandDeniedError
|
|
// via errors.As for in-process
|
|
// consumers
|
|
//
|
|
// Apply must be called once during the Bootstrap pipeline BEFORE
|
|
// cobra.Execute. It mutates the command tree in place and is not safe to
|
|
// call concurrently with command dispatch. Returns the number of commands
|
|
// modified.
|
|
func Apply(root *cobra.Command, deniedByPath map[string]Denial) int {
|
|
if root == nil || len(deniedByPath) == 0 {
|
|
return 0
|
|
}
|
|
|
|
count := 0
|
|
walkTree(root, func(c *cobra.Command) {
|
|
// Never install a denyStub on the binary root itself. Even if the
|
|
// aggregation pass somehow marked it (e.g. all-children-denied at
|
|
// the top), the binary entry point must remain dispatchable so
|
|
// cobra's own help / completion paths still work.
|
|
if !c.HasParent() {
|
|
return
|
|
}
|
|
path := CanonicalPath(c)
|
|
if path == "" {
|
|
return
|
|
}
|
|
d, ok := deniedByPath[path]
|
|
if !ok || d.Layer != LayerPolicy {
|
|
return
|
|
}
|
|
if installDenyStub(c, path, d) {
|
|
count++
|
|
}
|
|
})
|
|
return count
|
|
}
|
|
|
|
// AnnotationDenialLayer / AnnotationDenialSource carry the denial
|
|
// signal to internal/hook through cobra annotations, avoiding an
|
|
// import cycle between hook and cmdpolicy.
|
|
const (
|
|
AnnotationDenialLayer = "lark:policy_denied_layer"
|
|
AnnotationDenialSource = "lark:policy_denied_source"
|
|
|
|
// AnnotationPureGroup marks a cobra.Command that is logically a
|
|
// parent-only group but had a RunE attached by the bootstrap-time
|
|
// unknown-subcommand guard. The engine treats annotated commands
|
|
// the same as un-annotated parent groups (no RunE): they are not
|
|
// evaluated against the Rule, and aggregateParents does not treat
|
|
// them as hybrids.
|
|
//
|
|
// Without this signal, a user enabling a policy.yml with
|
|
// max_risk: read would see every group (`lark-cli drive --help`,
|
|
// `lark-cli docs --help`) return exit 2 + risk_not_annotated,
|
|
// because the guard's RunE flips Runnable()=true and the engine
|
|
// then demands a risk_level annotation on the group itself.
|
|
AnnotationPureGroup = "lark:cmd_pure_group"
|
|
)
|
|
|
|
// IsPureGroup reports whether cmd carries the AnnotationPureGroup marker.
|
|
// Used by the engine to skip evaluation and by the aggregator to treat the
|
|
// command as a parent-only group regardless of cobra's Runnable() answer.
|
|
func IsPureGroup(cmd *cobra.Command) bool {
|
|
if cmd == nil || cmd.Annotations == nil {
|
|
return false
|
|
}
|
|
return cmd.Annotations[AnnotationPureGroup] == "true"
|
|
}
|
|
|
|
// CommandDeniedFromDenial materialises the wrapped error type carried
|
|
// on ExitError.Err so errors.As works for in-process consumers.
|
|
func CommandDeniedFromDenial(path string, d Denial) *platform.CommandDeniedError {
|
|
return &platform.CommandDeniedError{
|
|
Path: path,
|
|
Layer: d.Layer,
|
|
PolicySource: d.PolicySource,
|
|
RuleName: d.RuleName,
|
|
ReasonCode: d.ReasonCode,
|
|
Reason: d.Reason,
|
|
}
|
|
}
|
|
|
|
// BuildDenialError is the default typed error for user-layer denials:
|
|
// Message comes from CommandDeniedError.Error(); the policy layer, source,
|
|
// rule name, and reason code are folded into the Hint. The
|
|
// *platform.CommandDeniedError is preserved as the Cause so errors.As
|
|
// works for in-process consumers.
|
|
func BuildDenialError(path string, d Denial) *errs.ValidationError {
|
|
cd := CommandDeniedFromDenial(path, d)
|
|
return errs.NewValidationError(errs.SubtypeFailedPrecondition, "%s", cd.Error()).
|
|
WithHint("denied by %s policy (source %s, rule %q, reason_code %s); adjust the policy configuration to allow this command",
|
|
cd.Layer, cd.PolicySource, cd.RuleName, cd.ReasonCode).
|
|
WithCause(cd)
|
|
}
|
|
|
|
// installDenyStub mutates a cobra.Command in place. Unlike cmd/prune.go
|
|
// which does RemoveCommand+AddCommand (changing the pointer), we modify
|
|
// the existing node so any external reference (snapshots, alias targets)
|
|
// continues to point at the same cmd.
|
|
//
|
|
// Help fields (cmd.Short / cmd.Long / cmd.Flags()) are deliberately
|
|
// preserved so `--help` on a denied command still describes what the
|
|
// command was intended to do.
|
|
//
|
|
// Two cobra Annotations are set as a denial signal that internal/hook
|
|
// reads (without taking a dependency on this package):
|
|
//
|
|
// - AnnotationDenialLayer -> "policy" or "strict_mode"
|
|
// - AnnotationDenialSource -> the PolicySource ("yaml", "plugin:foo", ...)
|
|
//
|
|
// Returns true when the stub was actually installed and false on the
|
|
// strict-mode early-return so callers can compute an accurate "commands
|
|
// modified" count.
|
|
func installDenyStub(cmd *cobra.Command, path string, d Denial) bool {
|
|
// strict-mode wins over user-layer pruning. If the command was
|
|
// already replaced by a strict-mode stub (cmd/prune.go::strictModeStubFrom
|
|
// writes layer=strict_mode), do NOT overwrite -- the user-layer
|
|
// rule cannot relax or relabel a credential-hard boundary.
|
|
//
|
|
// Behaviour without this guard (pre-fix): a user yaml rule matching
|
|
// a strict-mode stub's path would replace the RunE with the pruning
|
|
// denyStub, hiding the original strict-mode error message AND
|
|
// re-labelling detail.layer from "strict_mode" to "policy".
|
|
if cmd.Annotations != nil &&
|
|
cmd.Annotations[AnnotationDenialLayer] == LayerStrictMode {
|
|
return false
|
|
}
|
|
cmd.Hidden = true
|
|
cmd.DisableFlagParsing = true
|
|
|
|
// Bypass cobra's pre-RunE gates that would otherwise short-circuit
|
|
// before the wrapped RunE (= where observers + denial guard live):
|
|
//
|
|
// 1. Args validator: original commands often declare cobra.NoArgs
|
|
// or a custom Args function. With DisableFlagParsing=true,
|
|
// `--doc xxx` looks like positional args; cobra.ValidateArgs
|
|
// fires BEFORE PersistentPreRunE / PreRunE / RunE and would
|
|
// surface a Cobra usage error instead of our pruning envelope.
|
|
// ArbitraryArgs accepts everything.
|
|
//
|
|
// 2. Parent's PersistentPreRunE: cobra's "first PersistentPreRunE
|
|
// wins" walks UP from the leaf. cmd/auth/auth.go declares a
|
|
// PersistentPreRunE that returns external_provider when env
|
|
// credentials are set; without our leaf-level override, that
|
|
// fires before pruning's RunE and the caller sees the wrong
|
|
// envelope. We set a no-op leaf PersistentPreRunE that just
|
|
// silences usage and returns nil, so dispatch proceeds to the
|
|
// wrapped RunE (which produces the real pruning envelope and
|
|
// lets Before/After observers fire).
|
|
cmd.Args = cobra.ArbitraryArgs
|
|
cmd.PersistentPreRunE = func(c *cobra.Command, _ []string) error {
|
|
c.SilenceUsage = true
|
|
return nil
|
|
}
|
|
cmd.PersistentPreRun = nil
|
|
cmd.PreRunE = nil
|
|
cmd.PreRun = nil
|
|
|
|
if cmd.Annotations == nil {
|
|
cmd.Annotations = map[string]string{}
|
|
}
|
|
cmd.Annotations[AnnotationDenialLayer] = d.Layer
|
|
cmd.Annotations[AnnotationDenialSource] = d.PolicySource
|
|
|
|
denial := d // capture by value for the closure
|
|
cmd.RunE = func(c *cobra.Command, args []string) error {
|
|
// The typed message carries the user-facing semantic ("a command
|
|
// was denied"); the hint carries the layer / source / rule
|
|
// distinction ("policy" vs "strict_mode") for debugging.
|
|
return BuildDenialError(path, denial)
|
|
}
|
|
// Clear any pre-existing Run hook: cobra prefers RunE when both are
|
|
// set, but leaving a stale Run around is a foot-gun for future
|
|
// maintainers.
|
|
cmd.Run = nil
|
|
return true
|
|
}
|