mirror of
https://github.com/netbirdio/netbird.git
synced 2026-07-23 08:51:29 +02:00
## Summary Adds a unified `admin` CLI for self-hosted instance administrators in both the management and combined binaries. ## User Management ### `admin user change-password` - Changes a local embedded IdP user's password. - Selects the user with `--email` or `--user-id`. - Reads the new password from `--password` or `--password-file`. - Clears the user's local authentication session so the new password is required on the next login. - **Alias:** `admin user set-password`. ### `admin user reset-mfa` - Resets a local embedded IdP user's MFA enrollment. - Selects the user with `--email` or `--user-id`. - Clears TOTP/WebAuthn enrollment data and removes the local authentication session. - The user will re-enroll MFA on the next login. ## MFA Management ### `admin mfa status` - Shows whether local MFA is enabled in the account settings. - Checks the embedded IdP client configuration and reports whether MFA is enabled there. ### `admin mfa enable` - Enables local MFA for embedded IdP users. - Updates embedded IdP clients and saves the account MFA setting. - Records an audit event on a best-effort basis. ### `admin mfa disable` - Disables local MFA for embedded IdP users. - Updates embedded IdP clients and saves the account MFA setting. - Records an audit event on a best-effort basis. ## Reverse Proxy Tokens ### `admin token create --name <name> [--expires-in <duration>]` - Creates a reverse proxy access token. - Prints the plaintext token once, along with the token ID. - `--expires-in` supports values such as `24h`, `30d`, or `365d`. If omitted, the token never expires. ### `admin token list` - Lists reverse proxy access tokens. - Shows the token ID, name, creation date, expiration, last-used time, and revocation status. - **Alias:** `admin token ls`. ### `admin token revoke <token-id>` - Revokes a reverse proxy access token. - Revoked tokens can no longer authenticate reverse proxy instances. ## Reverse Proxy Management ### `admin proxy disconnect-all` - Lists registered reverse proxy instances and force-marks all connected instances as disconnected. - Useful for repairing stale proxy state after an unclean management server shutdown. - Prompts for confirmation by default. - `--dry-run` previews the changes without applying them. - `--force` skips the confirmation prompt. - Live proxies may appear again after their next heartbeat, reconnect, or re-registration. ## Compatibility Commands ### `token ...` - Deprecated top-level compatibility path. - Behaves the same as `admin token ...`. - Retained so existing scripts using `token create`, `token list`, or `token revoke` continue to work. ## Changes - Adds reusable `management/cmd/admin` command package. - Wires `admin` into `netbird-mgmt` and `combined`. - Adds local user password reset with existing password strength validation. - Adds local MFA enrollment reset by clearing Dex TOTP/WebAuthn credentials and local auth sessions. - Adds local MFA enable/disable/status helpers for embedded IdP deployments. - Moves proxy access token commands under `admin token` for a single admin-focused CLI entry point. - Exports `server.ValidatePassword` for reuse by CLI helpers. ## Tests ```bash go test ./management/cmd/... go test ./management/cmd/admin ./management/cmd ./combined/cmd go test ./management/server -run TestValidatePassword ``` Pre-push lint also passed. ### Checklist - [ ] Is it a bug fix - [ ] Is a typo/documentation fix - [x] Is a feature enhancement - [ ] It is a refactor - [ ] Created tests that fail without the change (if possible) - [ ] This change does **not** modify the public API, gRPC protocols, functionality behavior, CLI / service flags, or introduce a new feature — **OR** I have discussed it with the NetBird team beforehand (link the issue / Slack thread in the description). See [CONTRIBUTING.md](https://github.com/netbirdio/netbird/blob/main/CONTRIBUTING.md#discuss-changes-with-the-netbird-team-first). > By submitting this pull request, you confirm that you have read and agree to the terms of the [Contributor License Agreement](https://github.com/netbirdio/netbird/blob/main/CONTRIBUTOR_LICENSE_AGREEMENT.md). ## Documentation Select exactly one: - [x] I added/updated documentation for this change - [ ] Documentation is **not needed** for this change (explain why) ### Docs PR URL (required if "docs added" is checked) Paste the PR link from https://github.com/netbirdio/docs here: https://github.com/netbirdio/docs/pull/832 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Release Notes * **New Features** * Added self-hosted admin CLI commands for changing passwords, resetting MFA (including WebAuthn), and managing embedded IdP client MFA (enable/disable/status). * Introduced a unified admin command entry point and improved data-directory handling for embedded IdP storage. * **Refactor** * Centralized password strength validation into a shared exported validator. * **Tests** * Added a comprehensive admin command test suite covering password input, selectors, MFA reset, and client MFA state handling. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
142 lines
4.7 KiB
Go
142 lines
4.7 KiB
Go
// Package proxycmd provides reusable cobra commands for managing reverse proxy instances.
|
|
// Both the management and combined binaries use these commands, each providing
|
|
// their own StoreOpener to handle config loading and store initialization.
|
|
package proxycmd
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
"text/tabwriter"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
rpproxy "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/proxy"
|
|
"github.com/netbirdio/netbird/management/server/store"
|
|
)
|
|
|
|
// StoreOpener initializes a store from the command context and calls fn.
|
|
type StoreOpener func(cmd *cobra.Command, fn func(ctx context.Context, s store.Store) error) error
|
|
|
|
const disconnectAllConfirmation = "disconnect all proxies"
|
|
|
|
// NewCommands creates the proxy command tree with the given store opener.
|
|
// Returns the parent "proxy" command with the disconnect-all subcommand.
|
|
func NewCommands(opener StoreOpener) *cobra.Command {
|
|
var dryRun bool
|
|
var force bool
|
|
|
|
proxyCmd := &cobra.Command{
|
|
Use: "proxy",
|
|
Short: "Manage reverse proxy instances",
|
|
Long: "Commands for inspecting and repairing the reverse proxy instances registered with the management server.",
|
|
}
|
|
|
|
disconnectAllCmd := &cobra.Command{
|
|
Use: "disconnect-all",
|
|
Short: "Force-mark all reverse proxy instances as disconnected",
|
|
Long: "Lists all reverse proxy instances and force-marks them as disconnected, regardless of their session state. " +
|
|
"Use this to repair stale connection state, e.g. after an unclean management server shutdown. " +
|
|
"By default, it asks for manual confirmation before changing state. Use --dry-run to preview without changing state, or --force to skip confirmation. " +
|
|
"Run during a maintenance window; affected live proxies may stay hidden until their next heartbeat or reconnect/re-register.",
|
|
Args: cobra.NoArgs,
|
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
|
return opener(cmd, func(ctx context.Context, s store.Store) error {
|
|
return runDisconnectAll(ctx, s, cmd.OutOrStdout(), cmd.InOrStdin(), dryRun, force)
|
|
})
|
|
},
|
|
}
|
|
disconnectAllCmd.Flags().BoolVar(&dryRun, "dry-run", false, "List reverse proxy instances that would be disconnected without changing state")
|
|
disconnectAllCmd.Flags().BoolVar(&force, "force", false, "Skip the confirmation prompt and apply the repair")
|
|
|
|
proxyCmd.AddCommand(disconnectAllCmd)
|
|
return proxyCmd
|
|
}
|
|
|
|
func runDisconnectAll(ctx context.Context, s store.Store, out io.Writer, in io.Reader, dryRun, force bool) error {
|
|
proxies, err := s.GetAllProxies(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("list proxies: %w", err)
|
|
}
|
|
|
|
if len(proxies) == 0 {
|
|
_, _ = fmt.Fprintln(out, "No reverse proxy instances found.")
|
|
return nil
|
|
}
|
|
|
|
toDisconnect := 0
|
|
w := tabwriter.NewWriter(out, 0, 0, 2, ' ', 0)
|
|
_, _ = fmt.Fprintln(w, "ID\tCLUSTER\tIP\tACCOUNT\tSTATUS\tLAST SEEN")
|
|
_, _ = fmt.Fprintln(w, "--\t-------\t--\t-------\t------\t---------")
|
|
|
|
for _, p := range proxies {
|
|
if p.Status != rpproxy.StatusDisconnected {
|
|
toDisconnect++
|
|
}
|
|
|
|
account := "-"
|
|
if p.AccountID != nil {
|
|
account = *p.AccountID
|
|
}
|
|
|
|
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n",
|
|
p.ID,
|
|
p.ClusterAddress,
|
|
p.IPAddress,
|
|
account,
|
|
p.Status,
|
|
p.LastSeen.Format("2006-01-02 15:04:05"),
|
|
)
|
|
}
|
|
if err := w.Flush(); err != nil {
|
|
return fmt.Errorf("write proxy list: %w", err)
|
|
}
|
|
|
|
if dryRun {
|
|
_, _ = fmt.Fprintf(out, "\nDry run: would force-mark %d of %d reverse proxy instance(s) as disconnected.\n", toDisconnect, len(proxies))
|
|
return nil
|
|
}
|
|
|
|
if !force {
|
|
confirmed, err := confirmDisconnectAll(out, in)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !confirmed {
|
|
_, _ = fmt.Fprintln(out, "Aborted. No reverse proxy instances were changed.")
|
|
return nil
|
|
}
|
|
}
|
|
|
|
disconnected, err := s.DisconnectAllProxies(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("disconnect proxies: %w", err)
|
|
}
|
|
|
|
_, _ = fmt.Fprintf(out, "\nForce-marked %d of %d reverse proxy instance(s) as disconnected.\n", disconnected, len(proxies))
|
|
return nil
|
|
}
|
|
|
|
func confirmDisconnectAll(out io.Writer, in io.Reader) (bool, error) {
|
|
if in == nil {
|
|
in = strings.NewReader("")
|
|
}
|
|
|
|
_, _ = fmt.Fprintln(out, "\nWARNING: This command changes stored reverse proxy state for every non-disconnected instance.")
|
|
_, _ = fmt.Fprintln(out, "Run it during a maintenance window; affected live proxies may stay hidden until "+
|
|
"their next heartbeat or reconnect/re-register.")
|
|
_, _ = fmt.Fprintf(out, "Type %q to continue: ", disconnectAllConfirmation)
|
|
|
|
scanner := bufio.NewScanner(in)
|
|
if !scanner.Scan() {
|
|
if err := scanner.Err(); err != nil {
|
|
return false, fmt.Errorf("read confirmation: %w", err)
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
return strings.EqualFold(strings.TrimSpace(scanner.Text()), disconnectAllConfirmation), nil
|
|
}
|