diff --git a/cmd/d8/root.go b/cmd/d8/root.go index 3de206cd..3a4d921f 100644 --- a/cmd/d8/root.go +++ b/cmd/d8/root.go @@ -42,6 +42,7 @@ import ( backup "github.com/deckhouse/deckhouse-cli/internal/backup/cmd" data "github.com/deckhouse/deckhouse-cli/internal/data/cmd" iam "github.com/deckhouse/deckhouse-cli/internal/iam/cmd" + iamuser "github.com/deckhouse/deckhouse-cli/internal/iam/user/cmd" mirror "github.com/deckhouse/deckhouse-cli/internal/mirror/cmd" "github.com/deckhouse/deckhouse-cli/internal/network" status "github.com/deckhouse/deckhouse-cli/internal/status/cmd" @@ -108,6 +109,11 @@ func (r *RootCommand) registerCommands() { r.cmd.AddCommand(mirror.NewCommand()) r.cmd.AddCommand(status.NewCommand()) r.cmd.AddCommand(iam.NewCommand()) + // Backward-compatibility shim for the four UserOperation commands that + // used to live at the top level (d8 user lock|unlock|reset-password|reset-2fa) + // before they moved under d8 iam user. Hidden from help; emits a stderr + // deprecation banner on each invocation pointing to the new path. + r.cmd.AddCommand(iamuser.NewDeprecatedTopLevelCommand()) r.cmd.AddCommand(network.NewCommand()) r.cmd.AddCommand(tools.NewCommand()) r.cmd.AddCommand(commands.NewVirtualizationCommand()) diff --git a/internal/iam/user/cmd/deprecated.go b/internal/iam/user/cmd/deprecated.go new file mode 100644 index 00000000..2a879205 --- /dev/null +++ b/internal/iam/user/cmd/deprecated.go @@ -0,0 +1,150 @@ +/* +Copyright 2026 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package user + +import ( + "fmt" + "strings" + + "github.com/spf13/cobra" + + "github.com/deckhouse/deckhouse-cli/internal/system/flags" + "github.com/deckhouse/deckhouse-cli/internal/utilk8s" +) + +// NewDeprecatedTopLevelCommand returns a hidden top-level "user" command that +// preserves backward compatibility for the four UserOperation-issuing commands +// that existed at the root before the iam refactor: +// +// d8 user lock +// d8 user unlock +// d8 user reset-password (legacy 2-positional form) +// d8 user reset-2fa (alias of reset2fa) +// +// lock / unlock / reset2fa are reused verbatim from the in-package factory +// (newUserOpCommand) so their flags / completion / wait semantics stay +// byte-identical with the new `d8 iam user ...` counterparts. +// +// reset-password is the one shape we cannot reuse: the legacy command took a +// positional bcrypt hash, while the modern iam form expects --password-hash. +// We rebuild the legacy 2-arg signature explicitly here and delegate the +// actual UserOperation wire shape to runUserOperation (the same helper the +// modern command uses), so the only difference is the input-parsing layer. +// +// Other former d8 user surfaces (create / delete / get / list) are NOT +// re-exposed here — they were either added in this PR (no BC obligation) or +// never existed at the root (so there is nothing to break). +func NewDeprecatedTopLevelCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "user", + Short: "Deprecated: use 'd8 iam user' instead", + Hidden: true, + SilenceErrors: true, + SilenceUsage: true, + } + flags.AddPersistentFlags(cmd) + + cmd.AddCommand(newDeprecatedResetPasswordCommand()) + + for _, def := range userOpDefs { + sub := newUserOpCommand(def) + // Use head-of-Use as the canonical name segment for the new-path + // hint; e.g. "lock " -> "lock". + newPath := "d8 iam user " + strings.SplitN(sub.Use, " ", 2)[0] + cmd.AddCommand(deprecateForward(sub, newPath)) + } + + // reset-2fa: hyphenated form is the historical d8 user spelling; we keep + // it as a hidden alias on the underlying reset2fa command so old scripts + // keep working. Cobra resolves aliases identically to Use, so the banner + // (whose new-path hint comes from cmd.Use head segment) still names + // reset2fa, which matches the canonical iam form. + if reset2fa, _, err := cmd.Find([]string{"reset2fa"}); err == nil && reset2fa != nil { + reset2fa.Aliases = append(reset2fa.Aliases, "reset-2fa") + } + + return cmd +} + +// newDeprecatedResetPasswordCommand re-creates the legacy +// +// d8 user reset-password +// +// shape verbatim. The hash is forwarded raw into +// spec.resetPassword.newPasswordHash, matching the pre-refactor behaviour +// (no validation, no base64 wrap, no interactive prompt). Operators who want +// the new prompt / generate / stdin flow are pointed at the modern command +// in the deprecation banner. +func newDeprecatedResetPasswordCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "reset-password ", + Aliases: []string{"resetpass"}, + Short: "Deprecated: use 'd8 iam user reset-password' instead", + Long: "Deprecated. Reset a local user's password in Dex by submitting a UserOperation.\n\nThe second positional argument is a raw bcrypt hash (e.g. produced by `htpasswd -BinC 10`). For interactive prompt, --password-stdin or --generate-password support, use 'd8 iam user reset-password' instead.", + Args: cobra.ExactArgs(2), + ValidArgsFunction: completeUserNames, + SilenceErrors: true, + SilenceUsage: true, + PreRunE: func(cmd *cobra.Command, _ []string) error { + fmt.Fprintln(cmd.ErrOrStderr(), + "Warning: 'd8 user reset-password ' is deprecated; "+ + "please use 'd8 iam user reset-password --password-hash ' instead.") + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + username, bcryptHash := args[0], args[1] + + dyn, err := utilk8s.NewDynamicClient(cmd) + if err != nil { + return err + } + + return runUserOperation(cmd, dyn, userOpRequest{ + NamePrefix: "op-resetpass-", + OpType: "ResetPassword", + User: username, + ExtraSpec: map[string]any{ + "resetPassword": map[string]any{ + "newPasswordHash": bcryptHash, + }, + }, + }) + }, + } + addWaitFlags(cmd) + return cmd +} + +// deprecateForward attaches a stderr deprecation banner to sub, emitted before +// the original PreRunE/RunE chain executes. The banner format is intentionally +// stable so external tooling can grep it. +func deprecateForward(sub *cobra.Command, newPath string) *cobra.Command { + origPre := sub.PreRunE + sub.PreRunE = func(cmd *cobra.Command, args []string) error { + // "d8 user " reflects how the operator invoked the legacy + // command; cmd.Name() is the leaf command name regardless of which + // alias resolved to it. + fmt.Fprintf(cmd.ErrOrStderr(), + "Warning: 'd8 user %s' is deprecated; please use '%s' instead.\n", + cmd.Name(), newPath) + if origPre != nil { + return origPre(cmd, args) + } + return nil + } + return sub +}