diff options
Diffstat (limited to 'examples/redis-unstable/src/acl.c')
| -rw-r--r-- | examples/redis-unstable/src/acl.c | 3313 |
1 files changed, 0 insertions, 3313 deletions
diff --git a/examples/redis-unstable/src/acl.c b/examples/redis-unstable/src/acl.c deleted file mode 100644 index 37d504d..0000000 --- a/examples/redis-unstable/src/acl.c +++ /dev/null @@ -1,3313 +0,0 @@ -/* - * Copyright (c) 2018-Present, Redis Ltd. - * All rights reserved. - * - * Copyright (c) 2024-present, Valkey contributors. - * All rights reserved. - * - * Licensed under your choice of (a) the Redis Source Available License 2.0 - * (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the - * GNU Affero General Public License v3 (AGPLv3). - */ - -#include "server.h" -#include "cluster.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 .*/ - -long long ACLLogEntryCount = 0; /* Number of ACL log entries created */ - -static rax *commandId = NULL; /* Command name to id mapping */ - -static unsigned long nextid = 0; /* Next command id that has not been assigned */ - -#define ACL_MAX_CATEGORIES 64 /* Maximum number of command categories */ - -struct ACLCategoryItem { - char *name; - uint64_t flag; -} ACLDefaultCommandCategories[] = { /* 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. */ -}; - -static struct ACLCategoryItem *ACLCommandCategories = NULL; -static size_t nextCommandCategory = 0; /* Index of the next command category to be added */ - -/* Implements the ability to add to the list of ACL categories at runtime. Since each ACL category - * also requires a bit in the acl_categories flag, there is a limit to the number that can be added. - * The new ACL categories occupy the remaining bits of acl_categories flag, other than the bits - * occupied by the default ACL command categories. - * - * The optional `flag` argument allows the assignment of the `acl_categories` flag bit to the ACL category. - * When adding a new category, except for the default ACL command categories, this arguments should be `0` - * to allow the function to assign the next available `acl_categories` flag bit to the new ACL category. - * - * returns 1 -> Added, 0 -> Failed (out of space) - * - * This function is present here to gain access to the ACLCommandCategories array and add a new ACL category. - */ -int ACLAddCommandCategory(const char *name, uint64_t flag) { - if (nextCommandCategory >= ACL_MAX_CATEGORIES) return 0; - ACLCommandCategories[nextCommandCategory].name = zstrdup(name); - ACLCommandCategories[nextCommandCategory].flag = flag != 0 ? flag : (1ULL<<nextCommandCategory); - nextCommandCategory++; - return 1; -} - -/* Initializes ACLCommandCategories with default ACL categories and allocates space for - * new ACL categories. - */ -void ACLInitCommandCategories(void) { - ACLCommandCategories = zcalloc(sizeof(struct ACLCategoryItem) * (ACL_MAX_CATEGORIES + 1)); - for (int j = 0; ACLDefaultCommandCategories[j].flag; j++) { - serverAssert(ACLAddCommandCategory(ACLDefaultCommandCategories[j].name, ACLDefaultCommandCategories[j].flag)); - } -} - -/* This function removes the specified number of categories from the trailing end of - * the `ACLCommandCategories` array. - * The purpose of this is to remove the categories added by modules that fail - * during the onload function. - */ -void ACLCleanupCategoriesOnFailure(size_t num_acl_categories_added) { - for (size_t j = nextCommandCategory - num_acl_categories_added; j < nextCommandCategory; j++) { - zfree(ACLCommandCategories[j].name); - ACLCommandCategories[j].name = NULL; - ACLCommandCategories[j].flag = 0; - } - nextCommandCategory -= num_acl_categories_added; -} - -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. */ - sds command_rules; /* A string representation of the ordered categories and commands, this - * is used to regenerate the original ACL string for display. */ -} 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) { - sdsfreegeneric(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 ACL key permission types 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; - selector->command_rules = sdsempty(); - - 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); - sdsfree(selector->command_rules); - 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); - dst->command_rules = sdsdup(src->command_rules); - 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,NULL)) return NULL; - user *u = zmalloc(sizeof(*u)); - u->name = sdsnewlen(name,namelen); - atomicSet(u->flags, USER_FLAG_DISABLED | USER_FLAG_SANITIZE_PAYLOAD); - 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); -} - -/* Generic version of ACLFreeUser. */ -void ACLFreeUserGeneric(void *u) { - ACLFreeUser((user *)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. */ - deauthenticateAndCloseClient(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); - } -} - -/* 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; - } -} - -/* Remove a rule from the retained command rules. Always match rules - * verbatim, but also remove subcommand rules if we are adding or removing the - * entire command. */ -void ACLSelectorRemoveCommandRule(aclSelector *selector, sds new_rule) { - size_t new_len = sdslen(new_rule); - char *existing_rule = selector->command_rules; - - /* Loop over the existing rules, trying to find a rule that "matches" - * the new rule. If we find a match, then remove the command from the string by - * copying the later rules over it. */ - while(existing_rule[0]) { - /* The first character of the rule is +/-, which we don't need to compare. */ - char *copy_position = existing_rule; - existing_rule += 1; - - /* Assume a trailing space after a command is part of the command, like '+get ', so trim it - * as well if the command is removed. */ - char *rule_end = strchr(existing_rule, ' '); - if (!rule_end) { - /* This is the last rule, so move it to the end of the string. */ - rule_end = existing_rule + strlen(existing_rule); - - /* This approach can leave a trailing space if the last rule is removed, - * but only if it's not the first rule, so handle that case. */ - if (copy_position != selector->command_rules) copy_position -= 1; - } - char *copy_end = rule_end; - if (*copy_end == ' ') copy_end++; - - /* Exact match or the rule we are comparing is a subcommand denoted by '|' */ - size_t existing_len = rule_end - existing_rule; - if (!memcmp(existing_rule, new_rule, min(existing_len, new_len))) { - if ((existing_len == new_len) || (existing_len > new_len && (existing_rule[new_len]) == '|')) { - /* Copy the remaining rules starting at the next rule to replace the rule to be - * deleted, including the terminating NULL character. */ - memmove(copy_position, copy_end, strlen(copy_end) + 1); - existing_rule = copy_position; - continue; - } - } - existing_rule = copy_end; - } - - /* There is now extra padding at the end of the rules, so clean that up. */ - sdsupdatelen(selector->command_rules); -} - -/* This function is resopnsible for updating the command_rules struct so that relative ordering of - * commands and categories is maintained and can be reproduced without loss. */ -void ACLUpdateCommandRules(aclSelector *selector, const char *rule, int allow) { - sds new_rule = sdsnew(rule); - sdstolower(new_rule); - - ACLSelectorRemoveCommandRule(selector, new_rule); - if (sdslen(selector->command_rules)) selector->command_rules = sdscat(selector->command_rules, " "); - selector->command_rules = sdscatfmt(selector->command_rules, allow ? "+%S" : "-%S", new_rule); - sdsfree(new_rule); -} - -/* 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; - dictInitSafeIterator(&di, cmd->subcommands_dict); - while((de = dictNext(&di)) != NULL) { - struct redisCommand *sub = (struct redisCommand *)dictGetVal(de); - ACLSetSelectorCommandBit(selector,sub->id,allow); - } - dictResetIterator(&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. */ -void ACLSetSelectorCommandBitsForCategory(dict *commands, aclSelector *selector, uint64_t cflag, int value) { - dictIterator di; - dictEntry *de; - dictInitIterator(&di, commands); - while ((de = dictNext(&di)) != NULL) { - struct redisCommand *cmd = dictGetVal(de); - if (cmd->acl_categories & cflag) { - ACLChangeSelectorPerm(selector,cmd,value); - } - if (cmd->subcommands_dict) { - ACLSetSelectorCommandBitsForCategory(cmd->subcommands_dict, selector, cflag, value); - } - } - dictResetIterator(&di); -} - -/* This function is responsible for recomputing the command bits for all selectors of the existing users. - * It uses the 'command_rules', a string representation of the ordered categories and commands, - * to recompute the command bits. */ -void ACLRecomputeCommandBitsFromCommandRulesAllUsers(void) { - raxIterator ri; - raxStart(&ri,Users); - raxSeek(&ri,"^",NULL,0); - while(raxNext(&ri)) { - user *u = ri.data; - listIter li; - listNode *ln; - listRewind(u->selectors,&li); - while((ln = listNext(&li))) { - aclSelector *selector = (aclSelector *) listNodeValue(ln); - int argc = 0; - sds *argv = sdssplitargs(selector->command_rules, &argc); - serverAssert(argv != NULL); - /* Checking selector's permissions for all commands to start with a clean state. */ - if (ACLSelectorCanExecuteFutureCommands(selector)) { - int res = ACLSetSelector(selector,"+@all",-1); - serverAssert(res == C_OK); - } else { - int res = ACLSetSelector(selector,"-@all",-1); - serverAssert(res == C_OK); - } - - /* Apply all of the commands and categories to this selector. */ - for(int i = 0; i < argc; i++) { - int res = ACLSetSelector(selector, argv[i], sdslen(argv[i])); - serverAssert(res == C_OK); - } - sdsfreesplitres(argv, argc); - } - } - raxStop(&ri); - -} - -int ACLSetSelectorCategory(aclSelector *selector, const char *category, int allow) { - uint64_t cflag = ACLGetCommandCategoryFlagByName(category + 1); - if (!cflag) return C_ERR; - - ACLUpdateCommandRules(selector, category, allow); - - /* Set the actual command bits on the selector. */ - ACLSetSelectorCommandBitsForCategory(server.orig_commands, selector, cflag, allow); - return C_OK; -} - -void ACLCountCategoryBitsForCommands(dict *commands, aclSelector *selector, unsigned long *on, unsigned long *off, uint64_t cflag) { - dictIterator di; - dictEntry *de; - dictInitIterator(&di, commands); - 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); - } - } - dictResetIterator(&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; -} - -/* 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(); - - /* We use this fake selector as a "sanity" check to make sure the rules - * we generate have the same bitmap as those on the current selector. */ - aclSelector *fake_selector = ACLCreateSelector(0); - - /* Here we want to understand if we should start with +@all or -@all. - * Note that when 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)) { - rules = sdscat(rules,"+@all "); - ACLSetSelector(fake_selector,"+@all",-1); - } else { - rules = sdscat(rules,"-@all "); - ACLSetSelector(fake_selector,"-@all",-1); - } - - /* Apply all of the commands and categories to the fake selector. */ - int argc = 0; - sds *argv = sdssplitargs(selector->command_rules, &argc); - serverAssert(argv != NULL); - - for(int i = 0; i < argc; i++) { - int res = ACLSetSelector(fake_selector, argv[i], -1); - serverAssert(res == C_OK); - } - if (sdslen(selector->command_rules)) { - rules = sdscatfmt(rules, "%S ", selector->command_rules); - } - sdsfreesplitres(argv, argc); - - /* 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'", - redactLogCstr(rules)); - serverPanic("No bitmap match in ACLDescribeSelectorCommandRules()"); - } - ACLFreeSelector(fake_selector); - 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-arg 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; - sdsclear(selector->command_rules); - ACLResetFirstArgs(selector); - } else if (!strcasecmp(op,"nocommands") || - !strcasecmp(op,"-@all")) - { - memset(selector->allowed_commands,0,sizeof(selector->allowed_commands)); - selector->flags &= ~SELECTOR_FLAG_ALLCOMMANDS; - sdsclear(selector->command_rules); - 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] == '%') { - int perm_ok = 1; - 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 { - perm_ok = 0; - break; - } - } - if (!flags || !perm_ok) { - 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); - ACLUpdateCommandRules(selector,cmd->fullname,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)", redactLogCstr(op+1)); - ACLAddAllowedFirstArg(selector,cmd->id,sub); - } - ACLUpdateCommandRules(selector,op+1,1); - 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); - ACLUpdateCommandRules(selector,cmd->fullname,0); - } else if ((op[0] == '+' || op[0] == '-') && op[1] == '@') { - int bitval = op[0] == '+' ? 1 : 0; - if (ACLSetSelectorCategory(selector,op+1,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. - * skip-sanitize-payload RESTORE dump-payload sanitization is skipped. - * sanitize-payload RESTORE dump-payload is sanitized (default). - * ><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, resetchannels, - * allchannels (if acl-pubsub-default is set), off, clearselectors, -@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")) { - atomicSet(u->flags, (u->flags | USER_FLAG_ENABLED) & ~USER_FLAG_DISABLED); - } else if (!strcasecmp(op,"off")) { - atomicSet(u->flags, (u->flags | USER_FLAG_DISABLED) & ~USER_FLAG_ENABLED); - } else if (!strcasecmp(op,"skip-sanitize-payload")) { - atomicSet(u->flags, (u->flags | USER_FLAG_SANITIZE_PAYLOAD_SKIP) & ~USER_FLAG_SANITIZE_PAYLOAD); - } else if (!strcasecmp(op,"sanitize-payload")) { - atomicSet(u->flags, (u->flags | USER_FLAG_SANITIZE_PAYLOAD) & ~USER_FLAG_SANITIZE_PAYLOAD_SKIP); - } else if (!strcasecmp(op,"nopass")) { - atomicSet(u->flags, u->flags | USER_FLAG_NOPASS); - listEmpty(u->passwords); - } else if (!strcasecmp(op,"resetpass")) { - atomicSet(u->flags, 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); - atomicSet(u->flags, 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(); - ACLInitCommandCategories(); - 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. - * ENOENT: 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; -} - -/* If `err` is provided, this is added as an error reply to the client. - * Otherwise, the standard Auth error is added as a reply. */ -void addAuthErrReply(client *c, robj *err) { - if (clientHasPendingReplies(c)) return; - if (!err) { - addReplyError(c, "-WRONGPASS invalid username-password pair or user is disabled."); - return; - } - addReplyError(c, err->ptr); -} - -/* 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 AUTH_OK on success (valid username / password pair) & AUTH_ERR otherwise. */ -int checkPasswordBasedAuth(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 AUTH_OK; - } else { - addACLLogEntry(c,ACL_DENIED_AUTH,(c->flags & CLIENT_MULTI) ? ACL_LOG_CTX_MULTI : ACL_LOG_CTX_TOPLEVEL,0,username->ptr,NULL); - return AUTH_ERR; - } -} - -/* Attempt authenticating the user - first through module based authentication, - * and then, if needed, with normal password based authentication. - * Returns one of the following codes: - * AUTH_OK - Indicates that authentication succeeded. - * AUTH_ERR - Indicates that authentication failed. - * AUTH_BLOCKED - Indicates module authentication is in progress through a blocking implementation. - */ -int ACLAuthenticateUser(client *c, robj *username, robj *password, robj **err) { - int result = checkModuleAuthentication(c, username, password, err); - /* If authentication was not handled by any Module, attempt normal password based auth. */ - if (result == AUTH_NOT_HANDLED) { - result = checkPasswordBasedAuth(c, username, password); - } - return result; -} - -/* 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; - if (raxFind(commandId,(unsigned char*)lowername,sdslen(lowername),&id)) { - 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 = NULL; - raxFind(Users,(unsigned char*)name,namelen,&myuser); - 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; - - /* Is given key represent a prefix of a set of keys */ - int prefix = keyspec_flags & CMD_KEY_PREFIX; - - /* 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 (prefix) { - if (prefixmatch(pattern->pattern,plen,key,keylen,0)) - return ACL_OK; - } else { - 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. - * - * If the command fails an ACL check, idxptr will be to set to the first argv entry that - * causes the failure, either 0 if the command itself fails or the idx of the key/channel - * that causes the failure */ -int ACLCheckAllUserCommandPerm(user *u, struct redisCommand *cmd, robj **argv, int argc, getKeysResult *key_result, int *idxptr) { - listIter li; - listNode *ln; - - /* If there is no associated user, the connection can run anything. */ - if (u == NULL) return ACL_OK; - - /* Quick check if the user has all permissions, return early if so. */ - if (likely(listFirst(u->selectors) != NULL)) { - aclSelector *s = listNodeValue(listFirst(u->selectors)); - const uint32_t all_perms = SELECTOR_FLAG_ALLCOMMANDS | - SELECTOR_FLAG_ALLKEYS | - SELECTOR_FLAG_ALLCHANNELS; - if ((s->flags & all_perms) == all_perms) 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); - if (key_result) { - cache.keys = *key_result; - cache.keys_init = 1; - } - - /* 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) { - if (!key_result) 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; - if (!key_result) 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, getClientCachedKeyResult(c), idxptr); -} - -/* If 'new' can access all channels 'original' could then return NULL; - Otherwise return a list of channels that the new user can access */ -list *getUpcomingChannelList(user *new, user *original) { - listIter li, lpi; - listNode *ln, *lpn; - - /* Optimization: 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 NULL; - } - - /* Next, 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 NULL; - } - - return upcoming; -} - -/* Check if the client should be killed because it is subscribed to channels that were - * permitted in the past, are not in the `upcoming` channel list. */ -int ACLShouldKillPubsubClient(client *c, list *upcoming) { - robj *o; - int kill = 0; - - if (getClientType(c) == CLIENT_TYPE_PUBSUB) { - /* Check for pattern violations. */ - dictIterator di; - dictEntry *de; - dictInitIterator(&di, c->pubsub_patterns); - while (!kill && ((de = dictNext(&di)) != NULL)) { - o = dictGetKey(de); - int res = ACLCheckChannelAgainstList(upcoming, o->ptr, sdslen(o->ptr), 1); - kill = (res == ACL_DENIED_CHANNEL); - } - dictResetIterator(&di); - - /* Check for channel violations. */ - if (!kill) { - /* Check for global channels violation. */ - dictInitIterator(&di, c->pubsub_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); - } - dictResetIterator(&di); - } - if (!kill) { - /* Check for shard channels violation. */ - dictInitIterator(&di, 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); - } - dictResetIterator(&di); - } - - if (kill) { - return 1; - } - } - return 0; -} - -/* 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) { - /* Do nothing if there are no subscribers. */ - if (pubsubTotalSubscriptions() == 0) - return; - - list *channels = getUpcomingChannelList(new, original); - /* If the new user's pubsub permissions are a strict superset of the original, return early. */ - if (!channels) - return; - - listIter li; - listNode *ln; - - /* 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); - if (c->user != original) - continue; - if (ACLShouldKillPubsubClient(c, channels)) - deauthenticateAndCloseClient(c); - } - - listRelease(channels); -} - -/* ============================================================================= - * 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 (open_bracket_start == -1 && - (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; - } - - /* 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; - } - - /* Try to apply the user rules in a fake user to see if they - * are actually valid. */ - user *fakeuser = ACLCreateUnlinkedUser(); - - 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", - redactLogCstr(aclrules[j]),redactLogCstr(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.", - redactLogCstr(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); - - /* If there are some subscribers, we need to check if we need to drop some clients. */ - rax *user_channels = NULL; - if (pubsubTotalSubscriptions() > 0) { - user_channels = raxNew(); - } - - listIter li; - listNode *ln; - - listRewind(server.clients,&li); - while ((ln = listNext(&li)) != NULL) { - client *c = listNodeValue(ln); - /* a MASTER client can do everything (and user = NULL) so we can skip it */ - if (c->flags & CLIENT_MASTER) - continue; - user *original = c->user; - list *channels = NULL; - user *new = ACLGetUserByName(c->user->name, sdslen(c->user->name)); - if (new && user_channels) { - if (!raxFind(user_channels, (unsigned char*)(new->name), sdslen(new->name), (void**)&channels)) { - channels = getUpcomingChannelList(new, original); - raxInsert(user_channels, (unsigned char*)(new->name), sdslen(new->name), channels, NULL); - } - } - /* When the new channel list is NULL, it means the new user's channel list is a superset of the old user's list. */ - if (!new || (channels && ACLShouldKillPubsubClient(c, channels))) { - deauthenticateAndCloseClient(c); - continue; - } - c->user = new; - } - - if (user_channels) - raxFreeWithCallback(user_channels, listReleaseGeneric); - raxFreeWithCallback(old_users, ACLFreeUserGeneric); - sdsfree(errors); - return NULL; - } else { - raxFreeWithCallback(Users, ACLFreeUserGeneric); - 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(),commandTimeSnapshot()); - 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. */ - size_t offset = 0; - while (offset < sdslen(acl)) { - ssize_t written_bytes = write(fd,acl + offset,sdslen(acl) - offset); - if (written_bytes <= 0) { - if (errno == EINTR) continue; - serverLog(LL_WARNING,"Writing ACL file for ACL SAVE: %s", - strerror(errno)); - goto cleanup; - } - offset += written_bytes; - } - if (redis_fsync(fd) == -1) { - serverLog(LL_WARNING,"Syncing 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; - } - if (fsyncFileDir(filename) == -1) { - serverLog(LL_WARNING,"Syncing ACL directory 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). */ - long long entry_id; /* The pair (entry_id, timestamp_created) is a unique identifier of this entry - * in case the node dies and is restarted, it can detect that if it's a new series. */ - mstime_t timestamp_created; /* UNIX time in milliseconds at the time of this entry's creation. */ -} 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); -} - -/* Update the relevant counter by the reason */ -void ACLUpdateInfoMetrics(int reason){ - if (reason == ACL_DENIED_AUTH) { - server.acl_info.user_auth_failures++; - } else if (reason == ACL_DENIED_CMD) { - server.acl_info.invalid_cmd_accesses++; - } else if (reason == ACL_DENIED_KEY) { - server.acl_info.invalid_key_accesses++; - } else if (reason == ACL_DENIED_CHANNEL) { - server.acl_info.invalid_channel_accesses++; - } else if (reason == ACL_INVALID_TLS_CERT_AUTH) { - server.acl_info.acl_access_denied_tls_cert++; - } else { - serverPanic("Unknown ACL_DENIED encoding"); - } -} - -static void trimACLLogEntriesToMaxLen(void) { - while(listLength(ACLLog) > server.acllog_max_len) { - listNode *ln = listLast(ACLLog); - ACLLogEntry *le = listNodeValue(ln); - ACLFreeLogEntry(le); - listDelNode(ACLLog,ln); - } -} - -/* 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) { - /* Update ACL info metrics */ - ACLUpdateInfoMetrics(reason); - - if (server.acllog_max_len == 0) { - trimACLLogEntriesToMaxLen(); - return; - } - - /* 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 = commandTimeSnapshot(); - le->entry_id = ACLLogEntryCount; - le->timestamp_created = le->ctime; - - 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(); - } - } - - /* if we have a real client from the network, use it (could be missing on module timers) */ - client *realclient = server.current_client? server.current_client : c; - - 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. */ - ACLLogEntryCount++; /* Incrementing the entry_id count to make each record in the log unique. */ - listAddNodeHead(ACLLog, le); - trimACLLogEntriesToMaxLen(); - } -} - -sds getAclErrorMessage(int acl_res, user *user, struct redisCommand *cmd, sds errored_val, int verbose) { - switch (acl_res) { - case ACL_DENIED_CMD: - return sdscatfmt(sdsempty(), "User %S has no permissions to run " - "the '%S' command", user->name, cmd->fullname); - case ACL_DENIED_KEY: - if (verbose) { - return sdscatfmt(sdsempty(), "User %S has no permissions to access " - "the '%S' key", user->name, errored_val); - } else { - return sdsnew("No permissions to access a key"); - } - case ACL_DENIED_CHANNEL: - if (verbose) { - return sdscatfmt(sdsempty(), "User %S has no permissions to access " - "the '%S' channel", user->name, errored_val); - } else { - return sdsnew("No permissions to access a channel"); - } - } - 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; - dictInitIterator(&di, commands); - while ((de = dictNext(&di)) != NULL) { - struct redisCommand *cmd = dictGetVal(de); - if (cmd->acl_categories & cflag) { - addReplyBulkCBuffer(c, cmd->fullname, sdslen(cmd->fullname)); - (*arraylen)++; - } - - if (cmd->subcommands_dict) { - aclCatWithFlags(c, cmd->subcommands_dict, cflag, arraylen); - } - } - dictResetIterator(&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))) { - addReplyError(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) { - /* Initially redact all the arguments to not leak any information - * about the users. */ - for (int j = 2; j < c->argc; j++) redactClientCommandArgument(c, j); - - 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) { - /* Redact the username to not leak any information about the user. */ - redactClientCommandArgument(c, 2); - - 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 = commandTimeSnapshot(); - while (count-- && (ln = listNext(&li)) != NULL) { - ACLLogEntry *le = listNodeValue(ln); - addReplyMapLen(c,10); - 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; - case ACL_INVALID_TLS_CERT_AUTH: reasonstr = "tls-cert"; 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)); - addReplyBulkCString(c, "entry-id"); - addReplyLongLong(c, le->entry_id); - addReplyBulkCString(c, "timestamp-created"); - addReplyLongLong(c, le->timestamp_created); - addReplyBulkCString(c, "timestamp-last-updated"); - addReplyLongLong(c, le->ctime); - } - } 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, NULL, &idx); - if (result != ACL_OK) { - sds err = getAclErrorMessage(result, u, cmd, c->argv[idx+3]->ptr, 1); - 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); -} - -/* When successful, initiates an internal connection, that is able to execute - * internal commands (see CMD_INTERNAL). */ -static void internalAuth(client *c) { - if (!server.cluster_enabled) { - addReplyError(c, "Cannot authenticate as an internal connection on non-cluster instances"); - return; - } - - sds password = c->argv[2]->ptr; - - /* Get internal secret. */ - size_t len = -1; - const char *internal_secret = clusterGetSecret(&len); - if (sdslen(password) != len) { - addReplyError(c, "-WRONGPASS invalid internal password"); - return; - } - if (!time_independent_strcmp((char *)internal_secret, (char *)password, len)) { - c->flags |= CLIENT_INTERNAL; - /* No further authentication is needed. */ - c->authenticated = 1; - /* Set the user to the unrestricted user, if it is not already set (default). */ - if (c->user != NULL) { - c->user = NULL; - moduleNotifyUserChanged(c); - } - addReply(c, shared.ok); - } else { - addReplyError(c, "-WRONGPASS invalid internal password"); - } -} - -/* 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); - - /* Handle internal authentication commands. - * Note: No user-defined ACL user can have this username (no spaces - * allowed), thus no conflicts with ACL possible. */ - if (!strcmp(username->ptr, "internal connection")) { - internalAuth(c); - return; - } - } - - robj *err = NULL; - int result = ACLAuthenticateUser(c, username, password, &err); - if (result == AUTH_OK) { - addReply(c, shared.ok); - } else if (result == AUTH_ERR) { - addAuthErrReply(c, err); - } - if (err) decrRefCount(err); -} - -/* 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); - } -} |
