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
181 lines
6.3 KiB
Go
181 lines
6.3 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package config
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"github.com/larksuite/cli/errs"
|
|
"github.com/larksuite/cli/internal/cmdutil"
|
|
"github.com/larksuite/cli/internal/core"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
// NewCmdConfigStrictMode creates the "config strict-mode" subcommand.
|
|
func NewCmdConfigStrictMode(f *cmdutil.Factory) *cobra.Command {
|
|
var global bool
|
|
var reset bool
|
|
|
|
cmd := &cobra.Command{
|
|
Use: "strict-mode [bot|user|off]",
|
|
Short: "View or set strict mode (identity restriction policy)",
|
|
Long: `View or set strict mode — the identity restriction policy.
|
|
|
|
bot only bot identity allowed (user commands hidden)
|
|
user only user identity allowed (bot commands hidden)
|
|
off no restriction (default)
|
|
|
|
No args: show current mode. Switching does NOT require re-bind.
|
|
|
|
For AI agents: this is a security policy. DO NOT switch without
|
|
explicit user confirmation — never run on your own initiative.`,
|
|
Example: ` lark-cli config strict-mode # show current
|
|
lark-cli config strict-mode user # switch (after user confirms)
|
|
lark-cli config strict-mode bot --global # set globally
|
|
lark-cli config strict-mode --reset # clear profile override`,
|
|
Args: cobra.MaximumNArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
multi, err := core.LoadOrNotConfigured()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if reset {
|
|
app := multi.CurrentAppConfig(f.Invocation.Profile)
|
|
if app == nil {
|
|
return core.NoActiveProfileError()
|
|
}
|
|
return resetStrictMode(f, multi, app, global, args)
|
|
}
|
|
if len(args) == 0 {
|
|
app := multi.CurrentAppConfig(f.Invocation.Profile)
|
|
if app == nil {
|
|
return core.NoActiveProfileError()
|
|
}
|
|
return showStrictMode(cmd.Context(), f, multi, app)
|
|
}
|
|
app := multi.CurrentAppConfig(f.Invocation.Profile)
|
|
if !global && app == nil {
|
|
return core.NoActiveProfileError()
|
|
}
|
|
return setStrictMode(f, multi, app, args[0], global)
|
|
},
|
|
}
|
|
|
|
cmd.Flags().BoolVar(&global, "global", false, "set at global level (applies to all profiles)")
|
|
cmd.Flags().BoolVar(&reset, "reset", false, "reset profile setting to inherit global")
|
|
cmdutil.SetRisk(cmd, "write")
|
|
|
|
return cmd
|
|
}
|
|
|
|
func resetStrictMode(f *cmdutil.Factory, multi *core.MultiAppConfig, app *core.AppConfig, global bool, args []string) error {
|
|
if global {
|
|
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--reset cannot be used with --global").WithParam("--reset")
|
|
}
|
|
if len(args) > 0 {
|
|
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--reset cannot be used with a value argument").WithParam("--reset")
|
|
}
|
|
app.StrictMode = nil
|
|
if err := core.SaveMultiAppConfig(multi); err != nil {
|
|
return errs.NewInternalError(errs.SubtypeStorage, "failed to save config: %v", err).WithCause(err)
|
|
}
|
|
fmt.Fprintln(f.IOStreams.ErrOut, "Profile strict-mode reset (inherits global)")
|
|
return nil
|
|
}
|
|
|
|
func showStrictMode(ctx context.Context, f *cmdutil.Factory, multi *core.MultiAppConfig, app *core.AppConfig) error {
|
|
// Runtime effective mode from credential provider chain is the source of truth.
|
|
runtime := f.ResolveStrictMode(ctx)
|
|
configMode, configSource := resolveStrictModeStatus(multi, app)
|
|
|
|
if runtime != configMode {
|
|
fmt.Fprintf(f.IOStreams.Out, "strict-mode: %s (source: credential provider)\n", runtime)
|
|
return nil
|
|
}
|
|
fmt.Fprintf(f.IOStreams.Out, "strict-mode: %s (source: %s)\n", configMode, configSource)
|
|
return nil
|
|
}
|
|
|
|
func setStrictMode(f *cmdutil.Factory, multi *core.MultiAppConfig, app *core.AppConfig, value string, global bool) error {
|
|
mode := core.StrictMode(value)
|
|
switch mode {
|
|
case core.StrictModeBot, core.StrictModeUser, core.StrictModeOff:
|
|
default:
|
|
return errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid value %q, valid values: bot | user | off", value)
|
|
}
|
|
|
|
// Capture the old mode at the SAME scope being changed, so we can warn
|
|
// only when the policy actually expands user-identity at that scope.
|
|
// --global → compare raw multi.StrictMode (profiles with explicit
|
|
// overrides are unaffected; their warning comes from the existing
|
|
// "profile %q has strict-mode explicitly set" notice below).
|
|
// profile → compare effective mode (override > global > default), so
|
|
// a profile flipping from inherited bot to explicit off still warns.
|
|
// The previous version always used the profile's effective mode, which
|
|
// false-positived (--global change while current profile has an explicit
|
|
// override) and false-negatived (--global broadening that doesn't affect
|
|
// the current profile but does affect other inheriting profiles).
|
|
var oldMode core.StrictMode
|
|
if global {
|
|
oldMode = multi.StrictMode
|
|
} else {
|
|
oldMode, _ = resolveStrictModeStatus(multi, app)
|
|
}
|
|
|
|
if global {
|
|
multi.StrictMode = mode
|
|
for _, a := range multi.Apps {
|
|
if a.StrictMode != nil && *a.StrictMode != mode {
|
|
fmt.Fprintf(f.IOStreams.ErrOut,
|
|
"Warning: profile %q has strict-mode explicitly set to %q, "+
|
|
"which overrides the global setting. "+
|
|
"Use --reset in that profile to inherit global.\n",
|
|
a.ProfileName(), *a.StrictMode)
|
|
}
|
|
}
|
|
} else {
|
|
if app == nil {
|
|
return core.NoActiveProfileError()
|
|
}
|
|
app.StrictMode = &mode
|
|
}
|
|
|
|
if err := core.SaveMultiAppConfig(multi); err != nil {
|
|
return errs.NewInternalError(errs.SubtypeStorage, "failed to save config: %v", err).WithCause(err)
|
|
}
|
|
|
|
if oldMode == core.StrictModeBot && (mode == core.StrictModeUser || mode == core.StrictModeOff) {
|
|
fmt.Fprintln(f.IOStreams.ErrOut, "⚠️ "+strictModeRelaxLang(app).IdentityEscalationMessage)
|
|
}
|
|
|
|
scope := "profile"
|
|
if global {
|
|
scope = "global"
|
|
}
|
|
fmt.Fprintf(f.IOStreams.ErrOut, "Strict mode set to %s (%s)\n", mode, scope)
|
|
return nil
|
|
}
|
|
|
|
// strictModeRelaxLang picks the bind-message bundle whose language matches the
|
|
// active profile's Lang setting. Falls back to bindMsgZh when no profile is
|
|
// available (global mutation with no current app).
|
|
func strictModeRelaxLang(app *core.AppConfig) *bindMsg {
|
|
if app != nil {
|
|
return getBindMsg(app.Lang)
|
|
}
|
|
return getBindMsg("")
|
|
}
|
|
|
|
func resolveStrictModeStatus(multi *core.MultiAppConfig, app *core.AppConfig) (core.StrictMode, string) {
|
|
if app != nil && app.StrictMode != nil {
|
|
return *app.StrictMode, fmt.Sprintf("profile %q", app.ProfileName())
|
|
}
|
|
if multi.StrictMode.IsActive() {
|
|
return multi.StrictMode, "global"
|
|
}
|
|
return core.StrictModeOff, "global (default)"
|
|
}
|