Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:22:54 +08:00

118 lines
3.4 KiB
Go

// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package cmdpolicy
import (
"errors"
"fmt"
"os"
"github.com/larksuite/cli/extension/platform"
pyaml "github.com/larksuite/cli/internal/cmdpolicy/yaml"
"github.com/larksuite/cli/internal/vfs"
)
type SourceKind string
const (
SourcePlugin SourceKind = "plugin"
SourceYAML SourceKind = "yaml"
SourceNone SourceKind = "none"
)
type ResolveSource struct {
Kind SourceKind
Name string
}
type PluginRule struct {
PluginName string
Rule *platform.Rule
}
type Sources struct {
PluginRules []PluginRule
YAMLRules []*platform.Rule
YAMLPath string
}
var ErrMultipleRestricts = errors.New("multiple plugins called Restrict; only one plugin may own the policy")
// Resolve picks by precedence: plugin > yaml > none, returning the full
// rule set the winning source contributes. Pure function; load yaml via
// LoadYAMLPolicy first. Every returned rule is validated.
//
// Multi-rule semantics (single owner): one plugin may contribute several
// rules (each a scoped grant, OR-combined by the engine), but two or more
// DISTINCT plugins contributing rules is still a configuration error --
// the resolver aborts so independent plugins cannot silently widen each
// other's policy. yaml may likewise carry several rules under "rules:".
func Resolve(s Sources) ([]*platform.Rule, ResolveSource, error) {
owners := distinctOwners(s.PluginRules)
if len(owners) > 1 {
return nil, ResolveSource{}, fmt.Errorf("%w: %v", ErrMultipleRestricts, owners)
}
if len(s.PluginRules) > 0 {
rules := make([]*platform.Rule, 0, len(s.PluginRules))
for _, pr := range s.PluginRules {
if err := ValidateRule(pr.Rule); err != nil {
return nil, ResolveSource{}, fmt.Errorf("plugin %q rule invalid: %w", pr.PluginName, err)
}
rules = append(rules, pr.Rule)
}
return rules, ResolveSource{Kind: SourcePlugin, Name: owners[0]}, nil
}
if len(s.YAMLRules) > 0 {
for _, r := range s.YAMLRules {
if err := ValidateRule(r); err != nil {
return nil, ResolveSource{}, fmt.Errorf("policy yaml %q: %w", s.YAMLPath, err)
}
}
return s.YAMLRules, ResolveSource{Kind: SourceYAML, Name: s.YAMLPath}, nil
}
return nil, ResolveSource{Kind: SourceNone}, nil
}
// distinctOwners returns the unique plugin names contributing a rule, in
// first-seen order. A single plugin contributing N rules collapses to one
// owner; that is the case the single-owner check below permits.
func distinctOwners(prs []PluginRule) []string {
seen := map[string]bool{}
owners := make([]string, 0, len(prs))
for _, pr := range prs {
if !seen[pr.PluginName] {
seen[pr.PluginName] = true
owners = append(owners, pr.PluginName)
}
}
return owners
}
// LoadYAMLPolicy returns (nil, nil) when path is empty or file is absent,
// so callers can pass the result straight into Sources.YAMLRules. A
// present file yields one or more rules (see yaml.Parse).
func LoadYAMLPolicy(path string) ([]*platform.Rule, error) {
if path == "" {
return nil, nil
}
if _, err := vfs.Stat(path); err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil, nil
}
return nil, fmt.Errorf("stat policy yaml %q: %w", path, err)
}
data, err := vfs.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read policy yaml %q: %w", path, err)
}
rules, err := pyaml.Parse(data)
if err != nil {
return nil, fmt.Errorf("policy yaml %q: %w", path, err)
}
return rules, nil
}