Skip to content

Commit 39ea1ed

Browse files
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;
1 parent 5371736 commit 39ea1ed

File tree

4 files changed

+250
-50
lines changed

4 files changed

+250
-50
lines changed

controllers/server.go

Lines changed: 86 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package controller
33
import (
44
"encoding/json"
55
"errors"
6+
"github.com/google/go-cmp/cmp"
67
"net/http"
78
"os"
89
"strings"
@@ -274,11 +275,11 @@ func updateSettings(w http.ResponseWriter, r *http.Request) {
274275
currSettings := logic.GetServerSettings()
275276
err := logic.UpsertServerSettings(req)
276277
if err != nil {
277-
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("failed to udpate server settings "+err.Error()), "internal"))
278+
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("failed to update server settings "+err.Error()), "internal"))
278279
return
279280
}
280281
logic.LogEvent(&models.Event{
281-
Action: models.Update,
282+
Action: identifySettingsUpdateAction(currSettings, req),
282283
Source: models.Subject{
283284
ID: r.Header.Get("user"),
284285
Name: r.Header.Get("user"),
@@ -323,5 +324,88 @@ func reInit(curr, new models.ServerSettings, force bool) {
323324
}
324325
}
325326
go mq.PublishPeerUpdate(false)
327+
}
328+
329+
func identifySettingsUpdateAction(old, new models.ServerSettings) models.Action {
330+
// TODO: here we are relying on the dashboard to only
331+
// make singular updates, but it's possible that the
332+
// API can be called to make multiple changes to the
333+
// server settings. We should update it to log multiple
334+
// events or create singular update APIs.
335+
if old.MFAEnforced != new.MFAEnforced {
336+
if new.MFAEnforced {
337+
return models.EnforceMFA
338+
} else {
339+
return models.UnenforceMFA
340+
}
341+
}
342+
343+
if old.BasicAuth != new.BasicAuth {
344+
if new.BasicAuth {
345+
return models.EnableBasicAuth
346+
} else {
347+
return models.DisableBasicAuth
348+
}
349+
}
350+
351+
if old.Telemetry != new.Telemetry {
352+
if new.Telemetry == "off" {
353+
return models.DisableTelemetry
354+
} else {
355+
return models.EnableTelemetry
356+
}
357+
}
358+
359+
if old.NetclientAutoUpdate != new.NetclientAutoUpdate ||
360+
old.RacRestrictToSingleNetwork != new.RacRestrictToSingleNetwork ||
361+
old.ManageDNS != new.ManageDNS ||
362+
old.DefaultDomain != new.DefaultDomain ||
363+
old.EndpointDetection != new.EndpointDetection {
364+
return models.UpdateClientSettings
365+
}
366+
367+
if old.AllowedEmailDomains != new.AllowedEmailDomains ||
368+
old.JwtValidityDuration != new.JwtValidityDuration {
369+
return models.UpdateAuthenticationSecuritySettings
370+
}
371+
372+
if old.Verbosity != new.Verbosity ||
373+
old.MetricsPort != new.MetricsPort ||
374+
old.MetricInterval != new.MetricInterval ||
375+
old.AuditLogsRetentionPeriodInDays != new.AuditLogsRetentionPeriodInDays {
376+
return models.UpdateMonitoringAndDebuggingSettings
377+
}
378+
379+
if old.Theme != new.Theme {
380+
return models.UpdateDisplaySettings
381+
}
382+
383+
if old.TextSize != new.TextSize ||
384+
old.ReducedMotion != new.ReducedMotion {
385+
return models.UpdateAccessibilitySettings
386+
}
387+
388+
if old.EmailSenderAddr != new.EmailSenderAddr ||
389+
old.EmailSenderUser != new.EmailSenderUser ||
390+
old.EmailSenderPassword != new.EmailSenderPassword ||
391+
old.SmtpHost != new.SmtpHost ||
392+
old.SmtpPort != new.SmtpPort {
393+
return models.UpdateSMTPSettings
394+
}
395+
396+
if old.AuthProvider != new.AuthProvider ||
397+
old.OIDCIssuer != new.OIDCIssuer ||
398+
old.ClientID != new.ClientID ||
399+
old.ClientSecret != new.ClientSecret ||
400+
old.SyncEnabled != new.SyncEnabled ||
401+
old.IDPSyncInterval != new.IDPSyncInterval ||
402+
old.GoogleAdminEmail != new.GoogleAdminEmail ||
403+
old.GoogleSACredsJson != new.GoogleSACredsJson ||
404+
old.AzureTenant != new.AzureTenant ||
405+
!cmp.Equal(old.GroupFilters, new.GroupFilters) ||
406+
cmp.Equal(old.UserFilters, new.UserFilters) {
407+
return models.UpdateIDPSettings
408+
}
326409

410+
return models.Update
327411
}

controllers/user.go

Lines changed: 124 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"errors"
88
"fmt"
99
"github.com/pquerna/otp"
10+
"golang.org/x/crypto/bcrypt"
1011
"image/png"
1112
"net/http"
1213
"reflect"
@@ -38,6 +39,7 @@ func userHandlers(r *mux.Router) {
3839
r.HandleFunc("/api/users/adm/transfersuperadmin/{username}", logic.SecurityCheck(true, http.HandlerFunc(transferSuperAdmin))).
3940
Methods(http.MethodPost)
4041
r.HandleFunc("/api/users/adm/authenticate", authenticateUser).Methods(http.MethodPost)
42+
r.HandleFunc("/api/users/{username}/validate-identity", logic.SecurityCheck(false, logic.ContinueIfUserMatch(http.HandlerFunc(validateUserIdentity)))).Methods(http.MethodPost)
4143
r.HandleFunc("/api/users/{username}/auth/init-totp", logic.SecurityCheck(false, logic.ContinueIfUserMatch(http.HandlerFunc(initiateTOTPSetup)))).Methods(http.MethodPost)
4244
r.HandleFunc("/api/users/{username}/auth/complete-totp", logic.SecurityCheck(false, logic.ContinueIfUserMatch(http.HandlerFunc(completeTOTPSetup)))).Methods(http.MethodPost)
4345
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) {
308310
logic.ReturnErrorResponse(response, request, logic.FormatError(errors.New("access denied to dashboard"), "unauthorized"))
309311
return
310312
}
311-
// log user activity
312-
logic.LogEvent(&models.Event{
313-
Action: models.Login,
314-
Source: models.Subject{
315-
ID: user.UserName,
316-
Name: user.UserName,
317-
Type: models.UserSub,
318-
},
319-
TriggeredBy: user.UserName,
320-
Target: models.Subject{
321-
ID: models.DashboardSub.String(),
322-
Name: models.DashboardSub.String(),
323-
Type: models.DashboardSub,
324-
},
325-
Origin: models.Dashboard,
326-
})
327-
} else {
328-
logic.LogEvent(&models.Event{
329-
Action: models.Login,
330-
Source: models.Subject{
331-
ID: user.UserName,
332-
Name: user.UserName,
333-
Type: models.UserSub,
334-
},
335-
TriggeredBy: user.UserName,
336-
Target: models.Subject{
337-
ID: models.ClientAppSub.String(),
338-
Name: models.ClientAppSub.String(),
339-
Type: models.ClientAppSub,
340-
},
341-
Origin: models.ClientApp,
342-
})
343313
}
344314

345315
username := authRequest.UserName
@@ -393,6 +363,44 @@ func authenticateUser(response http.ResponseWriter, request *http.Request) {
393363
return
394364
}
395365
logger.Log(2, username, "was authenticated")
366+
367+
// log user activity
368+
if !user.IsMFAEnabled {
369+
if val := request.Header.Get("From-Ui"); val == "true" {
370+
logic.LogEvent(&models.Event{
371+
Action: models.Login,
372+
Source: models.Subject{
373+
ID: user.UserName,
374+
Name: user.UserName,
375+
Type: models.UserSub,
376+
},
377+
TriggeredBy: user.UserName,
378+
Target: models.Subject{
379+
ID: models.DashboardSub.String(),
380+
Name: models.DashboardSub.String(),
381+
Type: models.DashboardSub,
382+
},
383+
Origin: models.Dashboard,
384+
})
385+
} else {
386+
logic.LogEvent(&models.Event{
387+
Action: models.Login,
388+
Source: models.Subject{
389+
ID: user.UserName,
390+
Name: user.UserName,
391+
Type: models.UserSub,
392+
},
393+
TriggeredBy: user.UserName,
394+
Target: models.Subject{
395+
ID: models.ClientAppSub.String(),
396+
Name: models.ClientAppSub.String(),
397+
Type: models.ClientAppSub,
398+
},
399+
Origin: models.ClientApp,
400+
})
401+
}
402+
}
403+
396404
response.Header().Set("Content-Type", "application/json")
397405
response.Write(successJSONResponse)
398406

@@ -434,6 +442,43 @@ func authenticateUser(response http.ResponseWriter, request *http.Request) {
434442
}()
435443
}
436444

445+
// @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.
446+
// @Router /api/users/{username}/validate-identity [post]
447+
// @Tags Auth
448+
// @Accept json
449+
// @Param body body models.UserIdentityValidationRequest true "User Identity Validation Request"
450+
// @Success 200 {object} models.SuccessResponse
451+
// @Failure 400 {object} models.ErrorResponse
452+
func validateUserIdentity(w http.ResponseWriter, r *http.Request) {
453+
username := r.Header.Get("user")
454+
455+
var req models.UserIdentityValidationRequest
456+
err := json.NewDecoder(r.Body).Decode(&req)
457+
if err != nil {
458+
logger.Log(0, "failed to decode request body: ", err.Error())
459+
err = fmt.Errorf("invalid request body: %v", err)
460+
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
461+
return
462+
}
463+
464+
user, err := logic.GetUser(username)
465+
if err != nil {
466+
logger.Log(0, "failed to get user: ", err.Error())
467+
err = fmt.Errorf("user not found: %v", err)
468+
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
469+
return
470+
}
471+
472+
var resp models.UserIdentityValidationResponse
473+
err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password))
474+
if err != nil {
475+
logic.ReturnSuccessResponseWithJson(w, r, resp, "user identity validation failed")
476+
} else {
477+
resp.IdentityValidated = true
478+
logic.ReturnSuccessResponseWithJson(w, r, resp, "user identity validated")
479+
}
480+
}
481+
437482
// @Summary Initiate setting up TOTP 2FA for a user.
438483
// @Router /api/users/auth/init-totp [post]
439484
// @Tags Auth
@@ -557,6 +602,22 @@ func completeTOTPSetup(w http.ResponseWriter, r *http.Request) {
557602
return
558603
}
559604

605+
logic.LogEvent(&models.Event{
606+
Action: models.EnableMFA,
607+
Source: models.Subject{
608+
ID: user.UserName,
609+
Name: user.UserName,
610+
Type: models.UserSub,
611+
},
612+
TriggeredBy: user.UserName,
613+
Target: models.Subject{
614+
ID: user.UserName,
615+
Name: user.UserName,
616+
Type: models.UserSub,
617+
},
618+
Origin: models.Dashboard,
619+
})
620+
560621
logic.ReturnSuccessResponse(w, r, fmt.Sprintf("totp setup complete for user %s", username))
561622
} else {
562623
err = fmt.Errorf("cannot setup totp for user %s: invalid otp", username)
@@ -619,6 +680,22 @@ func verifyTOTP(w http.ResponseWriter, r *http.Request) {
619680
return
620681
}
621682

683+
logic.LogEvent(&models.Event{
684+
Action: models.Login,
685+
Source: models.Subject{
686+
ID: user.UserName,
687+
Name: user.UserName,
688+
Type: models.UserSub,
689+
},
690+
TriggeredBy: user.UserName,
691+
Target: models.Subject{
692+
ID: models.DashboardSub.String(),
693+
Name: models.DashboardSub.String(),
694+
Type: models.DashboardSub,
695+
},
696+
Origin: models.Dashboard,
697+
})
698+
622699
logic.ReturnSuccessResponseWithJson(w, r, models.SuccessfulUserLoginResponse{
623700
UserName: username,
624701
AuthToken: jwt,
@@ -1135,8 +1212,22 @@ func updateUser(w http.ResponseWriter, r *http.Request) {
11351212
UserName: logic.MasterUser,
11361213
}
11371214
}
1215+
action := models.Update
1216+
// TODO: here we are relying on the dashboard to only
1217+
// make singular updates, but it's possible that the
1218+
// API can be called to make multiple changes to the
1219+
// user. We should update it to log multiple events
1220+
// or create singular update APIs.
1221+
if userchange.IsMFAEnabled != user.IsMFAEnabled {
1222+
if userchange.IsMFAEnabled {
1223+
// the update API won't be used to enable MFA.
1224+
action = models.EnableMFA
1225+
} else {
1226+
action = models.DisableMFA
1227+
}
1228+
}
11381229
e := models.Event{
1139-
Action: models.Update,
1230+
Action: action,
11401231
Source: models.Subject{
11411232
ID: caller.UserName,
11421233
Name: caller.UserName,

models/events.go

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,36 @@ package models
33
type Action string
44

55
const (
6-
Create Action = "CREATE"
7-
Update Action = "UPDATE"
8-
Delete Action = "DELETE"
9-
DeleteAll Action = "DELETE_ALL"
10-
Login Action = "LOGIN"
11-
LogOut Action = "LOGOUT"
12-
Connect Action = "CONNECT"
13-
Sync Action = "SYNC"
14-
RefreshKey Action = "REFRESH_KEY"
15-
RefreshAllKeys Action = "REFRESH_ALL_KEYS"
16-
SyncAll Action = "SYNC_ALL"
17-
UpgradeAll Action = "UPGRADE_ALL"
18-
Disconnect Action = "DISCONNECT"
19-
JoinHostToNet Action = "JOIN_HOST_TO_NETWORK"
20-
RemoveHostFromNet Action = "REMOVE_HOST_FROM_NETWORK"
6+
Create Action = "CREATE"
7+
Update Action = "UPDATE"
8+
Delete Action = "DELETE"
9+
DeleteAll Action = "DELETE_ALL"
10+
Login Action = "LOGIN"
11+
LogOut Action = "LOGOUT"
12+
Connect Action = "CONNECT"
13+
Sync Action = "SYNC"
14+
RefreshKey Action = "REFRESH_KEY"
15+
RefreshAllKeys Action = "REFRESH_ALL_KEYS"
16+
SyncAll Action = "SYNC_ALL"
17+
UpgradeAll Action = "UPGRADE_ALL"
18+
Disconnect Action = "DISCONNECT"
19+
JoinHostToNet Action = "JOIN_HOST_TO_NETWORK"
20+
RemoveHostFromNet Action = "REMOVE_HOST_FROM_NETWORK"
21+
EnableMFA Action = "ENABLE_MFA"
22+
DisableMFA Action = "DISABLE_MFA"
23+
EnforceMFA Action = "ENFORCE_MFA"
24+
UnenforceMFA Action = "UNENFORCE_MFA"
25+
EnableBasicAuth Action = "ENABLE_BASIC_AUTH"
26+
DisableBasicAuth Action = "DISABLE_BASIC_AUTH"
27+
EnableTelemetry Action = "ENABLE_TELEMETRY"
28+
DisableTelemetry Action = "DISABLE_TELEMETRY"
29+
UpdateClientSettings Action = "UPDATE_CLIENT_SETTINGS"
30+
UpdateAuthenticationSecuritySettings Action = "UPDATE_AUTHENTICATION_SECURITY_SETTINGS"
31+
UpdateMonitoringAndDebuggingSettings Action = "UPDATE_MONITORING_AND_DEBUGGING_SETTINGS"
32+
UpdateDisplaySettings Action = "UPDATE_DISPLAY_SETTINGS"
33+
UpdateAccessibilitySettings Action = "UPDATE_ACCESSIBILITY_SETTINGS"
34+
UpdateSMTPSettings Action = "UPDATE_EMAIL_SETTINGS"
35+
UpdateIDPSettings Action = "UPDATE_IDP_SETTINGS"
2136
)
2237

2338
type SubjectType string

models/user_mgmt.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,16 @@ type UserAuthParams struct {
202202
Password string `json:"password"`
203203
}
204204

205+
// UserIdentityValidationRequest - user identity validation request struct
206+
type UserIdentityValidationRequest struct {
207+
Password string `json:"password"`
208+
}
209+
210+
// UserIdentityValidationResponse - user identity validation response struct
211+
type UserIdentityValidationResponse struct {
212+
IdentityValidated bool `json:"identity_validated"`
213+
}
214+
205215
type UserTOTPVerificationParams struct {
206216
OTPAuthURL string `json:"otp_auth_url"`
207217
OTPAuthURLSignature string `json:"otp_auth_url_signature"`

0 commit comments

Comments
 (0)