|
7 | 7 | "errors"
|
8 | 8 | "fmt"
|
9 | 9 | "github.com/pquerna/otp"
|
| 10 | + "golang.org/x/crypto/bcrypt" |
10 | 11 | "image/png"
|
11 | 12 | "net/http"
|
12 | 13 | "reflect"
|
@@ -38,6 +39,7 @@ func userHandlers(r *mux.Router) {
|
38 | 39 | r.HandleFunc("/api/users/adm/transfersuperadmin/{username}", logic.SecurityCheck(true, http.HandlerFunc(transferSuperAdmin))).
|
39 | 40 | Methods(http.MethodPost)
|
40 | 41 | 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) |
41 | 43 | r.HandleFunc("/api/users/{username}/auth/init-totp", logic.SecurityCheck(false, logic.ContinueIfUserMatch(http.HandlerFunc(initiateTOTPSetup)))).Methods(http.MethodPost)
|
42 | 44 | r.HandleFunc("/api/users/{username}/auth/complete-totp", logic.SecurityCheck(false, logic.ContinueIfUserMatch(http.HandlerFunc(completeTOTPSetup)))).Methods(http.MethodPost)
|
43 | 45 | 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) {
|
308 | 310 | logic.ReturnErrorResponse(response, request, logic.FormatError(errors.New("access denied to dashboard"), "unauthorized"))
|
309 | 311 | return
|
310 | 312 | }
|
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 |
| - }) |
343 | 313 | }
|
344 | 314 |
|
345 | 315 | username := authRequest.UserName
|
@@ -393,6 +363,44 @@ func authenticateUser(response http.ResponseWriter, request *http.Request) {
|
393 | 363 | return
|
394 | 364 | }
|
395 | 365 | 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 | + |
396 | 404 | response.Header().Set("Content-Type", "application/json")
|
397 | 405 | response.Write(successJSONResponse)
|
398 | 406 |
|
@@ -434,6 +442,43 @@ func authenticateUser(response http.ResponseWriter, request *http.Request) {
|
434 | 442 | }()
|
435 | 443 | }
|
436 | 444 |
|
| 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 | + |
437 | 482 | // @Summary Initiate setting up TOTP 2FA for a user.
|
438 | 483 | // @Router /api/users/auth/init-totp [post]
|
439 | 484 | // @Tags Auth
|
@@ -557,6 +602,22 @@ func completeTOTPSetup(w http.ResponseWriter, r *http.Request) {
|
557 | 602 | return
|
558 | 603 | }
|
559 | 604 |
|
| 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 | + |
560 | 621 | logic.ReturnSuccessResponse(w, r, fmt.Sprintf("totp setup complete for user %s", username))
|
561 | 622 | } else {
|
562 | 623 | err = fmt.Errorf("cannot setup totp for user %s: invalid otp", username)
|
@@ -619,6 +680,22 @@ func verifyTOTP(w http.ResponseWriter, r *http.Request) {
|
619 | 680 | return
|
620 | 681 | }
|
621 | 682 |
|
| 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 | + |
622 | 699 | logic.ReturnSuccessResponseWithJson(w, r, models.SuccessfulUserLoginResponse{
|
623 | 700 | UserName: username,
|
624 | 701 | AuthToken: jwt,
|
@@ -1135,8 +1212,22 @@ func updateUser(w http.ResponseWriter, r *http.Request) {
|
1135 | 1212 | UserName: logic.MasterUser,
|
1136 | 1213 | }
|
1137 | 1214 | }
|
| 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 | + } |
1138 | 1229 | e := models.Event{
|
1139 |
| - Action: models.Update, |
| 1230 | + Action: action, |
1140 | 1231 | Source: models.Subject{
|
1141 | 1232 | ID: caller.UserName,
|
1142 | 1233 | Name: caller.UserName,
|
|
0 commit comments