Skip to content

Commit

Permalink
Support for Redis 6 ACLs (#1791)
Browse files Browse the repository at this point in the history
Add support for Redis 6 ACLs in the `Redis`, `RedisCluster`, and `RedisArray` classes.

On a related note, it adds a mechanism for users to customize how we generate persistent connection IDs such that they can be grouped in different ways depending on the specific use case required (e.g. it would allow connections to be grouped by username, or by user-defined persistent_id, or both).
  • Loading branch information
michael-grunder committed Jun 25, 2020
1 parent 04def9f commit a311cc4
Show file tree
Hide file tree
Showing 25 changed files with 1,641 additions and 656 deletions.
21 changes: 11 additions & 10 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,22 +40,23 @@ before_install:
- ./configure $CFGARGS
install: make install
before_script:
- sudo add-apt-repository ppa:chris-lea/redis-server -y && sudo apt-get update && sudo apt install redis-server
- mkdir -p tests/nodes/ && echo > tests/nodes/nodemap
- redis-server --port 0 --daemonize yes --requirepass phpredis --unixsocket /tmp/redis.sock
- for PORT in $(seq 6379 6382) $(seq 32767 32769); do redis-server --port $PORT --daemonize yes --requirepass phpredis; done
- for PORT in $(seq 7000 7011); do redis-server --port $PORT --cluster-enabled yes --cluster-config-file $PORT.conf --daemonize yes --requirepass phpredis --masterauth phpredis; echo 127.0.0.1:$PORT >> tests/nodes/nodemap; done
- redis-server --port 0 --daemonize yes --aclfile tests/users.acl --unixsocket /tmp/redis.sock
- for PORT in $(seq 6379 6382) $(seq 32767 32769); do redis-server --port $PORT --daemonize yes --aclfile tests/users.acl; done
- for PORT in $(seq 7000 7011); do redis-server --port $PORT --cluster-enabled yes --cluster-config-file $PORT.conf --daemonize yes --aclfile tests/users.acl; echo 127.0.0.1:$PORT >> tests/nodes/nodemap; done
- for PORT in $(seq 26379 26380); do wget download.redis.io/redis-stable/sentinel.conf -O $PORT.conf; echo sentinel auth-pass mymaster phpredis >> $PORT.conf; redis-server $PORT.conf --port $PORT --daemonize yes --sentinel; done
- echo yes | redis-cli --cluster create $(seq -f 127.0.0.1:%g 7000 7011) --cluster-replicas 3 -a phpredis
- echo yes | redis-cli --cluster create $(seq -f 127.0.0.1:%g 7000 7011) --cluster-replicas 3 --user phpredis -a phpredis
- echo 'extension = redis.so' >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini
- openssl req -x509 -newkey rsa:1024 -nodes -keyout stunnel.key -out stunnel.pem -days 1 -subj '/CN=localhost'
- echo -e 'key=stunnel.key\ncert=stunnel.pem\npid=/tmp/stunnel.pid\n[redis]\naccept=6378\nconnect=6379' > stunnel.conf
- stunnel stunnel.conf
script:
- php tests/TestRedis.php --class Redis --auth phpredis
- php tests/TestRedis.php --class RedisArray --auth phpredis
- php tests/TestRedis.php --class RedisCluster --auth phpredis
- php tests/TestRedis.php --class Redis --user phpredis --auth phpredis
- php tests/TestRedis.php --class RedisArray --user phpredis --auth phpredis
- php tests/TestRedis.php --class RedisCluster --user phpredis --auth phpredis
- php tests/TestRedis.php --class RedisSentinel --auth phpredis
- USE_ZEND_ALLOC=0 valgrind --error-exitcode=1 php tests/TestRedis.php --class Redis --auth phpredis
- USE_ZEND_ALLOC=0 valgrind --error-exitcode=1 php tests/TestRedis.php --class RedisArray --auth phpredis
- USE_ZEND_ALLOC=0 valgrind --error-exitcode=1 php tests/TestRedis.php --class RedisCluster --auth phpredis
- USE_ZEND_ALLOC=0 valgrind --error-exitcode=1 php tests/TestRedis.php --class Redis --user phpredis --auth phpredis
- USE_ZEND_ALLOC=0 valgrind --error-exitcode=1 php tests/TestRedis.php --class RedisArray --user phpredis --auth phpredis
- USE_ZEND_ALLOC=0 valgrind --error-exitcode=1 php tests/TestRedis.php --class RedisCluster --user phpredis --auth phpredis
- USE_ZEND_ALLOC=0 valgrind --error-exitcode=1 php tests/TestRedis.php --class RedisSentinel --auth phpredis
33 changes: 30 additions & 3 deletions README.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ session.save_path = "tcp://host1:6379?weight=1, tcp://host2:6379?weight=2&timeou
* timeout (float): the connection timeout to a redis host, expressed in seconds. If the host is unreachable in that amount of time, the session storage will be unavailable for the client. The default timeout is very high (86400 seconds).
* persistent (integer, should be 1 or 0): defines if a persistent connection should be used. **(experimental setting)**
* prefix (string, defaults to "PHPREDIS_SESSION:"): used as a prefix to the Redis key in which the session is stored. The key is composed of the prefix followed by the session ID.
* auth (string, empty by default): used to authenticate with the server prior to sending commands.
* auth (string, or an array with one or two elements): used to authenticate with the server prior to sending commands.
* database (integer): selects a different database.

Sessions have a lifetime expressed in seconds and stored in the INI variable "session.gc_maxlifetime". You can change it with [`ini_set()`](http://php.net/ini_set).
Expand Down Expand Up @@ -270,18 +270,27 @@ $redis->pconnect('/tmp/redis.sock'); // unix domain socket - would be another co

### auth
-----
_**Description**_: Authenticate the connection using a password.
_**Description**_: Authenticate the connection using a password or a username and password.
*Warning*: The password is sent in plain-text over the network.

##### *Parameters*
*STRING*: password
*MIXED*: password

##### *Return value*
*BOOL*: `TRUE` if the connection is authenticated, `FALSE` otherwise.

*Note*: In order to authenticate with a username and password you need Redis >= 6.0.

##### *Example*
~~~php
/* Authenticate with the password 'foobared' */
$redis->auth('foobared');

/* Authenticate with the username 'phpredis', and password 'haxx00r' */
$redis->auth(['phpredis', 'haxx00r']);

/* Authenticate with the password 'foobared' */
$redis->auth(['foobared']);
~~~

### select
Expand Down Expand Up @@ -417,6 +426,7 @@ _**Description**_: Sends a string to Redis, which replies with the same string

## Server

1. [acl](#acl) - Manage Redis ACLs
1. [bgRewriteAOF](#bgrewriteaof) - Asynchronously rewrite the append-only file
1. [bgSave](#bgsave) - Asynchronously save the dataset to disk (in background)
1. [config](#config) - Get or Set the Redis server configuration parameters
Expand All @@ -431,6 +441,23 @@ _**Description**_: Sends a string to Redis, which replies with the same string
1. [time](#time) - Return the current server time
1. [slowLog](#slowlog) - Access the Redis slowLog entries

### acl
-----
_**Description**_: Execute the Redis ACL command.

##### *Parameters*
_variable_: Minumum of one argument for `Redis` and two for `RedisCluster`.

##### *Example*
~~~php
$redis->acl('USERS'); /* Get a list of users */
$redis->acl('LOG'); /* See log of Redis' ACL subsystem */
~~~

*Note*: In order to user the `ACL` command you must be communicating with Redis >= 6.0 and be logged into an account that has access to administration commands such as ACL. Please reference [this tutorial](https://redis.io/topics/acl) for an overview of Redis 6 ACLs and [the redis command reference](https://redis.io/commands) for every ACL subcommand.

*Note*: If you are connecting to Redis server >= 4.0.0 you can remove a key with the `unlink` method in the exact same way you would use `del`. The Redis [unlink](https://redis.io/commands/unlink) command is non-blocking and will perform the actual deletion asynchronously.

### bgRewriteAOF
-----
_**Description**_: Start the background rewrite of AOF (Append-Only File)
Expand Down
67 changes: 40 additions & 27 deletions cluster_library.c
Original file line number Diff line number Diff line change
Expand Up @@ -637,9 +637,7 @@ cluster_node_create(redisCluster *c, char *host, size_t host_len,
node->sock = redis_sock_create(host, host_len, port, c->timeout,
c->read_timeout, c->persistent, NULL, 0);

if (c->flags->auth) {
node->sock->auth = zend_string_copy(c->flags->auth);
}
redis_sock_set_auth(node->sock, c->flags->user, c->flags->pass);

return node;
}
Expand Down Expand Up @@ -850,8 +848,8 @@ cluster_free(redisCluster *c, int free_ctx)

/* Free any allocated prefix */
if (c->flags->prefix) zend_string_release(c->flags->prefix);
/* Free auth info we've got */
if (c->flags->auth) zend_string_release(c->flags->auth);

redis_sock_free_auth(c->flags);
efree(c->flags);

/* Call hash table destructors */
Expand Down Expand Up @@ -1050,10 +1048,8 @@ cluster_init_seeds(redisCluster *cluster, zend_string **seeds, uint32_t nseeds)
(unsigned short)atoi(sep+1), cluster->timeout,
cluster->read_timeout, cluster->persistent, NULL, 0);

// Set auth information if specified
if (cluster->flags->auth) {
sock->auth = zend_string_copy(cluster->flags->auth);
}
/* Credentials */
redis_sock_set_auth(sock, cluster->flags->user, cluster->flags->pass);

// Index this seed by host/port
key_len = snprintf(key, sizeof(key), "%s:%u", ZSTR_VAL(sock->host),
Expand Down Expand Up @@ -2294,11 +2290,40 @@ cluster_xinfo_resp(INTERNAL_FUNCTION_PARAMETERS, redisCluster *c, void *ctx)
add_next_index_zval(&c->multi_resp, &z_ret);
}

static void
cluster_acl_custom_resp(INTERNAL_FUNCTION_PARAMETERS, redisCluster *c, void *ctx,
int (*cb)(RedisSock*, zval*, long))
{
zval z_ret;

array_init(&z_ret);
if (cb(c->cmd_sock, &z_ret, c->reply_len) != SUCCESS) {
zval_dtor(&z_ret);
CLUSTER_RETURN_FALSE(c);
}

if (CLUSTER_IS_ATOMIC(c)) {
RETURN_ZVAL(&z_ret, 0, 1);
}
add_next_index_zval(&c->multi_resp, &z_ret);
}

PHP_REDIS_API void
cluster_acl_getuser_resp(INTERNAL_FUNCTION_PARAMETERS, redisCluster *c, void *ctx) {
cluster_acl_custom_resp(INTERNAL_FUNCTION_PARAM_PASSTHRU, c, ctx, redis_read_acl_getuser_reply);
}

PHP_REDIS_API void
cluster_acl_log_resp(INTERNAL_FUNCTION_PARAMETERS, redisCluster *c, void *ctx) {
cluster_acl_custom_resp(INTERNAL_FUNCTION_PARAM_PASSTHRU, c, ctx, redis_read_acl_log_reply);
}

/* MULTI BULK response loop where we might pull the next one */
PHP_REDIS_API zval *cluster_zval_mbulk_resp(INTERNAL_FUNCTION_PARAMETERS,
redisCluster *c, int pull, mbulk_cb cb, zval *z_ret)
{
ZVAL_NULL(z_ret);

// Pull our next response if directed
if (pull) {
if (cluster_check_response(c, &c->reply_type) < 0)
Expand Down Expand Up @@ -2712,7 +2737,7 @@ void free_seed_array(zend_string **seeds, uint32_t nseeds) {
if (seeds == NULL)
return;

for (i = 0; i < nseeds; i++)
for (i = 0; i < nseeds; i++)
zend_string_release(seeds[i]);

efree(seeds);
Expand Down Expand Up @@ -2771,11 +2796,11 @@ static zend_string **get_valid_seeds(HashTable *input, uint32_t *nseeds) {
/* Validate cluster construction arguments and return a sanitized and validated
* array of seeds */
zend_string**
cluster_validate_args(double timeout, double read_timeout, HashTable *seeds,
uint32_t *nseeds, char **errstr)
cluster_validate_args(double timeout, double read_timeout, HashTable *seeds,
uint32_t *nseeds, char **errstr)
{
zend_string **retval;

if (timeout < 0L || timeout > INT_MAX) {
if (errstr) *errstr = "Invalid timeout";
return NULL;
Expand Down Expand Up @@ -2862,21 +2887,9 @@ PHP_REDIS_API redisCachedCluster *cluster_cache_load(zend_string *hash) {

/* Cache a cluster's slot information in persistent_list if it's enabled */
PHP_REDIS_API int cluster_cache_store(zend_string *hash, HashTable *nodes) {
redisCachedCluster *cc;

/* Construct our cache */
cc = cluster_cache_create(hash, nodes);

/* Set up our resource */
#if PHP_VERSION_ID < 70300
zend_resource le;
le.type = le_cluster_slot_cache;
le.ptr = cc;
redisCachedCluster *cc = cluster_cache_create(hash, nodes);

zend_hash_update_mem(&EG(persistent_list), cc->hash, (void*)&le, sizeof(zend_resource));
#else
zend_register_persistent_resource_ex(cc->hash, cc, le_cluster_slot_cache);
#endif
redis_register_persistent_resource(cc->hash, cc, le_cluster_slot_cache);

return SUCCESS;
}
Expand Down
4 changes: 4 additions & 0 deletions cluster_library.h
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,10 @@ PHP_REDIS_API void cluster_xclaim_resp(INTERNAL_FUNCTION_PARAMETERS,
PHP_REDIS_API void cluster_xinfo_resp(INTERNAL_FUNCTION_PARAMETERS,
redisCluster *c, void *ctx);

/* Custom ACL handlers */
PHP_REDIS_API void cluster_acl_getuser_resp(INTERNAL_FUNCTION_PARAMETERS, redisCluster *c, void *ctx);
PHP_REDIS_API void cluster_acl_log_resp(INTERNAL_FUNCTION_PARAMETERS, redisCluster *c, void *ctx);

/* MULTI BULK processing callbacks */
int mbulk_resp_loop(RedisSock *redis_sock, zval *z_result,
long long count, void *ctx);
Expand Down
7 changes: 6 additions & 1 deletion common.h
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,10 @@ typedef enum {
REDIS_PROCESS_RESPONSE_CLOSURE(resp_func, ctx) \
}

/* Case sensitive compare against compile-time static string */
#define REDIS_STRCMP_STATIC(s, len, sstr) \
(len == sizeof(sstr) - 1 && !strncmp(s, sstr, len))

/* Case insensitive compare against compile-time static string */
#define REDIS_STRICMP_STATIC(s, len, sstr) \
(len == sizeof(sstr) - 1 && !strncasecmp(s, sstr, len))
Expand Down Expand Up @@ -263,7 +267,8 @@ typedef struct {
php_stream_context *stream_ctx;
zend_string *host;
int port;
zend_string *auth;
zend_string *user;
zend_string *pass;
double timeout;
double read_timeout;
long retry_interval;
Expand Down
Loading

0 comments on commit a311cc4

Please sign in to comment.