Prechádzať zdrojové kódy

Merge pull request #3568 from gravitl/master

Master
Abhishek K 1 mesiac pred
rodič
commit
3a173bb4b2

+ 86 - 2
controllers/server.go

@@ -3,6 +3,7 @@ package controller
 import (
 	"encoding/json"
 	"errors"
+	"github.com/google/go-cmp/cmp"
 	"net/http"
 	"os"
 	"strings"
@@ -275,11 +276,11 @@ func updateSettings(w http.ResponseWriter, r *http.Request) {
 	currSettings := logic.GetServerSettings()
 	err := logic.UpsertServerSettings(req)
 	if err != nil {
-		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("failed to udpate server settings "+err.Error()), "internal"))
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("failed to update server settings "+err.Error()), "internal"))
 		return
 	}
 	logic.LogEvent(&models.Event{
-		Action: models.Update,
+		Action: identifySettingsUpdateAction(currSettings, req),
 		Source: models.Subject{
 			ID:   r.Header.Get("user"),
 			Name: r.Header.Get("user"),
@@ -324,7 +325,90 @@ func reInit(curr, new models.ServerSettings, force bool) {
 		}
 	}
 	go mq.PublishPeerUpdate(false)
+}
+
+func identifySettingsUpdateAction(old, new models.ServerSettings) models.Action {
+	// TODO: here we are relying on the dashboard to only
+	// make singular updates, but it's possible that the
+	// API can be called to make multiple changes to the
+	// server settings. We should update it to log multiple
+	// events or create singular update APIs.
+	if old.MFAEnforced != new.MFAEnforced {
+		if new.MFAEnforced {
+			return models.EnforceMFA
+		} else {
+			return models.UnenforceMFA
+		}
+	}
+
+	if old.BasicAuth != new.BasicAuth {
+		if new.BasicAuth {
+			return models.EnableBasicAuth
+		} else {
+			return models.DisableBasicAuth
+		}
+	}
+
+	if old.Telemetry != new.Telemetry {
+		if new.Telemetry == "off" {
+			return models.DisableTelemetry
+		} else {
+			return models.EnableTelemetry
+		}
+	}
+
+	if old.NetclientAutoUpdate != new.NetclientAutoUpdate ||
+		old.RacRestrictToSingleNetwork != new.RacRestrictToSingleNetwork ||
+		old.ManageDNS != new.ManageDNS ||
+		old.DefaultDomain != new.DefaultDomain ||
+		old.EndpointDetection != new.EndpointDetection {
+		return models.UpdateClientSettings
+	}
+
+	if old.AllowedEmailDomains != new.AllowedEmailDomains ||
+		old.JwtValidityDuration != new.JwtValidityDuration {
+		return models.UpdateAuthenticationSecuritySettings
+	}
+
+	if old.Verbosity != new.Verbosity ||
+		old.MetricsPort != new.MetricsPort ||
+		old.MetricInterval != new.MetricInterval ||
+		old.AuditLogsRetentionPeriodInDays != new.AuditLogsRetentionPeriodInDays {
+		return models.UpdateMonitoringAndDebuggingSettings
+	}
+
+	if old.Theme != new.Theme {
+		return models.UpdateDisplaySettings
+	}
+
+	if old.TextSize != new.TextSize ||
+		old.ReducedMotion != new.ReducedMotion {
+		return models.UpdateAccessibilitySettings
+	}
+
+	if old.EmailSenderAddr != new.EmailSenderAddr ||
+		old.EmailSenderUser != new.EmailSenderUser ||
+		old.EmailSenderPassword != new.EmailSenderPassword ||
+		old.SmtpHost != new.SmtpHost ||
+		old.SmtpPort != new.SmtpPort {
+		return models.UpdateSMTPSettings
+	}
+
+	if old.AuthProvider != new.AuthProvider ||
+		old.OIDCIssuer != new.OIDCIssuer ||
+		old.ClientID != new.ClientID ||
+		old.ClientSecret != new.ClientSecret ||
+		old.SyncEnabled != new.SyncEnabled ||
+		old.IDPSyncInterval != new.IDPSyncInterval ||
+		old.GoogleAdminEmail != new.GoogleAdminEmail ||
+		old.GoogleSACredsJson != new.GoogleSACredsJson ||
+		old.AzureTenant != new.AzureTenant ||
+		!cmp.Equal(old.GroupFilters, new.GroupFilters) ||
+		cmp.Equal(old.UserFilters, new.UserFilters) {
+		return models.UpdateIDPSettings
+	}
 
+	return models.Update
 }
 
 // @Summary     Get feature flags for this server.

+ 124 - 33
controllers/user.go

@@ -7,6 +7,7 @@ import (
 	"errors"
 	"fmt"
 	"github.com/pquerna/otp"
+	"golang.org/x/crypto/bcrypt"
 	"image/png"
 	"net/http"
 	"reflect"
@@ -38,6 +39,7 @@ func userHandlers(r *mux.Router) {
 	r.HandleFunc("/api/users/adm/transfersuperadmin/{username}", logic.SecurityCheck(true, http.HandlerFunc(transferSuperAdmin))).
 		Methods(http.MethodPost)
 	r.HandleFunc("/api/users/adm/authenticate", authenticateUser).Methods(http.MethodPost)
+	r.HandleFunc("/api/users/{username}/validate-identity", logic.SecurityCheck(false, logic.ContinueIfUserMatch(http.HandlerFunc(validateUserIdentity)))).Methods(http.MethodPost)
 	r.HandleFunc("/api/users/{username}/auth/init-totp", logic.SecurityCheck(false, logic.ContinueIfUserMatch(http.HandlerFunc(initiateTOTPSetup)))).Methods(http.MethodPost)
 	r.HandleFunc("/api/users/{username}/auth/complete-totp", logic.SecurityCheck(false, logic.ContinueIfUserMatch(http.HandlerFunc(completeTOTPSetup)))).Methods(http.MethodPost)
 	r.HandleFunc("/api/users/{username}/auth/verify-totp", logic.PreAuthCheck(logic.ContinueIfUserMatch(http.HandlerFunc(verifyTOTP)))).Methods(http.MethodPost)
@@ -312,38 +314,6 @@ func authenticateUser(response http.ResponseWriter, request *http.Request) {
 			logic.ReturnErrorResponse(response, request, logic.FormatError(errors.New("access denied to dashboard"), "unauthorized"))
 			return
 		}
-		// log user activity
-		logic.LogEvent(&models.Event{
-			Action: models.Login,
-			Source: models.Subject{
-				ID:   user.UserName,
-				Name: user.UserName,
-				Type: models.UserSub,
-			},
-			TriggeredBy: user.UserName,
-			Target: models.Subject{
-				ID:   models.DashboardSub.String(),
-				Name: models.DashboardSub.String(),
-				Type: models.DashboardSub,
-			},
-			Origin: models.Dashboard,
-		})
-	} else {
-		logic.LogEvent(&models.Event{
-			Action: models.Login,
-			Source: models.Subject{
-				ID:   user.UserName,
-				Name: user.UserName,
-				Type: models.UserSub,
-			},
-			TriggeredBy: user.UserName,
-			Target: models.Subject{
-				ID:   models.ClientAppSub.String(),
-				Name: models.ClientAppSub.String(),
-				Type: models.ClientAppSub,
-			},
-			Origin: models.ClientApp,
-		})
 	}
 
 	username := authRequest.UserName
@@ -397,6 +367,44 @@ func authenticateUser(response http.ResponseWriter, request *http.Request) {
 		return
 	}
 	logger.Log(2, username, "was authenticated")
+
+	// log user activity
+	if !user.IsMFAEnabled {
+		if val := request.Header.Get("From-Ui"); val == "true" {
+			logic.LogEvent(&models.Event{
+				Action: models.Login,
+				Source: models.Subject{
+					ID:   user.UserName,
+					Name: user.UserName,
+					Type: models.UserSub,
+				},
+				TriggeredBy: user.UserName,
+				Target: models.Subject{
+					ID:   models.DashboardSub.String(),
+					Name: models.DashboardSub.String(),
+					Type: models.DashboardSub,
+				},
+				Origin: models.Dashboard,
+			})
+		} else {
+			logic.LogEvent(&models.Event{
+				Action: models.Login,
+				Source: models.Subject{
+					ID:   user.UserName,
+					Name: user.UserName,
+					Type: models.UserSub,
+				},
+				TriggeredBy: user.UserName,
+				Target: models.Subject{
+					ID:   models.ClientAppSub.String(),
+					Name: models.ClientAppSub.String(),
+					Type: models.ClientAppSub,
+				},
+				Origin: models.ClientApp,
+			})
+		}
+	}
+
 	response.Header().Set("Content-Type", "application/json")
 	response.Write(successJSONResponse)
 
@@ -438,6 +446,43 @@ func authenticateUser(response http.ResponseWriter, request *http.Request) {
 	}()
 }
 
+// @Summary     Validates a user's identity against it's token. This is used by UI before a user performing a critical operation to validate the user's identity.
+// @Router      /api/users/{username}/validate-identity [post]
+// @Tags        Auth
+// @Accept      json
+// @Param       body body models.UserIdentityValidationRequest true "User Identity Validation Request"
+// @Success     200 {object} models.SuccessResponse
+// @Failure     400 {object} models.ErrorResponse
+func validateUserIdentity(w http.ResponseWriter, r *http.Request) {
+	username := r.Header.Get("user")
+
+	var req models.UserIdentityValidationRequest
+	err := json.NewDecoder(r.Body).Decode(&req)
+	if err != nil {
+		logger.Log(0, "failed to decode request body: ", err.Error())
+		err = fmt.Errorf("invalid request body: %v", err)
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+
+	user, err := logic.GetUser(username)
+	if err != nil {
+		logger.Log(0, "failed to get user: ", err.Error())
+		err = fmt.Errorf("user not found: %v", err)
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+
+	var resp models.UserIdentityValidationResponse
+	err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password))
+	if err != nil {
+		logic.ReturnSuccessResponseWithJson(w, r, resp, "user identity validation failed")
+	} else {
+		resp.IdentityValidated = true
+		logic.ReturnSuccessResponseWithJson(w, r, resp, "user identity validated")
+	}
+}
+
 // @Summary     Initiate setting up TOTP 2FA for a user.
 // @Router      /api/users/auth/init-totp [post]
 // @Tags        Auth
@@ -561,6 +606,22 @@ func completeTOTPSetup(w http.ResponseWriter, r *http.Request) {
 			return
 		}
 
+		logic.LogEvent(&models.Event{
+			Action: models.EnableMFA,
+			Source: models.Subject{
+				ID:   user.UserName,
+				Name: user.UserName,
+				Type: models.UserSub,
+			},
+			TriggeredBy: user.UserName,
+			Target: models.Subject{
+				ID:   user.UserName,
+				Name: user.UserName,
+				Type: models.UserSub,
+			},
+			Origin: models.Dashboard,
+		})
+
 		logic.ReturnSuccessResponse(w, r, fmt.Sprintf("totp setup complete for user %s", username))
 	} else {
 		err = fmt.Errorf("cannot setup totp for user %s: invalid otp", username)
@@ -628,6 +689,22 @@ func verifyTOTP(w http.ResponseWriter, r *http.Request) {
 			return
 		}
 
+		logic.LogEvent(&models.Event{
+			Action: models.Login,
+			Source: models.Subject{
+				ID:   user.UserName,
+				Name: user.UserName,
+				Type: models.UserSub,
+			},
+			TriggeredBy: user.UserName,
+			Target: models.Subject{
+				ID:   models.DashboardSub.String(),
+				Name: models.DashboardSub.String(),
+				Type: models.DashboardSub,
+			},
+			Origin: models.Dashboard,
+		})
+
 		logic.ReturnSuccessResponseWithJson(w, r, models.SuccessfulUserLoginResponse{
 			UserName:  username,
 			AuthToken: jwt,
@@ -1144,8 +1221,22 @@ func updateUser(w http.ResponseWriter, r *http.Request) {
 			UserName: logic.MasterUser,
 		}
 	}
+	action := models.Update
+	// TODO: here we are relying on the dashboard to only
+	// make singular updates, but it's possible that the
+	// API can be called to make multiple changes to the
+	// user. We should update it to log multiple events
+	// or create singular update APIs.
+	if userchange.IsMFAEnabled != user.IsMFAEnabled {
+		if userchange.IsMFAEnabled {
+			// the update API won't be used to enable MFA.
+			action = models.EnableMFA
+		} else {
+			action = models.DisableMFA
+		}
+	}
 	e := models.Event{
-		Action: models.Update,
+		Action: action,
 		Source: models.Subject{
 			ID:   caller.UserName,
 			Name: caller.UserName,

+ 1 - 1
logic/extpeers.go

@@ -484,7 +484,7 @@ func GetAllExtClientsWithStatus(status models.NodeStatus) ([]models.ExtClient, e
 	var validExtClients []models.ExtClient
 	for _, extClient := range extClients {
 		node := extClient.ConvertToStaticNode()
-		GetNodeCheckInStatus(&node, false)
+		GetNodeStatus(&node, false)
 
 		if node.Status == status {
 			validExtClients = append(validExtClients, extClient)

+ 9 - 0
logic/settings.go

@@ -33,6 +33,11 @@ func UpsertServerSettings(s models.ServerSettings) error {
 	if s.ClientSecret == Mask() {
 		s.ClientSecret = currSettings.ClientSecret
 	}
+
+	if servercfg.DeployedByOperator() {
+		s.BasicAuth = true
+	}
+
 	data, err := json.Marshal(s)
 	if err != nil {
 		return err
@@ -330,6 +335,10 @@ func GetManageDNS() bool {
 
 // IsBasicAuthEnabled - checks if basic auth has been configured to be turned off
 func IsBasicAuthEnabled() bool {
+	if servercfg.DeployedByOperator() {
+		return true
+	}
+
 	return GetServerSettings().BasicAuth
 }
 

+ 6 - 3
logic/zombie.go

@@ -139,7 +139,10 @@ func ManageZombies(ctx context.Context, peerUpdate chan *models.Node) {
 			if servercfg.IsAutoCleanUpEnabled() {
 				nodes, _ := GetAllNodes()
 				for _, node := range nodes {
-					if time.Since(node.LastCheckIn) > time.Minute*ZOMBIE_DELETE_TIME {
+					if !node.Connected {
+						continue
+					}
+					if time.Since(node.LastCheckIn) > time.Hour*2 {
 						if err := DeleteNode(&node, true); err != nil {
 							continue
 						}
@@ -168,8 +171,8 @@ func checkPendingRemovalNodes(peerUpdate chan *models.Node) {
 			peerUpdate <- &node
 			continue
 		}
-		if servercfg.IsAutoCleanUpEnabled() {
-			if time.Since(node.LastCheckIn) > time.Minute*ZOMBIE_DELETE_TIME {
+		if servercfg.IsAutoCleanUpEnabled() && node.Connected {
+			if time.Since(node.LastCheckIn) > time.Hour*2 {
 				if err := DeleteNode(&node, true); err != nil {
 					continue
 				}

+ 12 - 6
migrate/migrate.go

@@ -536,21 +536,27 @@ func migrateToEgressV1() {
 	}
 	for _, node := range nodes {
 		if node.IsEgressGateway {
-			egressHost, err := logic.GetHost(node.HostID.String())
+			_, err := logic.GetHost(node.HostID.String())
 			if err != nil {
 				continue
 			}
-			for _, rangeI := range node.EgressGatewayRequest.Ranges {
-				e := schema.Egress{
+			for _, rangeMetric := range node.EgressGatewayRequest.RangesWithMetric {
+				e := &schema.Egress{Range: rangeMetric.Network}
+				if err := e.DoesEgressRouteExists(db.WithContext(context.TODO())); err == nil {
+					e.Nodes[node.ID.String()] = rangeMetric.RouteMetric
+					e.Update(db.WithContext(context.TODO()))
+					continue
+				}
+				e = &schema.Egress{
 					ID:          uuid.New().String(),
-					Name:        fmt.Sprintf("%s egress", egressHost.Name),
+					Name:        fmt.Sprintf("%s egress", rangeMetric.Network),
 					Description: "",
 					Network:     node.Network,
 					Nodes: datatypes.JSONMap{
-						node.ID.String(): 256,
+						node.ID.String(): rangeMetric.RouteMetric,
 					},
 					Tags:      make(datatypes.JSONMap),
-					Range:     rangeI,
+					Range:     rangeMetric.Network,
 					Nat:       node.EgressGatewayRequest.NatEnabled == "yes",
 					Status:    true,
 					CreatedBy: user.UserName,

+ 30 - 15
models/events.go

@@ -3,21 +3,36 @@ package models
 type Action string
 
 const (
-	Create            Action = "CREATE"
-	Update            Action = "UPDATE"
-	Delete            Action = "DELETE"
-	DeleteAll         Action = "DELETE_ALL"
-	Login             Action = "LOGIN"
-	LogOut            Action = "LOGOUT"
-	Connect           Action = "CONNECT"
-	Sync              Action = "SYNC"
-	RefreshKey        Action = "REFRESH_KEY"
-	RefreshAllKeys    Action = "REFRESH_ALL_KEYS"
-	SyncAll           Action = "SYNC_ALL"
-	UpgradeAll        Action = "UPGRADE_ALL"
-	Disconnect        Action = "DISCONNECT"
-	JoinHostToNet     Action = "JOIN_HOST_TO_NETWORK"
-	RemoveHostFromNet Action = "REMOVE_HOST_FROM_NETWORK"
+	Create                               Action = "CREATE"
+	Update                               Action = "UPDATE"
+	Delete                               Action = "DELETE"
+	DeleteAll                            Action = "DELETE_ALL"
+	Login                                Action = "LOGIN"
+	LogOut                               Action = "LOGOUT"
+	Connect                              Action = "CONNECT"
+	Sync                                 Action = "SYNC"
+	RefreshKey                           Action = "REFRESH_KEY"
+	RefreshAllKeys                       Action = "REFRESH_ALL_KEYS"
+	SyncAll                              Action = "SYNC_ALL"
+	UpgradeAll                           Action = "UPGRADE_ALL"
+	Disconnect                           Action = "DISCONNECT"
+	JoinHostToNet                        Action = "JOIN_HOST_TO_NETWORK"
+	RemoveHostFromNet                    Action = "REMOVE_HOST_FROM_NETWORK"
+	EnableMFA                            Action = "ENABLE_MFA"
+	DisableMFA                           Action = "DISABLE_MFA"
+	EnforceMFA                           Action = "ENFORCE_MFA"
+	UnenforceMFA                         Action = "UNENFORCE_MFA"
+	EnableBasicAuth                      Action = "ENABLE_BASIC_AUTH"
+	DisableBasicAuth                     Action = "DISABLE_BASIC_AUTH"
+	EnableTelemetry                      Action = "ENABLE_TELEMETRY"
+	DisableTelemetry                     Action = "DISABLE_TELEMETRY"
+	UpdateClientSettings                 Action = "UPDATE_CLIENT_SETTINGS"
+	UpdateAuthenticationSecuritySettings Action = "UPDATE_AUTHENTICATION_SECURITY_SETTINGS"
+	UpdateMonitoringAndDebuggingSettings Action = "UPDATE_MONITORING_AND_DEBUGGING_SETTINGS"
+	UpdateDisplaySettings                Action = "UPDATE_DISPLAY_SETTINGS"
+	UpdateAccessibilitySettings          Action = "UPDATE_ACCESSIBILITY_SETTINGS"
+	UpdateSMTPSettings                   Action = "UPDATE_EMAIL_SETTINGS"
+	UpdateIDPSettings                    Action = "UPDATE_IDP_SETTINGS"
 )
 
 type SubjectType string

+ 10 - 0
models/user_mgmt.go

@@ -202,6 +202,16 @@ type UserAuthParams struct {
 	Password string `json:"password"`
 }
 
+// UserIdentityValidationRequest - user identity validation request struct
+type UserIdentityValidationRequest struct {
+	Password string `json:"password"`
+}
+
+// UserIdentityValidationResponse - user identity validation response struct
+type UserIdentityValidationResponse struct {
+	IdentityValidated bool `json:"identity_validated"`
+}
+
 type UserTOTPVerificationParams struct {
 	OTPAuthURL          string `json:"otp_auth_url"`
 	OTPAuthURLSignature string `json:"otp_auth_url_signature"`

+ 4 - 0
schema/egress.go

@@ -50,6 +50,10 @@ func (e *Egress) UpdateEgressStatus(ctx context.Context) error {
 	}).Error
 }
 
+func (e *Egress) DoesEgressRouteExists(ctx context.Context) error {
+	return db.FromContext(ctx).Table(e.Table()).Where("range = ?", e.Range).First(&e).Error
+}
+
 func (e *Egress) Create(ctx context.Context) error {
 	return db.FromContext(ctx).Table(e.Table()).Create(&e).Error
 }

+ 4 - 0
servercfg/serverconf.go

@@ -646,6 +646,10 @@ func GetEmqxRestEndpoint() string {
 
 // IsBasicAuthEnabled - checks if basic auth has been configured to be turned off
 func IsBasicAuthEnabled() bool {
+	if DeployedByOperator() {
+		return true
+	}
+
 	var enabled = true //default
 	if os.Getenv("BASIC_AUTH") != "" {
 		enabled = os.Getenv("BASIC_AUTH") == "yes"