mirror of
https://github.com/netbirdio/netbird.git
synced 2026-07-16 11:39:57 +00:00
* Restores behavior to create profile if not there on Up * Allows to restore nerbird status showing of the profile name * [client] Reduce upFunc cognitive complexity Extract the profile switch/auto-create logic from upFunc into a dedicated switchOrCreateProfile helper. The inlined NotFound-retry branch pushed upFunc over SonarCloud's cognitive complexity threshold (S3776). No behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * [client] Make up --profile auto-create idempotent under concurrent runs Don't fail switchOrCreateProfile on a createProfile error: a concurrent run may create the profile between the NotFound check and our create call. Retry the switch regardless and only surface the create error if the switch also fails. Addresses CodeRabbit race-condition feedback. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Share createProfile with addProfileFunc * But allow conn reusage * moves switchOrCreateProfile to where it's used --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
394 lines
11 KiB
Go
394 lines
11 KiB
Go
package cmd
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"os/user"
|
|
"runtime"
|
|
"strings"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
"github.com/spf13/cobra"
|
|
"golang.org/x/term"
|
|
"google.golang.org/grpc/codes"
|
|
gstatus "google.golang.org/grpc/status"
|
|
|
|
"github.com/netbirdio/netbird/client/internal"
|
|
"github.com/netbirdio/netbird/client/internal/auth"
|
|
"github.com/netbirdio/netbird/client/internal/profilemanager"
|
|
"github.com/netbirdio/netbird/client/proto"
|
|
"github.com/netbirdio/netbird/client/system"
|
|
"github.com/netbirdio/netbird/util"
|
|
)
|
|
|
|
func init() {
|
|
loginCmd.PersistentFlags().BoolVar(&noBrowser, noBrowserFlag, false, noBrowserDesc)
|
|
loginCmd.PersistentFlags().BoolVar(&showQR, showQRFlag, false, showQRDesc)
|
|
loginCmd.PersistentFlags().StringVar(&profileName, profileNameFlag, "", profileNameDesc)
|
|
loginCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "", "(DEPRECATED) Netbird config file location")
|
|
}
|
|
|
|
var loginCmd = &cobra.Command{
|
|
Use: "login",
|
|
Short: "Log in to the NetBird network",
|
|
Long: "Log in to the NetBird network using a setup key or SSO",
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
if err := setEnvAndFlags(cmd); err != nil {
|
|
return fmt.Errorf("set env and flags: %v", err)
|
|
}
|
|
|
|
ctx := internal.CtxInitState(context.Background())
|
|
|
|
if hostName != "" {
|
|
// nolint
|
|
ctx = context.WithValue(ctx, system.DeviceNameCtxKey, hostName)
|
|
}
|
|
username, err := user.Current()
|
|
if err != nil {
|
|
return fmt.Errorf("get current user: %v", err)
|
|
}
|
|
|
|
pm := profilemanager.NewProfileManager()
|
|
|
|
activeProf, err := getActiveProfile(cmd.Context(), pm, profileName, username.Username)
|
|
if err != nil {
|
|
return fmt.Errorf("get active profile: %v", err)
|
|
}
|
|
|
|
providedSetupKey, err := getSetupKey()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// workaround to run without service
|
|
if util.FindFirstLogPath(logFiles) == "" {
|
|
if err := doForegroundLogin(ctx, cmd, providedSetupKey, activeProf); err != nil {
|
|
return fmt.Errorf("foreground login failed: %v", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if err := doDaemonLogin(ctx, cmd, providedSetupKey, activeProf, username.Username, pm); err != nil {
|
|
return fmt.Errorf("daemon login failed: %v", err)
|
|
}
|
|
|
|
cmd.Println("Logging successfully")
|
|
|
|
return nil
|
|
},
|
|
}
|
|
|
|
func doDaemonLogin(ctx context.Context, cmd *cobra.Command, providedSetupKey string, activeProf *profilemanager.Profile, username string, pm *profilemanager.ProfileManager) error {
|
|
conn, err := DialClientGRPCServer(ctx, daemonAddr)
|
|
if err != nil {
|
|
//nolint
|
|
return fmt.Errorf("failed to connect to daemon error: %v\n"+
|
|
"If the daemon is not running please run: "+
|
|
"\nnetbird service install \nnetbird service start\n", err)
|
|
}
|
|
defer conn.Close()
|
|
|
|
client := proto.NewDaemonServiceClient(conn)
|
|
|
|
var dnsLabelsReq []string
|
|
if dnsLabelsValidated != nil {
|
|
dnsLabelsReq = dnsLabelsValidated.ToSafeStringList()
|
|
}
|
|
|
|
handle := activeProf.ID.String()
|
|
|
|
loginRequest := proto.LoginRequest{
|
|
SetupKey: providedSetupKey,
|
|
ManagementUrl: managementURL,
|
|
IsUnixDesktopClient: isUnixRunningDesktop(),
|
|
Hostname: hostName,
|
|
DnsLabels: dnsLabelsReq,
|
|
ProfileName: &handle,
|
|
Username: &username,
|
|
}
|
|
|
|
profileState, err := pm.GetProfileState(activeProf.ID)
|
|
if err != nil {
|
|
log.Debugf("failed to get profile state for login hint: %v", err)
|
|
} else if profileState.Email != "" {
|
|
loginRequest.Hint = &profileState.Email
|
|
}
|
|
|
|
if rootCmd.PersistentFlags().Changed(preSharedKeyFlag) {
|
|
loginRequest.OptionalPreSharedKey = &preSharedKey
|
|
}
|
|
|
|
var loginErr error
|
|
|
|
var loginResp *proto.LoginResponse
|
|
|
|
err = WithBackOff(func() error {
|
|
var backOffErr error
|
|
loginResp, backOffErr = client.Login(ctx, &loginRequest)
|
|
if s, ok := gstatus.FromError(backOffErr); ok && (s.Code() == codes.InvalidArgument ||
|
|
s.Code() == codes.PermissionDenied ||
|
|
s.Code() == codes.NotFound ||
|
|
s.Code() == codes.Unimplemented) {
|
|
loginErr = backOffErr
|
|
return nil
|
|
}
|
|
return backOffErr
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("login backoff cycle failed: %v", err)
|
|
}
|
|
|
|
if loginErr != nil {
|
|
return fmt.Errorf("login failed: %v", loginErr)
|
|
}
|
|
|
|
if loginResp.NeedsSSOLogin {
|
|
if err := handleSSOLogin(ctx, cmd, loginResp, client, pm); err != nil {
|
|
return fmt.Errorf("sso login failed: %v", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func getActiveProfile(ctx context.Context, pm *profilemanager.ProfileManager, profileName string, username string) (*profilemanager.Profile, error) {
|
|
// switch profile if provided
|
|
|
|
if profileName != "" {
|
|
if err := switchProfileOnDaemon(ctx, pm, profileName, username); err != nil {
|
|
return nil, fmt.Errorf("switch profile: %v", err)
|
|
}
|
|
}
|
|
|
|
activeProf, err := pm.GetActiveProfile()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get active profile: %v", err)
|
|
}
|
|
|
|
if activeProf == nil {
|
|
return nil, fmt.Errorf("active profile not found, please run 'netbird profile create' first")
|
|
}
|
|
return activeProf, nil
|
|
}
|
|
|
|
func switchProfileOnDaemon(ctx context.Context, pm *profilemanager.ProfileManager, handle string, username string) error {
|
|
resolvedID, err := switchProfile(ctx, handle, username)
|
|
if err != nil {
|
|
return fmt.Errorf("switch profile on daemon: %v", err)
|
|
}
|
|
|
|
if err := pm.SwitchProfile(resolvedID); err != nil {
|
|
return fmt.Errorf("switch profile: %v", err)
|
|
}
|
|
|
|
conn, err := DialClientGRPCServer(ctx, daemonAddr)
|
|
if err != nil {
|
|
log.Errorf("failed to connect to service CLI interface %v", err)
|
|
return err
|
|
}
|
|
defer conn.Close()
|
|
|
|
client := proto.NewDaemonServiceClient(conn)
|
|
|
|
status, err := client.Status(ctx, &proto.StatusRequest{})
|
|
if err != nil {
|
|
return fmt.Errorf("unable to get daemon status: %v", err)
|
|
}
|
|
|
|
if status.Status == string(internal.StatusConnected) {
|
|
if _, err := client.Down(ctx, &proto.DownRequest{}); err != nil {
|
|
log.Errorf("call service down method: %v", err)
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// switchProfile asks the daemon to switch to the profile identified by
|
|
// handle (a name, ID, or unique ID prefix). Returns the resolved profile
|
|
// ID so the caller can update the local active-profile state without
|
|
// re-resolving the handle.
|
|
func switchProfile(ctx context.Context, handle string, username string) (profilemanager.ID, error) {
|
|
conn, err := DialClientGRPCServer(ctx, daemonAddr)
|
|
if err != nil {
|
|
//nolint
|
|
return "", fmt.Errorf("failed to connect to daemon error: %v\n"+
|
|
"If the daemon is not running please run: "+
|
|
"\nnetbird service install \nnetbird service start\n", err)
|
|
}
|
|
defer conn.Close()
|
|
|
|
client := proto.NewDaemonServiceClient(conn)
|
|
|
|
resp, err := client.SwitchProfile(ctx, &proto.SwitchProfileRequest{
|
|
ProfileName: &handle,
|
|
Username: &username,
|
|
})
|
|
if err != nil {
|
|
return "", fmt.Errorf("switch profile failed: %w", err)
|
|
}
|
|
|
|
return profilemanager.ID(resp.Id), nil
|
|
}
|
|
|
|
func doForegroundLogin(ctx context.Context, cmd *cobra.Command, setupKey string, activeProf *profilemanager.Profile) error {
|
|
|
|
err := handleRebrand(cmd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// update host's static platform and system information
|
|
system.UpdateStaticInfoAsync()
|
|
|
|
configFilePath, err := activeProf.FilePath()
|
|
if err != nil {
|
|
return fmt.Errorf("get active profile file path: %v", err)
|
|
|
|
}
|
|
|
|
config, err := profilemanager.ReadConfig(configFilePath)
|
|
if err != nil {
|
|
return fmt.Errorf("read config file %s: %v", configFilePath, err)
|
|
}
|
|
|
|
err = foregroundLogin(ctx, cmd, config, setupKey, activeProf.ID)
|
|
if err != nil {
|
|
return fmt.Errorf("foreground login failed: %v", err)
|
|
}
|
|
cmd.Println("Logging successfully")
|
|
return nil
|
|
}
|
|
|
|
func handleSSOLogin(ctx context.Context, cmd *cobra.Command, loginResp *proto.LoginResponse, client proto.DaemonServiceClient, pm *profilemanager.ProfileManager) error {
|
|
openURL(cmd, loginResp.VerificationURIComplete, loginResp.UserCode, noBrowser, showQR)
|
|
|
|
resp, err := client.WaitSSOLogin(ctx, &proto.WaitSSOLoginRequest{UserCode: loginResp.UserCode, Hostname: hostName})
|
|
if err != nil {
|
|
return fmt.Errorf("waiting sso login failed with: %v", err)
|
|
}
|
|
|
|
if resp.Email != "" {
|
|
err = pm.SetActiveProfileState(&profilemanager.ProfileState{
|
|
Email: resp.Email,
|
|
})
|
|
if err != nil {
|
|
log.Warnf("failed to set active profile email: %v", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func foregroundLogin(ctx context.Context, cmd *cobra.Command, config *profilemanager.Config, setupKey string, profileID profilemanager.ID) error {
|
|
authClient, err := auth.NewAuth(ctx, config.PrivateKey, config.ManagementURL, config)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create auth client: %v", err)
|
|
}
|
|
defer authClient.Close()
|
|
|
|
needsLogin, err := authClient.IsLoginRequired(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("check login required: %v", err)
|
|
}
|
|
|
|
jwtToken := ""
|
|
if setupKey == "" && needsLogin {
|
|
tokenInfo, err := foregroundGetTokenInfo(ctx, cmd, config, profileID)
|
|
if err != nil {
|
|
return fmt.Errorf("interactive sso login failed: %v", err)
|
|
}
|
|
jwtToken = tokenInfo.GetTokenToUse()
|
|
}
|
|
|
|
err, _ = authClient.Login(ctx, setupKey, jwtToken)
|
|
if err != nil {
|
|
return fmt.Errorf("login failed: %v", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func foregroundGetTokenInfo(ctx context.Context, cmd *cobra.Command, config *profilemanager.Config, profileID profilemanager.ID) (*auth.TokenInfo, error) {
|
|
hint := ""
|
|
pm := profilemanager.NewProfileManager()
|
|
profileState, err := pm.GetProfileState(profileID)
|
|
if err != nil {
|
|
log.Debugf("failed to get profile state for login hint: %v", err)
|
|
} else if profileState.Email != "" {
|
|
hint = profileState.Email
|
|
}
|
|
|
|
oAuthFlow, err := auth.NewOAuthFlow(ctx, config, isUnixRunningDesktop(), false, hint)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
flowInfo, err := oAuthFlow.RequestAuthInfo(context.TODO())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("getting a request OAuth flow info failed: %v", err)
|
|
}
|
|
|
|
openURL(cmd, flowInfo.VerificationURIComplete, flowInfo.UserCode, noBrowser, showQR)
|
|
|
|
tokenInfo, err := oAuthFlow.WaitToken(context.TODO(), flowInfo)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("waiting for browser login failed: %v", err)
|
|
}
|
|
|
|
return &tokenInfo, nil
|
|
}
|
|
|
|
func openURL(cmd *cobra.Command, verificationURIComplete, userCode string, noBrowser, showQR bool) {
|
|
var codeMsg string
|
|
if userCode != "" && !strings.Contains(verificationURIComplete, userCode) {
|
|
codeMsg = fmt.Sprintf("and enter the code %s to authenticate.", userCode)
|
|
}
|
|
|
|
if noBrowser {
|
|
cmd.Println("Use this URL to log in:\n\n" + verificationURIComplete + " " + codeMsg)
|
|
} else {
|
|
cmd.Println("Please do the SSO login in your browser. \n" +
|
|
"If your browser didn't open automatically, use this URL to log in:\n\n" +
|
|
verificationURIComplete + " " + codeMsg)
|
|
}
|
|
|
|
if showQR {
|
|
if f, ok := cmd.OutOrStdout().(*os.File); ok && term.IsTerminal(int(f.Fd())) {
|
|
printQRCode(f, verificationURIComplete)
|
|
}
|
|
}
|
|
|
|
cmd.Println("")
|
|
|
|
if !noBrowser {
|
|
if err := util.OpenBrowser(verificationURIComplete); err != nil {
|
|
cmd.Println("\nAlternatively, you may want to use a setup key, see:\n\n" +
|
|
"https://docs.netbird.io/how-to/register-machines-using-setup-keys")
|
|
}
|
|
}
|
|
}
|
|
|
|
// isUnixRunningDesktop checks if a Linux OS is running desktop environment
|
|
func isUnixRunningDesktop() bool {
|
|
if runtime.GOOS != "linux" && runtime.GOOS != "freebsd" {
|
|
return false
|
|
}
|
|
return os.Getenv("DESKTOP_SESSION") != "" || os.Getenv("XDG_CURRENT_DESKTOP") != ""
|
|
}
|
|
|
|
func setEnvAndFlags(cmd *cobra.Command) error {
|
|
SetFlagsFromEnvVars(rootCmd)
|
|
|
|
cmd.SetOut(cmd.OutOrStdout())
|
|
|
|
err := util.InitLog(logLevel, "console")
|
|
if err != nil {
|
|
return fmt.Errorf("failed initializing log %v", err)
|
|
}
|
|
|
|
return nil
|
|
}
|