From 969829296c266bf50faa589a5feaadc267486e60 Mon Sep 17 00:00:00 2001 From: Stephen Hoogendijk Date: Sun, 2 Aug 2015 22:13:17 +0200 Subject: [PATCH] Added admin functionality; allowed user credentials in guzzle driver --- README.md | 55 +++++++- src/InfluxDB/Client.php | 66 +++++---- src/InfluxDB/Client/Admin.php | 158 ++++++++++++++++++++++ src/InfluxDB/Database.php | 16 ++- src/InfluxDB/Driver/DriverInterface.php | 1 - src/InfluxDB/Driver/Guzzle.php | 33 ++++- tests/unit/AbstractTest.php | 124 +++++++++++++++++ tests/unit/AdminTest.php | 72 ++++++++++ tests/unit/ClientTest.php | 27 ++-- tests/unit/DatabaseTest.php | 56 +------- tests/unit/result-test-users.example.json | 24 ++++ 11 files changed, 532 insertions(+), 100 deletions(-) create mode 100644 src/InfluxDB/Client/Admin.php create mode 100644 tests/unit/AbstractTest.php create mode 100644 tests/unit/AdminTest.php create mode 100644 tests/unit/result-test-users.example.json diff --git a/README.md b/README.md index 9705151f88..06f60943af 100644 --- a/README.md +++ b/README.md @@ -262,9 +262,58 @@ Some functions are too general for a database. So these are available in the cli $result = $client->listDatabases(); ``` +### Admin functionality + +You can use the client's $client->admin functionality to administer InfluxDB via the API. + +```php + // add a new user without privileges + $client->admin->createUser('testuser123', 'testpassword'); + + // add a new user with ALL cluster-wide privileges + $client->admin->createUser('admin_user', 'password', \InfluxDB\Client\Admin::PRIVILEGE_ALL); + + // drop user testuser123 + $client->admin->dropUser('testuser123'); +``` + +List all the users: + +```php + // show a list of all users + $results = $client->admin->showUsers(); + + // show users returns a ResultSet object + $users = $results->getPoints(); +``` + +#### Granting and revoking privileges + +Granting permissions can be done on both the database level and cluster-wide. +To grant a user specific privileges on a database, provide a database object or a database name. + +```php + + // grant permissions using a database object + $database = $client->selectDB('test_db'); + $client->admin->grant(\InfluxDB\Client\Admin::PRIVILEGE_READ, 'testuser123', $database); + + // give user testuser123 read privileges on database test_db + $client->admin->grant(\InfluxDB\Client\Admin::PRIVILEGE_READ, 'testuser123', 'test_db'); + + // revoke user testuser123's read privileges on database test_db + $client->admin->revoke(\InfluxDB\Client\Admin::PRIVILEGE_READ, 'testuser123', 'test_db'); + + // grant a user cluster-wide privileges + $client->admin->grant(\InfluxDB\Client\Admin::PRIVILEGE_READ, 'testuser123'); + + // Revoke an admin's cluster-wide privileges + $client->admin->revoke(\InfluxDB\Client\Admin::PRIVILEGE_ALL, 'admin_user'); + +``` + ## Todo -* Add more admin features * More unit tests * Increase documentation (wiki?) * Add more features to the query builder @@ -273,6 +322,10 @@ Some functions are too general for a database. So these are available in the cli ## Changelog +####1.0.1 +* Added support for authentication in the guzzle driver +* Added admin functionality + ####1.0.0 * -BREAKING CHANGE- Dropped support for PHP 5.3 and PHP 5.4 * Allowing for custom drivers diff --git a/src/InfluxDB/Client.php b/src/InfluxDB/Client.php index 2c8b614fa4..b724c63f04 100644 --- a/src/InfluxDB/Client.php +++ b/src/InfluxDB/Client.php @@ -2,8 +2,10 @@ namespace InfluxDB; +use InfluxDB\Client\Admin; use InfluxDB\Client\Exception as ClientException; use InfluxDB\Driver\DriverInterface; +use InfluxDB\Driver\Exception as DriverException; use InfluxDB\Driver\Guzzle; use InfluxDB\Driver\QueryDriverInterface; use InfluxDB\Driver\UDP; @@ -16,6 +18,11 @@ use InfluxDB\Driver\UDP; */ class Client { + /** + * @var Admin + */ + public $admin; + /** * @var string */ @@ -124,6 +131,8 @@ class Client ] ) ); + + $this->admin = new Admin($this); } /** @@ -142,11 +151,12 @@ class Client * * @param string $database * @param string $query - * @param array $params + * @param array $parameters + * * @return ResultSet * @throws Exception */ - public function query($database, $query, $params = []) + public function query($database, $query, $parameters = []) { if (!$this->driver instanceof QueryDriverInterface) { @@ -154,22 +164,29 @@ class Client } if ($database) { - $params['db'] = $database; + $parameters['db'] = $database; } $driver = $this->getDriver(); - $driver->setParameters([ - 'url' => 'query?' . http_build_query(array_merge(['q' => $query], $params)), - 'database' => $database, - 'method' => 'get' - ]); + $parameters = [ + 'url' => 'query?' . http_build_query(array_merge(['q' => $query], $parameters)), + 'database' => $database, + 'method' => 'get' + ]; + + // add authentication to the driver if needed + if (!empty($this->username) && !empty($this->password)) { + $parameters += ['auth' => [$this->username, $this->password]]; + } + + $driver->setParameters($parameters); try { // perform the query and return the resultset return $driver->query(); - } catch (\Exception $e) { + } catch (DriverException $e) { throw new Exception('Query has failed', $e->getCode(), $e); } } @@ -265,21 +282,6 @@ class Client return $this->timeout; } - /** - * @param Point[] $points - * @return array - */ - protected function pointsToArray(array $points) - { - $names = []; - - foreach ($points as $item) { - $names[] = $item['name']; - } - - return $names; - } - /** * @param Driver\DriverInterface $driver */ @@ -303,4 +305,20 @@ class Client { return $this->host; } + + /** + * @param Point[] $points + * @return array + */ + protected function pointsToArray(array $points) + { + $names = []; + + foreach ($points as $item) { + $names[] = $item['name']; + } + + return $names; + } + } diff --git a/src/InfluxDB/Client/Admin.php b/src/InfluxDB/Client/Admin.php new file mode 100644 index 0000000000..b62782a38c --- /dev/null +++ b/src/InfluxDB/Client/Admin.php @@ -0,0 +1,158 @@ +client = $client; + } + + /** + * Create a user + * + * @param string $username + * @param string $password + * + * @param string $privilege + * + * @throws \InfluxDB\Exception + * @return \InfluxDB\ResultSet + */ + public function createUser($username, $password, $privilege = null) + { + $query = sprintf('CREATE USER %s WITH PASSWORD \'%s\'', $username, $password); + + if ($privilege) { + $query .= " WITH $privilege PRIVILEGES"; + } + + return $this->client->query(null, $query); + } + + /** + * @param string $username + * + * @return \InfluxDB\ResultSet + * @throws \InfluxDB\Exception + */ + public function dropUser($username) + { + return $this->client->query(null, 'DROP USER ' . $username); + } + + /** + * Change a users password + * + * @param string $username + * @param string $newPassword + * + * @return \InfluxDB\ResultSet + * @throws \InfluxDB\Exception + */ + public function changeUserPassword($username, $newPassword) + { + return $this->client->query(null, "SET PASSWORD FOR $username = '$newPassword'"); + } + + /** + * Shows a list of all the users + * + * @return \InfluxDB\ResultSet + * @throws \InfluxDB\Exception + */ + public function showUsers() + { + return $this->client->query(null, "SHOW USERS"); + } + + /** + * Grants permissions + * + * @param string $privilege + * @param string $username + * @param Database|string $database + * + * @return \InfluxDB\ResultSet + */ + public function grant($privilege, $username, $database = null) + { + return $this->executePrivilege('GRANT', $privilege, $username, $database); + } + + /** + * Revokes permissions + * + * @param string $privilege + * @param string $username + * @param Database|string $database + * + * @throws \InfluxDB\Exception + * @return \InfluxDB\ResultSet + */ + public function revoke($privilege, $username, $database = null) + { + return $this->executePrivilege('REVOKE', $privilege, $username, $database); + } + + /** + * @param string $type + * @param string $privilege + * @param string $username + * @param Database|string $database + * + * @throws \InfluxDB\Exception + * @return \InfluxDB\ResultSet + */ + private function executePrivilege($type, $privilege, $username, $database = null) + { + + if (!in_array($privilege, [self::PRIVILEGE_READ, self::PRIVILEGE_WRITE, self::PRIVILEGE_ALL])) { + throw new Exception($privilege . ' is not a valid privileges, allowed privileges: READ, WRITE, ALL'); + } + + if ($privilege != self::PRIVILEGE_ALL && !$database) { + throw new Exception('Only grant ALL cluster-wide privileges are allowed'); + } + + $database = ($database instanceof Database ? $database->getName() : (string) $database); + + $query = "$type $privilege"; + + if ($database) { + $query .= sprintf(' ON %s ', $database); + } else { + $query .= " PRIVILEGES "; + } + + if ($username && $type == 'GRANT') { + $query .= "TO $username"; + } elseif ($username && $type == 'REVOKE') { + $query .= "FROM $username"; + } + + return $this->client->query(null, $query); + } +} \ No newline at end of file diff --git a/src/InfluxDB/Database.php b/src/InfluxDB/Database.php index 515fb1cb54..3ca7163f85 100644 --- a/src/InfluxDB/Database.php +++ b/src/InfluxDB/Database.php @@ -9,8 +9,6 @@ use InfluxDB\Query\Builder as QueryBuilder; /** * Class Database * - * @todo admin functionality - * * @package InfluxDB * @author Stephen "TheCodeAssassin" Hoogendijk */ @@ -128,11 +126,19 @@ class Database try { $driver = $this->client->getDriver(); - $driver->setParameters([ + + $parameters = [ 'url' => sprintf('write?db=%s&precision=%s', $this->name, $precision), 'database' => $this->name, 'method' => 'post' - ]); + ]; + + // add authentication to the driver if needed + if (!empty($this->username) && !empty($this->password)) { + $parameters += ['auth' => [$this->username, $this->password]]; + } + + $driver->setParameters($parameters); // send the points to influxDB $driver->write(implode(PHP_EOL, $payload)); @@ -140,7 +146,7 @@ class Database return $driver->isSuccess(); } catch (\Exception $e) { - throw new Exception('Writing has failed', $e->getCode(), $e); + throw new Exception($e->getMessage(), $e->getCode()); } } diff --git a/src/InfluxDB/Driver/DriverInterface.php b/src/InfluxDB/Driver/DriverInterface.php index 5400ba0511..fc3a2c2f96 100644 --- a/src/InfluxDB/Driver/DriverInterface.php +++ b/src/InfluxDB/Driver/DriverInterface.php @@ -13,7 +13,6 @@ namespace InfluxDB\Driver; interface DriverInterface { - /** * Called by the client write() method, will pass an array of required parameters such as db name * diff --git a/src/InfluxDB/Driver/Guzzle.php b/src/InfluxDB/Driver/Guzzle.php index d014c1f3be..a3f1d91112 100644 --- a/src/InfluxDB/Driver/Guzzle.php +++ b/src/InfluxDB/Driver/Guzzle.php @@ -7,6 +7,7 @@ namespace InfluxDB\Driver; use GuzzleHttp\Client; use Guzzle\Http\Message\Response; +use GuzzleHttp\Exception\ClientException; use GuzzleHttp\Psr7\Request; use InfluxDB\ResultSet; @@ -77,18 +78,26 @@ class Guzzle implements DriverInterface, QueryDriverInterface */ public function write($data = null) { - $this->response = $this->httpClient->post($this->parameters['url'], ['body' => $data]); + $this->response = $this->httpClient->post($this->parameters['url'], $this->getRequestParameters($data)); } /** + * @throws Exception * @return ResultSet */ public function query() { - $response = $this->httpClient->get($this->parameters['url']); + + $response = $this->httpClient->get($this->parameters['url'], $this->getRequestParameters()); $raw = (string) $response->getBody(); + $responseJson = json_encode($raw); + + if (isset($responseJson->error)) { + throw new Exception($responseJson->error); + } + return new ResultSet($raw); } @@ -102,4 +111,24 @@ class Guzzle implements DriverInterface, QueryDriverInterface { return in_array($this->response->getStatusCode(), ['200', '204']); } + + /** + * @param null $data + * + * @return array + */ + protected function getRequestParameters($data = null) + { + $requestParameters = ['http_errors' => false]; + + if ($data) { + $requestParameters += ['body' => $data]; + } + + if (isset($this->parameters['auth'])) { + $requestParameters += ['auth' => $this->parameters['auth']]; + } + + return $requestParameters; + } } diff --git a/tests/unit/AbstractTest.php b/tests/unit/AbstractTest.php new file mode 100644 index 0000000000..160f6aea66 --- /dev/null +++ b/tests/unit/AbstractTest.php @@ -0,0 +1,124 @@ +mockClient = $this->getMockBuilder('\InfluxDB\Client') + ->disableOriginalConstructor() + ->getMock(); + + $this->resultData = file_get_contents(dirname(__FILE__) . '/result.example.json'); + + $this->mockClient->expects($this->any()) + ->method('getBaseURI') + ->will($this->returnValue($this->equalTo('http://localhost:8086'))); + + $this->mockClient->expects($this->any()) + ->method('query') + ->will($this->returnValue(new ResultSet($this->resultData))); + + $httpMockClient = new Guzzle($this->buildHttpMockClient('')); + + // make sure the client has a valid driver + $this->mockClient->expects($this->any()) + ->method('getDriver') + ->will($this->returnValue($httpMockClient)); + + $this->database = new Database('influx_test_db', $this->mockClient); + + } + + /** + * @return mixed + */ + public function getMockResultSet() + { + return $this->mockResultSet; + } + + /** + * @param mixed $mockResultSet + */ + public function setMockResultSet($mockResultSet) + { + $this->mockResultSet = $mockResultSet; + } + + + /** + * @return GuzzleClient + */ + public function buildHttpMockClient($body) + { + // Create a mock and queue two responses. + $mock = new MockHandler([new Response(200, array(), $body)]); + + $handler = HandlerStack::create($mock); + return new GuzzleClient(['handler' => $handler]); + } + + /** + * @return string + */ + public function getEmptyResult() + { + return $this->emptyResult; + } + + /** + * @param bool $emptyResult + * + * @return PHPUnit_Framework_MockObject_MockObject|Client + */ + public function getClientMock($emptyResult = false) + { + $mockClient = $this->getMockBuilder('\InfluxDB\Client') + ->disableOriginalConstructor() + ->getMock(); + + if ($emptyResult) { + $mockClient->expects($this->once()) + ->method('query') + ->will($this->returnValue(new ResultSet($this->getEmptyResult()))); + } + + return $mockClient; + } +} \ No newline at end of file diff --git a/tests/unit/AdminTest.php b/tests/unit/AdminTest.php new file mode 100644 index 0000000000..889874f4c6 --- /dev/null +++ b/tests/unit/AdminTest.php @@ -0,0 +1,72 @@ +getAdminObject(true); + + $this->assertEquals( + new ResultSet($this->emptyResult), + $adminObject->createUser('test', 'test', Client\Admin::PRIVILEGE_ALL) + ); + } + + public function testChangeUserPassword() + { + $adminObject = $this->getAdminObject(true); + + $this->assertEquals( + new ResultSet($this->emptyResult), + $adminObject->changeUserPassword('test', 'test') + ); + } + + public function testShowUsers() + { + $testJson = file_get_contents(dirname(__FILE__) . '/result-test-users.example.json'); + + $clientMock = $this->getClientMock(); + $testResult = new ResultSet($testJson); + + $clientMock->expects($this->once()) + ->method('query') + ->will($this->returnValue($testResult)); + + $adminMock = new Client\Admin($clientMock); + + $this->assertEquals($testResult, $adminMock->showUsers()); + } + + /** + * @return Client\Admin + */ + private function getAdminObject() + { + return new Client\Admin($this->getClientMock(true)); + } + +} \ No newline at end of file diff --git a/tests/unit/ClientTest.php b/tests/unit/ClientTest.php index 095d534b03..09f420d1bf 100644 --- a/tests/unit/ClientTest.php +++ b/tests/unit/ClientTest.php @@ -2,16 +2,20 @@ namespace InfluxDB\Test; -use GuzzleHttp\Client as GuzzleClient; -use GuzzleHttp\Handler\MockHandler; -use GuzzleHttp\HandlerStack; -use GuzzleHttp\Psr7\Response; use InfluxDB\Client; use InfluxDB\Driver\Guzzle; -class ClientTest extends \PHPUnit_Framework_TestCase +class ClientTest extends AbstractTest { + /** + * + */ + public function setUp() + { + parent::setUp(); + } + /** @var Client $client */ protected $client = null; @@ -43,7 +47,7 @@ class ClientTest extends \PHPUnit_Framework_TestCase $query = "some-bad-query"; $bodyResponse = file_get_contents(dirname(__FILE__) . '/result.example.json'); - $httpMockClient = self::buildHttpMockClient($bodyResponse); + $httpMockClient = $this->buildHttpMockClient($bodyResponse); $client->setDriver(new Guzzle($httpMockClient)); @@ -53,15 +57,4 @@ class ClientTest extends \PHPUnit_Framework_TestCase $this->assertInstanceOf('\InfluxDB\ResultSet', $result); } - /** - * @return GuzzleClient - */ - public static function buildHttpMockClient($body) - { - // Create a mock and queue two responses. - $mock = new MockHandler([new Response(200, array(), $body)]); - - $handler = HandlerStack::create($mock); - return new GuzzleClient(['handler' => $handler]); - } } \ No newline at end of file diff --git a/tests/unit/DatabaseTest.php b/tests/unit/DatabaseTest.php index 25fcd1cf7c..e410385476 100644 --- a/tests/unit/DatabaseTest.php +++ b/tests/unit/DatabaseTest.php @@ -10,30 +10,14 @@ use InfluxDB\ResultSet; use PHPUnit_Framework_MockObject_MockObject; use PHPUnit_Framework_TestCase; -class DatabaseTest extends PHPUnit_Framework_TestCase +class DatabaseTest extends AbstractTest { - /** @var Database $db */ - protected $db = null; - - /** @var Client|PHPUnit_Framework_MockObject_MockObject $client */ - protected $mockClient; - /** * @var string */ protected $dataToInsert; - /** - * @var string - */ - protected $resultData; - - /** - * @var string - */ - static $emptyResult = '{"results":[{}]}'; - /** * @var */ @@ -41,34 +25,14 @@ class DatabaseTest extends PHPUnit_Framework_TestCase public function setUp() { - $this->mockClient = $this->getMockBuilder('\InfluxDB\Client') - ->disableOriginalConstructor() - ->getMock(); + parent::setUp(); $this->resultData = file_get_contents(dirname(__FILE__) . '/result.example.json'); - $this->mockClient->expects($this->any()) - ->method('getBaseURI') - ->will($this->returnValue($this->equalTo('http://localhost:8086'))); - - $this->mockClient->expects($this->any()) - ->method('query') - ->will($this->returnValue(new ResultSet($this->resultData))); - - $this->mockClient->expects($this->any()) ->method('listDatabases') ->will($this->returnValue(array('test123', 'test'))); - $httpMockClient = new Guzzle(ClientTest::buildHttpMockClient('')); - - // make sure the client has a valid driver - $this->mockClient->expects($this->any()) - ->method('getDriver') - ->will($this->returnValue($httpMockClient)); - - $this->db = new Database('influx_test_db', $this->mockClient); - $this->dataToInsert = file_get_contents(dirname(__FILE__) . '/input.example.json'); } @@ -79,26 +43,18 @@ class DatabaseTest extends PHPUnit_Framework_TestCase public function testQuery() { $testResultSet = new ResultSet($this->resultData); - $this->assertEquals($this->db->query('SELECT * FROM test_metric'), $testResultSet); + $this->assertEquals($this->database->query('SELECT * FROM test_metric'), $testResultSet); } public function testCreateRetentionPolicy() { $retentionPolicy = new Database\RetentionPolicy('test', '1d', 1, true); - $mockClient = $this->getMockBuilder('\InfluxDB\Client') - ->disableOriginalConstructor() - ->getMock(); - - $mockClient->expects($this->once()) - ->method('query') - ->will($this->returnValue(new ResultSet(self::$emptyResult))); - - + $mockClient = $this->getClientMock(true); $database = new Database('test', $mockClient); - $this->assertEquals($database->createRetentionPolicy($retentionPolicy), new ResultSet(self::$emptyResult)); + $this->assertEquals($database->createRetentionPolicy($retentionPolicy), new ResultSet($this->getEmptyResult())); } /** @@ -139,6 +95,6 @@ class DatabaseTest extends PHPUnit_Framework_TestCase 0.84 ); - $this->assertEquals(true, $this->db->writePoints(array($point1, $point2))); + $this->assertEquals(true, $this->database->writePoints(array($point1, $point2))); } } \ No newline at end of file diff --git a/tests/unit/result-test-users.example.json b/tests/unit/result-test-users.example.json new file mode 100644 index 0000000000..34f5f999b6 --- /dev/null +++ b/tests/unit/result-test-users.example.json @@ -0,0 +1,24 @@ +{ + "results": [ + { + "series": [ + { + "columns": [ + "user", + "admin" + ], + "values": [ + [ + "test1", + true + ], + [ + "test2", + false + ] + ] + } + ] + } + ] +} \ No newline at end of file