summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--redis.conf26
-rw-r--r--src/acl.c227
-rw-r--r--src/config.c7
-rw-r--r--src/multi.c30
-rw-r--r--src/pubsub.c29
-rw-r--r--src/redis-cli.c9
-rw-r--r--src/scripting.c24
-rw-r--r--src/server.c2
-rw-r--r--src/server.h20
-rw-r--r--tests/unit/acl.tcl104
-rw-r--r--tests/unit/moduleapi/blockedclient.tcl4
11 files changed, 442 insertions, 40 deletions
diff --git a/redis.conf b/redis.conf
index b1c8e4f0c..b11459b26 100644
--- a/redis.conf
+++ b/redis.conf
@@ -747,6 +747,11 @@ replica-priority 100
# It is possible to specify multiple patterns.
# allkeys Alias for ~*
# resetkeys Flush the list of allowed keys patterns.
+# &<pattern> Add a glob-style pattern of Pub/Sub channels that can be
+# accessed by the user. It is possible to specify multiple channel
+# patterns.
+# allchannels Alias for &*
+# resetchannels Flush the list of allowed channel patterns.
# ><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).
@@ -820,6 +825,27 @@ acllog-max-len 128
#
# requirepass foobared
+# New users are initialized with restrictive permissions by default, via the
+# equivalent of this ACL rule 'off resetkeys -@all'. Starting with Redis 6.2, it
+# is possible to manage access to Pub/Sub channels with ACL rules as well. The
+# default Pub/Sub channels permission if new users is controlled by the
+# acl-pubsub-default configuration directive, which accepts one of these values:
+#
+# allchannels: grants access to all Pub/Sub channels
+# resetchannels: revokes access to all Pub/Sub channels
+#
+# To ensure backward compatibility while upgrading Redis 6.0, acl-pubsub-default
+# defaults to the 'allchannels' permission.
+#
+# Future compatibility note: it is very likely that in a future version of Redis
+# the directive's default of 'allchannels' will be changed to 'resetchannels' in
+# order to provide better out-of-the-box Pub/Sub security. Therefore, it is
+# recommended that you explicitly define Pub/Sub permissions for all users
+# rather then rely on implicit default values. Once you've set explicit
+# Pub/Sub for all exisitn users, you should uncomment the following line.
+#
+# acl-pubsub-default resetchannels
+
# Command renaming (DEPRECATED).
#
# ------------------------------------------------------------------------
diff --git a/src/acl.c b/src/acl.c
index e8f9d3724..e9acc11a9 100644
--- a/src/acl.c
+++ b/src/acl.c
@@ -92,6 +92,7 @@ struct ACLUserFlag {
{"on", USER_FLAG_ENABLED},
{"off", USER_FLAG_DISABLED},
{"allkeys", USER_FLAG_ALLKEYS},
+ {"allchannels", USER_FLAG_ALLCHANNELS},
{"allcommands", USER_FLAG_ALLCOMMANDS},
{"nopass", USER_FLAG_NOPASS},
{NULL,0} /* Terminator. */
@@ -241,16 +242,20 @@ user *ACLCreateUser(const char *name, size_t namelen) {
if (raxFind(Users,(unsigned char*)name,namelen) != raxNotFound) return NULL;
user *u = zmalloc(sizeof(*u));
u->name = sdsnewlen(name,namelen);
- u->flags = USER_FLAG_DISABLED;
+ u->flags = USER_FLAG_DISABLED | server.acl_pubusub_default;
u->allowed_subcommands = NULL;
u->passwords = listCreate();
u->patterns = listCreate();
+ u->channels = listCreate();
listSetMatchMethod(u->passwords,ACLListMatchSds);
listSetFreeMethod(u->passwords,ACLListFreeSds);
listSetDupMethod(u->passwords,ACLListDupSds);
listSetMatchMethod(u->patterns,ACLListMatchSds);
listSetFreeMethod(u->patterns,ACLListFreeSds);
listSetDupMethod(u->patterns,ACLListDupSds);
+ listSetMatchMethod(u->channels,ACLListMatchSds);
+ listSetFreeMethod(u->channels,ACLListFreeSds);
+ listSetDupMethod(u->channels,ACLListDupSds);
memset(u->allowed_commands,0,sizeof(u->allowed_commands));
raxInsert(Users,(unsigned char*)name,namelen,u,NULL);
return u;
@@ -279,6 +284,7 @@ void ACLFreeUser(user *u) {
sdsfree(u->name);
listRelease(u->passwords);
listRelease(u->patterns);
+ listRelease(u->channels);
ACLResetSubcommands(u);
zfree(u);
}
@@ -319,8 +325,10 @@ void ACLFreeUserAndKillClients(user *u) {
void ACLCopyUser(user *dst, user *src) {
listRelease(dst->passwords);
listRelease(dst->patterns);
+ listRelease(dst->channels);
dst->passwords = listDup(src->passwords);
dst->patterns = listDup(src->patterns);
+ dst->channels = listDup(src->channels);
memcpy(dst->allowed_commands,src->allowed_commands,
sizeof(dst->allowed_commands));
dst->flags = src->flags;
@@ -602,9 +610,10 @@ sds ACLDescribeUser(user *u) {
/* Flags. */
for (int j = 0; ACLUserFlags[j].flag; j++) {
- /* Skip the allcommands and allkeys flags because they'll be emitted
- * later as ~* and +@all. */
+ /* Skip the allcommands, allkeys and allchannels flags because they'll
+ * be emitted later as +@all, ~* and &*. */
if (ACLUserFlags[j].flag == USER_FLAG_ALLKEYS ||
+ ACLUserFlags[j].flag == USER_FLAG_ALLCHANNELS ||
ACLUserFlags[j].flag == USER_FLAG_ALLCOMMANDS) continue;
if (u->flags & ACLUserFlags[j].flag) {
res = sdscat(res,ACLUserFlags[j].name);
@@ -636,6 +645,19 @@ sds ACLDescribeUser(user *u) {
}
}
+ /* Pub/sub channel patterns. */
+ if (u->flags & USER_FLAG_ALLCHANNELS) {
+ res = sdscatlen(res,"&* ",3);
+ } else {
+ listRewind(u->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 = ACLDescribeUserCommandRules(u);
res = sdscatsds(res,rules);
@@ -681,7 +703,6 @@ void ACLResetSubcommands(user *u) {
u->allowed_subcommands = NULL;
}
-
/* Add a subcommand to the list of subcommands for the user 'u' and
* the command id specified. */
void ACLAddAllowedSubcommand(user *u, unsigned long id, const char *sub) {
@@ -742,6 +763,12 @@ void ACLAddAllowedSubcommand(user *u, unsigned long id, const char *sub) {
* It is possible to specify multiple patterns.
* 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 keys patterns.
* ><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).
@@ -780,7 +807,7 @@ void ACLAddAllowedSubcommand(user *u, unsigned long id, const char *sub) {
*
* When an error is returned, errno is set to the following values:
*
- * EINVAL: The specified opcode is not understood or the key pattern is
+ * 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.
@@ -788,6 +815,8 @@ void ACLAddAllowedSubcommand(user *u, unsigned long id, const char *sub) {
* fully added.
* 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.
*/
@@ -808,6 +837,14 @@ int ACLSetUser(user *u, const char *op, ssize_t oplen) {
} else if (!strcasecmp(op,"resetkeys")) {
u->flags &= ~USER_FLAG_ALLKEYS;
listEmpty(u->patterns);
+ } else if (!strcasecmp(op,"allchannels") ||
+ !strcasecmp(op,"&*"))
+ {
+ u->flags |= USER_FLAG_ALLCHANNELS;
+ listEmpty(u->channels);
+ } else if (!strcasecmp(op,"resetchannels")) {
+ u->flags &= ~USER_FLAG_ALLCHANNELS;
+ listEmpty(u->channels);
} else if (!strcasecmp(op,"allcommands") ||
!strcasecmp(op,"+@all"))
{
@@ -875,12 +912,29 @@ int ACLSetUser(user *u, const char *op, ssize_t oplen) {
}
sds newpat = sdsnewlen(op+1,oplen-1);
listNode *ln = listSearchKey(u->patterns,newpat);
- /* Avoid re-adding the same pattern multiple times. */
+ /* Avoid re-adding the same key pattern multiple times. */
if (ln == NULL)
listAddNodeTail(u->patterns,newpat);
else
sdsfree(newpat);
u->flags &= ~USER_FLAG_ALLKEYS;
+ } else if (op[0] == '&') {
+ if (u->flags & USER_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(u->channels,newpat);
+ /* Avoid re-adding the same channel pattern multiple times. */
+ if (ln == NULL)
+ listAddNodeTail(u->channels,newpat);
+ else
+ sdsfree(newpat);
+ u->flags &= ~USER_FLAG_ALLCHANNELS;
} else if (op[0] == '+' && op[1] != '@') {
if (strchr(op,'|') == NULL) {
if (ACLLookupCommand(op+1) == NULL) {
@@ -948,6 +1002,7 @@ int ACLSetUser(user *u, const char *op, ssize_t oplen) {
} 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);
serverAssert(ACLSetUser(u,"off",-1) == C_OK);
serverAssert(ACLSetUser(u,"-@all",-1) == C_OK);
} else {
@@ -974,6 +1029,11 @@ char *ACLSetUserStringError(void) {
"'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";
@@ -989,6 +1049,7 @@ void ACLInitDefaultUser(void) {
DefaultUser = ACLCreateUser("default",7);
ACLSetUser(DefaultUser,"+@all",-1);
ACLSetUser(DefaultUser,"~*",-1);
+ ACLSetUser(DefaultUser,"&*",-1);
ACLSetUser(DefaultUser,"on",-1);
ACLSetUser(DefaultUser,"nopass",-1);
}
@@ -1193,6 +1254,119 @@ int ACLCheckCommandPerm(client *c, int *keyidxptr) {
return ACL_OK;
}
+/* Check if the provided channel is whitelisted by the given allowed channels
+ * list. Glob-style pattern matching is employed, unless the literal flag is
+ * set. Returns ACL_OK if access is granted or ACL_DENIED_CHANNEL otherwise. */
+int ACLCheckPubsubChannelPerm(sds channel, list *allowed, int literal) {
+ listIter li;
+ listNode *ln;
+ size_t clen = sdslen(channel);
+ int match = 0;
+
+ listRewind(allowed,&li);
+ while((ln = listNext(&li))) {
+ sds pattern = listNodeValue(ln);
+ size_t plen = sdslen(pattern);
+
+ if ((literal && !sdscmp(pattern,channel)) ||
+ (!literal && stringmatchlen(pattern,plen,channel,clen,0)))
+ {
+ match = 1;
+ break;
+ }
+ }
+ if (!match) {
+ return ACL_DENIED_CHANNEL;
+ }
+ return ACL_OK;
+}
+
+/* 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 *u, list *upcoming) {
+ listIter li, lpi;
+ listNode *ln, *lpn;
+ robj *o;
+ int kill = 0;
+
+ /* Nothing to kill when the upcoming are a literal super set of the original
+ * permissions. */
+ listRewind(u->channels,&li);
+ while (!kill && ((ln = listNext(&li)) != NULL)) {
+ sds pattern = listNodeValue(ln);
+ kill = (ACLCheckPubsubChannelPerm(pattern,upcoming,1) ==
+ ACL_DENIED_CHANNEL);
+ }
+ if (!kill) return;
+
+ /* Scan all connected clients to find the user's pub/subs. */
+ listRewind(server.clients,&li);
+ while ((ln = listNext(&li)) != NULL) {
+ client *c = listNodeValue(ln);
+ kill = 0;
+
+ if (c->user == u && getClientType(c) == CLIENT_TYPE_PUBSUB) {
+ /* Check for pattern violations. */
+ listRewind(c->pubsub_patterns,&lpi);
+ while (!kill && ((lpn = listNext(&lpi)) != NULL)) {
+ o = lpn->value;
+ kill = (ACLCheckPubsubChannelPerm(o->ptr,upcoming,1) ==
+ ACL_DENIED_CHANNEL);
+ }
+ /* Check for channel violations. */
+ if (!kill) {
+ dictIterator *di = dictGetIterator(c->pubsub_channels);
+ dictEntry *de;
+ while (!kill && ((de = dictNext(di)) != NULL)) {
+ o = dictGetKey(de);
+ kill = (ACLCheckPubsubChannelPerm(o->ptr,upcoming,0) ==
+ ACL_DENIED_CHANNEL);
+ }
+ dictReleaseIterator(di);
+ }
+
+ /* Kill it. */
+ if (kill) {
+ freeClient(c);
+ }
+ }
+ }
+}
+
+/* Check if the pub/sub channels of the command, that's ready to be executed in
+ * the client 'c', can be executed by this client according to the ACLs channels
+ * associated to the client user c->user.
+ *
+ * idx and count are the index and count of channel arguments from the
+ * command. The literal argument controls whether the user's ACL channels are
+ * evaluated as literal values or matched as glob-like patterns.
+ *
+ * If the user can execute the command ACL_OK is returned, otherwise
+ * ACL_DENIED_CHANNEL. */
+int ACLCheckPubsubPerm(client *c, int idx, int count, int literal, int *idxptr) {
+ user *u = c->user;
+
+ /* If there is no associated user, the connection can run anything. */
+ if (u == NULL) return ACL_OK;
+
+ /* Check if the user can access the channels mentioned in the command's
+ * arguments. */
+ if (!(c->user->flags & USER_FLAG_ALLCHANNELS)) {
+ for (int j = idx; j < idx+count; j++) {
+ if (ACLCheckPubsubChannelPerm(c->argv[j]->ptr,u->channels,literal)
+ != ACL_OK) {
+ if (idxptr) *idxptr = j;
+ return ACL_DENIED_CHANNEL;
+ }
+ }
+ }
+
+ /* If we survived all the above checks, the user can execute the
+ * command. */
+ return ACL_OK;
+
+}
+
/* =============================================================================
* ACL loading / saving functions
* ==========================================================================*/
@@ -1608,13 +1782,13 @@ void ACLFreeLogEntry(void *leptr) {
* the log entry instead of creating many entries for very similar ACL
* rules issues.
*
- * The keypos argument is only used when the reason is ACL_DENIED_KEY, since
- * it allows the function to log the key name that caused the problem.
- * Similarly the username is only passed when we failed to authenticate the
- * user with AUTH or HELLO, for the ACL_DENIED_AUTH reason. Otherwise
- * it will just be NULL.
+ * The argpos argument is used when the reason is ACL_DENIED_KEY or
+ * ACL_DENIED_CHANNEL, since it allows the function to log the key or channel
+ * name that caused the problem. Similarly the username is only passed when we
+ * failed to authenticate the user with AUTH or HELLO, for the ACL_DENIED_AUTH
+ * reason. Otherwise it will just be NULL.
*/
-void addACLLogEntry(client *c, int reason, int keypos, sds username) {
+void addACLLogEntry(client *c, int reason, int argpos, sds username) {
/* Create a new entry. */
struct ACLLogEntry *le = zmalloc(sizeof(*le));
le->count = 1;
@@ -1624,8 +1798,9 @@ void addACLLogEntry(client *c, int reason, int keypos, sds username) {
switch(reason) {
case ACL_DENIED_CMD: le->object = sdsnew(c->cmd->name); break;
- case ACL_DENIED_KEY: le->object = sdsnew(c->argv[keypos]->ptr); break;
- case ACL_DENIED_AUTH: le->object = sdsnew(c->argv[0]->ptr); 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();
}
@@ -1733,6 +1908,11 @@ void aclCommand(client *c) {
}
}
+ /* Existing pub/sub clients authenticated with the user may need to be
+ * disconnected if (some of) their channel permissions were revoked. */
+ if (u && !(tempu->flags & USER_FLAG_ALLCHANNELS))
+ ACLKillPubsubClientsIfNeeded(u,tempu->channels);
+
/* Overwrite the user with the temporary user we modified above. */
if (!u) u = ACLCreateUser(username,sdslen(username));
serverAssert(u != NULL);
@@ -1768,7 +1948,7 @@ void aclCommand(client *c) {
return;
}
- addReplyMapLen(c,4);
+ addReplyMapLen(c,5);
/* Flags */
addReplyBulkCString(c,"flags");
@@ -1813,6 +1993,22 @@ void aclCommand(client *c) {
addReplyBulkCBuffer(c,thispat,sdslen(thispat));
}
}
+
+ /* Pub/sub patterns */
+ addReplyBulkCString(c,"channels");
+ if (u->flags & USER_FLAG_ALLCHANNELS) {
+ addReplyArrayLen(c,1);
+ addReplyBulkCBuffer(c,"*",1);
+ } else {
+ addReplyArrayLen(c,listLength(u->channels));
+ listIter li;
+ listNode *ln;
+ listRewind(u->channels,&li);
+ while((ln = listNext(&li))) {
+ sds thispat = listNodeValue(ln);
+ addReplyBulkCBuffer(c,thispat,sdslen(thispat));
+ }
+ }
} else if ((!strcasecmp(sub,"list") || !strcasecmp(sub,"users")) &&
c->argc == 2)
{
@@ -1950,6 +2146,7 @@ void aclCommand(client *c) {
switch(le->reason) {
case ACL_DENIED_CMD: reasonstr="command"; break;
case ACL_DENIED_KEY: reasonstr="key"; break;
+ case ACL_DENIED_CHANNEL: reasonstr="channel"; break;
case ACL_DENIED_AUTH: reasonstr="auth"; break;
default: reasonstr="unknown";
}
diff --git a/src/config.c b/src/config.c
index b4fd9bee9..8dc241ed7 100644
--- a/src/config.c
+++ b/src/config.c
@@ -113,6 +113,12 @@ configEnum oom_score_adj_enum[] = {
{NULL, 0}
};
+configEnum acl_pubsub_default_enum[] = {
+ {"allchannels", USER_FLAG_ALLCHANNELS},
+ {"resetchannels", 0},
+ {NULL, 0}
+};
+
/* Output buffer limits presets. */
clientBufferLimitsConfig clientBufferLimitsDefaults[CLIENT_TYPE_OBUF_COUNT] = {
{0, 0, 0}, /* normal */
@@ -2352,6 +2358,7 @@ standardConfig configs[] = {
createEnumConfig("maxmemory-policy", NULL, MODIFIABLE_CONFIG, maxmemory_policy_enum, server.maxmemory_policy, MAXMEMORY_NO_EVICTION, NULL, NULL),
createEnumConfig("appendfsync", NULL, MODIFIABLE_CONFIG, aof_fsync_enum, server.aof_fsync, AOF_FSYNC_EVERYSEC, NULL, NULL),
createEnumConfig("oom-score-adj", NULL, MODIFIABLE_CONFIG, oom_score_adj_enum, server.oom_score_adj, OOM_SCORE_ADJ_NO, NULL, updateOOMScoreAdj),
+ createEnumConfig("acl-pubsub-default", NULL, MODIFIABLE_CONFIG, acl_pubsub_default_enum, server.acl_pubusub_default, USER_FLAG_ALLCHANNELS, NULL, NULL),
/* Integer configs */
createIntConfig("databases", NULL, IMMUTABLE_CONFIG, 1, INT_MAX, server.dbnum, 16, INTEGER_CONFIG, NULL, NULL),
diff --git a/src/multi.c b/src/multi.c
index 7790b0223..c519828c4 100644
--- a/src/multi.c
+++ b/src/multi.c
@@ -198,18 +198,34 @@ void execCommand(client *c) {
must_propagate = 1;
}
- int acl_keypos;
- int acl_retval = ACLCheckCommandPerm(c,&acl_keypos);
+ /* ACL permissions are also checked at the time of execution in case
+ * they were changed after the commands were ququed. */
+ int acl_errpos;
+ int acl_retval = ACLCheckCommandPerm(c,&acl_errpos);
+ if (acl_retval == ACL_OK && c->cmd->proc == publishCommand)
+ acl_retval = ACLCheckPubsubPerm(c,1,1,0,&acl_errpos);
if (acl_retval != ACL_OK) {
- addACLLogEntry(c,acl_retval,acl_keypos,NULL);
+ char *reason;
+ switch (acl_retval) {
+ case ACL_DENIED_CMD:
+ reason = "no permission to execute the command or subcommand";
+ break;
+ case ACL_DENIED_KEY:
+ reason = "no permission to touch the specified keys";
+ break;
+ case ACL_DENIED_CHANNEL:
+ reason = "no permission to publish to the specified channel";
+ break;
+ default:
+ reason = "no permission";
+ break;
+ }
+ addACLLogEntry(c,acl_retval,acl_errpos,NULL);
addReplyErrorFormat(c,
"-NOPERM ACLs rules changed between the moment the "
"transaction was accumulated and the EXEC call. "
"This command is no longer allowed for the "
- "following reason: %s",
- (acl_retval == ACL_DENIED_CMD) ?
- "no permission to execute the command or subcommand" :
- "no permission to touch the specified keys");
+ "following reason: %s", reason);
} else {
call(c,server.loading ? CMD_CALL_NONE : CMD_CALL_FULL);
serverAssert((c->flags & CLIENT_BLOCKED) == 0);
diff --git a/src/pubsub.c b/src/pubsub.c
index b7f7d9673..9e41464bf 100644
--- a/src/pubsub.c
+++ b/src/pubsub.c
@@ -353,13 +353,29 @@ int pubsubPublishMessage(robj *channel, robj *message) {
return receivers;
}
+/* This wraps handling ACL channel permissions for the given client. */
+int pubsubCheckACLPermissionsOrReply(client *c, int idx, int count, int literal) {
+ /* Check if the user can run the command according to the current
+ * ACLs. */
+ int acl_chanpos;
+ int acl_retval = ACLCheckPubsubPerm(c,idx,count,literal,&acl_chanpos);
+ if (acl_retval == ACL_DENIED_CHANNEL) {
+ addACLLogEntry(c,acl_retval,acl_chanpos,NULL);
+ addReplyError(c,
+ "-NOPERM this user has no permissions to access "
+ "one of the channels used as arguments");
+ }
+ return acl_retval;
+}
+
/*-----------------------------------------------------------------------------
* Pubsub commands implementation
*----------------------------------------------------------------------------*/
+/* SUBSCRIBE channel [channel ...] */
void subscribeCommand(client *c) {
int j;
-
+ if (pubsubCheckACLPermissionsOrReply(c,1,c->argc-1,0) != ACL_OK) return;
if ((c->flags & CLIENT_DENY_BLOCKING) && !(c->flags & CLIENT_MULTI)) {
/**
* A client that has CLIENT_DENY_BLOCKING flag on
@@ -368,7 +384,7 @@ void subscribeCommand(client *c) {
* Notice that we have a special treatment for multi because of
* backword compatibility
*/
- addReplyError(c, "subscribe is not allow on DENY BLOCKING client");
+ addReplyError(c, "SUBSCRIBE isn't allowed for a DENY BLOCKING client");
return;
}
@@ -377,6 +393,7 @@ void subscribeCommand(client *c) {
c->flags |= CLIENT_PUBSUB;
}
+/* UNSUBSCRIBE [channel [channel ...]] */
void unsubscribeCommand(client *c) {
if (c->argc == 1) {
pubsubUnsubscribeAllChannels(c,1);
@@ -389,9 +406,10 @@ void unsubscribeCommand(client *c) {
if (clientSubscriptionsCount(c) == 0) c->flags &= ~CLIENT_PUBSUB;
}
+/* PSUBSCRIBE pattern [pattern ...] */
void psubscribeCommand(client *c) {
int j;
-
+ if (pubsubCheckACLPermissionsOrReply(c,1,c->argc-1,1) != ACL_OK) return;
if ((c->flags & CLIENT_DENY_BLOCKING) && !(c->flags & CLIENT_MULTI)) {
/**
* A client that has CLIENT_DENY_BLOCKING flag on
@@ -400,7 +418,7 @@ void psubscribeCommand(client *c) {
* Notice that we have a special treatment for multi because of
* backword compatibility
*/
- addReplyError(c, "PSUBSCRIBE is not allowed for DENY BLOCKING client");
+ addReplyError(c, "PSUBSCRIBE isn't allowed for a DENY BLOCKING client");
return;
}
@@ -409,6 +427,7 @@ void psubscribeCommand(client *c) {
c->flags |= CLIENT_PUBSUB;
}
+/* PUNSUBSCRIBE [pattern [pattern ...]] */
void punsubscribeCommand(client *c) {
if (c->argc == 1) {
pubsubUnsubscribeAllPatterns(c,1);
@@ -421,7 +440,9 @@ void punsubscribeCommand(client *c) {
if (clientSubscriptionsCount(c) == 0) c->flags &= ~CLIENT_PUBSUB;
}
+/* PUBLISH <channel> <message> */
void publishCommand(client *c) {
+ if (pubsubCheckACLPermissionsOrReply(c,1,1,0) != ACL_OK) return;
int receivers = pubsubPublishMessage(c->argv[1],c->argv[2]);
if (server.cluster_enabled)
clusterPropagatePublish(c->argv[1],c->argv[2]);
diff --git a/src/redis-cli.c b/src/redis-cli.c
index c09a6b798..cd10d22e4 100644
--- a/src/redis-cli.c
+++ b/src/redis-cli.c
@@ -1363,9 +1363,16 @@ static int cliSendCommand(int argc, char **argv, long repeat) {
/* Unset our default PUSH handler so this works in RESP2/RESP3 */
redisSetPushCallback(context, NULL);
- while (1) {
+ while (config.pubsub_mode) {
if (cliReadReply(output_raw) != REDIS_OK) exit(1);
+ if (config.last_cmd_type == REDIS_REPLY_ERROR) {
+ if (config.push_output) {
+ redisSetPushCallback(context, cliPushHandler);
+ }
+ config.pubsub_mode = 0;
+ }
}
+ continue;
}
if (config.slave_mode) {
diff --git a/src/scripting.c b/src/scripting.c
index 732cbfc62..3ed3e2f64 100644
--- a/src/scripting.c
+++ b/src/scripting.c
@@ -606,17 +606,31 @@ int luaRedisGenericCommand(lua_State *lua, int raise_error) {
}
/* Check the ACLs. */
- int acl_keypos;
- int acl_retval = ACLCheckCommandPerm(c,&acl_keypos);
+ int acl_errpos;
+ int acl_retval = ACLCheckCommandPerm(c,&acl_errpos);
+ if (acl_retval == ACL_OK && c->cmd->proc == publishCommand)
+ acl_retval = ACLCheckPubsubPerm(c,1,1,0,&acl_errpos);
if (acl_retval != ACL_OK) {
- addACLLogEntry(c,acl_retval,acl_keypos,NULL);
- if (acl_retval == ACL_DENIED_CMD)
+ addACLLogEntry(c,acl_retval,acl_errpos,NULL);
+ switch (acl_retval) {
+ case ACL_DENIED_CMD:
luaPushError(lua, "The user executing the script can't run this "
"command or subcommand");
- else
+ break;
+ case ACL_DENIED_KEY:
luaPushError(lua, "The user executing the script can't access "
"at least one of the keys mentioned in the "
"command arguments");
+ break;
+ case ACL_DENIED_AUTH:
+ luaPushError(lua, "The user executing the script can't publish "
+ "to the channel mentioned in the command");
+ break;
+ default:
+ luaPushError(lua, "The user executing the script is lacking the "
+ "permissions for the command");
+ break;
+ }
goto cleanup;
}
diff --git a/src/server.c b/src/server.c
index 7054b225e..16e7d3fba 100644
--- a/src/server.c
+++ b/src/server.c
@@ -4899,7 +4899,7 @@ void monitorCommand(client *c) {
/**
* A client that has CLIENT_DENY_BLOCKING flag on
* expects a reply per command and so can't execute MONITOR. */
- addReplyError(c, "MONITOR is not allowed for DENY BLOCKING client");
+ addReplyError(c, "MONITOR isn't allowed for DENY BLOCKING client");
return;
}
diff --git a/src/server.h b/src/server.h
index 4690151c0..a46954096 100644
--- a/src/server.h
+++ b/src/server.h
@@ -773,6 +773,8 @@ typedef struct readyList {
no AUTH is needed, and every
connection is immediately
authenticated. */
+#define USER_FLAG_ALLCHANNELS (1<<5) /* The user can mention any Pub/Sub
+ channel. */
typedef struct {
sds name; /* The username as an SDS string. */
uint64_t flags; /* See USER_FLAG_* */
@@ -796,6 +798,10 @@ typedef struct {
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][UNSUSBSCRIBE] command, unless the flag
+ ALLCHANNELS is set in the user. */
} user;
/* With multiplexing we need to take per-client state.
@@ -1488,11 +1494,12 @@ struct redisServer {
long long latency_monitor_threshold;
dict *latency_events;
/* ACLs */
- char *acl_filename; /* ACL Users file. NULL if not configured. */
+ char *acl_filename; /* ACL Users file. NULL if not configured. */
unsigned long acllog_max_len; /* Maximum length of the ACL LOG list. */
- sds requirepass; /* Remember the cleartext password set with the
- old "requirepass" directive for backward
- compatibility with Redis <= 5. */
+ sds requirepass; /* Remember the cleartext password set with
+ the old "requirepass" directive for
+ backward compatibility with Redis <= 5. */
+ int acl_pubusub_default; /* Default ACL pub/sub channels flag */
/* Assert & bug reporting */
int watchdog_period; /* Software watchdog period in ms. 0 = off */
/* System hardware info */
@@ -1961,17 +1968,19 @@ void sendChildCOWInfo(int ptype, char *pname);
extern rax *Users;
extern user *DefaultUser;
void ACLInit(void);
-/* Return values for ACLCheckCommandPerm(). */
+/* Return values for ACLCheckCommandPerm() and ACLCheckPubsubPerm(). */
#define ACL_OK 0
#define ACL_DENIED_CMD 1
#define ACL_DENIED_KEY 2
#define ACL_DENIED_AUTH 3 /* Only used for ACL LOG entries. */
+#define ACL_DENIED_CHANNEL 4 /* Only used for pub/sub commands */
int ACLCheckUserCredentials(robj *username, robj *password);
int ACLAuthenticateUser(client *c, robj *username, robj *password);
unsigned long ACLGetCommandID(const char *cmdname);
void ACLClearCommandID(void);
user *ACLGetUserByName(const char *name, size_t namelen);
int ACLCheckCommandPerm(client *c, int *keyidxptr);
+int ACLCheckPubsubPerm(client *c, int idx, int count, int literal, int *idxptr);
int ACLSetUser(user *u, const char *op, ssize_t oplen);
sds ACLDefaultUserFirstPassword(void);
uint64_t ACLGetCommandCategoryFlagByName(const char *name);
@@ -2096,6 +2105,7 @@ struct redisMemOverhead *getMemoryOverheadData(void);
void freeMemoryOverheadData(struct redisMemOverhead *mh);
void checkChildrenDone(void);
int setOOMScoreAdj(int process_class);
+void rejectCommandFormat(client *c, const char *fmt, ...);
#define RESTART_SERVER_NONE 0
#define RESTART_SERVER_GRACEFULLY (1<<0) /* Do proper shutdown. */
diff --git a/tests/unit/acl.tcl b/tests/unit/acl.tcl
index d35a012f4..856bf6196 100644
--- a/tests/unit/acl.tcl
+++ b/tests/unit/acl.tcl
@@ -81,6 +81,100 @@ start_server {tags {"acl"}} {
set e
} {*NOPERM*key*}
+ test {By default users are able to publish to any channel} {
+ r ACL setuser psuser on >pspass +acl +client +@pubsub
+ r AUTH psuser pspass
+ r PUBLISH foo bar
+ } {0}
+
+ test {By default users are able to subscribe to any channel} {
+ set rd [redis_deferring_client]
+ $rd AUTH psuser pspass
+ $rd read
+ $rd SUBSCRIBE foo
+ assert_match {subscribe foo 1} [$rd read]
+ $rd close
+ } {0}
+
+ test {By default users are able to subscribe to any pattern} {
+ set rd [redis_deferring_client]
+ $rd AUTH psuser pspass
+ $rd read
+ $rd PSUBSCRIBE bar*
+ assert_match {psubscribe bar\* 1} [$rd read]
+ $rd close
+ } {0}
+
+ test {It's possible to allow publishing to a subset of channels} {
+ r ACL setuser psuser resetchannels &foo:1 &bar:*
+ assert_equal {0} [r PUBLISH foo:1 somemessage]
+ assert_equal {0} [r PUBLISH bar:2 anothermessage]
+ catch {r PUBLISH zap:3 nosuchmessage} e
+ set e
+ } {*NOPERM*channel*}
+
+ test {It's possible to allow subscribing to a subset of channels} {
+ set rd [redis_deferring_client]
+ $rd AUTH psuser pspass
+ $rd read
+ $rd SUBSCRIBE foo:1
+ assert_match {subscribe foo:1 1} [$rd read]
+ $rd SUBSCRIBE bar:2
+ assert_match {subscribe bar:2 2} [$rd read]
+ $rd SUBSCRIBE zap:3
+ catch {$rd read} e
+ set e
+ } {*NOPERM*channel*}
+
+ test {It's possible to allow subscribing to a subset of channel patterns} {
+ set rd [redis_deferring_client]
+ $rd AUTH psuser pspass
+ $rd read
+ $rd PSUBSCRIBE foo:1
+ assert_match {psubscribe foo:1 1} [$rd read]
+ $rd PSUBSCRIBE bar:*
+ assert_match {psubscribe bar:\* 2} [$rd read]
+ $rd PSUBSCRIBE bar:baz
+ catch {$rd read} e
+ set e
+ } {*NOPERM*channel*}
+
+ test {Subscribers are killed when revoked of channel permission} {
+ set rd [redis_deferring_client]
+ r ACL setuser psuser resetchannels &foo:1
+ $rd AUTH psuser pspass
+ $rd CLIENT SETNAME deathrow
+ $rd SUBSCRIBE foo:1
+ r ACL setuser psuser resetchannels
+ assert_no_match {*deathrow*} [r CLIENT LIST]
+ $rd close
+ } {0}
+
+ test {Subscribers are killed when revoked of pattern permission} {
+ set rd [redis_deferring_client]
+ r ACL setuser psuser resetchannels &bar:*
+ $rd AUTH psuser pspass
+ $rd CLIENT SETNAME deathrow
+ $rd PSUBSCRIBE bar:*
+ r ACL setuser psuser resetchannels
+ assert_no_match {*deathrow*} [r CLIENT LIST]
+ $rd close
+ } {0}
+
+ test {Subscribers are pardoned if literal permissions are retained and/or gaining allchannels} {
+ set rd [redis_deferring_client]
+ r ACL setuser psuser resetchannels &foo:1 &bar:*
+ $rd AUTH psuser pspass
+ $rd CLIENT SETNAME pardoned
+ $rd SUBSCRIBE foo:1
+ $rd PSUBSCRIBE bar:*
+ r ACL setuser psuser resetchannels &foo:1 &bar:* &baz:qaz &zoo:*
+ assert_match {*pardoned*} [r CLIENT LIST]
+ r ACL setuser psuser allchannels
+ assert_match {*pardoned*} [r CLIENT LIST]
+ $rd close
+ } {0}
+
test {Users can be configured to authenticate with any password} {
r ACL setuser newuser nopass
r AUTH newuser zipzapblabla
@@ -166,6 +260,7 @@ start_server {tags {"acl"}} {
r ACL LOG RESET
r ACL setuser antirez >foo on +set ~object:1234
r ACL setuser antirez +eval +multi +exec
+ r ACL setuser antirez resetchannels +publish
r AUTH antirez foo
catch {r GET foo}
r AUTH default ""
@@ -195,6 +290,15 @@ start_server {tags {"acl"}} {
assert {[dict get $entry object] eq {somekeynotallowed}}
}
+ test {ACL LOG is able to log channel access violations and channel name} {
+ r AUTH antirez foo
+ catch {r PUBLISH somechannelnotallowed nullmsg}
+ r AUTH default ""
+ set entry [lindex [r ACL LOG] 0]
+ assert {[dict get $entry reason] eq {channel}}
+ assert {[dict get $entry object] eq {somechannelnotallowed}}
+ }
+
test {ACL LOG RESET is able to flush the entries in the log} {
r ACL LOG RESET
assert {[llength [r ACL LOG]] == 0}
diff --git a/tests/unit/moduleapi/blockedclient.tcl b/tests/unit/moduleapi/blockedclient.tcl
index 33ecfff22..60039659e 100644
--- a/tests/unit/moduleapi/blockedclient.tcl
+++ b/tests/unit/moduleapi/blockedclient.tcl
@@ -63,7 +63,7 @@ start_server {tags {"modules"}} {
r do_rm_call monitor
} e
set e
- } {*MONITOR is not allow*}
+ } {*ERR*DENY BLOCKING*}
test {subscribe disallow inside RM_Call} {
set e {}
@@ -71,5 +71,5 @@ start_server {tags {"modules"}} {
r do_rm_call subscribe x
} e
set e
- } {*subscribe is not allow*}
+ } {*ERR*DENY BLOCKING*}
}