From 2c9a13f370f0a24560e00c1d5628bea72addcb15 Mon Sep 17 00:00:00 2001 From: Dmitry Ivanov <ivadmi5@gmail.com> Date: Thu, 30 Nov 2023 18:52:29 +0300 Subject: [PATCH] feat(audit): add authentication events This patch adds two new events: * auth_ok * auth_fail, which may contain "verdict" describing user suspension. Examples: ```json { "id": "1.1.19", "message": "successfully authenticated user `guest`", "severity": "low", "time": "2023-11-30T19:02:10.708+0300", "title": "auth_ok", "user": "guest" } ``` ```json { "id": "1.0.11", "message": "failed to authenticate user `borat`", "severity": "high", "time": "2023-11-30T18:58:48.635+0300", "title": "auth_failed", "user": "borat" } ``` --- src/lib.rs | 124 +++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 86 insertions(+), 38 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 240cf5c487..3d1ddbe441 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -289,50 +289,98 @@ fn redirect_interactive_sql() { /// Sets a check for user exceeding maximum number of login attempts through `picodata connect`. /// Also see [`storage::PropertyName::MaxLoginAttempts`]. fn set_login_attempts_check(storage: Clusterwide) { - use std::collections::hash_map::Entry; + enum Verdict { + AuthOk, + AuthFail, + UserBlocked, + } + + // Determines the outcome of an authentication attempt. + let compute_auth_verdict = move |user: String, status: bool| { + use std::collections::hash_map::Entry; - // It's ok to loose this information during restart, so we keep it as a static. - static mut LOGIN_ATTEMPTS: OnceCell<HashMap<String, usize>> = OnceCell::new(); - const ERROR: &str = "Maximum number of login attempts exceeded"; + // It's ok to lose this information during restart, so we keep it as a static. + static mut LOGIN_ATTEMPTS: OnceCell<HashMap<String, usize>> = OnceCell::new(); + + // SAFETY: Accessing `USER_ATTEMPTS` is safe as it is only done from a single thread + let attempts = unsafe { + LOGIN_ATTEMPTS.get_or_init(HashMap::new); + LOGIN_ATTEMPTS.get_mut().expect("is initialized") + }; + + let max_login_attempts = || { + storage + .properties + .max_login_attempts() + .expect("accessing storage should not fail") + }; + + match attempts.entry(user) { + Entry::Occupied(e) if *e.get() >= max_login_attempts() => { + // The account is suspended until restart + Verdict::UserBlocked + } + Entry::Occupied(mut e) => { + if status { + // Forget about previous failures + e.remove(); + Verdict::AuthOk + } else { + *e.get_mut() += 1; + Verdict::AuthFail + } + } + Entry::Vacant(e) => { + if status { + Verdict::AuthOk + } else { + // Remember the failure, but don't raise an error yet + e.insert(1); + Verdict::AuthFail + } + } + } + }; let lua = ::tarantool::lua_state(); lua.exec_with( "box.session.on_auth(...)", - tlua::function3( - move |user: String, status: bool, lua: tlua::LuaState| unsafe { - // SAFETY: Accessing `USER_ATTEMPTS` is safe as it is only done from a single thread - LOGIN_ATTEMPTS.get_or_init(HashMap::new); - let attempts = LOGIN_ATTEMPTS.get_mut().expect("is initialized"); - - match attempts.entry(user) { - Entry::Occupied(mut count) => { - if *count.get() - >= storage - .properties - .max_login_attempts() - .expect("accessing storage should not fail") - { - // Raises an error instead of returning it as a function result. - // This is the behavior required by `on_auth` trigger to drop the connection. - // All the drop implementations are called, no need to clean anything up. - tlua::ffi::lua_pushlstring(lua, ERROR.as_ptr() as _, ERROR.len()); - tlua::ffi::lua_error(lua); - unreachable!(); - } else if status { - // reset count on successful login - count.remove(); - } else { - *count.get_mut() += 1 - } - } - Entry::Vacant(count) => { - if !status { - count.insert(1); - } - } + tlua::function3(move |user: String, status: bool, lua: tlua::LuaState| { + const ERROR: &str = "Maximum number of login attempts exceeded"; + + match compute_auth_verdict(user.clone(), status) { + Verdict::AuthOk => { + crate::audit!( + message: "successfully authenticated user `{user}`", + title: "auth_ok", + severity: High, + user: &user, + ); + } + Verdict::AuthFail => { + crate::audit!( + message: "failed to authenticate user `{user}`", + title: "auth_fail", + severity: High, + user: &user, + ); } - }, - ), + Verdict::UserBlocked => { + crate::audit!( + message: "failed to authenticate user `{user}`", + title: "auth_fail", + severity: High, + user: &user, + verdict: format_args!("{ERROR}; user will be blocked indefinitely"), + ); + + // Raises an error instead of returning it as a function result. + // This is the behavior required by `on_auth` trigger to drop the connection. + // All the drop implementations are called, no need to clean anything up. + tlua::error!(lua, "{}", ERROR); + } + } + }), ) .expect("setting on auth trigger should not fail") } -- GitLab