mirror of
https://github.com/netbirdio/netbird.git
synced 2026-07-23 17:01: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 -->
178 lines
6.0 KiB
Go
178 lines
6.0 KiB
Go
package cmd
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"path"
|
|
"path/filepath"
|
|
|
|
"github.com/dexidp/dex/storage"
|
|
log "github.com/sirupsen/logrus"
|
|
"github.com/spf13/cobra"
|
|
|
|
"github.com/netbirdio/netbird/formatter/hook"
|
|
admincmd "github.com/netbirdio/netbird/management/cmd/admin"
|
|
tokencmd "github.com/netbirdio/netbird/management/cmd/token"
|
|
nbconfig "github.com/netbirdio/netbird/management/internals/server/config"
|
|
"github.com/netbirdio/netbird/management/server/activity"
|
|
activitystore "github.com/netbirdio/netbird/management/server/activity/store"
|
|
"github.com/netbirdio/netbird/management/server/store"
|
|
"github.com/netbirdio/netbird/util"
|
|
)
|
|
|
|
var adminDatadir string
|
|
|
|
// newAdminCommands creates the admin command tree with management-specific resource openers.
|
|
func newAdminCommands() *cobra.Command {
|
|
cmd := admincmd.NewCommands(admincmd.Openers{
|
|
Resources: withAdminResources,
|
|
Store: withAdminStoreOnly,
|
|
IDP: withAdminIDPOnly,
|
|
})
|
|
cmd.PersistentFlags().StringVar(&adminDatadir, "datadir", "", "Override the data directory from config (used for store.db and the default idp.db)")
|
|
return cmd
|
|
}
|
|
|
|
func newLegacyTokenCommand() *cobra.Command {
|
|
cmd := tokencmd.NewCommands(tokencmd.StoreOpener(withAdminStoreOnly))
|
|
cmd.Deprecated = "use 'admin token' instead"
|
|
cmd.PersistentFlags().StringVar(&nbconfig.MgmtConfigPath, "config", defaultMgmtConfig, "Netbird config file location")
|
|
return cmd
|
|
}
|
|
|
|
// withAdminResources initializes logging, loads config, opens the management store
|
|
// and embedded IdP storage, and calls fn.
|
|
func withAdminResources(cmd *cobra.Command, fn func(ctx context.Context, resources admincmd.Resources) error) error {
|
|
return withAdminConfig(cmd, true, func(ctx context.Context, config *nbconfig.Config, datadir string) error {
|
|
managementStore, err := openAdminStore(ctx, config, datadir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer admincmd.CloseStore(ctx, managementStore)
|
|
|
|
idpStorage, idpStorageFile, err := admincmd.OpenIDPStorage(config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer admincmd.CloseIDPStorage(idpStorage)
|
|
|
|
eventStore, esErr := openAdminEventStore(ctx, config, datadir)
|
|
if esErr != nil {
|
|
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Warning: audit events will not be recorded: %v\n", esErr)
|
|
}
|
|
if eventStore != nil {
|
|
defer func() {
|
|
if err := eventStore.Close(ctx); err != nil {
|
|
log.Debugf("close activity event store: %v", err)
|
|
}
|
|
}()
|
|
}
|
|
|
|
return fn(ctx, admincmd.Resources{Store: managementStore, IDPStorage: idpStorage, IDPStorageFile: idpStorageFile, EventStore: eventStore})
|
|
})
|
|
}
|
|
|
|
// withAdminStoreOnly opens only the management store for admin subcommands that do not
|
|
// need embedded IdP storage.
|
|
func withAdminStoreOnly(cmd *cobra.Command, fn func(ctx context.Context, s store.Store) error) error {
|
|
return withAdminConfig(cmd, false, func(ctx context.Context, config *nbconfig.Config, datadir string) error {
|
|
managementStore, err := openAdminStore(ctx, config, datadir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer admincmd.CloseStore(ctx, managementStore)
|
|
|
|
return fn(ctx, managementStore)
|
|
})
|
|
}
|
|
|
|
func withAdminIDPOnly(cmd *cobra.Command, fn func(ctx context.Context, idpStorage storage.Storage, storageFile string) error) error {
|
|
return withAdminConfig(cmd, true, func(ctx context.Context, config *nbconfig.Config, _ string) error {
|
|
idpStorage, idpStorageFile, err := admincmd.OpenIDPStorage(config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer admincmd.CloseIDPStorage(idpStorage)
|
|
|
|
return fn(ctx, idpStorage, idpStorageFile)
|
|
})
|
|
}
|
|
|
|
func withAdminConfig(cmd *cobra.Command, applyIDPDefaults bool, fn func(ctx context.Context, config *nbconfig.Config, datadir string) error) error {
|
|
if err := util.InitLog("error", "console"); err != nil {
|
|
return fmt.Errorf("init log: %w", err)
|
|
}
|
|
|
|
ctx := context.WithValue(cmd.Context(), hook.ExecutionContextKey, hook.SystemSource) //nolint:staticcheck
|
|
|
|
config, datadir, err := loadAdminMgmtConfig(ctx, applyIDPDefaults)
|
|
if err != nil {
|
|
return fmt.Errorf("load config: %w", err)
|
|
}
|
|
|
|
return fn(ctx, config, datadir)
|
|
}
|
|
|
|
func loadAdminMgmtConfig(ctx context.Context, applyIDPDefaults bool) (*nbconfig.Config, string, error) {
|
|
config := &nbconfig.Config{}
|
|
if _, err := util.ReadJsonWithEnvSub(nbconfig.MgmtConfigPath, config); err != nil {
|
|
return nil, "", err
|
|
}
|
|
|
|
if applyIDPDefaults {
|
|
if err := ApplyEmbeddedIdPConfig(ctx, config); err != nil {
|
|
return nil, "", err
|
|
}
|
|
}
|
|
|
|
datadir := config.Datadir
|
|
applyAdminDatadirOverride(config, &datadir)
|
|
return config, datadir, nil
|
|
}
|
|
|
|
func applyAdminDatadirOverride(config *nbconfig.Config, datadir *string) {
|
|
if adminDatadir == "" {
|
|
return
|
|
}
|
|
|
|
oldDatadir := *datadir
|
|
*datadir = adminDatadir
|
|
if config.EmbeddedIdP != nil && config.EmbeddedIdP.Storage.Type == "sqlite3" && isDefaultIDPStorageFile(config.EmbeddedIdP.Storage.Config.File, oldDatadir) {
|
|
config.EmbeddedIdP.Storage.Config.File = filepath.Join(*datadir, "idp.db")
|
|
}
|
|
}
|
|
|
|
func isDefaultIDPStorageFile(file, datadir string) bool {
|
|
if file == "" {
|
|
return true
|
|
}
|
|
defaultFile := filepath.Join(datadir, "idp.db")
|
|
legacyDefaultFile := path.Join(datadir, "idp.db")
|
|
legacySlashDefaultFile := path.Join(filepath.ToSlash(datadir), "idp.db")
|
|
return filepath.Clean(file) == filepath.Clean(defaultFile) ||
|
|
file == legacyDefaultFile ||
|
|
filepath.ToSlash(file) == legacySlashDefaultFile
|
|
}
|
|
|
|
func openAdminStore(ctx context.Context, config *nbconfig.Config, datadir string) (store.Store, error) {
|
|
managementStore, err := store.NewStore(ctx, config.StoreConfig.Engine, datadir, nil, true)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create store: %w", err)
|
|
}
|
|
return managementStore, nil
|
|
}
|
|
|
|
func openAdminEventStore(ctx context.Context, config *nbconfig.Config, datadir string) (activity.Store, error) {
|
|
if config.DataStoreEncryptionKey == "" {
|
|
return nil, fmt.Errorf("data store encryption key is not configured")
|
|
}
|
|
eventStore, err := activitystore.NewSqlStore(ctx, datadir, config.DataStoreEncryptionKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("open activity event store: %w", err)
|
|
}
|
|
if eventStore == nil {
|
|
return nil, fmt.Errorf("open activity event store: returned nil store")
|
|
}
|
|
return eventStore, nil
|
|
}
|