NM-20: Add more refined event logs. (#3552)

* feat(go): add more refined event logs;

* feat(go): add more refined event logs;

* feat(go): add an api to validate user identity.

* feat(go): move validate-user-identity under user;
This commit is contained in:
Vishal Dalwadi
2025-07-23 14:45:41 +05:30
committed by GitHub
parent 5371736d78
commit 39ea1ed9fc
4 changed files with 251 additions and 51 deletions

View File

@@ -3,6 +3,7 @@ package controller
import (
"encoding/json"
"errors"
"github.com/google/go-cmp/cmp"
"net/http"
"os"
"strings"
@@ -274,11 +275,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 +324,88 @@ 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
}

View File

@@ -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)
@@ -308,38 +310,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
@@ -393,6 +363,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 +442,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 +602,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)
@@ -619,6 +680,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 +1212,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,

View File

@@ -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

View File

@@ -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"`