Skip to content

Commit

Permalink
Redis auth (#5)
Browse files Browse the repository at this point in the history
* added redis authentication support (#3)

* several improvements to support auth on redis 5 and 6

Co-authored-by: Amit Kumar Kannaujiya <[email protected]>
  • Loading branch information
palicao and AmitKannaujiya authored Aug 31, 2020
1 parent 4f91d29 commit ef4da71
Show file tree
Hide file tree
Showing 16 changed files with 333 additions and 104 deletions.
19 changes: 12 additions & 7 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
language: php

dist: bionic

php:
Expand All @@ -8,21 +7,27 @@ php:
- 7.4

env:
- REDIS_HOST=localhost
- REDIS_HOST=localhost REBLOOM_VERSION=2.2.0 # Redis 5.0.7
- REDIS_HOST=localhost REBLOOM_VERSION=2.2.4 # Redis 6.0.5
- REDIS_HOST=localhost REBLOOM_VERSION=edge

jobs:
allow_failures:
- env: REDIS_HOST=localhost REBLOOM_VERSION=edge

services:
- docker

before_install:
- docker pull redislabs/rebloom:2.2.0
- docker run -d -p 127.0.0.1:6379:6379 --name redis redislabs/rebloom:2.2.0
- yes | pecl install igbinary redis || true
- docker pull redislabs/rebloom:${REBLOOM_VERSION}
- docker run -d -p 127.0.0.1:6379:6379 --name redis redislabs/rebloom:${REBLOOM_VERSION}
- yes | pecl upgrade igbinary redis || true
- echo 'extension = redis.so' >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini

before_script:
- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
- chmod +x ./cc-test-reporter
- if [ $(phpenv version-name) = "7.4" ] && [ "$TRAVIS_PULL_REQUEST" == "false" ]; then ./cc-test-reporter before-build; fi
- if [ $(phpenv version-name) = "7.4" ] && [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$REBLOOM_VERSION" == "2.2.4" ]; then ./cc-test-reporter before-build; fi

script:
- composer install
Expand All @@ -32,4 +37,4 @@ script:
after_script:
- docker stop redis
- docker rm redis
- if [ $(phpenv version-name) = "7.4" ] && [ "$TRAVIS_PULL_REQUEST" == "false" ]; then ./cc-test-reporter after-build --coverage-input-type clover --id $CC_TEST_REPORTER_ID --exit-code $TRAVIS_TEST_RESULT; fi
- if [ $(phpenv version-name) = "7.4" ] && [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$REBLOOM_VERSION" == "2.2.4" ]; then ./cc-test-reporter after-build --coverage-input-type clover --id $CC_TEST_REPORTER_ID --exit-code $TRAVIS_TEST_RESULT; fi
1 change: 0 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,3 @@ RUN pecl install redis && \
RUN wget https://github.com/composer/composer/releases/download/1.9.1/composer.phar -q &&\
mv composer.phar /usr/bin/composer && \
chmod +x /usr/bin/composer

12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@

Use [Redis Bloom](https://oss.redislabs.com/redisbloom/) with PHP!

![Code Climate maintainability](https://img.shields.io/codeclimate/coverage-letter/palicao/phpRebloom?label=maintainability&logo=code-climate)
![Code Climate coverage](https://img.shields.io/codeclimate/coverage/palicao/phpRebloom?logo=code-climate)
[![Code Climate maintainability](https://img.shields.io/codeclimate/coverage-letter/palicao/phpRebloom?label=maintainability&logo=code-climate)](https://codeclimate.com/github/palicao/phpRebloom)
[![Code Climate coverage](https://img.shields.io/codeclimate/coverage/palicao/phpRebloom?logo=code-climate)](https://codeclimate.com/github/palicao/phpRebloom)
[![Build Status](https://travis-ci.com/palicao/phpRebloom.svg?branch=master)](https://travis-ci.com/palicao/phpRebloom)
[![Latest Stable Version](https://img.shields.io/packagist/v/palicao/php-rebloom.svg)](https://packagist.org/packages/palicao/php-rebloom)
![PHP version](https://img.shields.io/packagist/php-v/palicao/php-rebloom/0.1.0)
![GitHub](https://img.shields.io/github/license/palicao/phpRebloom)
[![PHP version](https://img.shields.io/packagist/php-v/palicao/php-rebloom/0.1.0)]((https://packagist.org/packages/palicao/php-rebloom))
[![GitHub](https://img.shields.io/github/license/palicao/phpRebloom)](https://github.com/palicao/phpRebloom/blob/master/LICENSE)

<small>Disclaimer: this is a very lightweight library. For a battery-included experience,
please checkout: https://github.com/averias/phpredis-bloom</small>
Disclaimer: this is a very lightweight library. For a battery-included experience,
please checkout: https://github.com/averias/phpredis-bloom

## Install
`composer require palicao/php-rebloom`
Expand Down
4 changes: 1 addition & 3 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,4 @@ services:
- .:/app
redis:
container_name: redis
image: redislabs/rebloom:2.2.0


image: redislabs/rebloom:2.2.4
2 changes: 1 addition & 1 deletion src/BaseFrequencyCounter.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,4 @@ protected function parseResult($result): bool
{
return $result === 'OK' ? true : (bool) $result;
}
}
}
10 changes: 10 additions & 0 deletions src/Exception/RedisAuthenticationException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);

namespace Palicao\PhpRebloom\Exception;

use RuntimeException;

final class RedisAuthenticationException extends RuntimeException
{
}
42 changes: 39 additions & 3 deletions src/RedisClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

namespace Palicao\PhpRebloom;

use Palicao\PhpRebloom\Exception\RedisAuthenticationException;
use Palicao\PhpRebloom\Exception\RedisClientException;
use Redis;
use RedisException;
Expand All @@ -18,12 +19,12 @@ class RedisClient
public function __construct(Redis $redis, RedisConnectionParams $connectionParams)
{
$this->redis = $redis;

$this->connectionParams = $connectionParams;
}

/**
* @throws RedisClientException
* @throws RedisAuthenticationException
*/
private function connectIfNeeded(): void
{
Expand Down Expand Up @@ -62,9 +63,13 @@ private function connectIfNeeded(): void
$this->redis->getLastError() ?? 'unknown error'
));
}

$this->authenticate($params->getUsername(), $params->getPassword());
}

/**
* @noinspection PhpDocRedundantThrowsInspection
*
* @param array $params
* @return mixed
* @throws RedisException
Expand All @@ -73,9 +78,40 @@ private function connectIfNeeded(): void
public function executeCommand(array $params)
{
$this->connectIfNeeded();
// UNDOCUMENTED FEATURE: option 8 is REDIS_OPT_REPLY_LITERAL

$value = (PHP_VERSION_ID < 70300) ? '1' : 1;
$this->redis->setOption(8, $value);
$this->redis->setOption(Redis::OPT_REPLY_LITERAL, $value);

return $this->redis->rawCommand(...$params);
}

/**
* @param string|null $username
* @param string|null $password
* @throws RedisAuthenticationException
*/
private function authenticate(?string $username, ?string $password): void
{
try {
if ($password) {
if ($username) {
// Calling auth() with an array throws a TypeError in some cases
/** @noinspection PhpMethodParametersCountMismatchInspection */
$result = $this->redis->rawCommand('AUTH', $username, $password);
} else {
/** @psalm-suppress PossiblyNullArgument */
$result = $this->redis->auth($password);
}
if ($result === false) {
throw new RedisAuthenticationException(sprintf(
'Failure authenticating user %s', $username ?: 'default'
));
}
}
} /** @noinspection PhpRedundantCatchClauseInspection */ catch (RedisException $e) {
throw new RedisAuthenticationException(sprintf(
'Failure authenticating user %s: %s', $username ?: 'default', $e->getMessage()
));
}
}
}
26 changes: 25 additions & 1 deletion src/RedisConnectionParams.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,25 @@ final class RedisConnectionParams
/** @var float */
private $readTimeout;

public function __construct(string $host = '127.0.0.1', int $port = 6379)
/** @var string|null */
private $username;

/** @var string|null */
private $password;

/**
* @param string $host
* @param int $port
* @param string|null $username Only supported by Redis 6
* @param string|null $password
*/
public function __construct(string $host = '127.0.0.1', int $port = 6379, string $username = null, string $password = null)
{
$this->persistentConnection = false;
$this->host = $host;
$this->port = $port;
$this->username = $username;
$this->password = $password;
$this->timeout = 0;
$this->retryInterval = 0;
$this->readTimeout = 0.0;
Expand Down Expand Up @@ -106,4 +120,14 @@ public function getReadTimeout(): float
{
return $this->readTimeout;
}

public function getUsername(): ?string
{
return $this->username;
}

public function getPassword(): ?string
{
return $this->password;
}
}
54 changes: 54 additions & 0 deletions tests/Integration/AuthRedis5Test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php
/* @noinspection PhpUnhandledExceptionInspection */

declare(strict_types=1);

namespace Palicao\PhpRebloom\Tests\Integration;

use Palicao\PhpRebloom\BloomFilter;
use Palicao\PhpRebloom\Exception\RedisAuthenticationException;
use Palicao\PhpRebloom\RedisClient;
use Palicao\PhpRebloom\RedisConnectionParams;
use Redis;

class AuthRedis5Test extends IntegrationTestCase
{
public function setUp(): void
{
if (self::getRedisMajorVersion() > 5) {
self::markTestSkipped('This test is supposed to run on Redis version 5 or lower');
}
parent::setUp();
}

public function testAuthWithPasswordSuccess(): void
{
$this->redisClient->executeCommand(['CONFIG', 'SET', 'requirepass', 'pass123']);
$this->redis->close();
$connectionParams = new RedisConnectionParams(self::getHost(), self::getPort(), null, 'pass123');
$authorizedRedisClient = new RedisClient(new Redis(), $connectionParams);
$bloomFilter = new BloomFilter($authorizedRedisClient);
$result = $bloomFilter->reserve('reserveTest', .0001, 100);
self::assertTrue($result);
$authorizedRedisClient->executeCommand(['CONFIG', 'SET', 'requirepass', '']);
}

public function testAuthWithPasswordFailure(): void
{
$this->expectException(RedisAuthenticationException::class);
$this->redisClient->executeCommand(['CONFIG', 'SET', 'requirepass', 'pass123']);
$this->redis->close();
$connectionParams = new RedisConnectionParams(self::getHost(), self::getPort(), null, 'foobar');
$nonAuthorizedRedisClient = new RedisClient(new Redis(), $connectionParams);
$bloomFilter = new BloomFilter($nonAuthorizedRedisClient);
try {
$bloomFilter->reserve('reserveTest', .0001, 100);
} catch (RedisAuthenticationException $exception) {
throw $exception;
} finally {
$connectionParams = new RedisConnectionParams(self::getHost(), self::getPort(), null, 'pass123');
$authorizedRedisClient = new RedisClient(new Redis(), $connectionParams);
$authorizedRedisClient->executeCommand(['CONFIG', 'SET', 'requirepass', '']);
}
}
}
85 changes: 85 additions & 0 deletions tests/Integration/AuthRedis6Test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?php
/* @noinspection PhpUnhandledExceptionInspection */

declare(strict_types=1);

namespace Palicao\PhpRebloom\Tests\Integration;

use Palicao\PhpRebloom\BloomFilter;
use Palicao\PhpRebloom\Exception\RedisAuthenticationException;
use Palicao\PhpRebloom\RedisClient;
use Palicao\PhpRebloom\RedisConnectionParams;
use Redis;

class AuthRedis6Test extends IntegrationTestCase
{
public function setUp(): void
{
if (self::getRedisMajorVersion() < 6) {
self::markTestSkipped('This test is supposed to run on Redis version 6 or higher');
}
parent::setUp();
}

public function testAuthWithPasswordSuccess(): void
{
$this->redisClient->executeCommand(['ACL', 'SETUSER', 'default', '>pass123', '+@all']);
$this->redis->close();
$connectionParams = new RedisConnectionParams(self::getHost(), self::getPort(), null, 'pass123');
$authorizedRedisClient = new RedisClient(new Redis(), $connectionParams);
$bloomFilter = new BloomFilter($authorizedRedisClient);
$result = $bloomFilter->reserve('reserveTest', .0001, 100);
self::assertTrue($result);
$authorizedRedisClient->executeCommand(['ACL', 'SETUSER', 'default', 'nopass']);
}

public function testAuthWithPasswordFailure(): void
{
$this->expectException(RedisAuthenticationException::class);
$this->redisClient->executeCommand(['ACL', 'SETUSER', 'default', '>pass123', '+@all']);
$this->redis->close();
$connectionParams = new RedisConnectionParams(self::getHost(), self::getPort(), null, 'foobar');
$nonAuthorizedRedisClient = new RedisClient(new Redis(), $connectionParams);
$bloomFilter = new BloomFilter($nonAuthorizedRedisClient);
try {
$bloomFilter->reserve('reserveTest', .0001, 100);
} catch (RedisAuthenticationException $exception) {
throw $exception;
} finally {
$connectionParams = new RedisConnectionParams(self::getHost(), self::getPort(), null, 'pass123');
$authorizedRedisClient = new RedisClient(new Redis(), $connectionParams);
$authorizedRedisClient->executeCommand(['ACL', 'SETUSER', 'default', 'nopass']);
}
}

public function testAuthWithUsernameAndPasswordSuccess(): void
{
$this->redisClient->executeCommand(['ACL', 'SETUSER', 'username', 'on', '>pass123', '~*', '+@all']);
$this->redis->close();
$connectionParams = new RedisConnectionParams(self::getHost(), self::getPort(), 'username', 'pass123');
$authorizedRedisClient = new RedisClient(new Redis(), $connectionParams);
$bloomFilter = new BloomFilter($authorizedRedisClient);
$result = $bloomFilter->reserve('reserveTest', .0001, 100);
self::assertTrue($result);
$authorizedRedisClient->executeCommand(['ACL', 'DELUSER', 'username']);
}

public function testAuthWithUsernameAndPasswordFailure(): void
{
$this->expectException(RedisAuthenticationException::class);
$this->redisClient->executeCommand(['ACL', 'SETUSER', 'username', 'on', '>pass123', '~*', '+@all']);
$this->redis->close();
$connectionParams = new RedisConnectionParams(self::getHost(), self::getPort(), 'username', 'foobar');
$nonAuthorizedRedisClient = new RedisClient(new Redis(), $connectionParams);
$bloomFilter = new BloomFilter($nonAuthorizedRedisClient);
try {
$bloomFilter->reserve('reserveTest', .0001, 100);
} catch (RedisAuthenticationException $exception) {
throw $exception;
} finally {
$connectionParams = new RedisConnectionParams(self::getHost(), self::getPort(), 'username', 'pass123');
$authorizedRedisClient = new RedisClient(new Redis(), $connectionParams);
$authorizedRedisClient->executeCommand(['ACL', 'DELUSER', 'username']);
}
}
}
Loading

0 comments on commit ef4da71

Please sign in to comment.