Skip to content

Commit b44de37

Browse files
authored
New module API event for tracking authentication attempts (#2237)
In this commit we introduce a new module API event called `ValkeyModuleEvent_AuthenticationAttempt` to track successful/failed authentication attempts. This event will fill a struct, called `ValkeyModuleAuthenticationInfo`, with the client ID of the connection, the username, the module name that handle the authentication event, and the result of the authentication attempt. Fixes: #2211 --------- Signed-off-by: Ricardo Dias <[email protected]>
1 parent c039b69 commit b44de37

File tree

8 files changed

+196
-29
lines changed

8 files changed

+196
-29
lines changed

src/acl.c

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1458,16 +1458,25 @@ void addAuthErrReply(client *c, robj *err) {
14581458
*
14591459
* The return value is AUTH_OK on success (valid username / password pair) & AUTH_ERR otherwise. */
14601460
static int checkPasswordBasedAuth(client *c, robj *username, robj *password) {
1461+
int result;
1462+
14611463
if (ACLCheckUserCredentials(username, password) == C_OK) {
14621464
user *user = ACLGetUserByName(username->ptr, sdslen(username->ptr));
14631465
clientSetUser(c, user, 1);
14641466
moduleNotifyUserChanged(c);
1465-
return AUTH_OK;
1467+
result = AUTH_OK;
14661468
} else {
14671469
addACLLogEntry(c, ACL_DENIED_AUTH, (c->flag.multi) ? ACL_LOG_CTX_MULTI : ACL_LOG_CTX_TOPLEVEL, 0, username->ptr,
14681470
NULL);
1469-
return AUTH_ERR;
1471+
result = AUTH_ERR;
14701472
}
1473+
1474+
moduleFireAuthenticationEvent(c->id,
1475+
username->ptr,
1476+
NULL,
1477+
result == AUTH_OK);
1478+
1479+
return result;
14711480
}
14721481

14731482
/* Attempt authenticating the user - first through module based authentication,

src/module.c

Lines changed: 70 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8012,7 +8012,7 @@ void moduleUnregisterAuthCBs(ValkeyModule *module) {
80128012

80138013
/* Search for & attempt next module auth callback after skipping the ones already attempted.
80148014
* Returns the result of the module auth callback. */
8015-
int attemptNextAuthCb(client *c, robj *username, robj *password, robj **err) {
8015+
static int attemptNextAuthCb(client *c, robj *username, robj *password, robj **err) {
80168016
int handle_next_callback = (!c->module_data || c->module_data->module_auth_ctx == NULL);
80178017
ValkeyModuleAuthCtx *cur_auth_ctx = NULL;
80188018
listNode *ln;
@@ -8047,7 +8047,7 @@ int attemptNextAuthCb(client *c, robj *username, robj *password, robj **err) {
80478047
* auth operation.
80488048
* Otherwise, we attempt the auth reply callback & the free priv data callback, update fields and
80498049
* return the result of the reply callback. */
8050-
int attemptBlockedAuthReplyCallback(client *c, robj *username, robj *password, robj **err) {
8050+
static int attemptBlockedAuthReplyCallback(client *c, robj *username, robj *password, robj **err) {
80518051
int result = VALKEYMODULE_AUTH_NOT_HANDLED;
80528052
if (!c->module_data || !c->module_data->module_blocked_client) return result;
80538053
ValkeyModuleBlockedClient *bc = (ValkeyModuleBlockedClient *)c->module_data->module_blocked_client;
@@ -8091,17 +8091,44 @@ int checkModuleAuthentication(client *c, robj *username, robj *password, robj **
80918091
serverAssert(result == VALKEYMODULE_AUTH_HANDLED);
80928092
return AUTH_BLOCKED;
80938093
}
8094+
8095+
ValkeyModuleAuthCtx *auth_ctx = c->module_data ? c->module_data->module_auth_ctx : NULL;
8096+
80948097
if (c->module_data) c->module_data->module_auth_ctx = NULL;
80958098
if (result == VALKEYMODULE_AUTH_NOT_HANDLED) {
80968099
c->flag.module_auth_has_result = 0;
80978100
return AUTH_NOT_HANDLED;
80988101
}
80998102

8103+
int auth_result = AUTH_ERR;
8104+
81008105
if (c->flag.module_auth_has_result) {
81018106
c->flag.module_auth_has_result = 0;
8102-
if (c->flag.authenticated) return AUTH_OK;
8107+
if (c->flag.authenticated) {
8108+
auth_result = AUTH_OK;
8109+
}
81038110
}
8104-
return AUTH_ERR;
8111+
8112+
const char *module_name = auth_ctx ? auth_ctx->module->name : NULL;
8113+
moduleFireAuthenticationEvent(c->id,
8114+
username->ptr,
8115+
module_name,
8116+
auth_result == AUTH_OK);
8117+
8118+
return auth_result;
8119+
}
8120+
8121+
void moduleFireAuthenticationEvent(uint64_t client_id,
8122+
const char *username,
8123+
const char *module_name,
8124+
int is_granted) {
8125+
ValkeyModuleAuthenticationInfo info = VALKEYMODULE_AUTHENTICATIONINFO_INITIALIZER_V1;
8126+
info.client_id = client_id;
8127+
info.username = username;
8128+
info.module_name = module_name;
8129+
info.result = is_granted ? VALKEYMODULE_AUTH_RESULT_GRANTED
8130+
: VALKEYMODULE_AUTH_RESULT_DENIED;
8131+
moduleFireServerEvent(VALKEYMODULE_EVENT_AUTHENTICATION_ATTEMPT, 0, &info);
81058132
}
81068133

81078134
/* This function is called from module.c in order to check if a module
@@ -11480,24 +11507,25 @@ void ModuleForkDoneHandler(int exitcode, int bysignal) {
1148011507
* a data structure associated with it. We use MAX_UINT64 on purpose,
1148111508
* in order to pass the check in ValkeyModule_SubscribeToServerEvent. */
1148211509
static uint64_t moduleEventVersions[] = {
11483-
VALKEYMODULE_REPLICATIONINFO_VERSION, /* VALKEYMODULE_EVENT_REPLICATION_ROLE_CHANGED */
11484-
-1, /* VALKEYMODULE_EVENT_PERSISTENCE */
11485-
VALKEYMODULE_FLUSHINFO_VERSION, /* VALKEYMODULE_EVENT_FLUSHDB */
11486-
-1, /* VALKEYMODULE_EVENT_LOADING */
11487-
VALKEYMODULE_CLIENTINFO_VERSION, /* VALKEYMODULE_EVENT_CLIENT_CHANGE */
11488-
-1, /* VALKEYMODULE_EVENT_SHUTDOWN */
11489-
-1, /* VALKEYMODULE_EVENT_REPLICA_CHANGE */
11490-
-1, /* VALKEYMODULE_EVENT_PRIMARY_LINK_CHANGE */
11491-
VALKEYMODULE_CRON_LOOP_VERSION, /* VALKEYMODULE_EVENT_CRON_LOOP */
11492-
VALKEYMODULE_MODULE_CHANGE_VERSION, /* VALKEYMODULE_EVENT_MODULE_CHANGE */
11493-
VALKEYMODULE_LOADING_PROGRESS_VERSION, /* VALKEYMODULE_EVENT_LOADING_PROGRESS */
11494-
VALKEYMODULE_SWAPDBINFO_VERSION, /* VALKEYMODULE_EVENT_SWAPDB */
11495-
-1, /* VALKEYMODULE_EVENT_REPL_BACKUP */
11496-
-1, /* VALKEYMODULE_EVENT_FORK_CHILD */
11497-
-1, /* VALKEYMODULE_EVENT_REPL_ASYNC_LOAD */
11498-
-1, /* VALKEYMODULE_EVENT_EVENTLOOP */
11499-
-1, /* VALKEYMODULE_EVENT_CONFIG */
11500-
VALKEYMODULE_KEYINFO_VERSION, /* VALKEYMODULE_EVENT_KEY */
11510+
VALKEYMODULE_REPLICATIONINFO_VERSION, /* VALKEYMODULE_EVENT_REPLICATION_ROLE_CHANGED */
11511+
-1, /* VALKEYMODULE_EVENT_PERSISTENCE */
11512+
VALKEYMODULE_FLUSHINFO_VERSION, /* VALKEYMODULE_EVENT_FLUSHDB */
11513+
-1, /* VALKEYMODULE_EVENT_LOADING */
11514+
VALKEYMODULE_CLIENTINFO_VERSION, /* VALKEYMODULE_EVENT_CLIENT_CHANGE */
11515+
-1, /* VALKEYMODULE_EVENT_SHUTDOWN */
11516+
-1, /* VALKEYMODULE_EVENT_REPLICA_CHANGE */
11517+
-1, /* VALKEYMODULE_EVENT_PRIMARY_LINK_CHANGE */
11518+
VALKEYMODULE_CRON_LOOP_VERSION, /* VALKEYMODULE_EVENT_CRON_LOOP */
11519+
VALKEYMODULE_MODULE_CHANGE_VERSION, /* VALKEYMODULE_EVENT_MODULE_CHANGE */
11520+
VALKEYMODULE_LOADING_PROGRESS_VERSION, /* VALKEYMODULE_EVENT_LOADING_PROGRESS */
11521+
VALKEYMODULE_SWAPDBINFO_VERSION, /* VALKEYMODULE_EVENT_SWAPDB */
11522+
-1, /* VALKEYMODULE_EVENT_REPL_BACKUP */
11523+
-1, /* VALKEYMODULE_EVENT_FORK_CHILD */
11524+
-1, /* VALKEYMODULE_EVENT_REPL_ASYNC_LOAD */
11525+
-1, /* VALKEYMODULE_EVENT_EVENTLOOP */
11526+
-1, /* VALKEYMODULE_EVENT_CONFIG */
11527+
VALKEYMODULE_KEYINFO_VERSION, /* VALKEYMODULE_EVENT_KEY */
11528+
VALKEYMODULE_AUTHENTICATION_INFO_VERSION, /* VALKEYMODULE_EVENT_AUTHENTICATION_ATTEMPT */
1150111529
};
1150211530

1150311531
/* Register to be notified, via a callback, when the specified server event
@@ -11758,7 +11786,7 @@ static uint64_t moduleEventVersions[] = {
1175811786
* * `VALKEYMODULE_SUBEVENT_EVENTLOOP_BEFORE_SLEEP`
1175911787
* * `VALKEYMODULE_SUBEVENT_EVENTLOOP_AFTER_SLEEP`
1176011788
*
11761-
* * ValkeyModule_Event_Config
11789+
* * ValkeyModuleEvent_Config
1176211790
*
1176311791
* Called when a configuration event happens
1176411792
* The following sub events are available:
@@ -11772,7 +11800,7 @@ static uint64_t moduleEventVersions[] = {
1177211800
* // name of each modified configuration item
1177311801
* uint32_t num_changes; // The number of elements in the config_names array
1177411802
*
11775-
* * ValkeyModule_Event_Key
11803+
* * ValkeyModuleEvent_Key
1177611804
*
1177711805
* Called when a key is removed from the keyspace. We can't modify any key in
1177811806
* the event.
@@ -11788,6 +11816,22 @@ static uint64_t moduleEventVersions[] = {
1178811816
*
1178911817
* ValkeyModuleKey *key; // Key name
1179011818
*
11819+
* * ValkeyModuleEvent_AuthenticationAttempt
11820+
*
11821+
* Called when an authentication attempt is made, either successful or not.
11822+
*
11823+
* The data pointer can be casted to a ValkeyModuleAuthenticationInfo
11824+
* structure with the following fields:
11825+
*
11826+
* uint64_t client_id; // Client ID.
11827+
* const char *username; // Username used for authentication.
11828+
* const char *module_name; // Name of the module that is handling the
11829+
* // authentication. It is NULL if the
11830+
* // authentication is handled by the core.
11831+
* ValkeyModuleAuthenticationResult result; // Result of the authentication:
11832+
* // VALKEYMODULE_AUTH_RESULT_GRANTED or
11833+
* // VALKEYMODULE_AUTH_RESULT_DENIED
11834+
*
1179111835
* The function returns VALKEYMODULE_OK if the module was successfully subscribed
1179211836
* for the specified event. If the API is called from a wrong context or unsupported event
1179311837
* is given then VALKEYMODULE_ERR is returned. */
@@ -11937,6 +11981,8 @@ void moduleFireServerEvent(uint64_t eid, int subid, void *data) {
1193711981
selectDb(ctx.client, info->dbnum);
1193811982
moduleInitKey(&key, &ctx, info->key, info->value, info->mode);
1193911983
moduledata = &ki;
11984+
} else if (eid == VALKEYMODULE_EVENT_AUTHENTICATION_ATTEMPT) {
11985+
moduledata = data;
1194011986
}
1194111987

1194211988
el->module->in_hook++;

src/module.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,5 +230,7 @@ void moduleDefragGlobals(void);
230230
void *moduleGetHandleByName(char *modulename);
231231
int moduleIsModuleCommand(void *module_handle, struct serverCommand *cmd);
232232
void freeClientModuleData(client *c);
233+
int checkModuleAuthentication(client *c, robj *username, robj *password, robj **err);
234+
void moduleFireAuthenticationEvent(uint64_t client_id, const char *username, const char *module_name, int is_granted);
233235

234236
#endif /* _MODULE_H_ */

src/modules/hellohook.c

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,15 @@ void flushdbCallback(ValkeyModuleCtx *ctx, ValkeyModuleEvent e, uint64_t sub, vo
7171
}
7272
}
7373

74+
void authenticationAttemptCallback(ValkeyModuleCtx *ctx, ValkeyModuleEvent e, uint64_t sub, void *data) {
75+
VALKEYMODULE_NOT_USED(ctx);
76+
VALKEYMODULE_NOT_USED(e);
77+
78+
ValkeyModuleAuthenticationInfo *ai = data;
79+
printf("Authentication attempt for client #%llu with username=%s module=%s success=%d\n",
80+
(unsigned long long)ai->client_id, ai->username, ai->module_name, ai->result == VALKEYMODULE_AUTH_RESULT_GRANTED);
81+
}
82+
7483
/* This function must be present on each module. It is used in order to
7584
* register the commands into the server. */
7685
int ValkeyModule_OnLoad(ValkeyModuleCtx *ctx, ValkeyModuleString **argv, int argc) {
@@ -81,5 +90,6 @@ int ValkeyModule_OnLoad(ValkeyModuleCtx *ctx, ValkeyModuleString **argv, int arg
8190

8291
ValkeyModule_SubscribeToServerEvent(ctx, ValkeyModuleEvent_ClientChange, clientChangeCallback);
8392
ValkeyModule_SubscribeToServerEvent(ctx, ValkeyModuleEvent_FlushDB, flushdbCallback);
93+
ValkeyModule_SubscribeToServerEvent(ctx, ValkeyModuleEvent_AuthenticationAttempt, authenticationAttemptCallback);
8494
return VALKEYMODULE_OK;
8595
}

src/server.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3198,7 +3198,6 @@ typedef enum {
31983198

31993199
int ACLCheckUserCredentials(robj *username, robj *password);
32003200
int ACLAuthenticateUser(client *c, robj *username, robj *password, robj **err);
3201-
int checkModuleAuthentication(client *c, robj *username, robj *password, robj **err);
32023201
void addAuthErrReply(client *c, robj *err);
32033202
unsigned long ACLGetCommandID(sds cmdname);
32043203
user *ACLGetUserByName(const char *name, size_t namelen);

src/valkeymodule.h

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -522,7 +522,8 @@ typedef void (*ValkeyModuleEventLoopOneShotFunc)(void *user_data);
522522
#define VALKEYMODULE_EVENT_EVENTLOOP 15
523523
#define VALKEYMODULE_EVENT_CONFIG 16
524524
#define VALKEYMODULE_EVENT_KEY 17
525-
#define _VALKEYMODULE_EVENT_NEXT 18 /* Next event flag, should be updated if a new event added. */
525+
#define VALKEYMODULE_EVENT_AUTHENTICATION_ATTEMPT 18
526+
#define _VALKEYMODULE_EVENT_NEXT 19 /* Next event flag, should be updated if a new event added. */
526527

527528
typedef struct ValkeyModuleEvent {
528529
uint64_t id; /* VALKEYMODULE_EVENT_... defines. */
@@ -579,7 +580,8 @@ static const ValkeyModuleEvent ValkeyModuleEvent_ReplicationRoleChanged = {VALKE
579580
ValkeyModuleEvent_ForkChild = {VALKEYMODULE_EVENT_FORK_CHILD, 1},
580581
ValkeyModuleEvent_EventLoop = {VALKEYMODULE_EVENT_EVENTLOOP, 1},
581582
ValkeyModuleEvent_Config = {VALKEYMODULE_EVENT_CONFIG, 1},
582-
ValkeyModuleEvent_Key = {VALKEYMODULE_EVENT_KEY, 1};
583+
ValkeyModuleEvent_Key = {VALKEYMODULE_EVENT_KEY, 1},
584+
ValkeyModuleEvent_AuthenticationAttempt = {VALKEYMODULE_EVENT_AUTHENTICATION_ATTEMPT, 1};
583585

584586
/* Those are values that are used for the 'subevent' callback argument. */
585587
#define VALKEYMODULE_SUBEVENT_PERSISTENCE_RDB_START 0
@@ -781,6 +783,25 @@ typedef struct ValkeyModuleKeyInfo {
781783

782784
#define ValkeyModuleKeyInfo ValkeyModuleKeyInfoV1
783785

786+
#define VALKEYMODULE_AUTHENTICATION_INFO_VERSION 1
787+
788+
typedef enum {
789+
VALKEYMODULE_AUTH_RESULT_GRANTED = 0, /* Authentication succeeded. */
790+
VALKEYMODULE_AUTH_RESULT_DENIED = 1, /* Authentication failed. */
791+
} ValkeyModuleAuthenticationResult;
792+
793+
typedef struct ValkeyModuleAuthenticationInfo {
794+
uint64_t version; /* Version of this structure for ABI compat. */
795+
uint64_t client_id; /* Client ID. */
796+
const char *username; /* Username used for authentication. */
797+
const char *module_name; /* Name of the module that is handling the authentication. */
798+
ValkeyModuleAuthenticationResult result; /* Result of the authentication */
799+
} ValkeyModuleAuthenticationInfoV1;
800+
801+
#define ValkeyModuleAuthenticationInfo ValkeyModuleAuthenticationInfoV1
802+
803+
#define VALKEYMODULE_AUTHENTICATIONINFO_INITIALIZER_V1 {.version = 1}
804+
784805
typedef enum {
785806
VALKEYMODULE_ACL_LOG_AUTH = 0, /* Authentication failure */
786807
VALKEYMODULE_ACL_LOG_CMD, /* Command authorization failure */

tests/modules/hooks.c

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,19 @@ void keyInfoCallback(ValkeyModuleCtx *ctx, ValkeyModuleEvent e, uint64_t sub, vo
365365
}
366366
}
367367

368+
void authAttemptCallback(ValkeyModuleCtx *ctx, ValkeyModuleEvent e, uint64_t sub, void *data)
369+
{
370+
VALKEYMODULE_NOT_USED(e);
371+
VALKEYMODULE_NOT_USED(sub);
372+
373+
ValkeyModuleAuthenticationInfo *ai = data;
374+
LogStringEvent(ctx, "auth-attempt", ai->username);
375+
if (ai->module_name) {
376+
LogStringEvent(ctx, "auth-attempt-module", ai->module_name);
377+
}
378+
LogNumericEvent(ctx, "auth-attempt-success", ai->result == VALKEYMODULE_AUTH_RESULT_GRANTED);
379+
}
380+
368381
static int cmdIsKeyRemoved(ValkeyModuleCtx *ctx, ValkeyModuleString **argv, int argc){
369382
if(argc != 2){
370383
return ValkeyModule_WrongArity(ctx);
@@ -453,6 +466,9 @@ int ValkeyModule_OnLoad(ValkeyModuleCtx *ctx, ValkeyModuleString **argv, int arg
453466
ValkeyModule_SubscribeToServerEvent(ctx,
454467
ValkeyModuleEvent_Key, keyInfoCallback);
455468

469+
ValkeyModule_SubscribeToServerEvent(ctx,
470+
ValkeyModuleEvent_AuthenticationAttempt, authAttemptCallback);
471+
456472
event_log = ValkeyModule_CreateDict(ctx);
457473
removed_event_log = ValkeyModule_CreateDict(ctx);
458474
removed_subevent_type = ValkeyModule_CreateDict(ctx);

tests/unit/moduleapi/hooks.tcl

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
set testmodule [file normalize tests/modules/hooks.so]
2+
set authmodule [file normalize tests/modules/auth.so]
23

34
tags "modules" {
45
start_server [list overrides [list loadmodule "$testmodule" appendonly yes]] {
@@ -226,6 +227,69 @@ tags "modules" {
226227
assert_equal [r hooks.event_last flush-end] -1
227228
}
228229

230+
test {Test success authentication attempt hooks} {
231+
r acl setuser testuser on >testpass ~* +@all
232+
r auth testuser testpass
233+
assert_equal [r hooks.event_last auth-attempt] "testuser"
234+
assert {[r hooks.event_count auth-attempt-module] < 1}
235+
assert_equal [r hooks.event_last auth-attempt-success] "1"
236+
}
237+
238+
test {Test failed authentication attempt hooks} {
239+
r acl setuser testuser on >testpass ~* +@all
240+
catch {r auth testuser wrongpass} e
241+
assert_match {*WRONGPASS invalid username-password*} $e
242+
assert_equal [r hooks.event_last auth-attempt] "testuser"
243+
assert {[r hooks.event_count auth-attempt-module] < 1}
244+
assert_equal [r hooks.event_last auth-attempt-success] "0"
245+
}
246+
247+
test {Test success module authentication attempt hooks} {
248+
r module load $authmodule
249+
r testmoduleone.rm_register_auth_cb
250+
r acl setuser foo on >testpass ~* +@all
251+
r auth foo allow
252+
assert_equal [r hooks.event_last auth-attempt] "foo"
253+
assert_equal [r hooks.event_last auth-attempt-module] "testacl"
254+
assert_equal [r hooks.event_last auth-attempt-success] "1"
255+
r module unload testacl
256+
}
257+
258+
test {Test failed module authentication attempt hooks} {
259+
r module load $authmodule
260+
r testmoduleone.rm_register_auth_cb
261+
r acl setuser foo on ~* +@all
262+
catch {r auth foo deny} e
263+
assert_match {*Auth denied by Misc Module*} $e
264+
assert_equal [r hooks.event_last auth-attempt] "foo"
265+
assert_equal [r hooks.event_last auth-attempt-module] "testacl"
266+
assert_equal [r hooks.event_last auth-attempt-success] "0"
267+
r module unload testacl
268+
}
269+
270+
test {Test success module blocking authentication attempt hooks} {
271+
r module load $authmodule
272+
r testmoduleone.rm_register_blocking_auth_cb
273+
r acl setuser foo on ~* +@all
274+
r auth foo block_allow
275+
assert_equal [r hooks.event_last auth-attempt] "foo"
276+
assert_equal [r hooks.event_last auth-attempt-module] "testacl"
277+
assert_equal [r hooks.event_last auth-attempt-success] "1"
278+
r module unload testacl
279+
}
280+
281+
test {Test failed module blocking authentication attempt hooks} {
282+
r module load $authmodule
283+
r testmoduleone.rm_register_blocking_auth_cb
284+
r acl setuser foo on ~* +@all
285+
catch {r auth foo block_deny} e
286+
assert_match {*Auth denied by Misc Module*} $e
287+
assert_equal [r hooks.event_last auth-attempt] "foo"
288+
assert_equal [r hooks.event_last auth-attempt-module] "testacl"
289+
assert_equal [r hooks.event_last auth-attempt-success] "0"
290+
r module unload testacl
291+
}
292+
229293
# replication related tests
230294
set master [srv 0 client]
231295
set master_host [srv 0 host]

0 commit comments

Comments
 (0)