summaryrefslogtreecommitdiffstats
path: root/src/acl.c
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-04 17:31:02 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-04 17:31:02 +0000
commitbb12c1fd00eb51118749bbbc69c5596835fcbd3b (patch)
tree88038a98bd31c1b765f3390767a2ec12e37c79ec /src/acl.c
parentInitial commit. (diff)
downloadredis-bb12c1fd00eb51118749bbbc69c5596835fcbd3b.tar.xz
redis-bb12c1fd00eb51118749bbbc69c5596835fcbd3b.zip
Adding upstream version 5:7.0.15.upstream/5%7.0.15upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/acl.c')
-rw-r--r--src/acl.c3051
1 files changed, 3051 insertions, 0 deletions
diff --git a/src/acl.c b/src/acl.c
new file mode 100644
index 0000000..6d86093
--- /dev/null
+++ b/src/acl.c
@@ -0,0 +1,3051 @@
+/*
+ * Copyright (c) 2018, Salvatore Sanfilippo <antirez at gmail dot com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * * Neither the name of Redis nor the names of its contributors may be used
+ * to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "server.h"
+#include "sha256.h"
+#include <fcntl.h>
+#include <ctype.h>
+
+/* =============================================================================
+ * Global state for ACLs
+ * ==========================================================================*/
+
+rax *Users; /* Table mapping usernames to user structures. */
+
+user *DefaultUser; /* Global reference to the default user.
+ Every new connection is associated to it, if no
+ AUTH or HELLO is used to authenticate with a
+ different user. */
+
+list *UsersToLoad; /* This is a list of users found in the configuration file
+ that we'll need to load in the final stage of Redis
+ initialization, after all the modules are already
+ loaded. Every list element is a NULL terminated
+ array of SDS pointers: the first is the user name,
+ all the remaining pointers are ACL rules in the same
+ format as ACLSetUser(). */
+list *ACLLog; /* Our security log, the user is able to inspect that
+ using the ACL LOG command .*/
+
+static rax *commandId = NULL; /* Command name to id mapping */
+
+static unsigned long nextid = 0; /* Next command id that has not been assigned */
+
+struct ACLCategoryItem {
+ const char *name;
+ uint64_t flag;
+} ACLCommandCategories[] = { /* See redis.conf for details on each category. */
+ {"keyspace", ACL_CATEGORY_KEYSPACE},
+ {"read", ACL_CATEGORY_READ},
+ {"write", ACL_CATEGORY_WRITE},
+ {"set", ACL_CATEGORY_SET},
+ {"sortedset", ACL_CATEGORY_SORTEDSET},
+ {"list", ACL_CATEGORY_LIST},
+ {"hash", ACL_CATEGORY_HASH},
+ {"string", ACL_CATEGORY_STRING},
+ {"bitmap", ACL_CATEGORY_BITMAP},
+ {"hyperloglog", ACL_CATEGORY_HYPERLOGLOG},
+ {"geo", ACL_CATEGORY_GEO},
+ {"stream", ACL_CATEGORY_STREAM},
+ {"pubsub", ACL_CATEGORY_PUBSUB},
+ {"admin", ACL_CATEGORY_ADMIN},
+ {"fast", ACL_CATEGORY_FAST},
+ {"slow", ACL_CATEGORY_SLOW},
+ {"blocking", ACL_CATEGORY_BLOCKING},
+ {"dangerous", ACL_CATEGORY_DANGEROUS},
+ {"connection", ACL_CATEGORY_CONNECTION},
+ {"transaction", ACL_CATEGORY_TRANSACTION},
+ {"scripting", ACL_CATEGORY_SCRIPTING},
+ {NULL,0} /* Terminator. */
+};
+
+struct ACLUserFlag {
+ const char *name;
+ uint64_t flag;
+} ACLUserFlags[] = {
+ /* Note: the order here dictates the emitted order at ACLDescribeUser */
+ {"on", USER_FLAG_ENABLED},
+ {"off", USER_FLAG_DISABLED},
+ {"nopass", USER_FLAG_NOPASS},
+ {"skip-sanitize-payload", USER_FLAG_SANITIZE_PAYLOAD_SKIP},
+ {"sanitize-payload", USER_FLAG_SANITIZE_PAYLOAD},
+ {NULL,0} /* Terminator. */
+};
+
+struct ACLSelectorFlags {
+ const char *name;
+ uint64_t flag;
+} ACLSelectorFlags[] = {
+ /* Note: the order here dictates the emitted order at ACLDescribeUser */
+ {"allkeys", SELECTOR_FLAG_ALLKEYS},
+ {"allchannels", SELECTOR_FLAG_ALLCHANNELS},
+ {"allcommands", SELECTOR_FLAG_ALLCOMMANDS},
+ {NULL,0} /* Terminator. */
+};
+
+/* ACL selectors are private and not exposed outside of acl.c. */
+typedef struct {
+ uint32_t flags; /* See SELECTOR_FLAG_* */
+ /* The bit in allowed_commands is set if this user has the right to
+ * execute this command.
+ *
+ * If the bit for a given command is NOT set and the command has
+ * allowed first-args, Redis will also check allowed_firstargs in order to
+ * understand if the command can be executed. */
+ uint64_t allowed_commands[USER_COMMAND_BITS_COUNT/64];
+ /* allowed_firstargs is used by ACL rules to block access to a command unless a
+ * specific argv[1] is given.
+ *
+ * For each command ID (corresponding to the command bit set in allowed_commands),
+ * This array points to an array of SDS strings, terminated by a NULL pointer,
+ * with all the first-args that are allowed for this command. When no first-arg
+ * matching is used, the field is just set to NULL to avoid allocating
+ * USER_COMMAND_BITS_COUNT pointers. */
+ sds **allowed_firstargs;
+ list *patterns; /* A list of allowed key patterns. If this field is NULL
+ the user cannot mention any key in a command, unless
+ the flag ALLKEYS is set in the user. */
+ list *channels; /* A list of allowed Pub/Sub channel patterns. If this
+ field is NULL the user cannot mention any channel in a
+ `PUBLISH` or [P][UNSUBSCRIBE] command, unless the flag
+ ALLCHANNELS is set in the user. */
+} aclSelector;
+
+void ACLResetFirstArgsForCommand(aclSelector *selector, unsigned long id);
+void ACLResetFirstArgs(aclSelector *selector);
+void ACLAddAllowedFirstArg(aclSelector *selector, unsigned long id, const char *sub);
+void ACLFreeLogEntry(void *le);
+int ACLSetSelector(aclSelector *selector, const char *op, size_t oplen);
+
+/* The length of the string representation of a hashed password. */
+#define HASH_PASSWORD_LEN (SHA256_BLOCK_SIZE*2)
+
+/* =============================================================================
+ * Helper functions for the rest of the ACL implementation
+ * ==========================================================================*/
+
+/* Return zero if strings are the same, non-zero if they are not.
+ * The comparison is performed in a way that prevents an attacker to obtain
+ * information about the nature of the strings just monitoring the execution
+ * time of the function. Note: The two strings must be the same length.
+ */
+int time_independent_strcmp(char *a, char *b, int len) {
+ int diff = 0;
+ for (int j = 0; j < len; j++) {
+ diff |= (a[j] ^ b[j]);
+ }
+ return diff; /* If zero strings are the same. */
+}
+
+/* Given an SDS string, returns the SHA256 hex representation as a
+ * new SDS string. */
+sds ACLHashPassword(unsigned char *cleartext, size_t len) {
+ SHA256_CTX ctx;
+ unsigned char hash[SHA256_BLOCK_SIZE];
+ char hex[HASH_PASSWORD_LEN];
+ char *cset = "0123456789abcdef";
+
+ sha256_init(&ctx);
+ sha256_update(&ctx,(unsigned char*)cleartext,len);
+ sha256_final(&ctx,hash);
+
+ for (int j = 0; j < SHA256_BLOCK_SIZE; j++) {
+ hex[j*2] = cset[((hash[j]&0xF0)>>4)];
+ hex[j*2+1] = cset[(hash[j]&0xF)];
+ }
+ return sdsnewlen(hex,HASH_PASSWORD_LEN);
+}
+
+/* Given a hash and the hash length, returns C_OK if it is a valid password
+ * hash, or C_ERR otherwise. */
+int ACLCheckPasswordHash(unsigned char *hash, int hashlen) {
+ if (hashlen != HASH_PASSWORD_LEN) {
+ return C_ERR;
+ }
+
+ /* Password hashes can only be characters that represent
+ * hexadecimal values, which are numbers and lowercase
+ * characters 'a' through 'f'. */
+ for(int i = 0; i < HASH_PASSWORD_LEN; i++) {
+ char c = hash[i];
+ if ((c < 'a' || c > 'f') && (c < '0' || c > '9')) {
+ return C_ERR;
+ }
+ }
+ return C_OK;
+}
+
+/* =============================================================================
+ * Low level ACL API
+ * ==========================================================================*/
+
+/* Return 1 if the specified string contains spaces or null characters.
+ * We do this for usernames and key patterns for simpler rewriting of
+ * ACL rules, presentation on ACL list, and to avoid subtle security bugs
+ * that may arise from parsing the rules in presence of escapes.
+ * The function returns 0 if the string has no spaces. */
+int ACLStringHasSpaces(const char *s, size_t len) {
+ for (size_t i = 0; i < len; i++) {
+ if (isspace(s[i]) || s[i] == 0) return 1;
+ }
+ return 0;
+}
+
+/* Given the category name the command returns the corresponding flag, or
+ * zero if there is no match. */
+uint64_t ACLGetCommandCategoryFlagByName(const char *name) {
+ for (int j = 0; ACLCommandCategories[j].flag != 0; j++) {
+ if (!strcasecmp(name,ACLCommandCategories[j].name)) {
+ return ACLCommandCategories[j].flag;
+ }
+ }
+ return 0; /* No match. */
+}
+
+/* Method for searching for a user within a list of user definitions. The
+ * list contains an array of user arguments, and we are only
+ * searching the first argument, the username, for a match. */
+int ACLListMatchLoadedUser(void *definition, void *user) {
+ sds *user_definition = definition;
+ return sdscmp(user_definition[0], user) == 0;
+}
+
+/* Method for passwords/pattern comparison used for the user->passwords list
+ * so that we can search for items with listSearchKey(). */
+int ACLListMatchSds(void *a, void *b) {
+ return sdscmp(a,b) == 0;
+}
+
+/* Method to free list elements from ACL users password/patterns lists. */
+void ACLListFreeSds(void *item) {
+ sdsfree(item);
+}
+
+/* Method to duplicate list elements from ACL users password/patterns lists. */
+void *ACLListDupSds(void *item) {
+ return sdsdup(item);
+}
+
+/* Structure used for handling key patterns with different key
+ * based permissions. */
+typedef struct {
+ int flags; /* The CMD_KEYS_* flags for this key pattern */
+ sds pattern; /* The pattern to match keys against */
+} keyPattern;
+
+/* Create a new key pattern. */
+keyPattern *ACLKeyPatternCreate(sds pattern, int flags) {
+ keyPattern *new = (keyPattern *) zmalloc(sizeof(keyPattern));
+ new->pattern = pattern;
+ new->flags = flags;
+ return new;
+}
+
+/* Free a key pattern and internal structures. */
+void ACLKeyPatternFree(keyPattern *pattern) {
+ sdsfree(pattern->pattern);
+ zfree(pattern);
+}
+
+/* Method for passwords/pattern comparison used for the user->passwords list
+ * so that we can search for items with listSearchKey(). */
+int ACLListMatchKeyPattern(void *a, void *b) {
+ return sdscmp(((keyPattern *) a)->pattern,((keyPattern *) b)->pattern) == 0;
+}
+
+/* Method to free list elements from ACL users password/patterns lists. */
+void ACLListFreeKeyPattern(void *item) {
+ ACLKeyPatternFree(item);
+}
+
+/* Method to duplicate list elements from ACL users password/patterns lists. */
+void *ACLListDupKeyPattern(void *item) {
+ keyPattern *old = (keyPattern *) item;
+ return ACLKeyPatternCreate(sdsdup(old->pattern), old->flags);
+}
+
+/* Append the string representation of a key pattern onto the
+ * provided base string. */
+sds sdsCatPatternString(sds base, keyPattern *pat) {
+ if (pat->flags == ACL_ALL_PERMISSION) {
+ base = sdscatlen(base,"~",1);
+ } else if (pat->flags == ACL_READ_PERMISSION) {
+ base = sdscatlen(base,"%R~",3);
+ } else if (pat->flags == ACL_WRITE_PERMISSION) {
+ base = sdscatlen(base,"%W~",3);
+ } else {
+ serverPanic("Invalid key pattern flag detected");
+ }
+ return sdscatsds(base, pat->pattern);
+}
+
+/* Create an empty selector with the provided set of initial
+ * flags. The selector will be default have no permissions. */
+aclSelector *ACLCreateSelector(int flags) {
+ aclSelector *selector = zmalloc(sizeof(aclSelector));
+ selector->flags = flags | server.acl_pubsub_default;
+ selector->patterns = listCreate();
+ selector->channels = listCreate();
+ selector->allowed_firstargs = NULL;
+
+ listSetMatchMethod(selector->patterns,ACLListMatchKeyPattern);
+ listSetFreeMethod(selector->patterns,ACLListFreeKeyPattern);
+ listSetDupMethod(selector->patterns,ACLListDupKeyPattern);
+ listSetMatchMethod(selector->channels,ACLListMatchSds);
+ listSetFreeMethod(selector->channels,ACLListFreeSds);
+ listSetDupMethod(selector->channels,ACLListDupSds);
+ memset(selector->allowed_commands,0,sizeof(selector->allowed_commands));
+
+ return selector;
+}
+
+/* Cleanup the provided selector, including all interior structures. */
+void ACLFreeSelector(aclSelector *selector) {
+ listRelease(selector->patterns);
+ listRelease(selector->channels);
+ ACLResetFirstArgs(selector);
+ zfree(selector);
+}
+
+/* Create an exact copy of the provided selector. */
+aclSelector *ACLCopySelector(aclSelector *src) {
+ aclSelector *dst = zmalloc(sizeof(aclSelector));
+ dst->flags = src->flags;
+ dst->patterns = listDup(src->patterns);
+ dst->channels = listDup(src->channels);
+ memcpy(dst->allowed_commands,src->allowed_commands,
+ sizeof(dst->allowed_commands));
+ dst->allowed_firstargs = NULL;
+ /* Copy the allowed first-args array of array of SDS strings. */
+ if (src->allowed_firstargs) {
+ for (int j = 0; j < USER_COMMAND_BITS_COUNT; j++) {
+ if (!(src->allowed_firstargs[j])) continue;
+ for (int i = 0; src->allowed_firstargs[j][i]; i++) {
+ ACLAddAllowedFirstArg(dst, j, src->allowed_firstargs[j][i]);
+ }
+ }
+ }
+ return dst;
+}
+
+/* List method for freeing a selector */
+void ACLListFreeSelector(void *a) {
+ ACLFreeSelector((aclSelector *) a);
+}
+
+/* List method for duplicating a selector */
+void *ACLListDuplicateSelector(void *src) {
+ return ACLCopySelector((aclSelector *)src);
+}
+
+/* All users have an implicit root selector which
+ * provides backwards compatibility to the old ACLs-
+ * permissions. */
+aclSelector *ACLUserGetRootSelector(user *u) {
+ serverAssert(listLength(u->selectors));
+ aclSelector *s = (aclSelector *) listNodeValue(listFirst(u->selectors));
+ serverAssert(s->flags & SELECTOR_FLAG_ROOT);
+ return s;
+}
+
+/* Create a new user with the specified name, store it in the list
+ * of users (the Users global radix tree), and returns a reference to
+ * the structure representing the user.
+ *
+ * If the user with such name already exists NULL is returned. */
+user *ACLCreateUser(const char *name, size_t namelen) {
+ if (raxFind(Users,(unsigned char*)name,namelen) != raxNotFound) return NULL;
+ user *u = zmalloc(sizeof(*u));
+ u->name = sdsnewlen(name,namelen);
+ u->flags = USER_FLAG_DISABLED;
+ u->passwords = listCreate();
+ u->acl_string = NULL;
+ listSetMatchMethod(u->passwords,ACLListMatchSds);
+ listSetFreeMethod(u->passwords,ACLListFreeSds);
+ listSetDupMethod(u->passwords,ACLListDupSds);
+
+ u->selectors = listCreate();
+ listSetFreeMethod(u->selectors,ACLListFreeSelector);
+ listSetDupMethod(u->selectors,ACLListDuplicateSelector);
+
+ /* Add the initial root selector */
+ aclSelector *s = ACLCreateSelector(SELECTOR_FLAG_ROOT);
+ listAddNodeHead(u->selectors, s);
+
+ raxInsert(Users,(unsigned char*)name,namelen,u,NULL);
+ return u;
+}
+
+/* This function should be called when we need an unlinked "fake" user
+ * we can use in order to validate ACL rules or for other similar reasons.
+ * The user will not get linked to the Users radix tree. The returned
+ * user should be released with ACLFreeUser() as usually. */
+user *ACLCreateUnlinkedUser(void) {
+ char username[64];
+ for (int j = 0; ; j++) {
+ snprintf(username,sizeof(username),"__fakeuser:%d__",j);
+ user *fakeuser = ACLCreateUser(username,strlen(username));
+ if (fakeuser == NULL) continue;
+ int retval = raxRemove(Users,(unsigned char*) username,
+ strlen(username),NULL);
+ serverAssert(retval != 0);
+ return fakeuser;
+ }
+}
+
+/* Release the memory used by the user structure. Note that this function
+ * will not remove the user from the Users global radix tree. */
+void ACLFreeUser(user *u) {
+ sdsfree(u->name);
+ if (u->acl_string) {
+ decrRefCount(u->acl_string);
+ u->acl_string = NULL;
+ }
+ listRelease(u->passwords);
+ listRelease(u->selectors);
+ zfree(u);
+}
+
+/* When a user is deleted we need to cycle the active
+ * connections in order to kill all the pending ones that
+ * are authenticated with such user. */
+void ACLFreeUserAndKillClients(user *u) {
+ listIter li;
+ listNode *ln;
+ listRewind(server.clients,&li);
+ while ((ln = listNext(&li)) != NULL) {
+ client *c = listNodeValue(ln);
+ if (c->user == u) {
+ /* We'll free the connection asynchronously, so
+ * in theory to set a different user is not needed.
+ * However if there are bugs in Redis, soon or later
+ * this may result in some security hole: it's much
+ * more defensive to set the default user and put
+ * it in non authenticated mode. */
+ c->user = DefaultUser;
+ c->authenticated = 0;
+ /* We will write replies to this client later, so we can't
+ * close it directly even if async. */
+ if (c == server.current_client) {
+ c->flags |= CLIENT_CLOSE_AFTER_COMMAND;
+ } else {
+ freeClientAsync(c);
+ }
+ }
+ }
+ ACLFreeUser(u);
+}
+
+/* Copy the user ACL rules from the source user 'src' to the destination
+ * user 'dst' so that at the end of the process they'll have exactly the
+ * same rules (but the names will continue to be the original ones). */
+void ACLCopyUser(user *dst, user *src) {
+ listRelease(dst->passwords);
+ listRelease(dst->selectors);
+ dst->passwords = listDup(src->passwords);
+ dst->selectors = listDup(src->selectors);
+ dst->flags = src->flags;
+ if (dst->acl_string) {
+ decrRefCount(dst->acl_string);
+ }
+ dst->acl_string = src->acl_string;
+ if (dst->acl_string) {
+ /* if src is NULL, we set it to NULL, if not, need to increment reference count */
+ incrRefCount(dst->acl_string);
+ }
+}
+
+/* Free all the users registered in the radix tree 'users' and free the
+ * radix tree itself. */
+void ACLFreeUsersSet(rax *users) {
+ raxFreeWithCallback(users,(void(*)(void*))ACLFreeUserAndKillClients);
+}
+
+/* Given a command ID, this function set by reference 'word' and 'bit'
+ * so that user->allowed_commands[word] will address the right word
+ * where the corresponding bit for the provided ID is stored, and
+ * so that user->allowed_commands[word]&bit will identify that specific
+ * bit. The function returns C_ERR in case the specified ID overflows
+ * the bitmap in the user representation. */
+int ACLGetCommandBitCoordinates(uint64_t id, uint64_t *word, uint64_t *bit) {
+ if (id >= USER_COMMAND_BITS_COUNT) return C_ERR;
+ *word = id / sizeof(uint64_t) / 8;
+ *bit = 1ULL << (id % (sizeof(uint64_t) * 8));
+ return C_OK;
+}
+
+/* Check if the specified command bit is set for the specified user.
+ * The function returns 1 is the bit is set or 0 if it is not.
+ * Note that this function does not check the ALLCOMMANDS flag of the user
+ * but just the lowlevel bitmask.
+ *
+ * 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 ACLGetSelectorCommandBit(const aclSelector *selector, unsigned long id) {
+ uint64_t word, bit;
+ if (ACLGetCommandBitCoordinates(id,&word,&bit) == C_ERR) return 0;
+ return (selector->allowed_commands[word] & bit) != 0;
+}
+
+/* When +@all or allcommands is given, we set a reserved bit as well that we
+ * can later test, to see if the user has the right to execute "future commands",
+ * that is, commands loaded later via modules. */
+int ACLSelectorCanExecuteFutureCommands(aclSelector *selector) {
+ return ACLGetSelectorCommandBit(selector,USER_COMMAND_BITS_COUNT-1);
+}
+
+/* Set the specified command bit for the specified user to 'value' (0 or 1).
+ * If the bit overflows the user internal representation, no operation
+ * is performed. As a side effect of calling this function with a value of
+ * zero, the user flag ALLCOMMANDS is cleared since it is no longer possible
+ * to skip the command bit explicit test. */
+void ACLSetSelectorCommandBit(aclSelector *selector, unsigned long id, int value) {
+ uint64_t word, bit;
+ if (ACLGetCommandBitCoordinates(id,&word,&bit) == C_ERR) return;
+ if (value) {
+ selector->allowed_commands[word] |= bit;
+ } else {
+ selector->allowed_commands[word] &= ~bit;
+ selector->flags &= ~SELECTOR_FLAG_ALLCOMMANDS;
+ }
+}
+
+/* This function is used to allow/block a specific command.
+ * Allowing/blocking a container command also applies for its subcommands */
+void ACLChangeSelectorPerm(aclSelector *selector, struct redisCommand *cmd, int allow) {
+ unsigned long id = cmd->id;
+ ACLSetSelectorCommandBit(selector,id,allow);
+ ACLResetFirstArgsForCommand(selector,id);
+ if (cmd->subcommands_dict) {
+ dictEntry *de;
+ dictIterator *di = dictGetSafeIterator(cmd->subcommands_dict);
+ while((de = dictNext(di)) != NULL) {
+ struct redisCommand *sub = (struct redisCommand *)dictGetVal(de);
+ ACLSetSelectorCommandBit(selector,sub->id,allow);
+ }
+ dictReleaseIterator(di);
+ }
+}
+
+void ACLSetSelectorCommandBitsForCategoryLogic(dict *commands, aclSelector *selector, uint64_t cflag, int value) {
+ dictIterator *di = dictGetIterator(commands);
+ dictEntry *de;
+ while ((de = dictNext(di)) != NULL) {
+ struct redisCommand *cmd = dictGetVal(de);
+ if (cmd->flags & CMD_MODULE) continue; /* Ignore modules commands. */
+ if (cmd->acl_categories & cflag) {
+ ACLChangeSelectorPerm(selector,cmd,value);
+ }
+ if (cmd->subcommands_dict) {
+ ACLSetSelectorCommandBitsForCategoryLogic(cmd->subcommands_dict, selector, cflag, value);
+ }
+ }
+ dictReleaseIterator(di);
+}
+
+/* This is like ACLSetSelectorCommandBit(), but instead of setting the specified
+ * ID, it will check all the commands in the category specified as argument,
+ * and will set all the bits corresponding to such commands to the specified
+ * value. Since the category passed by the user may be non existing, the
+ * function returns C_ERR if the category was not found, or C_OK if it was
+ * found and the operation was performed. */
+int ACLSetSelectorCommandBitsForCategory(aclSelector *selector, const char *category, int value) {
+ uint64_t cflag = ACLGetCommandCategoryFlagByName(category);
+ if (!cflag) return C_ERR;
+ ACLSetSelectorCommandBitsForCategoryLogic(server.orig_commands, selector, cflag, value);
+ return C_OK;
+}
+
+void ACLCountCategoryBitsForCommands(dict *commands, aclSelector *selector, unsigned long *on, unsigned long *off, uint64_t cflag) {
+ dictIterator *di = dictGetIterator(commands);
+ dictEntry *de;
+ while ((de = dictNext(di)) != NULL) {
+ struct redisCommand *cmd = dictGetVal(de);
+ if (cmd->acl_categories & cflag) {
+ if (ACLGetSelectorCommandBit(selector,cmd->id))
+ (*on)++;
+ else
+ (*off)++;
+ }
+ if (cmd->subcommands_dict) {
+ ACLCountCategoryBitsForCommands(cmd->subcommands_dict, selector, on, off, cflag);
+ }
+ }
+ dictReleaseIterator(di);
+}
+
+/* Return the number of commands allowed (on) and denied (off) for the user 'u'
+ * in the subset of commands flagged with the specified category name.
+ * If the category name is not valid, C_ERR is returned, otherwise C_OK is
+ * returned and on and off are populated by reference. */
+int ACLCountCategoryBitsForSelector(aclSelector *selector, unsigned long *on, unsigned long *off,
+ const char *category)
+{
+ uint64_t cflag = ACLGetCommandCategoryFlagByName(category);
+ if (!cflag) return C_ERR;
+
+ *on = *off = 0;
+ ACLCountCategoryBitsForCommands(server.orig_commands, selector, on, off, cflag);
+ return C_OK;
+}
+
+sds ACLDescribeSelectorCommandRulesSingleCommands(aclSelector *selector, aclSelector *fake_selector,
+ sds rules, dict *commands) {
+ dictIterator *di = dictGetIterator(commands);
+ dictEntry *de;
+ while ((de = dictNext(di)) != NULL) {
+ struct redisCommand *cmd = dictGetVal(de);
+ int userbit = ACLGetSelectorCommandBit(selector,cmd->id);
+ int fakebit = ACLGetSelectorCommandBit(fake_selector,cmd->id);
+ if (userbit != fakebit) {
+ rules = sdscatlen(rules, userbit ? "+" : "-", 1);
+ rules = sdscatsds(rules,cmd->fullname);
+ rules = sdscatlen(rules," ",1);
+ ACLChangeSelectorPerm(fake_selector,cmd,userbit);
+ }
+
+ if (cmd->subcommands_dict)
+ rules = ACLDescribeSelectorCommandRulesSingleCommands(selector,fake_selector,rules,cmd->subcommands_dict);
+
+ /* Emit the first-args if there are any. */
+ if (userbit == 0 && selector->allowed_firstargs &&
+ selector->allowed_firstargs[cmd->id])
+ {
+ for (int j = 0; selector->allowed_firstargs[cmd->id][j]; j++) {
+ rules = sdscatlen(rules,"+",1);
+ rules = sdscatsds(rules,cmd->fullname);
+ rules = sdscatlen(rules,"|",1);
+ rules = sdscatsds(rules,selector->allowed_firstargs[cmd->id][j]);
+ rules = sdscatlen(rules," ",1);
+ }
+ }
+ }
+ dictReleaseIterator(di);
+ return rules;
+}
+
+/* This function returns an SDS string representing the specified selector ACL
+ * rules related to command execution, in the same format you could set them
+ * back using ACL SETUSER. The function will return just the set of rules needed
+ * to recreate the user commands bitmap, without including other user flags such
+ * as on/off, passwords and so forth. The returned string always starts with
+ * the +@all or -@all rule, depending on the user bitmap, and is followed, if
+ * needed, by the other rules needed to narrow or extend what the user can do. */
+sds ACLDescribeSelectorCommandRules(aclSelector *selector) {
+ sds rules = sdsempty();
+ int additive; /* If true we start from -@all and add, otherwise if
+ false we start from +@all and remove. */
+
+ /* This code is based on a trick: as we generate the rules, we apply
+ * them to a fake user, so that as we go we still know what are the
+ * bit differences we should try to address by emitting more rules. */
+ aclSelector fs = {0};
+ aclSelector *fake_selector = &fs;
+
+ /* Here we want to understand if we should start with +@all and remove
+ * the commands corresponding to the bits that are not set in the user
+ * commands bitmap, or the contrary. Note that semantically the two are
+ * different. For instance starting with +@all and subtracting, the user
+ * will be able to execute future commands, while -@all and adding will just
+ * allow the user the run the selected commands and/or categories.
+ * How do we test for that? We use the trick of a reserved command ID bit
+ * that is set only by +@all (and its alias "allcommands"). */
+ if (ACLSelectorCanExecuteFutureCommands(selector)) {
+ additive = 0;
+ rules = sdscat(rules,"+@all ");
+ ACLSetSelector(fake_selector,"+@all",-1);
+ } else {
+ additive = 1;
+ rules = sdscat(rules,"-@all ");
+ ACLSetSelector(fake_selector,"-@all",-1);
+ }
+
+ /* Attempt to find a good approximation for categories and commands
+ * based on the current bits used, by looping over the category list
+ * and applying the best fit each time. Often a set of categories will not
+ * perfectly match the set of commands into it, so at the end we do a
+ * final pass adding/removing the single commands needed to make the bitmap
+ * exactly match. A temp user is maintained to keep track of categories
+ * already applied. */
+ aclSelector ts = {0};
+ aclSelector *temp_selector = &ts;
+
+ /* Keep track of the categories that have been applied, to prevent
+ * applying them twice. */
+ char applied[sizeof(ACLCommandCategories)/sizeof(ACLCommandCategories[0])];
+ memset(applied, 0, sizeof(applied));
+
+ memcpy(temp_selector->allowed_commands,
+ selector->allowed_commands,
+ sizeof(selector->allowed_commands));
+ while (1) {
+ int best = -1;
+ unsigned long mindiff = INT_MAX, maxsame = 0;
+ for (int j = 0; ACLCommandCategories[j].flag != 0; j++) {
+ if (applied[j]) continue;
+
+ unsigned long on, off, diff, same;
+ ACLCountCategoryBitsForSelector(temp_selector,&on,&off,ACLCommandCategories[j].name);
+ /* Check if the current category is the best this loop:
+ * * It has more commands in common with the user than commands
+ * that are different.
+ * AND EITHER
+ * * It has the fewest number of differences
+ * than the best match we have found so far.
+ * * OR it matches the fewest number of differences
+ * that we've seen but it has more in common. */
+ diff = additive ? off : on;
+ same = additive ? on : off;
+ if (same > diff &&
+ ((diff < mindiff) || (diff == mindiff && same > maxsame)))
+ {
+ best = j;
+ mindiff = diff;
+ maxsame = same;
+ }
+ }
+
+ /* We didn't find a match */
+ if (best == -1) break;
+
+ sds op = sdsnewlen(additive ? "+@" : "-@", 2);
+ op = sdscat(op,ACLCommandCategories[best].name);
+ ACLSetSelector(fake_selector,op,-1);
+
+ sds invop = sdsnewlen(additive ? "-@" : "+@", 2);
+ invop = sdscat(invop,ACLCommandCategories[best].name);
+ ACLSetSelector(temp_selector,invop,-1);
+
+ rules = sdscatsds(rules,op);
+ rules = sdscatlen(rules," ",1);
+ sdsfree(op);
+ sdsfree(invop);
+
+ applied[best] = 1;
+ }
+
+ /* Fix the final ACLs with single commands differences. */
+ rules = ACLDescribeSelectorCommandRulesSingleCommands(selector,fake_selector,rules,server.orig_commands);
+
+ /* Trim the final useless space. */
+ sdsrange(rules,0,-2);
+
+ /* This is technically not needed, but we want to verify that now the
+ * predicted bitmap is exactly the same as the user bitmap, and abort
+ * otherwise, because aborting is better than a security risk in this
+ * code path. */
+ if (memcmp(fake_selector->allowed_commands,
+ selector->allowed_commands,
+ sizeof(selector->allowed_commands)) != 0)
+ {
+ serverLog(LL_WARNING,
+ "CRITICAL ERROR: User ACLs don't match final bitmap: '%s'",
+ rules);
+ serverPanic("No bitmap match in ACLDescribeSelectorCommandRules()");
+ }
+ return rules;
+}
+
+sds ACLDescribeSelector(aclSelector *selector) {
+ listIter li;
+ listNode *ln;
+ sds res = sdsempty();
+ /* Key patterns. */
+ if (selector->flags & SELECTOR_FLAG_ALLKEYS) {
+ res = sdscatlen(res,"~* ",3);
+ } else {
+ listRewind(selector->patterns,&li);
+ while((ln = listNext(&li))) {
+ keyPattern *thispat = (keyPattern *)listNodeValue(ln);
+ res = sdsCatPatternString(res, thispat);
+ res = sdscatlen(res," ",1);
+ }
+ }
+
+ /* Pub/sub channel patterns. */
+ if (selector->flags & SELECTOR_FLAG_ALLCHANNELS) {
+ res = sdscatlen(res,"&* ",3);
+ } else {
+ res = sdscatlen(res,"resetchannels ",14);
+ listRewind(selector->channels,&li);
+ while((ln = listNext(&li))) {
+ sds thispat = listNodeValue(ln);
+ res = sdscatlen(res,"&",1);
+ res = sdscatsds(res,thispat);
+ res = sdscatlen(res," ",1);
+ }
+ }
+
+ /* Command rules. */
+ sds rules = ACLDescribeSelectorCommandRules(selector);
+ res = sdscatsds(res,rules);
+ sdsfree(rules);
+ return res;
+}
+
+/* This is similar to ACLDescribeSelectorCommandRules(), however instead of
+ * describing just the user command rules, everything is described: user
+ * flags, keys, passwords and finally the command rules obtained via
+ * the ACLDescribeSelectorCommandRules() function. This is the function we call
+ * when we want to rewrite the configuration files describing ACLs and
+ * in order to show users with ACL LIST. */
+robj *ACLDescribeUser(user *u) {
+ if (u->acl_string) {
+ incrRefCount(u->acl_string);
+ return u->acl_string;
+ }
+
+ sds res = sdsempty();
+
+ /* Flags. */
+ for (int j = 0; ACLUserFlags[j].flag; j++) {
+ if (u->flags & ACLUserFlags[j].flag) {
+ res = sdscat(res,ACLUserFlags[j].name);
+ res = sdscatlen(res," ",1);
+ }
+ }
+
+ /* Passwords. */
+ listIter li;
+ listNode *ln;
+ listRewind(u->passwords,&li);
+ while((ln = listNext(&li))) {
+ sds thispass = listNodeValue(ln);
+ res = sdscatlen(res,"#",1);
+ res = sdscatsds(res,thispass);
+ res = sdscatlen(res," ",1);
+ }
+
+ /* Selectors (Commands and keys) */
+ listRewind(u->selectors,&li);
+ while((ln = listNext(&li))) {
+ aclSelector *selector = (aclSelector *) listNodeValue(ln);
+ sds default_perm = ACLDescribeSelector(selector);
+ if (selector->flags & SELECTOR_FLAG_ROOT) {
+ res = sdscatfmt(res, "%s", default_perm);
+ } else {
+ res = sdscatfmt(res, " (%s)", default_perm);
+ }
+ sdsfree(default_perm);
+ }
+
+ u->acl_string = createObject(OBJ_STRING, res);
+ /* because we are returning it, have to increase count */
+ incrRefCount(u->acl_string);
+
+ return u->acl_string;
+}
+
+/* Get a command from the original command table, that is not affected
+ * by the command renaming operations: we base all the ACL work from that
+ * table, so that ACLs are valid regardless of command renaming. */
+struct redisCommand *ACLLookupCommand(const char *name) {
+ struct redisCommand *cmd;
+ sds sdsname = sdsnew(name);
+ cmd = lookupCommandBySdsLogic(server.orig_commands,sdsname);
+ sdsfree(sdsname);
+ return cmd;
+}
+
+/* Flush the array of allowed first-args for the specified user
+ * and command ID. */
+void ACLResetFirstArgsForCommand(aclSelector *selector, unsigned long id) {
+ if (selector->allowed_firstargs && selector->allowed_firstargs[id]) {
+ for (int i = 0; selector->allowed_firstargs[id][i]; i++)
+ sdsfree(selector->allowed_firstargs[id][i]);
+ zfree(selector->allowed_firstargs[id]);
+ selector->allowed_firstargs[id] = NULL;
+ }
+}
+
+/* Flush the entire table of first-args. This is useful on +@all, -@all
+ * or similar to return back to the minimal memory usage (and checks to do)
+ * for the user. */
+void ACLResetFirstArgs(aclSelector *selector) {
+ if (selector->allowed_firstargs == NULL) return;
+ for (int j = 0; j < USER_COMMAND_BITS_COUNT; j++) {
+ if (selector->allowed_firstargs[j]) {
+ for (int i = 0; selector->allowed_firstargs[j][i]; i++)
+ sdsfree(selector->allowed_firstargs[j][i]);
+ zfree(selector->allowed_firstargs[j]);
+ }
+ }
+ zfree(selector->allowed_firstargs);
+ selector->allowed_firstargs = NULL;
+}
+
+/* Add a first-arh to the list of subcommands for the user 'u' and
+ * the command id specified. */
+void ACLAddAllowedFirstArg(aclSelector *selector, unsigned long id, const char *sub) {
+ /* If this is the first first-arg to be configured for
+ * this user, we have to allocate the first-args array. */
+ if (selector->allowed_firstargs == NULL) {
+ selector->allowed_firstargs = zcalloc(USER_COMMAND_BITS_COUNT * sizeof(sds*));
+ }
+
+ /* We also need to enlarge the allocation pointing to the
+ * null terminated SDS array, to make space for this one.
+ * To start check the current size, and while we are here
+ * make sure the first-arg is not already specified inside. */
+ long items = 0;
+ if (selector->allowed_firstargs[id]) {
+ while(selector->allowed_firstargs[id][items]) {
+ /* If it's already here do not add it again. */
+ if (!strcasecmp(selector->allowed_firstargs[id][items],sub))
+ return;
+ items++;
+ }
+ }
+
+ /* Now we can make space for the new item (and the null term). */
+ items += 2;
+ selector->allowed_firstargs[id] = zrealloc(selector->allowed_firstargs[id], sizeof(sds)*items);
+ selector->allowed_firstargs[id][items-2] = sdsnew(sub);
+ selector->allowed_firstargs[id][items-1] = NULL;
+}
+
+/* Create an ACL selector from the given ACL operations, which should be
+ * a list of space separate ACL operations that starts and ends
+ * with parentheses.
+ *
+ * If any of the operations are invalid, NULL will be returned instead
+ * and errno will be set corresponding to the interior error. */
+aclSelector *aclCreateSelectorFromOpSet(const char *opset, size_t opsetlen) {
+ serverAssert(opset[0] == '(' && opset[opsetlen - 1] == ')');
+ aclSelector *s = ACLCreateSelector(0);
+
+ int argc = 0;
+ sds trimmed = sdsnewlen(opset + 1, opsetlen - 2);
+ sds *argv = sdssplitargs(trimmed, &argc);
+ for (int i = 0; i < argc; i++) {
+ if (ACLSetSelector(s, argv[i], sdslen(argv[i])) == C_ERR) {
+ ACLFreeSelector(s);
+ s = NULL;
+ goto cleanup;
+ }
+ }
+
+cleanup:
+ sdsfreesplitres(argv, argc);
+ sdsfree(trimmed);
+ return s;
+}
+
+/* Set a selector's properties with the provided 'op'.
+ *
+ * +<command> Allow the execution of that command.
+ * May be used with `|` for allowing subcommands (e.g "+config|get")
+ * -<command> Disallow the execution of that command.
+ * May be used with `|` for blocking subcommands (e.g "-config|set")
+ * +@<category> Allow the execution of all the commands in such category
+ * with valid categories are like @admin, @set, @sortedset, ...
+ * and so forth, see the full list in the server.c file where
+ * the Redis command table is described and defined.
+ * The special category @all means all the commands, but currently
+ * present in the server, and that will be loaded in the future
+ * via modules.
+ * +<command>|first-arg Allow a specific first argument of an otherwise
+ * disabled command. Note that this form is not
+ * allowed as negative like -SELECT|1, but
+ * only additive starting with "+".
+ * allcommands Alias for +@all. Note that it implies the ability to execute
+ * all the future commands loaded via the modules system.
+ * nocommands Alias for -@all.
+ * ~<pattern> Add a pattern of keys that can be mentioned as part of
+ * commands. For instance ~* allows all the keys. The pattern
+ * is a glob-style pattern like the one of KEYS.
+ * It is possible to specify multiple patterns.
+ * %R~<pattern> Add key read pattern that specifies which keys can be read
+ * from.
+ * %W~<pattern> Add key write pattern that specifies which keys can be
+ * written to.
+ * allkeys Alias for ~*
+ * resetkeys Flush the list of allowed keys patterns.
+ * &<pattern> Add a pattern of channels that can be mentioned as part of
+ * Pub/Sub commands. For instance &* allows all the channels. The
+ * pattern is a glob-style pattern like the one of PSUBSCRIBE.
+ * It is possible to specify multiple patterns.
+ * allchannels Alias for &*
+ * resetchannels Flush the list of allowed channel patterns.
+ */
+int ACLSetSelector(aclSelector *selector, const char* op, size_t oplen) {
+ if (!strcasecmp(op,"allkeys") ||
+ !strcasecmp(op,"~*"))
+ {
+ selector->flags |= SELECTOR_FLAG_ALLKEYS;
+ listEmpty(selector->patterns);
+ } else if (!strcasecmp(op,"resetkeys")) {
+ selector->flags &= ~SELECTOR_FLAG_ALLKEYS;
+ listEmpty(selector->patterns);
+ } else if (!strcasecmp(op,"allchannels") ||
+ !strcasecmp(op,"&*"))
+ {
+ selector->flags |= SELECTOR_FLAG_ALLCHANNELS;
+ listEmpty(selector->channels);
+ } else if (!strcasecmp(op,"resetchannels")) {
+ selector->flags &= ~SELECTOR_FLAG_ALLCHANNELS;
+ listEmpty(selector->channels);
+ } else if (!strcasecmp(op,"allcommands") ||
+ !strcasecmp(op,"+@all"))
+ {
+ memset(selector->allowed_commands,255,sizeof(selector->allowed_commands));
+ selector->flags |= SELECTOR_FLAG_ALLCOMMANDS;
+ ACLResetFirstArgs(selector);
+ } else if (!strcasecmp(op,"nocommands") ||
+ !strcasecmp(op,"-@all"))
+ {
+ memset(selector->allowed_commands,0,sizeof(selector->allowed_commands));
+ selector->flags &= ~SELECTOR_FLAG_ALLCOMMANDS;
+ ACLResetFirstArgs(selector);
+ } else if (op[0] == '~' || op[0] == '%') {
+ if (selector->flags & SELECTOR_FLAG_ALLKEYS) {
+ errno = EEXIST;
+ return C_ERR;
+ }
+ int flags = 0;
+ size_t offset = 1;
+ if (op[0] == '%') {
+ for (; offset < oplen; offset++) {
+ if (toupper(op[offset]) == 'R' && !(flags & ACL_READ_PERMISSION)) {
+ flags |= ACL_READ_PERMISSION;
+ } else if (toupper(op[offset]) == 'W' && !(flags & ACL_WRITE_PERMISSION)) {
+ flags |= ACL_WRITE_PERMISSION;
+ } else if (op[offset] == '~') {
+ offset++;
+ break;
+ } else {
+ errno = EINVAL;
+ return C_ERR;
+ }
+ }
+ } else {
+ flags = ACL_ALL_PERMISSION;
+ }
+
+ if (ACLStringHasSpaces(op+offset,oplen-offset)) {
+ errno = EINVAL;
+ return C_ERR;
+ }
+ keyPattern *newpat = ACLKeyPatternCreate(sdsnewlen(op+offset,oplen-offset), flags);
+ listNode *ln = listSearchKey(selector->patterns,newpat);
+ /* Avoid re-adding the same key pattern multiple times. */
+ if (ln == NULL) {
+ listAddNodeTail(selector->patterns,newpat);
+ } else {
+ ((keyPattern *)listNodeValue(ln))->flags |= flags;
+ ACLKeyPatternFree(newpat);
+ }
+ selector->flags &= ~SELECTOR_FLAG_ALLKEYS;
+ } else if (op[0] == '&') {
+ if (selector->flags & SELECTOR_FLAG_ALLCHANNELS) {
+ errno = EISDIR;
+ return C_ERR;
+ }
+ if (ACLStringHasSpaces(op+1,oplen-1)) {
+ errno = EINVAL;
+ return C_ERR;
+ }
+ sds newpat = sdsnewlen(op+1,oplen-1);
+ listNode *ln = listSearchKey(selector->channels,newpat);
+ /* Avoid re-adding the same channel pattern multiple times. */
+ if (ln == NULL)
+ listAddNodeTail(selector->channels,newpat);
+ else
+ sdsfree(newpat);
+ selector->flags &= ~SELECTOR_FLAG_ALLCHANNELS;
+ } else if (op[0] == '+' && op[1] != '@') {
+ if (strrchr(op,'|') == NULL) {
+ struct redisCommand *cmd = ACLLookupCommand(op+1);
+ if (cmd == NULL) {
+ errno = ENOENT;
+ return C_ERR;
+ }
+ ACLChangeSelectorPerm(selector,cmd,1);
+ } else {
+ /* Split the command and subcommand parts. */
+ char *copy = zstrdup(op+1);
+ char *sub = strrchr(copy,'|');
+ sub[0] = '\0';
+ sub++;
+
+ struct redisCommand *cmd = ACLLookupCommand(copy);
+
+ /* Check if the command exists. We can't check the
+ * first-arg to see if it is valid. */
+ if (cmd == NULL) {
+ zfree(copy);
+ errno = ENOENT;
+ return C_ERR;
+ }
+
+ /* We do not support allowing first-arg of a subcommand */
+ if (cmd->parent) {
+ zfree(copy);
+ errno = ECHILD;
+ return C_ERR;
+ }
+
+ /* The subcommand cannot be empty, so things like DEBUG|
+ * are syntax errors of course. */
+ if (strlen(sub) == 0) {
+ zfree(copy);
+ errno = EINVAL;
+ return C_ERR;
+ }
+
+ if (cmd->subcommands_dict) {
+ /* If user is trying to allow a valid subcommand we can just add its unique ID */
+ cmd = ACLLookupCommand(op+1);
+ if (cmd == NULL) {
+ zfree(copy);
+ errno = ENOENT;
+ return C_ERR;
+ }
+ ACLChangeSelectorPerm(selector,cmd,1);
+ } else {
+ /* If user is trying to use the ACL mech to block SELECT except SELECT 0 or
+ * block DEBUG except DEBUG OBJECT (DEBUG subcommands are not considered
+ * subcommands for now) we use the allowed_firstargs mechanism. */
+
+ /* Add the first-arg to the list of valid ones. */
+ serverLog(LL_WARNING, "Deprecation warning: Allowing a first arg of an otherwise "
+ "blocked command is a misuse of ACL and may get disabled "
+ "in the future (offender: +%s)", op+1);
+ ACLAddAllowedFirstArg(selector,cmd->id,sub);
+ }
+
+ zfree(copy);
+ }
+ } else if (op[0] == '-' && op[1] != '@') {
+ struct redisCommand *cmd = ACLLookupCommand(op+1);
+ if (cmd == NULL) {
+ errno = ENOENT;
+ return C_ERR;
+ }
+ ACLChangeSelectorPerm(selector,cmd,0);
+ } else if ((op[0] == '+' || op[0] == '-') && op[1] == '@') {
+ int bitval = op[0] == '+' ? 1 : 0;
+ if (ACLSetSelectorCommandBitsForCategory(selector,op+2,bitval) == C_ERR) {
+ errno = ENOENT;
+ return C_ERR;
+ }
+ } else {
+ errno = EINVAL;
+ return C_ERR;
+ }
+ return C_OK;
+}
+
+/* Set user properties according to the string "op". The following
+ * is a description of what different strings will do:
+ *
+ * on Enable the user: it is possible to authenticate as this user.
+ * off Disable the user: it's no longer possible to authenticate
+ * with this user, however the already authenticated connections
+ * will still work.
+ * ><password> Add this password to the list of valid password for the user.
+ * For example >mypass will add "mypass" to the list.
+ * This directive clears the "nopass" flag (see later).
+ * #<hash> Add this password hash to the list of valid hashes for
+ * the user. This is useful if you have previously computed
+ * the hash, and don't want to store it in plaintext.
+ * This directive clears the "nopass" flag (see later).
+ * <<password> Remove this password from the list of valid passwords.
+ * !<hash> Remove this hashed password from the list of valid passwords.
+ * This is useful when you want to remove a password just by
+ * hash without knowing its plaintext version at all.
+ * nopass All the set passwords of the user are removed, and the user
+ * is flagged as requiring no password: it means that every
+ * password will work against this user. If this directive is
+ * used for the default user, every new connection will be
+ * immediately authenticated with the default user without
+ * any explicit AUTH command required. Note that the "resetpass"
+ * directive will clear this condition.
+ * resetpass Flush the list of allowed passwords. Moreover removes the
+ * "nopass" status. After "resetpass" the user has no associated
+ * passwords and there is no way to authenticate without adding
+ * some password (or setting it as "nopass" later).
+ * reset Performs the following actions: resetpass, resetkeys, off,
+ * -@all. The user returns to the same state it has immediately
+ * after its creation.
+ * (<options>) Create a new selector with the options specified within the
+ * parentheses and attach it to the user. Each option should be
+ * space separated. The first character must be ( and the last
+ * character must be ).
+ * clearselectors Remove all of the currently attached selectors.
+ * Note this does not change the "root" user permissions,
+ * which are the permissions directly applied onto the
+ * user (outside the parentheses).
+ *
+ * Selector options can also be specified by this function, in which case
+ * they update the root selector for the user.
+ *
+ * The 'op' string must be null terminated. The 'oplen' argument should
+ * specify the length of the 'op' string in case the caller requires to pass
+ * binary data (for instance the >password form may use a binary password).
+ * Otherwise the field can be set to -1 and the function will use strlen()
+ * to determine the length.
+ *
+ * The function returns C_OK if the action to perform was understood because
+ * the 'op' string made sense. Otherwise C_ERR is returned if the operation
+ * is unknown or has some syntax error.
+ *
+ * When an error is returned, errno is set to the following values:
+ *
+ * EINVAL: The specified opcode is not understood or the key/channel pattern is
+ * invalid (contains non allowed characters).
+ * ENOENT: The command name or command category provided with + or - is not
+ * known.
+ * EEXIST: You are adding a key pattern after "*" was already added. This is
+ * almost surely an error on the user side.
+ * EISDIR: You are adding a channel pattern after "*" was already added. This is
+ * almost surely an error on the user side.
+ * ENODEV: The password you are trying to remove from the user does not exist.
+ * EBADMSG: The hash you are trying to add is not a valid hash.
+ * ECHILD: Attempt to allow a specific first argument of a subcommand
+ */
+int ACLSetUser(user *u, const char *op, ssize_t oplen) {
+ /* as we are changing the ACL, the old generated string is now invalid */
+ if (u->acl_string) {
+ decrRefCount(u->acl_string);
+ u->acl_string = NULL;
+ }
+
+ if (oplen == -1) oplen = strlen(op);
+ if (oplen == 0) return C_OK; /* Empty string is a no-operation. */
+ if (!strcasecmp(op,"on")) {
+ u->flags |= USER_FLAG_ENABLED;
+ u->flags &= ~USER_FLAG_DISABLED;
+ } else if (!strcasecmp(op,"off")) {
+ u->flags |= USER_FLAG_DISABLED;
+ u->flags &= ~USER_FLAG_ENABLED;
+ } else if (!strcasecmp(op,"skip-sanitize-payload")) {
+ u->flags |= USER_FLAG_SANITIZE_PAYLOAD_SKIP;
+ u->flags &= ~USER_FLAG_SANITIZE_PAYLOAD;
+ } else if (!strcasecmp(op,"sanitize-payload")) {
+ u->flags &= ~USER_FLAG_SANITIZE_PAYLOAD_SKIP;
+ u->flags |= USER_FLAG_SANITIZE_PAYLOAD;
+ } else if (!strcasecmp(op,"nopass")) {
+ u->flags |= USER_FLAG_NOPASS;
+ listEmpty(u->passwords);
+ } else if (!strcasecmp(op,"resetpass")) {
+ u->flags &= ~USER_FLAG_NOPASS;
+ listEmpty(u->passwords);
+ } else if (op[0] == '>' || op[0] == '#') {
+ sds newpass;
+ if (op[0] == '>') {
+ newpass = ACLHashPassword((unsigned char*)op+1,oplen-1);
+ } else {
+ if (ACLCheckPasswordHash((unsigned char*)op+1,oplen-1) == C_ERR) {
+ errno = EBADMSG;
+ return C_ERR;
+ }
+ newpass = sdsnewlen(op+1,oplen-1);
+ }
+
+ listNode *ln = listSearchKey(u->passwords,newpass);
+ /* Avoid re-adding the same password multiple times. */
+ if (ln == NULL)
+ listAddNodeTail(u->passwords,newpass);
+ else
+ sdsfree(newpass);
+ u->flags &= ~USER_FLAG_NOPASS;
+ } else if (op[0] == '<' || op[0] == '!') {
+ sds delpass;
+ if (op[0] == '<') {
+ delpass = ACLHashPassword((unsigned char*)op+1,oplen-1);
+ } else {
+ if (ACLCheckPasswordHash((unsigned char*)op+1,oplen-1) == C_ERR) {
+ errno = EBADMSG;
+ return C_ERR;
+ }
+ delpass = sdsnewlen(op+1,oplen-1);
+ }
+ listNode *ln = listSearchKey(u->passwords,delpass);
+ sdsfree(delpass);
+ if (ln) {
+ listDelNode(u->passwords,ln);
+ } else {
+ errno = ENODEV;
+ return C_ERR;
+ }
+ } else if (op[0] == '(' && op[oplen - 1] == ')') {
+ aclSelector *selector = aclCreateSelectorFromOpSet(op, oplen);
+ if (!selector) {
+ /* No errorno set, propagate it from interior error. */
+ return C_ERR;
+ }
+ listAddNodeTail(u->selectors, selector);
+ return C_OK;
+ } else if (!strcasecmp(op,"clearselectors")) {
+ listIter li;
+ listNode *ln;
+ listRewind(u->selectors,&li);
+ /* There has to be a root selector */
+ serverAssert(listNext(&li));
+ while((ln = listNext(&li))) {
+ listDelNode(u->selectors, ln);
+ }
+ return C_OK;
+ } else if (!strcasecmp(op,"reset")) {
+ serverAssert(ACLSetUser(u,"resetpass",-1) == C_OK);
+ serverAssert(ACLSetUser(u,"resetkeys",-1) == C_OK);
+ serverAssert(ACLSetUser(u,"resetchannels",-1) == C_OK);
+ if (server.acl_pubsub_default & SELECTOR_FLAG_ALLCHANNELS)
+ serverAssert(ACLSetUser(u,"allchannels",-1) == C_OK);
+ serverAssert(ACLSetUser(u,"off",-1) == C_OK);
+ serverAssert(ACLSetUser(u,"sanitize-payload",-1) == C_OK);
+ serverAssert(ACLSetUser(u,"clearselectors",-1) == C_OK);
+ serverAssert(ACLSetUser(u,"-@all",-1) == C_OK);
+ } else {
+ aclSelector *selector = ACLUserGetRootSelector(u);
+ if (ACLSetSelector(selector, op, oplen) == C_ERR) {
+ return C_ERR;
+ }
+ }
+ return C_OK;
+}
+
+/* Return a description of the error that occurred in ACLSetUser() according to
+ * the errno value set by the function on error. */
+const char *ACLSetUserStringError(void) {
+ const char *errmsg = "Wrong format";
+ if (errno == ENOENT)
+ errmsg = "Unknown command or category name in ACL";
+ else if (errno == EINVAL)
+ errmsg = "Syntax error";
+ else if (errno == EEXIST)
+ errmsg = "Adding a pattern after the * pattern (or the "
+ "'allkeys' flag) is not valid and does not have any "
+ "effect. Try 'resetkeys' to start with an empty "
+ "list of patterns";
+ else if (errno == EISDIR)
+ errmsg = "Adding a pattern after the * pattern (or the "
+ "'allchannels' flag) is not valid and does not have any "
+ "effect. Try 'resetchannels' to start with an empty "
+ "list of channels";
+ else if (errno == ENODEV)
+ errmsg = "The password you are trying to remove from the user does "
+ "not exist";
+ else if (errno == EBADMSG)
+ errmsg = "The password hash must be exactly 64 characters and contain "
+ "only lowercase hexadecimal characters";
+ else if (errno == EALREADY)
+ errmsg = "Duplicate user found. A user can only be defined once in "
+ "config files";
+ else if (errno == ECHILD)
+ errmsg = "Allowing first-arg of a subcommand is not supported";
+ return errmsg;
+}
+
+/* Create the default user, this has special permissions. */
+user *ACLCreateDefaultUser(void) {
+ user *new = ACLCreateUser("default",7);
+ ACLSetUser(new,"+@all",-1);
+ ACLSetUser(new,"~*",-1);
+ ACLSetUser(new,"&*",-1);
+ ACLSetUser(new,"on",-1);
+ ACLSetUser(new,"nopass",-1);
+ return new;
+}
+
+/* Initialization of the ACL subsystem. */
+void ACLInit(void) {
+ Users = raxNew();
+ UsersToLoad = listCreate();
+ listSetMatchMethod(UsersToLoad, ACLListMatchLoadedUser);
+ ACLLog = listCreate();
+ DefaultUser = ACLCreateDefaultUser();
+}
+
+/* Check the username and password pair and return C_OK if they are valid,
+ * otherwise C_ERR is returned and errno is set to:
+ *
+ * EINVAL: if the username-password do not match.
+ * ENONENT: if the specified user does not exist at all.
+ */
+int ACLCheckUserCredentials(robj *username, robj *password) {
+ user *u = ACLGetUserByName(username->ptr,sdslen(username->ptr));
+ if (u == NULL) {
+ errno = ENOENT;
+ return C_ERR;
+ }
+
+ /* Disabled users can't login. */
+ if (u->flags & USER_FLAG_DISABLED) {
+ errno = EINVAL;
+ return C_ERR;
+ }
+
+ /* If the user is configured to don't require any password, we
+ * are already fine here. */
+ if (u->flags & USER_FLAG_NOPASS) return C_OK;
+
+ /* Check all the user passwords for at least one to match. */
+ listIter li;
+ listNode *ln;
+ listRewind(u->passwords,&li);
+ sds hashed = ACLHashPassword(password->ptr,sdslen(password->ptr));
+ while((ln = listNext(&li))) {
+ sds thispass = listNodeValue(ln);
+ if (!time_independent_strcmp(hashed, thispass, HASH_PASSWORD_LEN)) {
+ sdsfree(hashed);
+ return C_OK;
+ }
+ }
+ sdsfree(hashed);
+
+ /* If we reached this point, no password matched. */
+ errno = EINVAL;
+ return C_ERR;
+}
+
+/* This is like ACLCheckUserCredentials(), however if the user/pass
+ * are correct, the connection is put in authenticated state and the
+ * connection user reference is populated.
+ *
+ * The return value is C_OK or C_ERR with the same meaning as
+ * ACLCheckUserCredentials(). */
+int ACLAuthenticateUser(client *c, robj *username, robj *password) {
+ if (ACLCheckUserCredentials(username,password) == C_OK) {
+ c->authenticated = 1;
+ c->user = ACLGetUserByName(username->ptr,sdslen(username->ptr));
+ moduleNotifyUserChanged(c);
+ return C_OK;
+ } else {
+ addACLLogEntry(c,ACL_DENIED_AUTH,(c->flags & CLIENT_MULTI) ? ACL_LOG_CTX_MULTI : ACL_LOG_CTX_TOPLEVEL,0,username->ptr,NULL);
+ return C_ERR;
+ }
+}
+
+/* For ACL purposes, every user has a bitmap with the commands that such
+ * user is allowed to execute. In order to populate the bitmap, every command
+ * should have an assigned ID (that is used to index the bitmap). This function
+ * creates such an ID: it uses sequential IDs, reusing the same ID for the same
+ * command name, so that a command retains the same ID in case of modules that
+ * are unloaded and later reloaded.
+ *
+ * The function does not take ownership of the 'cmdname' SDS string.
+ * */
+unsigned long ACLGetCommandID(sds cmdname) {
+ sds lowername = sdsdup(cmdname);
+ sdstolower(lowername);
+ if (commandId == NULL) commandId = raxNew();
+ void *id = raxFind(commandId,(unsigned char*)lowername,sdslen(lowername));
+ if (id != raxNotFound) {
+ sdsfree(lowername);
+ return (unsigned long)id;
+ }
+ raxInsert(commandId,(unsigned char*)lowername,strlen(lowername),
+ (void*)nextid,NULL);
+ sdsfree(lowername);
+ unsigned long thisid = nextid;
+ nextid++;
+
+ /* We never assign the last bit in the user commands bitmap structure,
+ * this way we can later check if this bit is set, understanding if the
+ * current ACL for the user was created starting with a +@all to add all
+ * the possible commands and just subtracting other single commands or
+ * categories, or if, instead, the ACL was created just adding commands
+ * and command categories from scratch, not allowing future commands by
+ * default (loaded via modules). This is useful when rewriting the ACLs
+ * with ACL SAVE. */
+ if (nextid == USER_COMMAND_BITS_COUNT-1) nextid++;
+ return thisid;
+}
+
+/* Clear command id table and reset nextid to 0. */
+void ACLClearCommandID(void) {
+ if (commandId) raxFree(commandId);
+ commandId = NULL;
+ nextid = 0;
+}
+
+/* Return an username by its name, or NULL if the user does not exist. */
+user *ACLGetUserByName(const char *name, size_t namelen) {
+ void *myuser = raxFind(Users,(unsigned char*)name,namelen);
+ if (myuser == raxNotFound) return NULL;
+ return myuser;
+}
+
+/* =============================================================================
+ * ACL permission checks
+ * ==========================================================================*/
+
+/* Check if the key can be accessed by the selector.
+ *
+ * If the selector can access the key, ACL_OK is returned, otherwise
+ * ACL_DENIED_KEY is returned. */
+static int ACLSelectorCheckKey(aclSelector *selector, const char *key, int keylen, int keyspec_flags) {
+ /* The selector can access any key */
+ if (selector->flags & SELECTOR_FLAG_ALLKEYS) return ACL_OK;
+
+ listIter li;
+ listNode *ln;
+ listRewind(selector->patterns,&li);
+
+ int key_flags = 0;
+ if (keyspec_flags & CMD_KEY_ACCESS) key_flags |= ACL_READ_PERMISSION;
+ if (keyspec_flags & CMD_KEY_INSERT) key_flags |= ACL_WRITE_PERMISSION;
+ if (keyspec_flags & CMD_KEY_DELETE) key_flags |= ACL_WRITE_PERMISSION;
+ if (keyspec_flags & CMD_KEY_UPDATE) key_flags |= ACL_WRITE_PERMISSION;
+
+ /* Test this key against every pattern. */
+ while((ln = listNext(&li))) {
+ keyPattern *pattern = listNodeValue(ln);
+ if ((pattern->flags & key_flags) != key_flags)
+ continue;
+ size_t plen = sdslen(pattern->pattern);
+ if (stringmatchlen(pattern->pattern,plen,key,keylen,0))
+ return ACL_OK;
+ }
+ return ACL_DENIED_KEY;
+}
+
+/* Checks if the provided selector selector has access specified in flags
+ * to all keys in the keyspace. For example, CMD_KEY_READ access requires either
+ * '%R~*', '~*', or allkeys to be granted to the selector. Returns 1 if all
+ * the access flags are satisfied with this selector or 0 otherwise.
+ */
+static int ACLSelectorHasUnrestrictedKeyAccess(aclSelector *selector, int flags) {
+ /* The selector can access any key */
+ if (selector->flags & SELECTOR_FLAG_ALLKEYS) return 1;
+
+ listIter li;
+ listNode *ln;
+ listRewind(selector->patterns,&li);
+
+ int access_flags = 0;
+ if (flags & CMD_KEY_ACCESS) access_flags |= ACL_READ_PERMISSION;
+ if (flags & CMD_KEY_INSERT) access_flags |= ACL_WRITE_PERMISSION;
+ if (flags & CMD_KEY_DELETE) access_flags |= ACL_WRITE_PERMISSION;
+ if (flags & CMD_KEY_UPDATE) access_flags |= ACL_WRITE_PERMISSION;
+
+ /* Test this key against every pattern. */
+ while((ln = listNext(&li))) {
+ keyPattern *pattern = listNodeValue(ln);
+ if ((pattern->flags & access_flags) != access_flags)
+ continue;
+ if (!strcmp(pattern->pattern,"*")) {
+ return 1;
+ }
+ }
+ return 0;
+}
+
+/* Checks a channel against a provided list of channels. The is_pattern
+ * argument should only be used when subscribing (not when publishing)
+ * and controls whether the input channel is evaluated as a channel pattern
+ * (like in PSUBSCRIBE) or a plain channel name (like in SUBSCRIBE).
+ *
+ * Note that a plain channel name like in PUBLISH or SUBSCRIBE can be
+ * matched against ACL channel patterns, but the pattern provided in PSUBSCRIBE
+ * can only be matched as a literal against an ACL pattern (using plain string compare). */
+static int ACLCheckChannelAgainstList(list *reference, const char *channel, int channellen, int is_pattern) {
+ listIter li;
+ listNode *ln;
+
+ listRewind(reference, &li);
+ while((ln = listNext(&li))) {
+ sds pattern = listNodeValue(ln);
+ size_t plen = sdslen(pattern);
+ /* Channel patterns are matched literally against the channels in
+ * the list. Regular channels perform pattern matching. */
+ if ((is_pattern && !strcmp(pattern,channel)) ||
+ (!is_pattern && stringmatchlen(pattern,plen,channel,channellen,0)))
+ {
+ return ACL_OK;
+ }
+ }
+ return ACL_DENIED_CHANNEL;
+}
+
+/* To prevent duplicate calls to getKeysResult, a cache is maintained
+ * in between calls to the various selectors. */
+typedef struct {
+ int keys_init;
+ getKeysResult keys;
+} aclKeyResultCache;
+
+void initACLKeyResultCache(aclKeyResultCache *cache) {
+ cache->keys_init = 0;
+}
+
+void cleanupACLKeyResultCache(aclKeyResultCache *cache) {
+ if (cache->keys_init) getKeysFreeResult(&(cache->keys));
+}
+
+/* Check if the command is ready to be executed according to the
+ * ACLs associated with the specified selector.
+ *
+ * If the selector can execute the command ACL_OK is returned, otherwise
+ * ACL_DENIED_CMD, ACL_DENIED_KEY, or ACL_DENIED_CHANNEL is returned: the first in case the
+ * command cannot be executed because the selector is not allowed to run such
+ * command, the second and third if the command is denied because the selector is trying
+ * to access a key or channel that are not among the specified patterns. */
+static int ACLSelectorCheckCmd(aclSelector *selector, struct redisCommand *cmd, robj **argv, int argc, int *keyidxptr, aclKeyResultCache *cache) {
+ uint64_t id = cmd->id;
+ int ret;
+ if (!(selector->flags & SELECTOR_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 first argument. */
+ if (ACLGetSelectorCommandBit(selector,id) == 0) {
+ /* Check if the first argument matches. */
+ if (argc < 2 ||
+ selector->allowed_firstargs == NULL ||
+ selector->allowed_firstargs[id] == NULL)
+ {
+ return ACL_DENIED_CMD;
+ }
+
+ long subid = 0;
+ while (1) {
+ if (selector->allowed_firstargs[id][subid] == NULL)
+ return ACL_DENIED_CMD;
+ int idx = cmd->parent ? 2 : 1;
+ if (!strcasecmp(argv[idx]->ptr,selector->allowed_firstargs[id][subid]))
+ break; /* First argument match found. Stop here. */
+ subid++;
+ }
+ }
+ }
+
+ /* Check if the user can execute commands explicitly touching the keys
+ * mentioned in the command arguments. */
+ if (!(selector->flags & SELECTOR_FLAG_ALLKEYS) && doesCommandHaveKeys(cmd)) {
+ if (!(cache->keys_init)) {
+ cache->keys = (getKeysResult) GETKEYS_RESULT_INIT;
+ getKeysFromCommandWithSpecs(cmd, argv, argc, GET_KEYSPEC_DEFAULT, &(cache->keys));
+ cache->keys_init = 1;
+ }
+ getKeysResult *result = &(cache->keys);
+ keyReference *resultidx = result->keys;
+ for (int j = 0; j < result->numkeys; j++) {
+ int idx = resultidx[j].pos;
+ ret = ACLSelectorCheckKey(selector, argv[idx]->ptr, sdslen(argv[idx]->ptr), resultidx[j].flags);
+ if (ret != ACL_OK) {
+ if (keyidxptr) *keyidxptr = resultidx[j].pos;
+ return ret;
+ }
+ }
+ }
+
+ /* Check if the user can execute commands explicitly touching the channels
+ * mentioned in the command arguments */
+ const int channel_flags = CMD_CHANNEL_PUBLISH | CMD_CHANNEL_SUBSCRIBE;
+ if (!(selector->flags & SELECTOR_FLAG_ALLCHANNELS) && doesCommandHaveChannelsWithFlags(cmd, channel_flags)) {
+ getKeysResult channels = (getKeysResult) GETKEYS_RESULT_INIT;
+ getChannelsFromCommand(cmd, argv, argc, &channels);
+ keyReference *channelref = channels.keys;
+ for (int j = 0; j < channels.numkeys; j++) {
+ int idx = channelref[j].pos;
+ if (!(channelref[j].flags & channel_flags)) continue;
+ int is_pattern = channelref[j].flags & CMD_CHANNEL_PATTERN;
+ int ret = ACLCheckChannelAgainstList(selector->channels, argv[idx]->ptr, sdslen(argv[idx]->ptr), is_pattern);
+ if (ret != ACL_OK) {
+ if (keyidxptr) *keyidxptr = channelref[j].pos;
+ getKeysFreeResult(&channels);
+ return ret;
+ }
+ }
+ getKeysFreeResult(&channels);
+ }
+ return ACL_OK;
+}
+
+/* Check if the key can be accessed by the client according to
+ * the ACLs associated with the specified user according to the
+ * keyspec access flags.
+ *
+ * If the user can access the key, ACL_OK is returned, otherwise
+ * ACL_DENIED_KEY is returned. */
+int ACLUserCheckKeyPerm(user *u, const char *key, int keylen, int flags) {
+ listIter li;
+ listNode *ln;
+
+ /* If there is no associated user, the connection can run anything. */
+ if (u == NULL) return ACL_OK;
+
+ /* Check all of the selectors */
+ listRewind(u->selectors,&li);
+ while((ln = listNext(&li))) {
+ aclSelector *s = (aclSelector *) listNodeValue(ln);
+ if (ACLSelectorCheckKey(s, key, keylen, flags) == ACL_OK) {
+ return ACL_OK;
+ }
+ }
+ return ACL_DENIED_KEY;
+}
+
+/* Checks if the user can execute the given command with the added restriction
+ * it must also have the access specified in flags to any key in the key space.
+ * For example, CMD_KEY_READ access requires either '%R~*', '~*', or allkeys to be
+ * granted in addition to the access required by the command. Returns 1
+ * if the user has access or 0 otherwise.
+ */
+int ACLUserCheckCmdWithUnrestrictedKeyAccess(user *u, struct redisCommand *cmd, robj **argv, int argc, int flags) {
+ listIter li;
+ listNode *ln;
+ int local_idxptr;
+
+ /* If there is no associated user, the connection can run anything. */
+ if (u == NULL) return 1;
+
+ /* For multiple selectors, we cache the key result in between selector
+ * calls to prevent duplicate lookups. */
+ aclKeyResultCache cache;
+ initACLKeyResultCache(&cache);
+
+ /* Check each selector sequentially */
+ listRewind(u->selectors,&li);
+ while((ln = listNext(&li))) {
+ aclSelector *s = (aclSelector *) listNodeValue(ln);
+ int acl_retval = ACLSelectorCheckCmd(s, cmd, argv, argc, &local_idxptr, &cache);
+ if (acl_retval == ACL_OK && ACLSelectorHasUnrestrictedKeyAccess(s, flags)) {
+ cleanupACLKeyResultCache(&cache);
+ return 1;
+ }
+ }
+ cleanupACLKeyResultCache(&cache);
+ return 0;
+}
+
+/* Check if the channel 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_CHANNEL is returned. */
+int ACLUserCheckChannelPerm(user *u, sds channel, int is_pattern) {
+ listIter li;
+ listNode *ln;
+
+ /* If there is no associated user, the connection can run anything. */
+ if (u == NULL) return ACL_OK;
+
+ /* Check all of the selectors */
+ listRewind(u->selectors,&li);
+ while((ln = listNext(&li))) {
+ aclSelector *s = (aclSelector *) listNodeValue(ln);
+ /* The selector can run any keys */
+ if (s->flags & SELECTOR_FLAG_ALLCHANNELS) return ACL_OK;
+
+ /* Otherwise, loop over the selectors list and check each channel */
+ if (ACLCheckChannelAgainstList(s->channels, channel, sdslen(channel), is_pattern) == ACL_OK) {
+ return ACL_OK;
+ }
+ }
+ return ACL_DENIED_CHANNEL;
+}
+
+/* Lower level API that checks if a specified user is able to execute a given command. */
+int ACLCheckAllUserCommandPerm(user *u, struct redisCommand *cmd, robj **argv, int argc, int *idxptr) {
+ listIter li;
+ listNode *ln;
+
+ /* If there is no associated user, the connection can run anything. */
+ if (u == NULL) return ACL_OK;
+
+ /* We have to pick a single error to log, the logic for picking is as follows:
+ * 1) If no selector can execute the command, return the command.
+ * 2) Return the last key or channel that no selector could match. */
+ int relevant_error = ACL_DENIED_CMD;
+ int local_idxptr = 0, last_idx = 0;
+
+ /* For multiple selectors, we cache the key result in between selector
+ * calls to prevent duplicate lookups. */
+ aclKeyResultCache cache;
+ initACLKeyResultCache(&cache);
+
+ /* Check each selector sequentially */
+ listRewind(u->selectors,&li);
+ while((ln = listNext(&li))) {
+ aclSelector *s = (aclSelector *) listNodeValue(ln);
+ int acl_retval = ACLSelectorCheckCmd(s, cmd, argv, argc, &local_idxptr, &cache);
+ if (acl_retval == ACL_OK) {
+ cleanupACLKeyResultCache(&cache);
+ return ACL_OK;
+ }
+ if (acl_retval > relevant_error ||
+ (acl_retval == relevant_error && local_idxptr > last_idx))
+ {
+ relevant_error = acl_retval;
+ last_idx = local_idxptr;
+ }
+ }
+
+ *idxptr = last_idx;
+ cleanupACLKeyResultCache(&cache);
+ return relevant_error;
+}
+
+/* High level API for checking if a client can execute the queued up command */
+int ACLCheckAllPerm(client *c, int *idxptr) {
+ return ACLCheckAllUserCommandPerm(c->user, c->cmd, c->argv, c->argc, idxptr);
+}
+
+/* Check if the user's existing pub/sub clients violate the ACL pub/sub
+ * permissions specified via the upcoming argument, and kill them if so. */
+void ACLKillPubsubClientsIfNeeded(user *new, user *original) {
+ listIter li, lpi;
+ listNode *ln, *lpn;
+ robj *o;
+ int kill = 0;
+
+ /* First optimization is we check if any selector has all channel
+ * permissions. */
+ listRewind(new->selectors,&li);
+ while((ln = listNext(&li))) {
+ aclSelector *s = (aclSelector *) listNodeValue(ln);
+ if (s->flags & SELECTOR_FLAG_ALLCHANNELS) return;
+ }
+
+ /* Second optimization is to check if the new list of channels
+ * is a strict superset of the original. This is done by
+ * created an "upcoming" list of all channels that are in
+ * the new user and checking each of the existing channels
+ * against it. */
+ list *upcoming = listCreate();
+ listRewind(new->selectors,&li);
+ while((ln = listNext(&li))) {
+ aclSelector *s = (aclSelector *) listNodeValue(ln);
+ listRewind(s->channels, &lpi);
+ while((lpn = listNext(&lpi))) {
+ listAddNodeTail(upcoming, listNodeValue(lpn));
+ }
+ }
+
+ int match = 1;
+ listRewind(original->selectors,&li);
+ while((ln = listNext(&li)) && match) {
+ aclSelector *s = (aclSelector *) listNodeValue(ln);
+ /* If any of the original selectors has the all-channels permission, but
+ * the new ones don't (this is checked earlier in this function), then the
+ * new list is not a strict superset of the original. */
+ if (s->flags & SELECTOR_FLAG_ALLCHANNELS) {
+ match = 0;
+ break;
+ }
+ listRewind(s->channels, &lpi);
+ while((lpn = listNext(&lpi)) && match) {
+ if (!listSearchKey(upcoming, listNodeValue(lpn))) {
+ match = 0;
+ break;
+ }
+ }
+ }
+
+ if (match) {
+ /* All channels were matched, no need to kill clients. */
+ listRelease(upcoming);
+ return;
+ }
+
+ /* Permissions have changed, so we need to iterate through all
+ * the clients and disconnect those that are no longer valid.
+ * Scan all connected clients to find the user's pub/subs. */
+ listRewind(server.clients,&li);
+ while ((ln = listNext(&li)) != NULL) {
+ client *c = listNodeValue(ln);
+ kill = 0;
+
+ if (c->user == original && getClientType(c) == CLIENT_TYPE_PUBSUB) {
+ /* Check for pattern violations. */
+ listRewind(c->pubsub_patterns,&lpi);
+ while (!kill && ((lpn = listNext(&lpi)) != NULL)) {
+
+ o = lpn->value;
+ int res = ACLCheckChannelAgainstList(upcoming, o->ptr, sdslen(o->ptr), 1);
+ kill = (res == ACL_DENIED_CHANNEL);
+ }
+ /* Check for channel violations. */
+ if (!kill) {
+ /* Check for global channels violation. */
+ dictIterator *di = dictGetIterator(c->pubsub_channels);
+
+ dictEntry *de;
+ while (!kill && ((de = dictNext(di)) != NULL)) {
+ o = dictGetKey(de);
+ int res = ACLCheckChannelAgainstList(upcoming, o->ptr, sdslen(o->ptr), 0);
+ kill = (res == ACL_DENIED_CHANNEL);
+ }
+ dictReleaseIterator(di);
+
+ /* Check for shard channels violation. */
+ di = dictGetIterator(c->pubsubshard_channels);
+ while (!kill && ((de = dictNext(di)) != NULL)) {
+ o = dictGetKey(de);
+ int res = ACLCheckChannelAgainstList(upcoming, o->ptr, sdslen(o->ptr), 0);
+ kill = (res == ACL_DENIED_CHANNEL);
+ }
+
+ dictReleaseIterator(di);
+ }
+
+ /* Kill it. */
+ if (kill) {
+ freeClient(c);
+ }
+ }
+ }
+ listRelease(upcoming);
+}
+
+/* =============================================================================
+ * ACL loading / saving functions
+ * ==========================================================================*/
+
+
+/* Selector definitions should be sent as a single argument, however
+ * we will be lenient and try to find selector definitions spread
+ * across multiple arguments since it makes for a simpler user experience
+ * for ACL SETUSER as well as when loading from conf files.
+ *
+ * This function takes in an array of ACL operators, excluding the username,
+ * and merges selector operations that are spread across multiple arguments. The return
+ * value is a new SDS array, with length set to the passed in merged_argc. Arguments
+ * that are untouched are still duplicated. If there is an unmatched parenthesis, NULL
+ * is returned and invalid_idx is set to the argument with the start of the opening
+ * parenthesis. */
+sds *ACLMergeSelectorArguments(sds *argv, int argc, int *merged_argc, int *invalid_idx) {
+ *merged_argc = 0;
+ int open_bracket_start = -1;
+
+ sds *acl_args = (sds *) zmalloc(sizeof(sds) * argc);
+
+ sds selector = NULL;
+ for (int j = 0; j < argc; j++) {
+ char *op = argv[j];
+
+ if (op[0] == '(' && op[sdslen(op) - 1] != ')') {
+ selector = sdsdup(argv[j]);
+ open_bracket_start = j;
+ continue;
+ }
+
+ if (open_bracket_start != -1) {
+ selector = sdscatfmt(selector, " %s", op);
+ if (op[sdslen(op) - 1] == ')') {
+ open_bracket_start = -1;
+ acl_args[*merged_argc] = selector;
+ (*merged_argc)++;
+ }
+ continue;
+ }
+
+ acl_args[*merged_argc] = sdsdup(argv[j]);
+ (*merged_argc)++;
+ }
+
+ if (open_bracket_start != -1) {
+ for (int i = 0; i < *merged_argc; i++) sdsfree(acl_args[i]);
+ zfree(acl_args);
+ sdsfree(selector);
+ if (invalid_idx) *invalid_idx = open_bracket_start;
+ return NULL;
+ }
+
+ return acl_args;
+}
+
+/* takes an acl string already split on spaces and adds it to the given user
+ * if the user object is NULL, will create a user with the given username
+ *
+ * Returns an error as an sds string if the ACL string is not parsable
+ */
+sds ACLStringSetUser(user *u, sds username, sds *argv, int argc) {
+ serverAssert(u != NULL || username != NULL);
+
+ sds error = NULL;
+
+ int merged_argc = 0, invalid_idx = 0;
+ sds *acl_args = ACLMergeSelectorArguments(argv, argc, &merged_argc, &invalid_idx);
+
+ if (!acl_args) {
+ return sdscatfmt(sdsempty(),
+ "Unmatched parenthesis in acl selector starting "
+ "at '%s'.", (char *) argv[invalid_idx]);
+ }
+
+ /* Create a temporary user to validate and stage all changes against
+ * before applying to an existing user or creating a new user. If all
+ * arguments are valid the user parameters will all be applied together.
+ * If there are any errors then none of the changes will be applied. */
+ user *tempu = ACLCreateUnlinkedUser();
+ if (u) {
+ ACLCopyUser(tempu, u);
+ }
+
+ for (int j = 0; j < merged_argc; j++) {
+ if (ACLSetUser(tempu,acl_args[j],(ssize_t) sdslen(acl_args[j])) != C_OK) {
+ const char *errmsg = ACLSetUserStringError();
+ error = sdscatfmt(sdsempty(),
+ "Error in ACL SETUSER modifier '%s': %s",
+ (char*)acl_args[j], errmsg);
+ goto cleanup;
+ }
+ }
+
+ /* Existing pub/sub clients authenticated with the user may need to be
+ * disconnected if (some of) their channel permissions were revoked. */
+ if (u) {
+ ACLKillPubsubClientsIfNeeded(tempu, u);
+ }
+
+ /* Overwrite the user with the temporary user we modified above. */
+ if (!u) {
+ u = ACLCreateUser(username,sdslen(username));
+ }
+ serverAssert(u != NULL);
+
+ ACLCopyUser(u, tempu);
+
+cleanup:
+ ACLFreeUser(tempu);
+ for (int i = 0; i < merged_argc; i++) {
+ sdsfree(acl_args[i]);
+ }
+ zfree(acl_args);
+
+ return error;
+}
+
+/* Given an argument vector describing a user in the form:
+ *
+ * user <username> ... ACL rules and flags ...
+ *
+ * this function validates, and if the syntax is valid, appends
+ * the user definition to a list for later loading.
+ *
+ * The rules are tested for validity and if there obvious syntax errors
+ * the function returns C_ERR and does nothing, otherwise C_OK is returned
+ * and the user is appended to the list.
+ *
+ * Note that this function cannot stop in case of commands that are not found
+ * and, in that case, the error will be emitted later, because certain
+ * commands may be defined later once modules are loaded.
+ *
+ * When an error is detected and C_ERR is returned, the function populates
+ * by reference (if not set to NULL) the argc_err argument with the index
+ * of the argv vector that caused the error. */
+int ACLAppendUserForLoading(sds *argv, int argc, int *argc_err) {
+ if (argc < 2 || strcasecmp(argv[0],"user")) {
+ if (argc_err) *argc_err = 0;
+ return C_ERR;
+ }
+
+ if (listSearchKey(UsersToLoad, argv[1])) {
+ if (argc_err) *argc_err = 1;
+ errno = EALREADY;
+ return C_ERR;
+ }
+
+ /* Try to apply the user rules in a fake user to see if they
+ * are actually valid. */
+ user *fakeuser = ACLCreateUnlinkedUser();
+
+ /* Merged selectors before trying to process */
+ int merged_argc;
+ sds *acl_args = ACLMergeSelectorArguments(argv + 2, argc - 2, &merged_argc, argc_err);
+
+ if (!acl_args) {
+ return C_ERR;
+ }
+
+ for (int j = 0; j < merged_argc; j++) {
+ if (ACLSetUser(fakeuser,acl_args[j],sdslen(acl_args[j])) == C_ERR) {
+ if (errno != ENOENT) {
+ ACLFreeUser(fakeuser);
+ if (argc_err) *argc_err = j;
+ for (int i = 0; i < merged_argc; i++) sdsfree(acl_args[i]);
+ zfree(acl_args);
+ return C_ERR;
+ }
+ }
+ }
+
+ /* Rules look valid, let's append the user to the list. */
+ sds *copy = zmalloc(sizeof(sds)*(merged_argc + 2));
+ copy[0] = sdsdup(argv[1]);
+ for (int j = 0; j < merged_argc; j++) copy[j+1] = sdsdup(acl_args[j]);
+ copy[merged_argc + 1] = NULL;
+ listAddNodeTail(UsersToLoad,copy);
+ ACLFreeUser(fakeuser);
+ for (int i = 0; i < merged_argc; i++) sdsfree(acl_args[i]);
+ zfree(acl_args);
+ return C_OK;
+}
+
+/* This function will load the configured users appended to the server
+ * configuration via ACLAppendUserForLoading(). On loading errors it will
+ * log an error and return C_ERR, otherwise C_OK will be returned. */
+int ACLLoadConfiguredUsers(void) {
+ listIter li;
+ listNode *ln;
+ listRewind(UsersToLoad,&li);
+ while ((ln = listNext(&li)) != NULL) {
+ sds *aclrules = listNodeValue(ln);
+ sds username = aclrules[0];
+
+ if (ACLStringHasSpaces(aclrules[0],sdslen(aclrules[0]))) {
+ serverLog(LL_WARNING,"Spaces not allowed in ACL usernames");
+ return C_ERR;
+ }
+
+ user *u = ACLCreateUser(username,sdslen(username));
+ if (!u) {
+ /* Only valid duplicate user is the default one. */
+ serverAssert(!strcmp(username, "default"));
+ u = ACLGetUserByName("default",7);
+ ACLSetUser(u,"reset",-1);
+ }
+
+ /* Load every rule defined for this user. */
+ for (int j = 1; aclrules[j]; j++) {
+ if (ACLSetUser(u,aclrules[j],sdslen(aclrules[j])) != C_OK) {
+ const char *errmsg = ACLSetUserStringError();
+ serverLog(LL_WARNING,"Error loading ACL rule '%s' for "
+ "the user named '%s': %s",
+ aclrules[j],aclrules[0],errmsg);
+ return C_ERR;
+ }
+ }
+
+ /* Having a disabled user in the configuration may be an error,
+ * warn about it without returning any error to the caller. */
+ if (u->flags & USER_FLAG_DISABLED) {
+ serverLog(LL_NOTICE, "The user '%s' is disabled (there is no "
+ "'on' modifier in the user description). Make "
+ "sure this is not a configuration error.",
+ aclrules[0]);
+ }
+ }
+ return C_OK;
+}
+
+/* This function loads the ACL from the specified filename: every line
+ * is validated and should be either empty or in the format used to specify
+ * users in the redis.conf configuration or in the ACL file, that is:
+ *
+ * user <username> ... rules ...
+ *
+ * Note that this function considers comments starting with '#' as errors
+ * because the ACL file is meant to be rewritten, and comments would be
+ * lost after the rewrite. Yet empty lines are allowed to avoid being too
+ * strict.
+ *
+ * One important part of implementing ACL LOAD, that uses this function, is
+ * to avoid ending with broken rules if the ACL file is invalid for some
+ * reason, so the function will attempt to validate the rules before loading
+ * each user. For every line that will be found broken the function will
+ * collect an error message.
+ *
+ * IMPORTANT: If there is at least a single error, nothing will be loaded
+ * and the rules will remain exactly as they were.
+ *
+ * At the end of the process, if no errors were found in the whole file then
+ * NULL is returned. Otherwise an SDS string describing in a single line
+ * a description of all the issues found is returned. */
+sds ACLLoadFromFile(const char *filename) {
+ FILE *fp;
+ char buf[1024];
+
+ /* Open the ACL file. */
+ if ((fp = fopen(filename,"r")) == NULL) {
+ sds errors = sdscatprintf(sdsempty(),
+ "Error loading ACLs, opening file '%s': %s",
+ filename, strerror(errno));
+ return errors;
+ }
+
+ /* Load the whole file as a single string in memory. */
+ sds acls = sdsempty();
+ while(fgets(buf,sizeof(buf),fp) != NULL)
+ acls = sdscat(acls,buf);
+ fclose(fp);
+
+ /* Split the file into lines and attempt to load each line. */
+ int totlines;
+ sds *lines, errors = sdsempty();
+ lines = sdssplitlen(acls,strlen(acls),"\n",1,&totlines);
+ sdsfree(acls);
+
+ /* We do all the loading in a fresh instance of the Users radix tree,
+ * so if there are errors loading the ACL file we can rollback to the
+ * old version. */
+ rax *old_users = Users;
+ Users = raxNew();
+
+ /* Load each line of the file. */
+ for (int i = 0; i < totlines; i++) {
+ sds *argv;
+ int argc;
+ int linenum = i+1;
+
+ lines[i] = sdstrim(lines[i]," \t\r\n");
+
+ /* Skip blank lines */
+ if (lines[i][0] == '\0') continue;
+
+ /* Split into arguments */
+ argv = sdssplitlen(lines[i],sdslen(lines[i])," ",1,&argc);
+ if (argv == NULL) {
+ errors = sdscatprintf(errors,
+ "%s:%d: unbalanced quotes in acl line. ",
+ server.acl_filename, linenum);
+ continue;
+ }
+
+ /* Skip this line if the resulting command vector is empty. */
+ if (argc == 0) {
+ sdsfreesplitres(argv,argc);
+ continue;
+ }
+
+ /* The line should start with the "user" keyword. */
+ if (strcmp(argv[0],"user") || argc < 2) {
+ errors = sdscatprintf(errors,
+ "%s:%d should start with user keyword followed "
+ "by the username. ", server.acl_filename,
+ linenum);
+ sdsfreesplitres(argv,argc);
+ continue;
+ }
+
+ /* Spaces are not allowed in usernames. */
+ if (ACLStringHasSpaces(argv[1],sdslen(argv[1]))) {
+ errors = sdscatprintf(errors,
+ "'%s:%d: username '%s' contains invalid characters. ",
+ server.acl_filename, linenum, argv[1]);
+ sdsfreesplitres(argv,argc);
+ continue;
+ }
+
+ user *u = ACLCreateUser(argv[1],sdslen(argv[1]));
+
+ /* If the user already exists we assume it's an error and abort. */
+ if (!u) {
+ errors = sdscatprintf(errors,"WARNING: Duplicate user '%s' found on line %d. ", argv[1], linenum);
+ sdsfreesplitres(argv,argc);
+ continue;
+ }
+
+ /* Finally process the options and validate they can
+ * be cleanly applied to the user. If any option fails
+ * to apply, the other values won't be applied since
+ * all the pending changes will get dropped. */
+ int merged_argc;
+ sds *acl_args = ACLMergeSelectorArguments(argv + 2, argc - 2, &merged_argc, NULL);
+ if (!acl_args) {
+ errors = sdscatprintf(errors,
+ "%s:%d: Unmatched parenthesis in selector definition.",
+ server.acl_filename, linenum);
+ }
+
+ int syntax_error = 0;
+ for (int j = 0; j < merged_argc; j++) {
+ acl_args[j] = sdstrim(acl_args[j],"\t\r\n");
+ if (ACLSetUser(u,acl_args[j],sdslen(acl_args[j])) != C_OK) {
+ const char *errmsg = ACLSetUserStringError();
+ if (errno == ENOENT) {
+ /* For missing commands, we print out more information since
+ * it shouldn't contain any sensitive information. */
+ errors = sdscatprintf(errors,
+ "%s:%d: Error in applying operation '%s': %s. ",
+ server.acl_filename, linenum, acl_args[j], errmsg);
+ } else if (syntax_error == 0) {
+ /* For all other errors, only print out the first error encountered
+ * since it might affect future operations. */
+ errors = sdscatprintf(errors,
+ "%s:%d: %s. ",
+ server.acl_filename, linenum, errmsg);
+ syntax_error = 1;
+ }
+ }
+ }
+
+ for (int i = 0; i < merged_argc; i++) sdsfree(acl_args[i]);
+ zfree(acl_args);
+
+ /* Apply the rule to the new users set only if so far there
+ * are no errors, otherwise it's useless since we are going
+ * to discard the new users set anyway. */
+ if (sdslen(errors) != 0) {
+ sdsfreesplitres(argv,argc);
+ continue;
+ }
+
+ sdsfreesplitres(argv,argc);
+ }
+
+ sdsfreesplitres(lines,totlines);
+
+ /* Check if we found errors and react accordingly. */
+ if (sdslen(errors) == 0) {
+ /* The default user pointer is referenced in different places: instead
+ * of replacing such occurrences it is much simpler to copy the new
+ * default user configuration in the old one. */
+ user *new_default = ACLGetUserByName("default",7);
+ if (!new_default) {
+ new_default = ACLCreateDefaultUser();
+ }
+
+ ACLCopyUser(DefaultUser,new_default);
+ ACLFreeUser(new_default);
+ raxInsert(Users,(unsigned char*)"default",7,DefaultUser,NULL);
+ raxRemove(old_users,(unsigned char*)"default",7,NULL);
+ ACLFreeUsersSet(old_users);
+ sdsfree(errors);
+ return NULL;
+ } else {
+ ACLFreeUsersSet(Users);
+ Users = old_users;
+ errors = sdscat(errors,"WARNING: ACL errors detected, no change to the previously active ACL rules was performed");
+ return errors;
+ }
+}
+
+/* Generate a copy of the ACLs currently in memory in the specified filename.
+ * Returns C_OK on success or C_ERR if there was an error during the I/O.
+ * When C_ERR is returned a log is produced with hints about the issue. */
+int ACLSaveToFile(const char *filename) {
+ sds acl = sdsempty();
+ int fd = -1;
+ sds tmpfilename = NULL;
+ int retval = C_ERR;
+
+ /* Let's generate an SDS string containing the new version of the
+ * ACL file. */
+ raxIterator ri;
+ raxStart(&ri,Users);
+ raxSeek(&ri,"^",NULL,0);
+ while(raxNext(&ri)) {
+ user *u = ri.data;
+ /* Return information in the configuration file format. */
+ sds user = sdsnew("user ");
+ user = sdscatsds(user,u->name);
+ user = sdscatlen(user," ",1);
+ robj *descr = ACLDescribeUser(u);
+ user = sdscatsds(user,descr->ptr);
+ decrRefCount(descr);
+ acl = sdscatsds(acl,user);
+ acl = sdscatlen(acl,"\n",1);
+ sdsfree(user);
+ }
+ raxStop(&ri);
+
+ /* Create a temp file with the new content. */
+ tmpfilename = sdsnew(filename);
+ tmpfilename = sdscatfmt(tmpfilename,".tmp-%i-%I",
+ (int)getpid(),(int)mstime());
+ if ((fd = open(tmpfilename,O_WRONLY|O_CREAT,0644)) == -1) {
+ serverLog(LL_WARNING,"Opening temp ACL file for ACL SAVE: %s",
+ strerror(errno));
+ goto cleanup;
+ }
+
+ /* Write it. */
+ if (write(fd,acl,sdslen(acl)) != (ssize_t)sdslen(acl)) {
+ serverLog(LL_WARNING,"Writing ACL file for ACL SAVE: %s",
+ strerror(errno));
+ goto cleanup;
+ }
+ close(fd); fd = -1;
+
+ /* Let's replace the new file with the old one. */
+ if (rename(tmpfilename,filename) == -1) {
+ serverLog(LL_WARNING,"Renaming ACL file for ACL SAVE: %s",
+ strerror(errno));
+ goto cleanup;
+ }
+ sdsfree(tmpfilename); tmpfilename = NULL;
+ retval = C_OK; /* If we reached this point, everything is fine. */
+
+cleanup:
+ if (fd != -1) close(fd);
+ if (tmpfilename) unlink(tmpfilename);
+ sdsfree(tmpfilename);
+ sdsfree(acl);
+ return retval;
+}
+
+/* This function is called once the server is already running, modules are
+ * loaded, and we are ready to start, in order to load the ACLs either from
+ * the pending list of users defined in redis.conf, or from the ACL file.
+ * The function will just exit with an error if the user is trying to mix
+ * both the loading methods. */
+void ACLLoadUsersAtStartup(void) {
+ if (server.acl_filename[0] != '\0' && listLength(UsersToLoad) != 0) {
+ serverLog(LL_WARNING,
+ "Configuring Redis with users defined in redis.conf and at "
+ "the same setting an ACL file path is invalid. This setup "
+ "is very likely to lead to configuration errors and security "
+ "holes, please define either an ACL file or declare users "
+ "directly in your redis.conf, but not both.");
+ exit(1);
+ }
+
+ if (ACLLoadConfiguredUsers() == C_ERR) {
+ serverLog(LL_WARNING,
+ "Critical error while loading ACLs. Exiting.");
+ exit(1);
+ }
+
+ if (server.acl_filename[0] != '\0') {
+ sds errors = ACLLoadFromFile(server.acl_filename);
+ if (errors) {
+ serverLog(LL_WARNING,
+ "Aborting Redis startup because of ACL errors: %s", errors);
+ sdsfree(errors);
+ exit(1);
+ }
+ }
+}
+
+/* =============================================================================
+ * ACL log
+ * ==========================================================================*/
+
+#define ACL_LOG_GROUPING_MAX_TIME_DELTA 60000
+
+/* This structure defines an entry inside the ACL log. */
+typedef struct ACLLogEntry {
+ uint64_t count; /* Number of times this happened recently. */
+ int reason; /* Reason for denying the command. ACL_DENIED_*. */
+ int context; /* Toplevel, Lua or MULTI/EXEC? ACL_LOG_CTX_*. */
+ sds object; /* The key name or command name. */
+ sds username; /* User the client is authenticated with. */
+ mstime_t ctime; /* Milliseconds time of last update to this entry. */
+ sds cinfo; /* Client info (last client if updated). */
+} ACLLogEntry;
+
+/* This function will check if ACL entries 'a' and 'b' are similar enough
+ * that we should actually update the existing entry in our ACL log instead
+ * of creating a new one. */
+int ACLLogMatchEntry(ACLLogEntry *a, ACLLogEntry *b) {
+ if (a->reason != b->reason) return 0;
+ if (a->context != b->context) return 0;
+ mstime_t delta = a->ctime - b->ctime;
+ if (delta < 0) delta = -delta;
+ if (delta > ACL_LOG_GROUPING_MAX_TIME_DELTA) return 0;
+ if (sdscmp(a->object,b->object) != 0) return 0;
+ if (sdscmp(a->username,b->username) != 0) return 0;
+ return 1;
+}
+
+/* Release an ACL log entry. */
+void ACLFreeLogEntry(void *leptr) {
+ ACLLogEntry *le = leptr;
+ sdsfree(le->object);
+ sdsfree(le->username);
+ sdsfree(le->cinfo);
+ zfree(le);
+}
+
+/* Adds a new entry in the ACL log, making sure to delete the old entry
+ * if we reach the maximum length allowed for the log. This function attempts
+ * to find similar entries in the current log in order to bump the counter of
+ * the log entry instead of creating many entries for very similar ACL
+ * rules issues.
+ *
+ * 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.
+ *
+ * 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).
+ *
+ * If `object` is not NULL, this functions takes over it.
+ */
+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(username ? username : c->user->name);
+ le->ctime = mstime();
+
+ if (object) {
+ le->object = object;
+ } else {
+ switch(reason) {
+ case ACL_DENIED_CMD: le->object = sdsdup(c->cmd->fullname); 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_SCRIPT) realclient = server.script_caller;
+
+ le->cinfo = catClientInfoString(sdsempty(),realclient);
+ 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. */
+ long toscan = 10; /* Do a limited work trying to find duplicated. */
+ listIter li;
+ listNode *ln;
+ listRewind(ACLLog,&li);
+ ACLLogEntry *match = NULL;
+ while (toscan-- && (ln = listNext(&li)) != NULL) {
+ ACLLogEntry *current = listNodeValue(ln);
+ if (ACLLogMatchEntry(current,le)) {
+ match = current;
+ listDelNode(ACLLog,ln);
+ listAddNodeHead(ACLLog,current);
+ break;
+ }
+ }
+
+ /* If there is a match update the entry, otherwise add it as a
+ * new one. */
+ if (match) {
+ /* We update a few fields of the existing entry and bump the
+ * counter of events for this entry. */
+ sdsfree(match->cinfo);
+ match->cinfo = le->cinfo;
+ match->ctime = le->ctime;
+ match->count++;
+
+ /* Release the old entry. */
+ le->cinfo = NULL;
+ ACLFreeLogEntry(le);
+ } else {
+ /* Add it to our list of entries. We'll have to trim the list
+ * to its maximum size. */
+ listAddNodeHead(ACLLog, le);
+ while(listLength(ACLLog) > server.acllog_max_len) {
+ listNode *ln = listLast(ACLLog);
+ ACLLogEntry *le = listNodeValue(ln);
+ ACLFreeLogEntry(le);
+ listDelNode(ACLLog,ln);
+ }
+ }
+}
+
+const char* getAclErrorMessage(int acl_res) {
+ /* Notice that a variant of this code also exists on aclCommand so
+ * it also need to be updated on changed. */
+ switch (acl_res) {
+ case ACL_DENIED_CMD:
+ return "can't run this command or subcommand";
+ case ACL_DENIED_KEY:
+ return "can't access at least one of the keys mentioned in the command arguments";
+ case ACL_DENIED_CHANNEL:
+ return "can't publish to the channel mentioned in the command";
+ default:
+ return "lacking the permissions for the command";
+ }
+ serverPanic("Reached deadcode on getAclErrorMessage");
+}
+
+/* =============================================================================
+ * ACL related commands
+ * ==========================================================================*/
+
+/* ACL CAT category */
+void aclCatWithFlags(client *c, dict *commands, uint64_t cflag, int *arraylen) {
+ dictEntry *de;
+ dictIterator *di = dictGetIterator(commands);
+
+ while ((de = dictNext(di)) != NULL) {
+ struct redisCommand *cmd = dictGetVal(de);
+ if (cmd->flags & CMD_MODULE) continue;
+ if (cmd->acl_categories & cflag) {
+ addReplyBulkCBuffer(c, cmd->fullname, sdslen(cmd->fullname));
+ (*arraylen)++;
+ }
+
+ if (cmd->subcommands_dict) {
+ aclCatWithFlags(c, cmd->subcommands_dict, cflag, arraylen);
+ }
+ }
+ dictReleaseIterator(di);
+}
+
+/* Add the formatted response from a single selector to the ACL GETUSER
+ * response. This function returns the number of fields added.
+ *
+ * Setting verbose to 1 means that the full qualifier for key and channel
+ * permissions are shown.
+ */
+int aclAddReplySelectorDescription(client *c, aclSelector *s) {
+ listIter li;
+ listNode *ln;
+
+ /* Commands */
+ addReplyBulkCString(c,"commands");
+ sds cmddescr = ACLDescribeSelectorCommandRules(s);
+ addReplyBulkSds(c,cmddescr);
+
+ /* Key patterns */
+ addReplyBulkCString(c,"keys");
+ if (s->flags & SELECTOR_FLAG_ALLKEYS) {
+ addReplyBulkCBuffer(c,"~*",2);
+ } else {
+ sds dsl = sdsempty();
+ listRewind(s->patterns,&li);
+ while((ln = listNext(&li))) {
+ keyPattern *thispat = (keyPattern *) listNodeValue(ln);
+ if (ln != listFirst(s->patterns)) dsl = sdscat(dsl, " ");
+ dsl = sdsCatPatternString(dsl, thispat);
+ }
+ addReplyBulkSds(c, dsl);
+ }
+
+ /* Pub/sub patterns */
+ addReplyBulkCString(c,"channels");
+ if (s->flags & SELECTOR_FLAG_ALLCHANNELS) {
+ addReplyBulkCBuffer(c,"&*",2);
+ } else {
+ sds dsl = sdsempty();
+ listRewind(s->channels,&li);
+ while((ln = listNext(&li))) {
+ sds thispat = listNodeValue(ln);
+ if (ln != listFirst(s->channels)) dsl = sdscat(dsl, " ");
+ dsl = sdscatfmt(dsl, "&%S", thispat);
+ }
+ addReplyBulkSds(c, dsl);
+ }
+ return 3;
+}
+
+/* ACL -- show and modify the configuration of ACL users.
+ * ACL HELP
+ * ACL LOAD
+ * ACL SAVE
+ * ACL LIST
+ * ACL USERS
+ * ACL CAT [<category>]
+ * ACL SETUSER <username> ... acl rules ...
+ * ACL DELUSER <username> [...]
+ * ACL GETUSER <username>
+ * ACL GENPASS [<bits>]
+ * ACL WHOAMI
+ * ACL LOG [<count> | RESET]
+ */
+void aclCommand(client *c) {
+ char *sub = c->argv[1]->ptr;
+ if (!strcasecmp(sub,"setuser") && c->argc >= 3) {
+ /* Initially redact all of the arguments to not leak any information
+ * about the user. */
+ for (int j = 2; j < c->argc; j++) {
+ redactClientCommandArgument(c, j);
+ }
+
+ sds username = c->argv[2]->ptr;
+ /* Check username validity. */
+ if (ACLStringHasSpaces(username,sdslen(username))) {
+ addReplyErrorFormat(c,
+ "Usernames can't contain spaces or null characters");
+ return;
+ }
+
+ user *u = ACLGetUserByName(username,sdslen(username));
+
+ sds *temp_argv = zmalloc(c->argc * sizeof(sds));
+ for (int i = 3; i < c->argc; i++) temp_argv[i-3] = c->argv[i]->ptr;
+
+ sds error = ACLStringSetUser(u, username, temp_argv, c->argc - 3);
+ zfree(temp_argv);
+ if (error == NULL) {
+ addReply(c,shared.ok);
+ } else {
+ addReplyErrorSdsSafe(c, error);
+ }
+ return;
+ } else if (!strcasecmp(sub,"deluser") && c->argc >= 3) {
+ int deleted = 0;
+ for (int j = 2; j < c->argc; j++) {
+ sds username = c->argv[j]->ptr;
+ if (!strcmp(username,"default")) {
+ addReplyError(c,"The 'default' user cannot be removed");
+ return;
+ }
+ }
+
+ for (int j = 2; j < c->argc; j++) {
+ sds username = c->argv[j]->ptr;
+ user *u;
+ if (raxRemove(Users,(unsigned char*)username,
+ sdslen(username),
+ (void**)&u))
+ {
+ ACLFreeUserAndKillClients(u);
+ deleted++;
+ }
+ }
+ addReplyLongLong(c,deleted);
+ } else if (!strcasecmp(sub,"getuser") && c->argc == 3) {
+ user *u = ACLGetUserByName(c->argv[2]->ptr,sdslen(c->argv[2]->ptr));
+ if (u == NULL) {
+ addReplyNull(c);
+ return;
+ }
+
+ void *ufields = addReplyDeferredLen(c);
+ int fields = 3;
+
+ /* Flags */
+ addReplyBulkCString(c,"flags");
+ void *deflen = addReplyDeferredLen(c);
+ int numflags = 0;
+ for (int j = 0; ACLUserFlags[j].flag; j++) {
+ if (u->flags & ACLUserFlags[j].flag) {
+ addReplyBulkCString(c,ACLUserFlags[j].name);
+ numflags++;
+ }
+ }
+ setDeferredSetLen(c,deflen,numflags);
+
+ /* Passwords */
+ addReplyBulkCString(c,"passwords");
+ addReplyArrayLen(c,listLength(u->passwords));
+ listIter li;
+ listNode *ln;
+ listRewind(u->passwords,&li);
+ while((ln = listNext(&li))) {
+ sds thispass = listNodeValue(ln);
+ addReplyBulkCBuffer(c,thispass,sdslen(thispass));
+ }
+ /* Include the root selector at the top level for backwards compatibility */
+ fields += aclAddReplySelectorDescription(c, ACLUserGetRootSelector(u));
+
+ /* Describe all of the selectors on this user, including duplicating the root selector */
+ addReplyBulkCString(c,"selectors");
+ addReplyArrayLen(c, listLength(u->selectors) - 1);
+ listRewind(u->selectors,&li);
+ serverAssert(listNext(&li));
+ while((ln = listNext(&li))) {
+ void *slen = addReplyDeferredLen(c);
+ int sfields = aclAddReplySelectorDescription(c, (aclSelector *)listNodeValue(ln));
+ setDeferredMapLen(c, slen, sfields);
+ }
+ setDeferredMapLen(c, ufields, fields);
+ } else if ((!strcasecmp(sub,"list") || !strcasecmp(sub,"users")) &&
+ c->argc == 2)
+ {
+ int justnames = !strcasecmp(sub,"users");
+ addReplyArrayLen(c,raxSize(Users));
+ raxIterator ri;
+ raxStart(&ri,Users);
+ raxSeek(&ri,"^",NULL,0);
+ while(raxNext(&ri)) {
+ user *u = ri.data;
+ if (justnames) {
+ addReplyBulkCBuffer(c,u->name,sdslen(u->name));
+ } else {
+ /* Return information in the configuration file format. */
+ sds config = sdsnew("user ");
+ config = sdscatsds(config,u->name);
+ config = sdscatlen(config," ",1);
+ robj *descr = ACLDescribeUser(u);
+ config = sdscatsds(config,descr->ptr);
+ decrRefCount(descr);
+ addReplyBulkSds(c,config);
+ }
+ }
+ raxStop(&ri);
+ } else if (!strcasecmp(sub,"whoami") && c->argc == 2) {
+ if (c->user != NULL) {
+ addReplyBulkCBuffer(c,c->user->name,sdslen(c->user->name));
+ } else {
+ addReplyNull(c);
+ }
+ } else if (server.acl_filename[0] == '\0' &&
+ (!strcasecmp(sub,"load") || !strcasecmp(sub,"save")))
+ {
+ addReplyError(c,"This Redis instance is not configured to use an ACL file. You may want to specify users via the ACL SETUSER command and then issue a CONFIG REWRITE (assuming you have a Redis configuration file set) in order to store users in the Redis configuration.");
+ return;
+ } else if (!strcasecmp(sub,"load") && c->argc == 2) {
+ sds errors = ACLLoadFromFile(server.acl_filename);
+ if (errors == NULL) {
+ addReply(c,shared.ok);
+ } else {
+ addReplyError(c,errors);
+ sdsfree(errors);
+ }
+ } else if (!strcasecmp(sub,"save") && c->argc == 2) {
+ if (ACLSaveToFile(server.acl_filename) == C_OK) {
+ addReply(c,shared.ok);
+ } else {
+ addReplyError(c,"There was an error trying to save the ACLs. "
+ "Please check the server logs for more "
+ "information");
+ }
+ } else if (!strcasecmp(sub,"cat") && c->argc == 2) {
+ void *dl = addReplyDeferredLen(c);
+ int j;
+ for (j = 0; ACLCommandCategories[j].flag != 0; j++)
+ addReplyBulkCString(c,ACLCommandCategories[j].name);
+ setDeferredArrayLen(c,dl,j);
+ } else if (!strcasecmp(sub,"cat") && c->argc == 3) {
+ uint64_t cflag = ACLGetCommandCategoryFlagByName(c->argv[2]->ptr);
+ if (cflag == 0) {
+ addReplyErrorFormat(c, "Unknown category '%.128s'", (char*)c->argv[2]->ptr);
+ return;
+ }
+ int arraylen = 0;
+ void *dl = addReplyDeferredLen(c);
+ aclCatWithFlags(c, server.orig_commands, cflag, &arraylen);
+ setDeferredArrayLen(c,dl,arraylen);
+ } else if (!strcasecmp(sub,"genpass") && (c->argc == 2 || c->argc == 3)) {
+ #define GENPASS_MAX_BITS 4096
+ char pass[GENPASS_MAX_BITS/8*2]; /* Hex representation. */
+ long bits = 256; /* By default generate 256 bits passwords. */
+
+ if (c->argc == 3 && getLongFromObjectOrReply(c,c->argv[2],&bits,NULL)
+ != C_OK) return;
+
+ if (bits <= 0 || bits > GENPASS_MAX_BITS) {
+ addReplyErrorFormat(c,
+ "ACL GENPASS argument must be the number of "
+ "bits for the output password, a positive number "
+ "up to %d",GENPASS_MAX_BITS);
+ return;
+ }
+
+ long chars = (bits+3)/4; /* Round to number of characters to emit. */
+ getRandomHexChars(pass,chars);
+ addReplyBulkCBuffer(c,pass,chars);
+ } else if (!strcasecmp(sub,"log") && (c->argc == 2 || c->argc ==3)) {
+ long count = 10; /* Number of entries to emit by default. */
+
+ /* Parse the only argument that LOG may have: it could be either
+ * the number of entries the user wants to display, or alternatively
+ * the "RESET" command in order to flush the old entries. */
+ if (c->argc == 3) {
+ if (!strcasecmp(c->argv[2]->ptr,"reset")) {
+ listSetFreeMethod(ACLLog,ACLFreeLogEntry);
+ listEmpty(ACLLog);
+ listSetFreeMethod(ACLLog,NULL);
+ addReply(c,shared.ok);
+ return;
+ } else if (getLongFromObjectOrReply(c,c->argv[2],&count,NULL)
+ != C_OK)
+ {
+ return;
+ }
+ if (count < 0) count = 0;
+ }
+
+ /* Fix the count according to the number of entries we got. */
+ if ((size_t)count > listLength(ACLLog))
+ count = listLength(ACLLog);
+
+ addReplyArrayLen(c,count);
+ listIter li;
+ listNode *ln;
+ listRewind(ACLLog,&li);
+ mstime_t now = mstime();
+ while (count-- && (ln = listNext(&li)) != NULL) {
+ ACLLogEntry *le = listNodeValue(ln);
+ addReplyMapLen(c,7);
+ addReplyBulkCString(c,"count");
+ addReplyLongLong(c,le->count);
+
+ addReplyBulkCString(c,"reason");
+ char *reasonstr;
+ switch(le->reason) {
+ case ACL_DENIED_CMD: reasonstr="command"; break;
+ case ACL_DENIED_KEY: reasonstr="key"; break;
+ case ACL_DENIED_CHANNEL: reasonstr="channel"; break;
+ case ACL_DENIED_AUTH: reasonstr="auth"; break;
+ default: reasonstr="unknown";
+ }
+ addReplyBulkCString(c,reasonstr);
+
+ addReplyBulkCString(c,"context");
+ char *ctxstr;
+ switch(le->context) {
+ 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);
+
+ addReplyBulkCString(c,"object");
+ addReplyBulkCBuffer(c,le->object,sdslen(le->object));
+ addReplyBulkCString(c,"username");
+ addReplyBulkCBuffer(c,le->username,sdslen(le->username));
+ addReplyBulkCString(c,"age-seconds");
+ double age = (double)(now - le->ctime)/1000;
+ addReplyDouble(c,age);
+ addReplyBulkCString(c,"client-info");
+ addReplyBulkCBuffer(c,le->cinfo,sdslen(le->cinfo));
+ }
+ } else if (!strcasecmp(sub,"dryrun") && c->argc >= 4) {
+ struct redisCommand *cmd;
+ user *u = ACLGetUserByName(c->argv[2]->ptr,sdslen(c->argv[2]->ptr));
+ if (u == NULL) {
+ addReplyErrorFormat(c, "User '%s' not found", (char *)c->argv[2]->ptr);
+ return;
+ }
+
+ if ((cmd = lookupCommand(c->argv + 3, c->argc - 3)) == NULL) {
+ addReplyErrorFormat(c, "Command '%s' not found", (char *)c->argv[3]->ptr);
+ return;
+ }
+
+ if ((cmd->arity > 0 && cmd->arity != c->argc-3) ||
+ (c->argc-3 < -cmd->arity))
+ {
+ addReplyErrorFormat(c,"wrong number of arguments for '%s' command", cmd->fullname);
+ return;
+ }
+
+ int idx;
+ int result = ACLCheckAllUserCommandPerm(u, cmd, c->argv + 3, c->argc - 3, &idx);
+ /* Notice that a variant of this code also exists on getAclErrorMessage so
+ * it also need to be updated on changed. */
+ if (result != ACL_OK) {
+ sds err = sdsempty();
+ if (result == ACL_DENIED_CMD) {
+ err = sdscatfmt(err, "This user has no permissions to run "
+ "the '%s' command", cmd->fullname);
+ } else if (result == ACL_DENIED_KEY) {
+ err = sdscatfmt(err, "This user has no permissions to access "
+ "the '%s' key", c->argv[idx + 3]->ptr);
+ } else if (result == ACL_DENIED_CHANNEL) {
+ err = sdscatfmt(err, "This user has no permissions to access "
+ "the '%s' channel", c->argv[idx + 3]->ptr);
+ } else {
+ serverPanic("Invalid permission result");
+ }
+ addReplyBulkSds(c, err);
+ return;
+ }
+
+ addReply(c,shared.ok);
+ } else if (c->argc == 2 && !strcasecmp(sub,"help")) {
+ const char *help[] = {
+"CAT [<category>]",
+" List all commands that belong to <category>, or all command categories",
+" when no category is specified.",
+"DELUSER <username> [<username> ...]",
+" Delete a list of users.",
+"DRYRUN <username> <command> [<arg> ...]",
+" Returns whether the user can execute the given command without executing the command.",
+"GETUSER <username>",
+" Get the user's details.",
+"GENPASS [<bits>]",
+" Generate a secure 256-bit user password. The optional `bits` argument can",
+" be used to specify a different size.",
+"LIST",
+" Show users details in config file format.",
+"LOAD",
+" Reload users from the ACL file.",
+"LOG [<count> | RESET]",
+" Show the ACL log entries.",
+"SAVE",
+" Save the current config to the ACL file.",
+"SETUSER <username> <attribute> [<attribute> ...]",
+" Create or modify a user with the specified attributes.",
+"USERS",
+" List all the registered usernames.",
+"WHOAMI",
+" Return the current connection username.",
+NULL
+ };
+ addReplyHelp(c,help);
+ } else {
+ addReplySubcommandSyntaxError(c);
+ }
+}
+
+void addReplyCommandCategories(client *c, struct redisCommand *cmd) {
+ int flagcount = 0;
+ void *flaglen = addReplyDeferredLen(c);
+ for (int j = 0; ACLCommandCategories[j].flag != 0; j++) {
+ if (cmd->acl_categories & ACLCommandCategories[j].flag) {
+ addReplyStatusFormat(c, "@%s", ACLCommandCategories[j].name);
+ flagcount++;
+ }
+ }
+ setDeferredSetLen(c, flaglen, flagcount);
+}
+
+/* AUTH <password>
+ * AUTH <username> <password> (Redis >= 6.0 form)
+ *
+ * When the user is omitted it means that we are trying to authenticate
+ * against the default user. */
+void authCommand(client *c) {
+ /* Only two or three argument forms are allowed. */
+ if (c->argc > 3) {
+ addReplyErrorObject(c,shared.syntaxerr);
+ return;
+ }
+ /* Always redact the second argument */
+ redactClientCommandArgument(c, 1);
+
+ /* Handle the two different forms here. The form with two arguments
+ * will just use "default" as username. */
+ robj *username, *password;
+ if (c->argc == 2) {
+ /* Mimic the old behavior of giving an error for the two argument
+ * form if no password is configured. */
+ if (DefaultUser->flags & USER_FLAG_NOPASS) {
+ addReplyError(c,"AUTH <password> called without any password "
+ "configured for the default user. Are you sure "
+ "your configuration is correct?");
+ return;
+ }
+
+ username = shared.default_username;
+ password = c->argv[1];
+ } else {
+ username = c->argv[1];
+ password = c->argv[2];
+ redactClientCommandArgument(c, 2);
+ }
+
+ if (ACLAuthenticateUser(c,username,password) == C_OK) {
+ addReply(c,shared.ok);
+ } else {
+ addReplyError(c,"-WRONGPASS invalid username-password pair or user is disabled.");
+ }
+}
+
+/* Set the password for the "default" ACL user. This implements supports for
+ * requirepass config, so passing in NULL will set the user to be nopass. */
+void ACLUpdateDefaultUserPassword(sds password) {
+ ACLSetUser(DefaultUser,"resetpass",-1);
+ if (password) {
+ sds aclop = sdscatlen(sdsnew(">"), password, sdslen(password));
+ ACLSetUser(DefaultUser,aclop,sdslen(aclop));
+ sdsfree(aclop);
+ } else {
+ ACLSetUser(DefaultUser,"nopass",-1);
+ }
+}