diff --git a/runtest-moduleapi b/runtest-moduleapi index 33b2711d4..0e78e3e84 100755 --- a/runtest-moduleapi +++ b/runtest-moduleapi @@ -40,4 +40,5 @@ $TCLSH tests/test_helper.tcl \ --single unit/moduleapi/list \ --single unit/moduleapi/stream \ --single unit/moduleapi/datatype2 \ +--single unit/moduleapi/aclcheck \ "${@}" diff --git a/src/acl.c b/src/acl.c index 4f735c16f..6947fc204 100644 --- a/src/acl.c +++ b/src/acl.c @@ -384,7 +384,7 @@ int ACLGetCommandBitCoordinates(uint64_t id, uint64_t *word, uint64_t *bit) { * * If the bit overflows the user internal representation, zero is returned * in order to disallow the execution of the command in such edge case. */ -int ACLGetUserCommandBit(user *u, unsigned long id) { +int ACLGetUserCommandBit(const user *u, unsigned long id) { uint64_t word, bit; if (ACLGetCommandBitCoordinates(id,&word,&bit) == C_ERR) return 0; return (u->allowed_commands[word] & bit) != 0; @@ -1126,7 +1126,7 @@ int ACLAuthenticateUser(client *c, robj *username, robj *password) { moduleNotifyUserChanged(c); return C_OK; } else { - addACLLogEntry(c,ACL_DENIED_AUTH,0,username->ptr); + addACLLogEntry(c,ACL_DENIED_AUTH,(c->flags & CLIENT_MULTI) ? ACL_LOG_CTX_MULTI : ACL_LOG_CTX_TOPLEVEL,0,username->ptr,NULL); return C_ERR; } } @@ -1179,31 +1179,56 @@ user *ACLGetUserByName(const char *name, size_t namelen) { return myuser; } -/* Check if the command is ready to be executed in the client 'c', already - * referenced by c->cmd, and can be executed by this client according to the - * ACLs associated to the client user c->user. +/* Check if the key can be accessed by the client according to + * the ACLs associated with the specified user. + * + * If the user can access the key, ACL_OK is returned, otherwise + * ACL_DENIED_KEY is returned. */ +int ACLCheckKey(const user *u, const char *key, int keylen) { + /* If there is no associated user, the connection can run anything. */ + if (u == NULL) return ACL_OK; + + /* The user can run any keys */ + if (u->flags & USER_FLAG_ALLKEYS) return ACL_OK; + + listIter li; + listNode *ln; + listRewind(u->patterns,&li); + + /* Test this key against every pattern. */ + while((ln = listNext(&li))) { + sds pattern = listNodeValue(ln); + size_t plen = sdslen(pattern); + if (stringmatchlen(pattern,plen,key,keylen,0)) + return ACL_OK; + } + return ACL_DENIED_KEY; +} + +/* Check if the command is ready to be executed according to the + * ACLs associated with the specified user. * * If the user can execute the command ACL_OK is returned, otherwise * ACL_DENIED_CMD or ACL_DENIED_KEY is returned: the first in case the * command cannot be executed because the user is not allowed to run such * command, the second if the command is denied because the user is trying * to access keys that are not among the specified patterns. */ -int ACLCheckCommandPerm(client *c, int *keyidxptr) { - user *u = c->user; - uint64_t id = c->cmd->id; +int ACLCheckCommandPerm(const user *u, struct redisCommand *cmd, robj **argv, int argc, int *keyidxptr) { + int ret; + uint64_t id = cmd->id; /* If there is no associated user, the connection can run anything. */ if (u == NULL) return ACL_OK; /* Check if the user can execute this command or if the command * doesn't need to be authenticated (hello, auth). */ - if (!(u->flags & USER_FLAG_ALLCOMMANDS) && !(c->cmd->flags & CMD_NO_AUTH)) + if (!(u->flags & USER_FLAG_ALLCOMMANDS) && !(cmd->flags & CMD_NO_AUTH)) { /* If the bit is not set we have to check further, in case the * command is allowed just with that specific subcommand. */ if (ACLGetUserCommandBit(u,id) == 0) { /* Check if the subcommand matches. */ - if (c->argc < 2 || + if (argc < 2 || u->allowed_subcommands == NULL || u->allowed_subcommands[id] == NULL) { @@ -1214,7 +1239,7 @@ int ACLCheckCommandPerm(client *c, int *keyidxptr) { while (1) { if (u->allowed_subcommands[id][subid] == NULL) return ACL_DENIED_CMD; - if (!strcasecmp(c->argv[1]->ptr, + if (!strcasecmp(argv[1]->ptr, u->allowed_subcommands[id][subid])) break; /* Subcommand match found. Stop here. */ subid++; @@ -1224,34 +1249,19 @@ int ACLCheckCommandPerm(client *c, int *keyidxptr) { /* Check if the user can execute commands explicitly touching the keys * mentioned in the command arguments. */ - if (!(c->user->flags & USER_FLAG_ALLKEYS) && - (c->cmd->getkeys_proc || c->cmd->key_specs_num)) + if (!(u->flags & USER_FLAG_ALLKEYS) && + (cmd->getkeys_proc || cmd->key_specs_num)) { getKeysResult result = GETKEYS_RESULT_INIT; - int numkeys = getKeysFromCommand(c->cmd,c->argv,c->argc,&result); + int numkeys = getKeysFromCommand(cmd,argv,argc,&result); int *keyidx = result.keys; for (int j = 0; j < numkeys; j++) { - listIter li; - listNode *ln; - listRewind(u->patterns,&li); - - /* Test this key against every pattern. */ - int match = 0; - while((ln = listNext(&li))) { - sds pattern = listNodeValue(ln); - size_t plen = sdslen(pattern); - int idx = keyidx[j]; - if (stringmatchlen(pattern,plen,c->argv[idx]->ptr, - sdslen(c->argv[idx]->ptr),0)) - { - match = 1; - break; - } - } - if (!match) { + int idx = keyidx[j]; + ret = ACLCheckKey(u, argv[idx]->ptr, sdslen(argv[idx]->ptr)); + if (ret != ACL_OK) { if (keyidxptr) *keyidxptr = keyidx[j]; getKeysFreeResult(&result); - return ACL_DENIED_KEY; + return ret; } } getKeysFreeResult(&result); @@ -1341,9 +1351,8 @@ void ACLKillPubsubClientsIfNeeded(user *u, list *upcoming) { } } -/* Check if the pub/sub channels of the command, that's ready to be executed in - * the client 'c', can be executed by this client according to the ACLs channels - * associated to the client user c->user. +/* Check if the pub/sub channels of the command, that's ready to be executed + * according to the ACLs channels associated with the specified user. * * idx and count are the index and count of channel arguments from the * command. The literal argument controls whether the user's ACL channels are @@ -1351,17 +1360,15 @@ void ACLKillPubsubClientsIfNeeded(user *u, list *upcoming) { * * If the user can execute the command ACL_OK is returned, otherwise * ACL_DENIED_CHANNEL. */ -int ACLCheckPubsubPerm(client *c, int idx, int count, int literal, int *idxptr) { - user *u = c->user; - +int ACLCheckPubsubPerm(const user *u, robj **argv, int idx, int count, int literal, int *idxptr) { /* If there is no associated user, the connection can run anything. */ if (u == NULL) return ACL_OK; /* Check if the user can access the channels mentioned in the command's * arguments. */ - if (!(c->user->flags & USER_FLAG_ALLCHANNELS)) { + if (!(u->flags & USER_FLAG_ALLCHANNELS)) { for (int j = idx; j < idx+count; j++) { - if (ACLCheckPubsubChannelPerm(c->argv[j]->ptr,u->channels,literal) + if (ACLCheckPubsubChannelPerm(argv[j]->ptr,u->channels,literal) != ACL_OK) { if (idxptr) *idxptr = j; return ACL_DENIED_CHANNEL; @@ -1378,19 +1385,23 @@ int ACLCheckPubsubPerm(client *c, int idx, int count, int literal, int *idxptr) /* Check whether the command is ready to be executed by ACLCheckCommandPerm. * If check passes, then check whether pub/sub channels of the command is * ready to be executed by ACLCheckPubsubPerm */ -int ACLCheckAllPerm(client *c, int *idxptr) { - int acl_retval = ACLCheckCommandPerm(c,idxptr); +int ACLCheckAllUserCommandPerm(const user *u, struct redisCommand *cmd, robj **argv, int argc, int *idxptr) { + int acl_retval = ACLCheckCommandPerm(u,cmd,argv,argc,idxptr); if (acl_retval != ACL_OK) return acl_retval; - if (c->cmd->proc == publishCommand) - acl_retval = ACLCheckPubsubPerm(c,1,1,0,idxptr); - else if (c->cmd->proc == subscribeCommand) - acl_retval = ACLCheckPubsubPerm(c,1,c->argc-1,0,idxptr); - else if (c->cmd->proc == psubscribeCommand) - acl_retval = ACLCheckPubsubPerm(c,1,c->argc-1,1,idxptr); + if (cmd->proc == publishCommand) + acl_retval = ACLCheckPubsubPerm(u,argv,1,1,0,idxptr); + else if (cmd->proc == subscribeCommand) + acl_retval = ACLCheckPubsubPerm(u,argv,1,argc-1,0,idxptr); + else if (cmd->proc == psubscribeCommand) + acl_retval = ACLCheckPubsubPerm(u,argv,1,argc-1,1,idxptr); return acl_retval; } +int ACLCheckAllPerm(client *c, int *idxptr) { + return ACLCheckAllUserCommandPerm(c->user, c->cmd, c->argv, c->argc, idxptr); +} + /* ============================================================================= * ACL loading / saving functions * ==========================================================================*/ @@ -1757,9 +1768,6 @@ void ACLLoadUsersAtStartup(void) { * ACL log * ==========================================================================*/ -#define ACL_LOG_CTX_TOPLEVEL 0 -#define ACL_LOG_CTX_LUA 1 -#define ACL_LOG_CTX_MULTI 2 #define ACL_LOG_GROUPING_MAX_TIME_DELTA 60000 /* This structure defines an entry inside the ACL log. */ @@ -1804,37 +1812,36 @@ void ACLFreeLogEntry(void *leptr) { * * The argpos argument is used when the reason is ACL_DENIED_KEY or * ACL_DENIED_CHANNEL, since it allows the function to log the key or channel - * name that caused the problem. Similarly the username is only passed when we - * failed to authenticate the user with AUTH or HELLO, for the ACL_DENIED_AUTH - * reason. Otherwise it will just be NULL. + * name that caused the problem. + * + * The last 2 arguments are a manual override to be used, instead of any of the automatic + * ones which depend on the client and reason arguments (use NULL for default). */ -void addACLLogEntry(client *c, int reason, int argpos, sds username) { +void addACLLogEntry(client *c, int reason, int context, int argpos, sds username, sds object) { /* Create a new entry. */ struct ACLLogEntry *le = zmalloc(sizeof(*le)); le->count = 1; le->reason = reason; - le->username = sdsdup(reason == ACL_DENIED_AUTH ? username : c->user->name); + le->username = sdsdup(username ? username : c->user->name); le->ctime = mstime(); - switch(reason) { - case ACL_DENIED_CMD: le->object = sdsnew(c->cmd->name); break; - case ACL_DENIED_KEY: le->object = sdsdup(c->argv[argpos]->ptr); break; - case ACL_DENIED_CHANNEL: le->object = sdsdup(c->argv[argpos]->ptr); break; - case ACL_DENIED_AUTH: le->object = sdsdup(c->argv[0]->ptr); break; - default: le->object = sdsempty(); + if (object) { + le->object = sdsnew(object); + } else { + switch(reason) { + case ACL_DENIED_CMD: le->object = sdsnew(c->cmd->name); break; + case ACL_DENIED_KEY: le->object = sdsdup(c->argv[argpos]->ptr); break; + case ACL_DENIED_CHANNEL: le->object = sdsdup(c->argv[argpos]->ptr); break; + case ACL_DENIED_AUTH: le->object = sdsdup(c->argv[0]->ptr); break; + default: le->object = sdsempty(); + } } client *realclient = c; if (realclient->flags & CLIENT_LUA) realclient = server.lua_caller; le->cinfo = catClientInfoString(sdsempty(),realclient); - if (c->flags & CLIENT_MULTI) { - le->context = ACL_LOG_CTX_MULTI; - } else if (c->flags & CLIENT_LUA) { - le->context = ACL_LOG_CTX_LUA; - } else { - le->context = ACL_LOG_CTX_TOPLEVEL; - } + le->context = context; /* Try to match this entry with past ones, to see if we can just * update an existing entry instead of creating a new one. */ @@ -2184,6 +2191,7 @@ void aclCommand(client *c) { case ACL_LOG_CTX_TOPLEVEL: ctxstr="toplevel"; break; case ACL_LOG_CTX_MULTI: ctxstr="multi"; break; case ACL_LOG_CTX_LUA: ctxstr="lua"; break; + case ACL_LOG_CTX_MODULE: ctxstr="module"; break; default: ctxstr="unknown"; } addReplyBulkCString(c,ctxstr); diff --git a/src/module.c b/src/module.c index 7e4dc61dc..6c47f1530 100644 --- a/src/module.c +++ b/src/module.c @@ -343,6 +343,7 @@ typedef struct RedisModuleServerInfoData { #define REDISMODULE_ARGV_NO_REPLICAS (1<<2) #define REDISMODULE_ARGV_RESP_3 (1<<3) #define REDISMODULE_ARGV_RESP_AUTO (1<<4) +#define REDISMODULE_ARGV_CHECK_ACL (1<<5) /* Determine whether Redis should signalModifiedKey implicitly. * In case 'ctx' has no 'module' member (and therefore no module->options), @@ -373,6 +374,7 @@ unsigned long long ModulesInHooks = 0; /* Total number of modules in hooks * clients using such newly created users. */ typedef struct RedisModuleUser { user *user; /* Reference to the real redis user */ + int free_user; /* Indicates that user should also be freed when this object is freed */ } RedisModuleUser; /* This is a structure used to export some meta-information such as dbid to the module. */ @@ -4605,6 +4607,7 @@ RedisModuleString *RM_CreateStringFromCallReply(RedisModuleCallReply *reply) { * "R" -> REDISMODULE_ARGV_NO_REPLICAS * "3" -> REDISMODULE_ARGV_RESP_3 * "0" -> REDISMODULE_ARGV_RESP_AUTO + * "C" -> REDISMODULE_ARGV_CHECK_ACL * * On error (format specifier error) NULL is returned and nothing is * allocated. On success the argument vector is returned. */ @@ -4667,6 +4670,8 @@ robj **moduleCreateArgvFromUserFormat(const char *cmdname, const char *fmt, int if (flags) (*flags) |= REDISMODULE_ARGV_RESP_3; } else if (*p == '0') { if (flags) (*flags) |= REDISMODULE_ARGV_RESP_AUTO; + } else if (*p == 'C') { + if (flags) (*flags) |= REDISMODULE_ARGV_CHECK_ACL; } else { goto fmterr; } @@ -4704,6 +4709,7 @@ fmterr: * * `0` -- Return the reply in auto mode, i.e. the reply format will be the * same as the client attached to the given RedisModuleCtx. This will * probably used when you want to pass the reply directly to the client. + * * `C` -- Check if command can be executed according to ACL rules. * * **...**: The actual arguments to the Redis command. * * On success a RedisModuleCallReply object is returned, otherwise @@ -4716,6 +4722,8 @@ fmterr: * * EROFS: operation in Cluster instance when a write command is sent * in a readonly state. * * ENETDOWN: operation in Cluster instance when cluster is down. + * * ENOTSUP: No ACL user for the specified module context + * * EACCES: Command cannot be executed, according to ACL rules * * Example code fragment: * @@ -4754,6 +4762,7 @@ RedisModuleCallReply *RM_Call(RedisModuleCtx *ctx, const char *cmdname, const ch * recursive call to this module.) */ c = createClient(NULL); } + c->user = NULL; /* Root user. */ c->flags = CLIENT_MODULE; @@ -4797,6 +4806,25 @@ RedisModuleCallReply *RM_Call(RedisModuleCtx *ctx, const char *cmdname, const ch goto cleanup; } + /* Check if the user can run this command according to the current + * ACLs. */ + if (flags & REDISMODULE_ARGV_CHECK_ACL) { + int acl_errpos; + int acl_retval; + + if (ctx->client->user == NULL) { + errno = ENOTSUP; + goto cleanup; + } + acl_retval = ACLCheckAllUserCommandPerm(ctx->client->user,c->cmd,c->argv,c->argc,&acl_errpos); + if (acl_retval != ACL_OK) { + sds object = (acl_retval == ACL_DENIED_CMD) ? c->cmd->name : c->argv[acl_errpos]->ptr; + addACLLogEntry(ctx->client, acl_retval, ACL_LOG_CTX_MODULE, -1, ctx->client->user->name, object); + errno = EACCES; + goto cleanup; + } + } + /* If this is a Redis Cluster node, we need to make sure the module is not * trying to access non-local keys, with the exception of commands * received from our master. */ @@ -7197,6 +7225,7 @@ static void moduleFreeAuthenticatedClients(RedisModule *module) { RedisModuleUser *RM_CreateModuleUser(const char *name) { RedisModuleUser *new_user = zmalloc(sizeof(RedisModuleUser)); new_user->user = ACLCreateUnlinkedUser(); + new_user->free_user = 1; /* Free the previous temporarily assigned name to assign the new one */ sdsfree(new_user->user->name); @@ -7207,7 +7236,8 @@ RedisModuleUser *RM_CreateModuleUser(const char *name) { /* Frees a given user and disconnects all of the clients that have been * authenticated with it. See RM_CreateModuleUser for detailed usage.*/ int RM_FreeModuleUser(RedisModuleUser *user) { - ACLFreeUserAndKillClients(user->user); + if (user->free_user) + ACLFreeUserAndKillClients(user->user); zfree(user); return REDISMODULE_OK; } @@ -7223,6 +7253,98 @@ int RM_SetModuleUserACL(RedisModuleUser *user, const char* acl) { return ACLSetUser(user->user, acl, -1); } +/* Retrieve the user name of the client connection behind the current context. + * The user name can be used later, in order to get a RedisModuleUser. + * See more information in RM_GetModuleUserFromUserName. + * + * The returned string must be released with RedisModule_FreeString() or by + * enabling automatic memory management. */ +RedisModuleString *RM_GetCurrentUserName(RedisModuleCtx *ctx) { + return RM_CreateString(ctx,ctx->client->user->name,sdslen(ctx->client->user->name)); +} + +/* A RedisModuleUser can be used to check if command, key or channel can be executed or + * accessed according to the ACLs rules associated with that user. + * When a Module wants to do ACL checks on a general ACL user (not created by RM_CreateModuleUser), + * it can get the RedisModuleUser from this API, based on the user name retrieved by RM_GetCurrentUserName. + * + * Since a general ACL user can be deleted at any time, this RedisModuleUser should be used only in the context + * where this function was called. In order to do ACL checks out of that context, the Module can store the user name, + * and call this API at any other context. + * + * Returns NULL if the user is disabled or the user does not exist. + * The caller should later free the user using the function RM_FreeModuleUser().*/ +RedisModuleUser *RM_GetModuleUserFromUserName(RedisModuleString *name) { + /* First, verfify that the user exist */ + user *acl_user = ACLGetUserByName(name->ptr, sdslen(name->ptr)); + if (acl_user == NULL) { + return NULL; + } + + RedisModuleUser *new_user = zmalloc(sizeof(RedisModuleUser)); + new_user->user = acl_user; + new_user->free_user = 0; + return new_user; +} + +/* Checks if the command can be executed by the user, according to the ACLs associated with it. + * + * On success a REDISMODULE_OK is returned, otherwise + * REDISMODULE_ERR is returned and errno is set to the following values: + * + * * ENOENT: Specified command does not exist. + * * EACCES: Command cannot be executed, according to ACL rules + */ +int RM_ACLCheckCommandPermissions(RedisModuleUser *user, RedisModuleString **argv, int argc) { + int keyidxptr; + struct redisCommand *cmd; + + /* Find command */ + if ((cmd = lookupCommand(argv[0]->ptr)) == NULL) { + errno = ENOENT; + return REDISMODULE_ERR; + } + + if (ACLCheckAllUserCommandPerm(user->user, cmd, argv, argc, &keyidxptr) != ACL_OK) { + errno = EACCES; + return REDISMODULE_ERR; + } + + return REDISMODULE_OK; +} + +/* Check if the key can be accessed by the user, according to the ACLs associated with it. + * + * If the user can access the key, REDISMODULE_OK is returned, otherwise + * REDISMODULE_ERR is returned. */ +int RM_ACLCheckKeyPermissions(RedisModuleUser *user, RedisModuleString *key) { + if (ACLCheckKey(user->user, key->ptr, sdslen(key->ptr)) != ACL_OK) + return REDISMODULE_ERR; + + return REDISMODULE_OK; +} + +/* Check if the pubsub channel can be accessed by the user, according to the ACLs associated with it. + * Glob-style pattern matching is employed, unless the literal flag is + * set. + * + * If the user can access the pubsub channel, REDISMODULE_OK is returned, otherwise + * REDISMODULE_ERR is returned. */ +int RM_ACLCheckChannelPermissions(RedisModuleUser *user, RedisModuleString *ch, int literal) { + if (ACLCheckPubsubChannelPerm(ch->ptr, user->user->channels, literal) != ACL_OK) + return REDISMODULE_ERR; + + return REDISMODULE_OK; +} + +/* Adds a new entry in the ACL log. + * Returns REDISMODULE_OK on success and REDISMODULE_ERR on error. + * + * For more information about ACL log, please refer to https://redis.io/commands/acl-log */ +void RM_ACLAddLogEntry(RedisModuleCtx *ctx, RedisModuleUser *user, RedisModuleString *object) { + addACLLogEntry(ctx->client, 0, ACL_LOG_CTX_MODULE, -1, user->user->name, object->ptr); +} + /* Authenticate the client associated with the context with * the provided user. Returns REDISMODULE_OK on success and * REDISMODULE_ERR on error. @@ -10316,6 +10438,12 @@ void moduleRegisterCoreAPI(void) { REGISTER_API(ScanKey); REGISTER_API(CreateModuleUser); REGISTER_API(SetModuleUserACL); + REGISTER_API(GetCurrentUserName); + REGISTER_API(GetModuleUserFromUserName); + REGISTER_API(ACLCheckCommandPermissions); + REGISTER_API(ACLCheckKeyPermissions); + REGISTER_API(ACLCheckChannelPermissions); + REGISTER_API(ACLAddLogEntry); REGISTER_API(FreeModuleUser); REGISTER_API(DeauthenticateAndCloseClient); REGISTER_API(AuthenticateClientWithACLUser); diff --git a/src/multi.c b/src/multi.c index 5c229d11b..e40d2a447 100644 --- a/src/multi.c +++ b/src/multi.c @@ -228,7 +228,7 @@ void execCommand(client *c) { reason = "no permission"; break; } - addACLLogEntry(c,acl_retval,acl_errpos,NULL); + addACLLogEntry(c,acl_retval,ACL_LOG_CTX_MULTI,acl_errpos,NULL,NULL); addReplyErrorFormat(c, "-NOPERM ACLs rules changed between the moment the " "transaction was accumulated and the EXEC call. " diff --git a/src/redismodule.h b/src/redismodule.h index 79c28bbcb..43089adaf 100644 --- a/src/redismodule.h +++ b/src/redismodule.h @@ -886,6 +886,12 @@ REDISMODULE_API size_t (*RedisModule_MallocSize)(void* ptr) REDISMODULE_ATTR; REDISMODULE_API RedisModuleUser * (*RedisModule_CreateModuleUser)(const char *name) REDISMODULE_ATTR; REDISMODULE_API void (*RedisModule_FreeModuleUser)(RedisModuleUser *user) REDISMODULE_ATTR; REDISMODULE_API int (*RedisModule_SetModuleUserACL)(RedisModuleUser *user, const char* acl) REDISMODULE_ATTR; +REDISMODULE_API RedisModuleString * (*RedisModule_GetCurrentUserName)(RedisModuleCtx *ctx) REDISMODULE_ATTR; +REDISMODULE_API RedisModuleUser * (*RedisModule_GetModuleUserFromUserName)(RedisModuleString *name) REDISMODULE_ATTR; +REDISMODULE_API int (*RedisModule_ACLCheckCommandPermissions)(RedisModuleUser *user, RedisModuleString **argv, int argc) REDISMODULE_ATTR; +REDISMODULE_API int (*RedisModule_ACLCheckKeyPermissions)(RedisModuleUser *user, RedisModuleString *key) REDISMODULE_ATTR; +REDISMODULE_API int (*RedisModule_ACLCheckChannelPermissions)(RedisModuleUser *user, RedisModuleString *ch, int literal) REDISMODULE_ATTR; +REDISMODULE_API void (*RedisModule_ACLAddLogEntry)(RedisModuleCtx *ctx, RedisModuleUser *user, RedisModuleString *object) REDISMODULE_ATTR; REDISMODULE_API int (*RedisModule_AuthenticateClientWithACLUser)(RedisModuleCtx *ctx, const char *name, size_t len, RedisModuleUserChangedFunc callback, void *privdata, uint64_t *client_id) REDISMODULE_ATTR; REDISMODULE_API int (*RedisModule_AuthenticateClientWithUser)(RedisModuleCtx *ctx, RedisModuleUser *user, RedisModuleUserChangedFunc callback, void *privdata, uint64_t *client_id) REDISMODULE_ATTR; REDISMODULE_API int (*RedisModule_DeauthenticateAndCloseClient)(RedisModuleCtx *ctx, uint64_t client_id) REDISMODULE_ATTR; @@ -1195,6 +1201,12 @@ static int RedisModule_Init(RedisModuleCtx *ctx, const char *name, int ver, int REDISMODULE_GET_API(CreateModuleUser); REDISMODULE_GET_API(FreeModuleUser); REDISMODULE_GET_API(SetModuleUserACL); + REDISMODULE_GET_API(GetCurrentUserName); + REDISMODULE_GET_API(GetModuleUserFromUserName); + REDISMODULE_GET_API(ACLCheckCommandPermissions); + REDISMODULE_GET_API(ACLCheckKeyPermissions); + REDISMODULE_GET_API(ACLCheckChannelPermissions); + REDISMODULE_GET_API(ACLAddLogEntry); REDISMODULE_GET_API(DeauthenticateAndCloseClient); REDISMODULE_GET_API(AuthenticateClientWithACLUser); REDISMODULE_GET_API(AuthenticateClientWithUser); diff --git a/src/scripting.c b/src/scripting.c index 3b807912a..c75858d31 100644 --- a/src/scripting.c +++ b/src/scripting.c @@ -760,7 +760,7 @@ int luaRedisGenericCommand(lua_State *lua, int raise_error) { int acl_errpos; int acl_retval = ACLCheckAllPerm(c,&acl_errpos); if (acl_retval != ACL_OK) { - addACLLogEntry(c,acl_retval,acl_errpos,NULL); + addACLLogEntry(c,acl_retval,ACL_LOG_CTX_LUA,acl_errpos,NULL,NULL); switch (acl_retval) { case ACL_DENIED_CMD: luaPushError(lua, "The user executing the script can't run this " diff --git a/src/server.c b/src/server.c index cf0322c18..d8555619c 100644 --- a/src/server.c +++ b/src/server.c @@ -4559,7 +4559,7 @@ int processCommand(client *c) { int acl_errpos; int acl_retval = ACLCheckAllPerm(c,&acl_errpos); if (acl_retval != ACL_OK) { - addACLLogEntry(c,acl_retval,acl_errpos,NULL); + addACLLogEntry(c,acl_retval,(c->flags & CLIENT_MULTI) ? ACL_LOG_CTX_MULTI : ACL_LOG_CTX_TOPLEVEL,acl_errpos,NULL,NULL); switch (acl_retval) { case ACL_DENIED_CMD: rejectCommandFormat(c, diff --git a/src/server.h b/src/server.h index 73dd2a2ce..39184591d 100644 --- a/src/server.h +++ b/src/server.h @@ -2274,11 +2274,21 @@ void ACLInit(void); #define ACL_DENIED_KEY 2 #define ACL_DENIED_AUTH 3 /* Only used for ACL LOG entries. */ #define ACL_DENIED_CHANNEL 4 /* Only used for pub/sub commands */ + +/* Context values for addACLLogEntry(). */ +#define ACL_LOG_CTX_TOPLEVEL 0 +#define ACL_LOG_CTX_LUA 1 +#define ACL_LOG_CTX_MULTI 2 +#define ACL_LOG_CTX_MODULE 3 + int ACLCheckUserCredentials(robj *username, robj *password); int ACLAuthenticateUser(client *c, robj *username, robj *password); unsigned long ACLGetCommandID(const char *cmdname); void ACLClearCommandID(void); user *ACLGetUserByName(const char *name, size_t namelen); +int ACLCheckKey(const user *u, const char *key, int keylen); +int ACLCheckPubsubChannelPerm(sds channel, list *allowed, int literal); +int ACLCheckAllUserCommandPerm(const user *u, struct redisCommand *cmd, robj **argv, int argc, int *idxptr); int ACLCheckAllPerm(client *c, int *idxptr); int ACLSetUser(user *u, const char *op, ssize_t oplen); sds ACLDefaultUserFirstPassword(void); @@ -2291,7 +2301,7 @@ void ACLLoadUsersAtStartup(void); void addReplyCommandCategories(client *c, struct redisCommand *cmd); user *ACLCreateUnlinkedUser(); void ACLFreeUserAndKillClients(user *u); -void addACLLogEntry(client *c, int reason, int keypos, sds username); +void addACLLogEntry(client *c, int reason, int context, int argpos, sds username, sds object); void ACLUpdateDefaultUserPassword(sds password); /* Sorted sets data type */ diff --git a/tests/modules/Makefile b/tests/modules/Makefile index ac033f7dc..0c797855f 100644 --- a/tests/modules/Makefile +++ b/tests/modules/Makefile @@ -41,6 +41,7 @@ TEST_MODULES = \ hash.so \ zset.so \ stream.so \ + aclcheck.so \ list.so diff --git a/tests/modules/aclcheck.c b/tests/modules/aclcheck.c new file mode 100644 index 000000000..739890132 --- /dev/null +++ b/tests/modules/aclcheck.c @@ -0,0 +1,176 @@ +#define REDISMODULE_EXPERIMENTAL_API + +#include "redismodule.h" +#include +#include + +/* A wrap for SET command with ACL check on the key. */ +int set_aclcheck_key(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { + if (argc < 3) { + return RedisModule_WrongArity(ctx); + } + + /* Check that the key can be accessed */ + RedisModuleString *user_name = RedisModule_GetCurrentUserName(ctx); + RedisModuleUser *user = RedisModule_GetModuleUserFromUserName(user_name); + int ret = RedisModule_ACLCheckKeyPermissions(user, argv[1]); + if (ret != 0) { + RedisModule_ReplyWithError(ctx, "DENIED KEY"); + RedisModule_FreeModuleUser(user); + RedisModule_FreeString(ctx, user_name); + return REDISMODULE_OK; + } + + RedisModuleCallReply *rep = RedisModule_Call(ctx, "SET", "v", argv + 1, argc - 1); + if (!rep) { + RedisModule_ReplyWithError(ctx, "NULL reply returned"); + } else { + RedisModule_ReplyWithCallReply(ctx, rep); + RedisModule_FreeCallReply(rep); + } + + RedisModule_FreeModuleUser(user); + RedisModule_FreeString(ctx, user_name); + return REDISMODULE_OK; +} + +/* A wrap for PUBLISH command with ACL check on the channel. */ +int publish_aclcheck_channel(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { + if (argc != 3) { + return RedisModule_WrongArity(ctx); + } + + /* Check that the pubsub channel can be accessed */ + RedisModuleString *user_name = RedisModule_GetCurrentUserName(ctx); + RedisModuleUser *user = RedisModule_GetModuleUserFromUserName(user_name); + int ret = RedisModule_ACLCheckChannelPermissions(user, argv[1], 1); + if (ret != 0) { + RedisModule_ReplyWithError(ctx, "DENIED CHANNEL"); + RedisModule_FreeModuleUser(user); + RedisModule_FreeString(ctx, user_name); + return REDISMODULE_OK; + } + + RedisModuleCallReply *rep = RedisModule_Call(ctx, "PUBLISH", "v", argv + 1, argc - 1); + if (!rep) { + RedisModule_ReplyWithError(ctx, "NULL reply returned"); + } else { + RedisModule_ReplyWithCallReply(ctx, rep); + RedisModule_FreeCallReply(rep); + } + + RedisModule_FreeModuleUser(user); + RedisModule_FreeString(ctx, user_name); + return REDISMODULE_OK; +} + +/* A wrap for RM_Call that check first that the command can be executed */ +int rm_call_aclcheck_cmd(RedisModuleCtx *ctx, RedisModuleUser *user, RedisModuleString **argv, int argc) { + if (argc < 2) { + return RedisModule_WrongArity(ctx); + } + + /* Check that the command can be executed */ + int ret = RedisModule_ACLCheckCommandPermissions(user, argv + 1, argc - 1); + if (ret != 0) { + RedisModule_ReplyWithError(ctx, "DENIED CMD"); + /* Add entry to ACL log */ + RedisModule_ACLAddLogEntry(ctx, user, argv[1]); + return REDISMODULE_OK; + } + + const char* cmd = RedisModule_StringPtrLen(argv[1], NULL); + + RedisModuleCallReply* rep = RedisModule_Call(ctx, cmd, "v", argv + 2, argc - 2); + if(!rep){ + RedisModule_ReplyWithError(ctx, "NULL reply returned"); + }else{ + RedisModule_ReplyWithCallReply(ctx, rep); + RedisModule_FreeCallReply(rep); + } + + return REDISMODULE_OK; +} + +int rm_call_aclcheck_cmd_default_user(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { + RedisModuleString *user_name = RedisModule_GetCurrentUserName(ctx); + RedisModuleUser *user = RedisModule_GetModuleUserFromUserName(user_name); + + int res = rm_call_aclcheck_cmd(ctx, user, argv, argc); + + RedisModule_FreeModuleUser(user); + RedisModule_FreeString(ctx, user_name); + return res; +} + +int rm_call_aclcheck_cmd_module_user(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { + /* Create a user and authenticate */ + RedisModuleUser *user = RedisModule_CreateModuleUser("testuser1"); + RedisModule_SetModuleUserACL(user, "allcommands"); + RedisModule_SetModuleUserACL(user, "allkeys"); + RedisModule_SetModuleUserACL(user, "on"); + RedisModule_AuthenticateClientWithUser(ctx, user, NULL, NULL, NULL); + + int res = rm_call_aclcheck_cmd(ctx, user, argv, argc); + + /* authenticated back to "default" user (so once we free testuser1 we will not disconnected */ + RedisModule_AuthenticateClientWithACLUser(ctx, "default", 7, NULL, NULL, NULL); + RedisModule_FreeModuleUser(user); + return res; +} + +/* A wrap for RM_Call that pass the 'C' flag to do ACL check on the command. */ +int rm_call_aclcheck(RedisModuleCtx *ctx, RedisModuleString **argv, int argc){ + REDISMODULE_NOT_USED(argv); + REDISMODULE_NOT_USED(argc); + + if(argc < 2){ + return RedisModule_WrongArity(ctx); + } + + const char* cmd = RedisModule_StringPtrLen(argv[1], NULL); + + RedisModuleCallReply* rep = RedisModule_Call(ctx, cmd, "vC", argv + 2, argc - 2); + if(!rep) { + char err[100]; + switch (errno) { + case EACCES: + RedisModule_ReplyWithError(ctx, "ERR NOPERM"); + break; + default: + snprintf(err, sizeof(err) - 1, "ERR errno=%d", errno); + RedisModule_ReplyWithError(ctx, err); + break; + } + } else { + RedisModule_ReplyWithCallReply(ctx, rep); + RedisModule_FreeCallReply(rep); + } + + return REDISMODULE_OK; +} + +int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { + REDISMODULE_NOT_USED(argv); + REDISMODULE_NOT_USED(argc); + + if (RedisModule_Init(ctx,"aclcheck",1,REDISMODULE_APIVER_1)== REDISMODULE_ERR) + return REDISMODULE_ERR; + + if (RedisModule_CreateCommand(ctx,"aclcheck.set.check.key", set_aclcheck_key,"",0,0,0) == REDISMODULE_ERR) + return REDISMODULE_ERR; + + if (RedisModule_CreateCommand(ctx,"aclcheck.publish.check.channel", publish_aclcheck_channel,"",0,0,0) == REDISMODULE_ERR) + return REDISMODULE_ERR; + + if (RedisModule_CreateCommand(ctx,"aclcheck.rm_call.check.cmd", rm_call_aclcheck_cmd_default_user,"",0,0,0) == REDISMODULE_ERR) + return REDISMODULE_ERR; + + if (RedisModule_CreateCommand(ctx,"aclcheck.rm_call.check.cmd.module.user", rm_call_aclcheck_cmd_module_user,"",0,0,0) == REDISMODULE_ERR) + return REDISMODULE_ERR; + + if (RedisModule_CreateCommand(ctx,"aclcheck.rm_call", rm_call_aclcheck,"",0,0,0) == REDISMODULE_ERR) + return REDISMODULE_ERR; + + return REDISMODULE_OK; +} diff --git a/tests/unit/moduleapi/aclcheck.tcl b/tests/unit/moduleapi/aclcheck.tcl new file mode 100644 index 000000000..94af2cc00 --- /dev/null +++ b/tests/unit/moduleapi/aclcheck.tcl @@ -0,0 +1,66 @@ +set testmodule [file normalize tests/modules/aclcheck.so] + +start_server {tags {"modules acl"}} { + r module load $testmodule + + test {test module check acl for command perm} { + # by default all commands allowed + assert_equal [r aclcheck.rm_call.check.cmd set x 5] OK + # block SET command for user + r acl setuser default -set + catch {r aclcheck.rm_call.check.cmd set x 5} e + assert_match {*DENIED CMD*} $e + + # verify that new log entry added + set entry [lindex [r ACL LOG] 0] + assert {[dict get $entry username] eq {default}} + assert {[dict get $entry context] eq {module}} + assert {[dict get $entry object] eq {set}} + } + + test {test module check acl for key perm} { + # give permission for SET and block all keys but x + r acl setuser default +set resetkeys ~x + assert_equal [r aclcheck.set.check.key x 5] OK + catch {r aclcheck.set.check.key y 5} e + set e + } {*DENIED KEY*} + + test {test module check acl for module user} { + # the module user has access to all keys + assert_equal [r aclcheck.rm_call.check.cmd.module.user set y 5] OK + } + + test {test module check acl for channel perm} { + # block all channels but ch1 + r acl setuser default resetchannels &ch1 + assert_equal [r aclcheck.publish.check.channel ch1 msg] 0 + catch {r aclcheck.publish.check.channel ch2 msg} e + set e + } {*DENIED CHANNEL*} + + test {test module check acl in rm_call} { + # rm call check for key permission (x can be accessed) + assert_equal [r aclcheck.rm_call set x 5] OK + # rm call check for key permission (y can't be accessed) + catch {r aclcheck.rm_call set y 5} e + assert_match {*NOPERM*} $e + + # verify that new log entry added + set entry [lindex [r ACL LOG] 0] + assert {[dict get $entry username] eq {default}} + assert {[dict get $entry context] eq {module}} + assert {[dict get $entry object] eq {y}} + + # rm call check for command permission + r acl setuser default -set + catch {r aclcheck.rm_call set x 5} e + assert_match {*NOPERM*} $e + + # verify that new log entry added + set entry [lindex [r ACL LOG] 0] + assert {[dict get $entry username] eq {default}} + assert {[dict get $entry context] eq {module}} + assert {[dict get $entry object] eq {set}} + } +}