diff --git a/auth/host_session.go b/auth/host_session.go index 6e01f7e9..128ad2c6 100644 --- a/auth/host_session.go +++ b/auth/host_session.go @@ -77,7 +77,7 @@ func SessionHandler(conn *websocket.Conn) { _, err := logic.VerifyAuthRequest(models.UserAuthParams{ UserName: registerMessage.User, Password: registerMessage.Password, - }) + }, logic.NetclientApp) if err != nil { err = conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) if err != nil { diff --git a/config/config.go b/config/config.go index f39f77c3..e4bf7543 100644 --- a/config/config.go +++ b/config/config.go @@ -89,6 +89,7 @@ type ServerConfig struct { DeployedByOperator bool `yaml:"deployed_by_operator"` Environment string `yaml:"environment"` JwtValidityDuration time.Duration `yaml:"jwt_validity_duration" swaggertype:"primitive,integer" format:"int64"` + JwtValidityDurationClients time.Duration `yaml:"jwt_validity_duration_clients" swaggertype:"primitive,integer" format:"int64"` RacRestrictToSingleNetwork bool `yaml:"rac_restrict_to_single_network"` CacheEnabled string `yaml:"caching_enabled"` EndpointDetection bool `yaml:"endpoint_detection"` diff --git a/controllers/controller.go b/controllers/controller.go index 3713d373..18f0c082 100644 --- a/controllers/controller.go +++ b/controllers/controller.go @@ -55,6 +55,7 @@ func HandleRESTRequests(wg *sync.WaitGroup, ctx context.Context) { "Content-Type", "authorization", "From-Ui", + "X-Application-Name", }, ) originsOk := handlers.AllowedOrigins(strings.Split(servercfg.GetAllowedOrigin(), ",")) diff --git a/controllers/server.go b/controllers/server.go index 916a4256..eec9ed06 100644 --- a/controllers/server.go +++ b/controllers/server.go @@ -3,6 +3,7 @@ package controller import ( "encoding/json" "errors" + "github.com/google/go-cmp/cmp" "net/http" "os" "strings" @@ -56,6 +57,7 @@ func serverHandlers(r *mux.Router) { Methods(http.MethodPost) r.HandleFunc("/api/server/mem_profile", logic.SecurityCheck(false, http.HandlerFunc(memProfile))). Methods(http.MethodPost) + r.HandleFunc("/api/server/feature_flags", getFeatureFlags).Methods(http.MethodGet) } func cpuProfile(w http.ResponseWriter, r *http.Request) { @@ -274,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"), @@ -323,5 +325,97 @@ 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. +// @Router /api/server/feature_flags [get] +// @Tags Server +// @Security oauth2 +// @Success 200 {object} config.ServerSettings +func getFeatureFlags(w http.ResponseWriter, r *http.Request) { + logic.ReturnSuccessResponseWithJson(w, r, logic.GetFeatureFlags(), "") } diff --git a/controllers/user.go b/controllers/user.go index 37e24618..cf15eedf 100644 --- a/controllers/user.go +++ b/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) @@ -253,6 +255,10 @@ func deleteUserAccessTokens(w http.ResponseWriter, r *http.Request) { // @Failure 401 {object} models.ErrorResponse // @Failure 500 {object} models.ErrorResponse func authenticateUser(response http.ResponseWriter, request *http.Request) { + appName := request.Header.Get("X-Application-Name") + if appName == "" { + appName = logic.NetmakerDesktopApp + } // Auth request consists of Mac Address and Password (from node that is authorizing // in case of Master, auth is ignored and mac is set to "mastermac" @@ -308,42 +314,10 @@ 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 - jwt, err := logic.VerifyAuthRequest(authRequest) + jwt, err := logic.VerifyAuthRequest(authRequest, appName) if err != nil { logger.Log(0, username, "user validation failed: ", err.Error()) @@ -393,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) @@ -434,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 @@ -557,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) @@ -576,6 +641,11 @@ func completeTOTPSetup(w http.ResponseWriter, r *http.Request) { func verifyTOTP(w http.ResponseWriter, r *http.Request) { username := r.Header.Get("user") + appName := r.Header.Get("X-Application-Name") + if appName == "" { + appName = logic.NetmakerDesktopApp + } + var req models.UserTOTPVerificationParams err := json.NewDecoder(r.Body).Decode(&req) if err != nil { @@ -601,7 +671,7 @@ func verifyTOTP(w http.ResponseWriter, r *http.Request) { } if totp.Validate(req.TOTP, user.TOTPSecret) { - jwt, err := logic.CreateUserJWT(user.UserName, user.PlatformRoleID) + jwt, err := logic.CreateUserJWT(user.UserName, user.PlatformRoleID, appName) if err != nil { err = fmt.Errorf("error creating token: %v", err) logger.Log(0, err.Error()) @@ -619,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, @@ -1135,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, @@ -1233,6 +1333,14 @@ func deleteUser(w http.ResponseWriter, r *http.Request) { return } } + + if user.AuthType == models.OAuth || user.ExternalIdentityProviderID != "" { + err = fmt.Errorf("cannot delete idp user %s", username) + logger.Log(0, username, "failed to delete user: ", err.Error()) + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest")) + return + } + err = logic.DeleteUser(username) if err != nil { logger.Log(0, username, diff --git a/go.mod b/go.mod index 75fe52a4..306d8349 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ toolchain go1.23.7 require ( github.com/blang/semver v3.5.1+incompatible github.com/eclipse/paho.mqtt.golang v1.5.0 - github.com/go-playground/validator/v10 v10.26.0 + github.com/go-playground/validator/v10 v10.27.0 github.com/golang-jwt/jwt/v4 v4.5.2 github.com/google/uuid v1.6.0 github.com/gorilla/handlers v1.5.2 @@ -49,9 +49,9 @@ require ( github.com/matryer/is v1.4.1 github.com/pquerna/otp v1.5.0 github.com/spf13/cobra v1.9.1 - google.golang.org/api v0.238.0 + google.golang.org/api v0.240.0 gopkg.in/mail.v2 v2.3.1 - gorm.io/datatypes v1.2.5 + gorm.io/datatypes v1.2.6 gorm.io/driver/postgres v1.6.0 gorm.io/driver/sqlite v1.6.0 gorm.io/gorm v1.30.0 diff --git a/go.sum b/go.sum index fcb77592..6be815c9 100644 --- a/go.sum +++ b/go.sum @@ -39,8 +39,8 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= -github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= +github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= @@ -176,8 +176,8 @@ golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.zx2c4.com/wireguard/wgctrl v0.0.0-20221104135756-97bc4ad4a1cb h1:9aqVcYEDHmSNb0uOWukxV5lHV09WqiSiCuhEgWNETLY= golang.zx2c4.com/wireguard/wgctrl v0.0.0-20221104135756-97bc4ad4a1cb/go.mod h1:mQqgjkW8GQQcJQsbBvK890TKqUK1DfKWkuBGbOkuMHQ= -google.golang.org/api v0.238.0 h1:+EldkglWIg/pWjkq97sd+XxH7PxakNYoe/rkSTbnvOs= -google.golang.org/api v0.238.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50= +google.golang.org/api v0.240.0 h1:PxG3AA2UIqT1ofIzWV2COM3j3JagKTKSwy7L6RHNXNU= +google.golang.org/api v0.240.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50= google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 h1:1tXaIXCracvtsRxSBsYDiSBN0cuJvM7QYW+MrpIRY78= google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:49MsLSx0oWMOZqcpB3uL8ZOkAh1+TndpJ8ONoCBWiZk= google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2 h1:vPV0tzlsK6EzEDHNNH5sa7Hs9bd7iXR7B1tSiPepkV0= @@ -198,16 +198,16 @@ gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gorm.io/datatypes v1.2.5 h1:9UogU3jkydFVW1bIVVeoYsTpLRgwDVW3rHfJG6/Ek9I= -gorm.io/datatypes v1.2.5/go.mod h1:I5FUdlKpLb5PMqeMQhm30CQ6jXP8Rj89xkTeCSAaAD4= +gorm.io/datatypes v1.2.6 h1:KafLdXvFUhzNeL2ncm03Gl3eTLONQfNKZ+wJ+9Y4Nck= +gorm.io/datatypes v1.2.6/go.mod h1:M2iO+6S3hhi4nAyYe444Pcb0dcIiOMJ7QHaUXxyiNZY= gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8= gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= -gorm.io/driver/sqlserver v1.5.4 h1:xA+Y1KDNspv79q43bPyjDMUgHoYHLhXYmdFcYPobg8g= -gorm.io/driver/sqlserver v1.5.4/go.mod h1:+frZ/qYmuna11zHPlh5oc2O6ZA/lS88Keb0XSH1Zh/g= +gorm.io/driver/sqlserver v1.6.0 h1:VZOBQVsVhkHU/NzNhRJKoANt5pZGQAS1Bwc6m6dgfnc= +gorm.io/driver/sqlserver v1.6.0/go.mod h1:WQzt4IJo/WHKnckU9jXBLMJIVNMVeTu25dnOzehntWw= gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs= gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= diff --git a/logic/acls.go b/logic/acls.go index 80ba5cfb..3bdbc34c 100644 --- a/logic/acls.go +++ b/logic/acls.go @@ -655,12 +655,12 @@ var IsAclPolicyValid = func(acl models.Acl) (err error) { var IsPeerAllowed = func(node, peer models.Node, checkDefaultPolicy bool) bool { var nodeId, peerId string - if node.IsGw && peer.IsRelayed && peer.RelayedBy == node.ID.String() { - return true - } - if peer.IsGw && node.IsRelayed && node.RelayedBy == peer.ID.String() { - return true - } + // if node.IsGw && peer.IsRelayed && peer.RelayedBy == node.ID.String() { + // return true + // } + // if peer.IsGw && node.IsRelayed && node.RelayedBy == peer.ID.String() { + // return true + // } if node.IsStatic { nodeId = node.StaticNode.ClientID node = node.StaticNode.ConvertToStaticNode() diff --git a/logic/auth.go b/logic/auth.go index c8c0709a..d7ad94d8 100644 --- a/logic/auth.go +++ b/logic/auth.go @@ -24,6 +24,12 @@ const ( auth_key = "netmaker_auth" ) +const ( + DashboardApp = "dashboard" + NetclientApp = "netclient" + NetmakerDesktopApp = "netmaker-desktop" +) + var ( superUser = models.User{} ) @@ -178,7 +184,8 @@ func CreateUser(user *models.User) error { user.AuthType = models.OAuth } AddGlobalNetRolesToAdmins(user) - _, err = CreateUserJWT(user.UserName, user.PlatformRoleID) + // create user will always be called either from API or Dashboard. + _, err = CreateUserJWT(user.UserName, user.PlatformRoleID, DashboardApp) if err != nil { logger.Log(0, "failed to generate token", err.Error()) return err @@ -212,7 +219,7 @@ func CreateSuperAdmin(u *models.User) error { } // VerifyAuthRequest - verifies an auth request -func VerifyAuthRequest(authRequest models.UserAuthParams) (string, error) { +func VerifyAuthRequest(authRequest models.UserAuthParams, appName string) (string, error) { var result models.User if authRequest.UserName == "" { return "", errors.New("username can't be empty") @@ -245,7 +252,7 @@ func VerifyAuthRequest(authRequest models.UserAuthParams) (string, error) { return tokenString, nil } else { // Create a new JWT for the node - tokenString, err := CreateUserJWT(authRequest.UserName, result.PlatformRoleID) + tokenString, err := CreateUserJWT(authRequest.UserName, result.PlatformRoleID, appName) if err != nil { slog.Error("error creating jwt", "error", err) return "", err @@ -483,8 +490,9 @@ func GetState(state string) (*models.SsoState, error) { } // SetState - sets a state with new expiration -func SetState(state string) error { +func SetState(appName, state string) error { s := models.SsoState{ + AppName: appName, Value: state, Expiration: time.Now().Add(models.DefaultExpDuration), } diff --git a/logic/extpeers.go b/logic/extpeers.go index 926ffee8..71af0276 100644 --- a/logic/extpeers.go +++ b/logic/extpeers.go @@ -465,7 +465,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) diff --git a/logic/hosts.go b/logic/hosts.go index 6689d8bd..26e20293 100644 --- a/logic/hosts.go +++ b/logic/hosts.go @@ -4,6 +4,9 @@ import ( "context" "errors" "fmt" + "os" + "reflect" + "github.com/google/uuid" "github.com/gravitl/netmaker/converters" "github.com/gravitl/netmaker/database" @@ -15,7 +18,6 @@ import ( "golang.org/x/crypto/bcrypt" "golang.org/x/exp/slog" "gorm.io/gorm" - "os" ) var ( @@ -219,6 +221,10 @@ func UpdateHostFromClient(newHost, currHost *models.Host) (sendPeerUpdate bool) sendPeerUpdate = true isEndpointChanged = true } + if !reflect.DeepEqual(currHost.Interfaces, newHost.Interfaces) { + currHost.Interfaces = newHost.Interfaces + sendPeerUpdate = true + } if isEndpointChanged { for _, nodeID := range currHost.Nodes { diff --git a/logic/jwts.go b/logic/jwts.go index f34014a0..e2e508a0 100644 --- a/logic/jwts.go +++ b/logic/jwts.go @@ -83,9 +83,13 @@ func CreateUserAccessJwtToken(username string, role models.UserRoleID, d time.Ti } // CreateUserJWT - creates a user jwt token -func CreateUserJWT(username string, role models.UserRoleID) (response string, err error) { - settings := GetServerSettings() - expirationTime := time.Now().Add(time.Duration(settings.JwtValidityDuration) * time.Minute) +func CreateUserJWT(username string, role models.UserRoleID, appName string) (response string, err error) { + duration := GetJwtValidityDuration() + if appName == NetclientApp || appName == NetmakerDesktopApp { + duration = GetJwtValidityDurationForClients() + } + + expirationTime := time.Now().Add(duration) claims := &models.UserClaims{ UserName: username, Role: role, diff --git a/logic/server.go b/logic/server.go index 00202026..f6897d3c 100644 --- a/logic/server.go +++ b/logic/server.go @@ -1,7 +1,12 @@ package logic +import "github.com/gravitl/netmaker/models" + // EnterpriseCheckFuncs - can be set to run functions for EE var EnterpriseCheckFuncs []func() +var GetFeatureFlags = func() models.FeatureFlags { + return models.FeatureFlags{} +} // == Join, Checkin, and Leave for Server == diff --git a/logic/settings.go b/logic/settings.go index 632e5c39..8428be21 100644 --- a/logic/settings.go +++ b/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 @@ -46,22 +51,28 @@ func UpsertServerSettings(s models.ServerSettings) error { func ValidateNewSettings(req models.ServerSettings) bool { // TODO: add checks for different fields + if req.JwtValidityDuration > 525600 || req.JwtValidityDuration < 5 { + return false + } return true } func GetServerSettingsFromEnv() (s models.ServerSettings) { s = models.ServerSettings{ - NetclientAutoUpdate: servercfg.AutoUpdateEnabled(), - Verbosity: servercfg.GetVerbosity(), - AuthProvider: os.Getenv("AUTH_PROVIDER"), - OIDCIssuer: os.Getenv("OIDC_ISSUER"), - ClientID: os.Getenv("CLIENT_ID"), - ClientSecret: os.Getenv("CLIENT_SECRET"), - AzureTenant: servercfg.GetAzureTenant(), - Telemetry: servercfg.Telemetry(), - BasicAuth: servercfg.IsBasicAuthEnabled(), - JwtValidityDuration: servercfg.GetJwtValidityDurationFromEnv() / 60, + NetclientAutoUpdate: servercfg.AutoUpdateEnabled(), + Verbosity: servercfg.GetVerbosity(), + AuthProvider: os.Getenv("AUTH_PROVIDER"), + OIDCIssuer: os.Getenv("OIDC_ISSUER"), + ClientID: os.Getenv("CLIENT_ID"), + ClientSecret: os.Getenv("CLIENT_SECRET"), + AzureTenant: servercfg.GetAzureTenant(), + Telemetry: servercfg.Telemetry(), + BasicAuth: servercfg.IsBasicAuthEnabled(), + JwtValidityDuration: servercfg.GetJwtValidityDurationFromEnv() / 60, + // setting client's jwt validity duration to be the same as that of + // dashboard. + JwtValidityDurationClients: servercfg.GetJwtValidityDurationFromEnv() / 60, RacRestrictToSingleNetwork: servercfg.GetRacRestrictToSingleNetwork(), EndpointDetection: servercfg.IsEndpointDetectionEnabled(), AllowedEmailDomains: servercfg.GetAllowedEmailDomains(), @@ -139,6 +150,7 @@ func GetServerConfig() config.ServerConfig { cfg.IsPro = "yes" } cfg.JwtValidityDuration = time.Duration(settings.JwtValidityDuration) * time.Minute + cfg.JwtValidityDurationClients = time.Duration(settings.JwtValidityDurationClients) * time.Minute cfg.RacRestrictToSingleNetwork = settings.RacRestrictToSingleNetwork cfg.MetricInterval = settings.MetricInterval cfg.ManageDNS = settings.ManageDNS @@ -201,7 +213,13 @@ func Telemetry() string { // GetJwtValidityDuration - returns the JWT validity duration in minutes func GetJwtValidityDuration() time.Duration { - return GetServerConfig().JwtValidityDuration + return time.Duration(GetServerSettings().JwtValidityDuration) * time.Minute +} + +// GetJwtValidityDurationForClients returns the JWT validity duration in +// minutes for clients. +func GetJwtValidityDurationForClients() time.Duration { + return time.Duration(GetServerSettings().JwtValidityDurationClients) * time.Minute } // GetRacRestrictToSingleNetwork - returns whether the feature to allow simultaneous network connections via RAC is enabled @@ -317,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 } diff --git a/logic/util.go b/logic/util.go index d0f73c88..e466a751 100644 --- a/logic/util.go +++ b/logic/util.go @@ -21,6 +21,7 @@ import ( "github.com/blang/semver" "github.com/c-robinson/iplib" "github.com/gravitl/netmaker/logger" + "github.com/gravitl/netmaker/models" ) // IsBase64 - checks if a string is in base64 format @@ -242,3 +243,22 @@ func GetClientIP(r *http.Request) string { } return ip } + +// CompareIfaceSlices compares two slices of Iface for deep equality (order-sensitive) +func CompareIfaceSlices(a, b []models.Iface) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if !compareIface(a[i], b[i]) { + return false + } + } + return true +} +func compareIface(a, b models.Iface) bool { + return a.Name == b.Name && + a.Address.IP.Equal(b.Address.IP) && + a.Address.Mask.String() == b.Address.Mask.String() && + a.AddressString == b.AddressString +} diff --git a/logic/zombie.go b/logic/zombie.go index 406c2806..862e732a 100644 --- a/logic/zombie.go +++ b/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 } diff --git a/migrate/migrate.go b/migrate/migrate.go index 7a45856b..efc86124 100644 --- a/migrate/migrate.go +++ b/migrate/migrate.go @@ -537,21 +537,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, @@ -642,5 +648,8 @@ func settings() { if settings.DefaultDomain == "" { settings.DefaultDomain = servercfg.GetDefaultDomain() } + if settings.JwtValidityDurationClients == 0 { + settings.JwtValidityDurationClients = servercfg.GetJwtValidityDurationFromEnv() / 60 + } logic.UpsertServerSettings(settings) } diff --git a/models/events.go b/models/events.go index 12a1d1d4..4a6e1603 100644 --- a/models/events.go +++ b/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 diff --git a/models/settings.go b/models/settings.go index 8e5f7781..0328886a 100644 --- a/models/settings.go +++ b/models/settings.go @@ -9,39 +9,44 @@ const ( ) type ServerSettings struct { - NetclientAutoUpdate bool `json:"netclientautoupdate"` - Verbosity int32 `json:"verbosity"` - AuthProvider string `json:"authprovider"` - OIDCIssuer string `json:"oidcissuer"` - ClientID string `json:"client_id"` - ClientSecret string `json:"client_secret"` - SyncEnabled bool `json:"sync_enabled"` - GoogleAdminEmail string `json:"google_admin_email"` - GoogleSACredsJson string `json:"google_sa_creds_json"` - AzureTenant string `json:"azure_tenant"` - UserFilters []string `json:"user_filters"` - GroupFilters []string `json:"group_filters"` - IDPSyncInterval string `json:"idp_sync_interval"` - Telemetry string `json:"telemetry"` - BasicAuth bool `json:"basic_auth"` - JwtValidityDuration int `json:"jwt_validity_duration"` - MFAEnforced bool `json:"mfa_enforced"` - RacRestrictToSingleNetwork bool `json:"rac_restrict_to_single_network"` - EndpointDetection bool `json:"endpoint_detection"` - AllowedEmailDomains string `json:"allowed_email_domains"` - EmailSenderAddr string `json:"email_sender_addr"` - EmailSenderUser string `json:"email_sender_user"` - EmailSenderPassword string `json:"email_sender_password"` - SmtpHost string `json:"smtp_host"` - SmtpPort int `json:"smtp_port"` - MetricInterval string `json:"metric_interval"` - MetricsPort int `json:"metrics_port"` - ManageDNS bool `json:"manage_dns"` - DefaultDomain string `json:"default_domain"` - Stun bool `json:"stun"` - StunServers string `json:"stun_servers"` - Theme Theme `json:"theme"` - TextSize string `json:"text_size"` - ReducedMotion bool `json:"reduced_motion"` - AuditLogsRetentionPeriodInDays int `json:"audit_logs_retention_period"` + NetclientAutoUpdate bool `json:"netclientautoupdate"` + Verbosity int32 `json:"verbosity"` + AuthProvider string `json:"authprovider"` + OIDCIssuer string `json:"oidcissuer"` + ClientID string `json:"client_id"` + ClientSecret string `json:"client_secret"` + SyncEnabled bool `json:"sync_enabled"` + GoogleAdminEmail string `json:"google_admin_email"` + GoogleSACredsJson string `json:"google_sa_creds_json"` + AzureTenant string `json:"azure_tenant"` + UserFilters []string `json:"user_filters"` + GroupFilters []string `json:"group_filters"` + IDPSyncInterval string `json:"idp_sync_interval"` + Telemetry string `json:"telemetry"` + BasicAuth bool `json:"basic_auth"` + // JwtValidityDuration is the validity duration of auth tokens for users + // on the dashboard (NMUI). + JwtValidityDuration int `json:"jwt_validity_duration"` + // JwtValidityDurationClients is the validity duration of auth tokens for + // users on the clients (NetDesk). + JwtValidityDurationClients int `json:"jwt_validity_duration_clients"` + MFAEnforced bool `json:"mfa_enforced"` + RacRestrictToSingleNetwork bool `json:"rac_restrict_to_single_network"` + EndpointDetection bool `json:"endpoint_detection"` + AllowedEmailDomains string `json:"allowed_email_domains"` + EmailSenderAddr string `json:"email_sender_addr"` + EmailSenderUser string `json:"email_sender_user"` + EmailSenderPassword string `json:"email_sender_password"` + SmtpHost string `json:"smtp_host"` + SmtpPort int `json:"smtp_port"` + MetricInterval string `json:"metric_interval"` + MetricsPort int `json:"metrics_port"` + ManageDNS bool `json:"manage_dns"` + DefaultDomain string `json:"default_domain"` + Stun bool `json:"stun"` + StunServers string `json:"stun_servers"` + Theme Theme `json:"theme"` + TextSize string `json:"text_size"` + ReducedMotion bool `json:"reduced_motion"` + AuditLogsRetentionPeriodInDays int `json:"audit_logs_retention_period"` } diff --git a/models/ssocache.go b/models/ssocache.go index 90e61285..e71f1be9 100644 --- a/models/ssocache.go +++ b/models/ssocache.go @@ -7,6 +7,7 @@ const DefaultExpDuration = time.Minute * 5 // SsoState - holds SSO sign-in session data type SsoState struct { + AppName string `json:"app_name"` Value string `json:"value"` Expiration time.Time `json:"expiration"` } diff --git a/models/structs.go b/models/structs.go index 8a03746b..fa97a2af 100644 --- a/models/structs.go +++ b/models/structs.go @@ -16,6 +16,13 @@ const ( PLACEHOLDER_TOKEN_TEXT = "ACCESS_TOKEN" ) +type FeatureFlags struct { + EnableNetworkActivity bool `json:"enable_network_activity"` + EnableOAuth bool `json:"enable_oauth"` + EnableIDPIntegration bool `json:"enable_idp_integration"` + AllowMultiServerLicense bool `json:"allow_multi_server_license"` +} + // AuthParams - struct for auth params type AuthParams struct { MacAddress string `json:"macaddress"` diff --git a/models/user_mgmt.go b/models/user_mgmt.go index 75737ea0..a16e6ec4 100644 --- a/models/user_mgmt.go +++ b/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"` diff --git a/mq/handlers.go b/mq/handlers.go index 52f2786c..73427a20 100644 --- a/mq/handlers.go +++ b/mq/handlers.go @@ -274,14 +274,14 @@ func HandleHostCheckin(h, currentHost *models.Host) bool { return false } } - ifaceDelta := len(h.Interfaces) != len(currentHost.Interfaces) || + ifaceDelta := len(h.Interfaces) != len(currentHost.Interfaces) || !logic.CompareIfaceSlices(h.Interfaces, currentHost.Interfaces) || !h.EndpointIP.Equal(currentHost.EndpointIP) || (len(h.NatType) > 0 && h.NatType != currentHost.NatType) || h.DefaultInterface != currentHost.DefaultInterface || (h.ListenPort != 0 && h.ListenPort != currentHost.ListenPort) || (h.WgPublicListenPort != 0 && h.WgPublicListenPort != currentHost.WgPublicListenPort) || (!h.EndpointIPv6.Equal(currentHost.EndpointIPv6)) if ifaceDelta { // only save if something changes - if !h.EndpointIP.Equal(currentHost.EndpointIP) || !h.EndpointIPv6.Equal(currentHost.EndpointIPv6) { + if !h.EndpointIP.Equal(currentHost.EndpointIP) || !h.EndpointIPv6.Equal(currentHost.EndpointIPv6) || currentHost.Location == "" { if h.EndpointIP != nil { h.Location = logic.GetHostLocInfo(h.EndpointIP.String(), os.Getenv("IP_INFO_TOKEN")) } else if h.EndpointIPv6 != nil { @@ -293,6 +293,9 @@ func HandleHostCheckin(h, currentHost *models.Host) bool { currentHost.Interfaces = h.Interfaces currentHost.DefaultInterface = h.DefaultInterface currentHost.NatType = h.NatType + if h.Location != "" { + currentHost.Location = h.Location + } if h.ListenPort != 0 { currentHost.ListenPort = h.ListenPort } diff --git a/pro/auth/azure-ad.go b/pro/auth/azure-ad.go index e5d9d4a8..ff5c1191 100644 --- a/pro/auth/azure-ad.go +++ b/pro/auth/azure-ad.go @@ -40,13 +40,18 @@ func initAzureAD(redirectURL string, clientID string, clientSecret string) { } func handleAzureLogin(w http.ResponseWriter, r *http.Request) { + appName := r.Header.Get("X-Application-Name") + if appName == "" { + appName = logic.NetmakerDesktopApp + } + var oauth_state_string = logic.RandomString(user_signin_length) if auth_provider == nil { handleOauthNotConfigured(w) return } - if err := logic.SetState(oauth_state_string); err != nil { + if err := logic.SetState(appName, oauth_state_string); err != nil { handleOauthNotConfigured(w) return } @@ -56,9 +61,15 @@ func handleAzureLogin(w http.ResponseWriter, r *http.Request) { } func handleAzureCallback(w http.ResponseWriter, r *http.Request) { - var rState, rCode = getStateAndCode(r) - var content, err = getAzureUserInfo(rState, rCode) + + state, err := logic.GetState(rState) + if err != nil { + handleOauthNotValid(w) + return + } + + content, err := getAzureUserInfo(rState, rCode) if err != nil { logger.Log(1, "error when getting user info from azure:", err.Error()) if strings.Contains(err.Error(), "invalid oauth state") || strings.Contains(err.Error(), "failed to fetch user email from SSO state") { @@ -179,7 +190,7 @@ func handleAzureCallback(w http.ResponseWriter, r *http.Request) { Password: newPass, } - var jwt, jwtErr = logic.VerifyAuthRequest(authRequest) + var jwt, jwtErr = logic.VerifyAuthRequest(authRequest, state.AppName) if jwtErr != nil { logger.Log(1, "could not parse jwt for user", authRequest.UserName) return diff --git a/pro/auth/error.go b/pro/auth/error.go index 61ba7f15..eac61b27 100644 --- a/pro/auth/error.go +++ b/pro/auth/error.go @@ -93,7 +93,7 @@ var htmlBaseTemplate = ` ` var oauthNotConfigured = fmt.Sprintf(htmlBaseTemplate, `

Your Netmaker server does not have OAuth configured.

-

Please visit the docs here to learn how to.

`) +

Please visit the docs here to learn how to.

`) var oauthStateInvalid = fmt.Sprintf(htmlBaseTemplate, `

Invalid OAuth Session. Please re-try again.

`) diff --git a/pro/auth/github.go b/pro/auth/github.go index 1bd8cc63..4147b1e8 100644 --- a/pro/auth/github.go +++ b/pro/auth/github.go @@ -40,13 +40,18 @@ func initGithub(redirectURL string, clientID string, clientSecret string) { } func handleGithubLogin(w http.ResponseWriter, r *http.Request) { + appName := r.Header.Get("X-Application-Name") + if appName == "" { + appName = logic.NetmakerDesktopApp + } + var oauth_state_string = logic.RandomString(user_signin_length) if auth_provider == nil { handleOauthNotConfigured(w) return } - if err := logic.SetState(oauth_state_string); err != nil { + if err := logic.SetState(appName, oauth_state_string); err != nil { handleOauthNotConfigured(w) return } @@ -56,9 +61,15 @@ func handleGithubLogin(w http.ResponseWriter, r *http.Request) { } func handleGithubCallback(w http.ResponseWriter, r *http.Request) { - var rState, rCode = getStateAndCode(r) - var content, err = getGithubUserInfo(rState, rCode) + + state, err := logic.GetState(rState) + if err != nil { + handleOauthNotValid(w) + return + } + + content, err := getGithubUserInfo(rState, rCode) if err != nil { logger.Log(1, "error when getting user info from github:", err.Error()) if strings.Contains(err.Error(), "invalid oauth state") || strings.Contains(err.Error(), "failed to fetch user email from SSO state") { @@ -170,7 +181,7 @@ func handleGithubCallback(w http.ResponseWriter, r *http.Request) { Password: newPass, } - var jwt, jwtErr = logic.VerifyAuthRequest(authRequest) + var jwt, jwtErr = logic.VerifyAuthRequest(authRequest, state.AppName) if jwtErr != nil { logger.Log(1, "could not parse jwt for user", authRequest.UserName) return diff --git a/pro/auth/google.go b/pro/auth/google.go index e127edee..c132af44 100644 --- a/pro/auth/google.go +++ b/pro/auth/google.go @@ -40,13 +40,18 @@ func initGoogle(redirectURL string, clientID string, clientSecret string) { } func handleGoogleLogin(w http.ResponseWriter, r *http.Request) { + appName := r.Header.Get("X-Application-Name") + if appName == "" { + appName = logic.NetmakerDesktopApp + } + var oauth_state_string = logic.RandomString(user_signin_length) if auth_provider == nil { handleOauthNotConfigured(w) return } logger.Log(0, "Setting OAuth State ", oauth_state_string) - if err := logic.SetState(oauth_state_string); err != nil { + if err := logic.SetState(appName, oauth_state_string); err != nil { handleOauthNotConfigured(w) return } @@ -56,10 +61,16 @@ func handleGoogleLogin(w http.ResponseWriter, r *http.Request) { } func handleGoogleCallback(w http.ResponseWriter, r *http.Request) { - var rState, rCode = getStateAndCode(r) logger.Log(0, "Fetched OAuth State ", rState) - var content, err = getGoogleUserInfo(rState, rCode) + + state, err := logic.GetState(rState) + if err != nil { + handleOauthNotValid(w) + return + } + + content, err := getGoogleUserInfo(rState, rCode) if err != nil { logger.Log(1, "error when getting user info from google:", err.Error()) if strings.Contains(err.Error(), "invalid oauth state") { @@ -162,7 +173,7 @@ func handleGoogleCallback(w http.ResponseWriter, r *http.Request) { Password: newPass, } - var jwt, jwtErr = logic.VerifyAuthRequest(authRequest) + var jwt, jwtErr = logic.VerifyAuthRequest(authRequest, state.AppName) if jwtErr != nil { logger.Log(1, "could not parse jwt for user", authRequest.UserName) return diff --git a/pro/auth/headless_callback.go b/pro/auth/headless_callback.go index c039a54a..bf1fcfff 100644 --- a/pro/auth/headless_callback.go +++ b/pro/auth/headless_callback.go @@ -86,7 +86,7 @@ func HandleHeadlessSSOCallback(w http.ResponseWriter, r *http.Request) { jwt, jwtErr := logic.VerifyAuthRequest(models.UserAuthParams{ UserName: user.UserName, Password: newPass, - }) + }, logic.NetclientApp) if jwtErr != nil { logger.Log(1, "could not parse jwt for user", userClaims.getUserName()) return diff --git a/pro/auth/oidc.go b/pro/auth/oidc.go index d88cb4eb..feb58dfd 100644 --- a/pro/auth/oidc.go +++ b/pro/auth/oidc.go @@ -52,13 +52,18 @@ func initOIDC(redirectURL string, clientID string, clientSecret string, issuer s } func handleOIDCLogin(w http.ResponseWriter, r *http.Request) { + appName := r.Header.Get("X-Application-Name") + if appName == "" { + appName = logic.NetmakerDesktopApp + } + var oauth_state_string = logic.RandomString(user_signin_length) if auth_provider == nil { handleOauthNotConfigured(w) return } - if err := logic.SetState(oauth_state_string); err != nil { + if err := logic.SetState(appName, oauth_state_string); err != nil { handleOauthNotConfigured(w) return } @@ -67,10 +72,15 @@ func handleOIDCLogin(w http.ResponseWriter, r *http.Request) { } func handleOIDCCallback(w http.ResponseWriter, r *http.Request) { - var rState, rCode = getStateAndCode(r) - var content, err = getOIDCUserInfo(rState, rCode) + state, err := logic.GetState(rState) + if err != nil { + handleOauthNotValid(w) + return + } + + content, err := getOIDCUserInfo(rState, rCode) if err != nil { logger.Log(1, "error when getting user info from callback:", err.Error()) if strings.Contains(err.Error(), "invalid oauth state") { @@ -170,7 +180,7 @@ func handleOIDCCallback(w http.ResponseWriter, r *http.Request) { Password: newPass, } - var jwt, jwtErr = logic.VerifyAuthRequest(authRequest) + var jwt, jwtErr = logic.VerifyAuthRequest(authRequest, state.AppName) if jwtErr != nil { logger.Log(1, "could not parse jwt for user", authRequest.UserName, jwtErr.Error()) return diff --git a/pro/auth/sync.go b/pro/auth/sync.go index 65e7016a..3c4e44a9 100644 --- a/pro/auth/sync.go +++ b/pro/auth/sync.go @@ -122,6 +122,12 @@ func syncUsers(idpUsers []idp.User) error { filters := logic.GetServerSettings().UserFilters for _, user := range idpUsers { + if user.AccountArchived { + // delete the user if it has been archived. + _ = logic.DeleteUser(user.Username) + continue + } + var found bool for _, filter := range filters { if strings.HasPrefix(user.Username, filter) { @@ -150,6 +156,13 @@ func syncUsers(idpUsers []idp.User) error { if err != nil { return err } + + // It's possible that a user can attempt to log in to Netmaker + // after the IDP is configured but before the users are synced. + // Since the user doesn't exist, a pending user will be + // created. Now, since the user is created, the pending user + // can be deleted. + _ = logic.DeletePendingUser(user.Username) } else if dbUser.AuthType == models.OAuth { if dbUser.AccountDisabled != user.AccountDisabled || dbUser.DisplayName != user.DisplayName || diff --git a/pro/idp/google/google.go b/pro/idp/google/google.go index 12f961c2..f1039ef2 100644 --- a/pro/idp/google/google.go +++ b/pro/idp/google/google.go @@ -63,7 +63,7 @@ func (g *Client) GetUsers() ([]idp.User, error) { var retval []idp.User err := g.service.Users.List(). Customer("my_customer"). - Fields("users(id,primaryEmail,name,suspended)", "nextPageToken"). + Fields("users(id,primaryEmail,name,suspended,archived)", "nextPageToken"). Pages(context.TODO(), func(users *admindir.Users) error { for _, user := range users.Users { retval = append(retval, idp.User{ @@ -71,6 +71,7 @@ func (g *Client) GetUsers() ([]idp.User, error) { Username: user.PrimaryEmail, DisplayName: user.Name.FullName, AccountDisabled: user.Suspended, + AccountArchived: user.Archived, }) } diff --git a/pro/idp/idp.go b/pro/idp/idp.go index a76b65ff..7e190c73 100644 --- a/pro/idp/idp.go +++ b/pro/idp/idp.go @@ -10,6 +10,7 @@ type User struct { Username string DisplayName string AccountDisabled bool + AccountArchived bool } type Group struct { diff --git a/pro/initialize.go b/pro/initialize.go index 19883985..3d6b7210 100644 --- a/pro/initialize.go +++ b/pro/initialize.go @@ -155,7 +155,7 @@ func InitPro() { logic.GetFwRulesForNodeAndPeerOnGw = proLogic.GetFwRulesForNodeAndPeerOnGw logic.GetFwRulesForUserNodesOnGw = proLogic.GetFwRulesForUserNodesOnGw logic.GetHostLocInfo = proLogic.GetHostLocInfo - + logic.GetFeatureFlags = proLogic.GetFeatureFlags } func retrieveProLogo() string { diff --git a/pro/license.go b/pro/license.go index aaa16259..9d60491b 100644 --- a/pro/license.go +++ b/pro/license.go @@ -135,6 +135,8 @@ func ValidateLicense() (err error) { return err } + proLogic.SetFeatureFlags(licenseResponse.FeatureFlags) + slog.Info("License validation succeeded!") return nil } @@ -200,6 +202,7 @@ func validateLicenseKey(encryptedData []byte, publicKey *[32]byte) ([]byte, bool LicenseKey: servercfg.GetLicenseKey(), NmServerPubKey: base64encode(publicKeyBytes), EncryptedPart: base64encode(encryptedData), + NmBaseDomain: servercfg.GetNmBaseDomain(), } requestBody, err := json.Marshal(msg) diff --git a/pro/logic/server.go b/pro/logic/server.go new file mode 100644 index 00000000..e9b262ee --- /dev/null +++ b/pro/logic/server.go @@ -0,0 +1,13 @@ +package logic + +import "github.com/gravitl/netmaker/models" + +var featureFlagsCache models.FeatureFlags + +func SetFeatureFlags(featureFlags models.FeatureFlags) { + featureFlagsCache = featureFlags +} + +func GetFeatureFlags() models.FeatureFlags { + return featureFlagsCache +} diff --git a/pro/types.go b/pro/types.go index 4c1c85bb..73f2cb86 100644 --- a/pro/types.go +++ b/pro/types.go @@ -5,6 +5,7 @@ package pro import ( "errors" + "github.com/gravitl/netmaker/models" ) const ( @@ -32,8 +33,9 @@ type LicenseKey struct { // ValidatedLicense - the validated license struct type ValidatedLicense struct { - LicenseValue string `json:"license_value" binding:"required"` // license that validation is being requested for - EncryptedLicense string `json:"encrypted_license" binding:"required"` // to be decrypted by Netmaker using Netmaker server's private key + LicenseValue string `json:"license_value" binding:"required"` // license that validation is being requested for + EncryptedLicense string `json:"encrypted_license" binding:"required"` // to be decrypted by Netmaker using Netmaker server's private key + FeatureFlags models.FeatureFlags `json:"feature_flags" binding:"required"` } // LicenseSecret - the encrypted struct for sending user-id @@ -74,6 +76,7 @@ type ValidateLicenseRequest struct { LicenseKey string `json:"license_key" binding:"required"` NmServerPubKey string `json:"nm_server_pub_key" binding:"required"` // Netmaker server public key used to send data back to Netmaker for the Netmaker server to decrypt (eg output from validating license) EncryptedPart string `json:"secret" binding:"required"` + NmBaseDomain string `json:"nm_base_domain"` } type licenseResponseCache struct { diff --git a/schema/egress.go b/schema/egress.go index d420198a..2c711f58 100644 --- a/schema/egress.go +++ b/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 } diff --git a/scripts/nm-quick.sh b/scripts/nm-quick.sh index cc3562a4..06159778 100755 --- a/scripts/nm-quick.sh +++ b/scripts/nm-quick.sh @@ -6,7 +6,7 @@ SCRIPT_DIR=$(dirname "$(realpath "$0")") CONFIG_PATH="$SCRIPT_DIR/$CONFIG_FILE" NM_QUICK_VERSION="0.1.1" #LATEST=$(curl -s https://api.github.com/repos/gravitl/netmaker/releases/latest | grep "tag_name" | cut -d : -f 2,3 | tr -d [:space:],\") -LATEST=v0.99.0 +LATEST=v1.0.0 BRANCH=master if [ $(id -u) -ne 0 ]; then echo "This script must be run as root" diff --git a/servercfg/serverconf.go b/servercfg/serverconf.go index ab78cbb9..9a9d083f 100644 --- a/servercfg/serverconf.go +++ b/servercfg/serverconf.go @@ -38,82 +38,7 @@ func SetHost() error { return nil } -// GetServerConfig - gets the server config into memory from file or env -func GetServerConfig() config.ServerConfig { - var cfg config.ServerConfig - cfg.APIConnString = GetAPIConnString() - cfg.CoreDNSAddr = GetCoreDNSAddr() - cfg.APIHost = GetAPIHost() - cfg.APIPort = GetAPIPort() - cfg.MasterKey = "(hidden)" - cfg.DNSKey = "(hidden)" - cfg.AllowedOrigin = GetAllowedOrigin() - cfg.RestBackend = "off" - cfg.NodeID = GetNodeID() - cfg.BrokerType = GetBrokerType() - cfg.EmqxRestEndpoint = GetEmqxRestEndpoint() - if AutoUpdateEnabled() { - cfg.NetclientAutoUpdate = "enabled" - } else { - cfg.NetclientAutoUpdate = "disabled" - } - if IsRestBackend() { - cfg.RestBackend = "on" - } - cfg.DNSMode = "off" - if IsDNSMode() { - cfg.DNSMode = "on" - } - cfg.DisplayKeys = "off" - if IsDisplayKeys() { - cfg.DisplayKeys = "on" - } - cfg.DisableRemoteIPCheck = "off" - if DisableRemoteIPCheck() { - cfg.DisableRemoteIPCheck = "on" - } - cfg.Database = GetDB() - cfg.Platform = GetPlatform() - cfg.Version = GetVersion() - cfg.PublicIp = GetServerHostIP() - - // == auth config == - var authInfo = GetAuthProviderInfo() - cfg.AuthProvider = authInfo[0] - cfg.ClientID = authInfo[1] - cfg.ClientSecret = authInfo[2] - cfg.FrontendURL = GetFrontendURL() - cfg.Telemetry = Telemetry() - cfg.Server = GetServer() - cfg.Verbosity = GetVerbosity() - cfg.IsPro = "no" - if IsPro { - cfg.IsPro = "yes" - } - cfg.JwtValidityDuration = GetJwtValidityDuration() - cfg.RacRestrictToSingleNetwork = GetRacRestrictToSingleNetwork() - cfg.MetricInterval = GetMetricInterval() - cfg.ManageDNS = GetManageDNS() - cfg.Stun = IsStunEnabled() - cfg.StunServers = GetStunServers() - cfg.DefaultDomain = GetDefaultDomain() - return cfg -} - -// GetJwtValidityDuration - returns the JWT validity duration in seconds -func GetJwtValidityDuration() time.Duration { - var defaultDuration = time.Duration(24) * time.Hour - if os.Getenv("JWT_VALIDITY_DURATION") != "" { - t, err := strconv.Atoi(os.Getenv("JWT_VALIDITY_DURATION")) - if err != nil { - return defaultDuration - } - return time.Duration(t) * time.Second - } - return defaultDuration -} - -// GetJwtValidityDuration - returns the JWT validity duration in seconds +// GetJwtValidityDurationFromEnv - returns the JWT validity duration in seconds func GetJwtValidityDurationFromEnv() int { var defaultDuration = 43200 if os.Getenv("JWT_VALIDITY_DURATION") != "" { @@ -721,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"