Lukas Metzger пре 7 година
родитељ
комит
f48b0e8a11

+ 230 - 0
backend/src/controllers/Users.php

@@ -0,0 +1,230 @@
+<?php
+
+namespace Controllers;
+
+require '../vendor/autoload.php';
+
+use \Slim\Http\Request as Request;
+use \Slim\Http\Response as Response;
+
+class Users
+{
+    /** @var \Monolog\Logger */
+    private $logger;
+
+    /** @var \Slim\Container */
+    private $c;
+
+    public function __construct(\Slim\Container $c)
+    {
+        $this->logger = $c->logger;
+        $this->c = $c;
+    }
+
+    public function getList(Request $req, Response $res, array $args)
+    {
+        $ac = new \Operations\AccessControl($this->c);
+        if (!$ac->isAdmin($req->getAttribute('userId'))) {
+            $this->logger->info('Non admin user tries to get users');
+            return $res->withJson(['error' => 'You must be admin to use this feature'], 403);
+        }
+
+        $users = new \Operations\Users($this->c);
+
+        $paging = new \Utils\PagingInfo($req->getQueryParam('page'), $req->getQueryParam('pagesize'));
+        $query = $req->getQueryParam('query');
+        $sort = $req->getQueryParam('sort');
+        $type = $req->getQueryParam('type');
+
+        $results = $users->getUsers($paging, $query, $type, $sort);
+
+        return $res->withJson([
+            'paging' => $paging->toArray(),
+            'results' => $results
+        ], 200);
+    }
+
+    public function postNew(Request $req, Response $res, array $args)
+    {
+        $ac = new \Operations\AccessControl($this->c);
+        if (!$ac->isAdmin($req->getAttribute('userId'))) {
+            $this->logger->info('Non admin user tries to add domain');
+            return $res->withJson(['error' => 'You must be admin to use this feature'], 403);
+        }
+
+        $body = $req->getParsedBody();
+
+        if (!array_key_exists('name', $body) ||
+            !array_key_exists('type', $body) || ($body['type'] === 'SLAVE' && !array_key_exists('master', $body))) {
+            $this->logger->debug('One of the required fields is missing');
+            return $res->withJson(['error' => 'One of the required fields is missing'], 422);
+        }
+
+        $name = $body['name'];
+        $type = $body['type'];
+        $master = isset($body['master']) ? $body['master'] : null;
+
+        $domains = new \Operations\Domains($this->c);
+
+        try {
+            $result = $domains->addDomain($name, $type, $master);
+
+            $this->logger->info('Created domain', $result);
+            return $res->withJson($result, 201);
+        } catch (\Exceptions\AlreadyExistentException $e) {
+            $this->logger->debug('Zone with name ' . $name . ' already exists.');
+            return $res->withJson(['error' => 'Zone with name ' . $name . ' already exists.'], 409);
+        } catch (\Exceptions\SemanticException $e) {
+            $this->logger->info('Invalid type for new domain', ['type' => $type]);
+            return $res->withJson(['error' => 'Invalid type allowed are MASTER, NATIVE and SLAVE'], 400);
+        }
+    }
+
+    public function delete(Request $req, Response $res, array $args)
+    {
+        $ac = new \Operations\AccessControl($this->c);
+        if (!$ac->isAdmin($req->getAttribute('userId'))) {
+            $this->logger->info('Non admin user tries to delete domain');
+            return $res->withJson(['error' => 'You must be admin to use this feature'], 403);
+        }
+
+        $domains = new \Operations\Domains($this->c);
+
+        $domainId = intval($args['domainId']);
+
+        try {
+            $domains->deleteDomain($domainId);
+
+            $this->logger->info('Deleted domain', ['id' => $domainId]);
+            return $res->withStatus(204);
+        } catch (\Exceptions\NotFoundException $e) {
+            return $res->withJson(['error' => 'No domain found for id ' . $domainId], 404);
+        }
+    }
+
+    public function getSingle(Request $req, Response $res, array $args)
+    {
+        $userId = $req->getAttribute('userId');
+        $domainId = intval($args['domainId']);
+
+        $ac = new \Operations\AccessControl($this->c);
+        if (!$ac->canAccessDomain($userId, $domainId)) {
+            $this->logger->info('Non admin user tries to get domain without permission.');
+            return $res->withJson(['error' => 'You have no permissions for this domain.'], 403);
+        }
+
+        $domains = new \Operations\Domains($this->c);
+
+        try {
+            $result = $domains->getDomain($domainId);
+
+            $this->logger->debug('Get domain info', ['id' => $domainId]);
+            return $res->withJson($result, 200);
+        } catch (\Exceptions\NotFoundException $e) {
+            return $res->withJson(['error' => 'No domain found for id ' . $domainId], 404);
+        }
+    }
+
+    public function put(Request $req, Response $res, array $args)
+    {
+        $ac = new \Operations\AccessControl($this->c);
+        if (!$ac->isAdmin($req->getAttribute('userId'))) {
+            $this->logger->info('Non admin user tries to delete domain');
+            return $res->withJson(['error' => 'You must be admin to use this feature'], 403);
+        }
+
+        $body = $req->getParsedBody();
+
+        if (!array_key_exists('master', $body)) {
+            $this->logger->debug('One of the required fields is missing');
+            return $res->withJson(['error' => 'One of the required fields is missing'], 422);
+        }
+
+        $domainId = $args['domainId'];
+        $master = $body['master'];
+
+        $domains = new \Operations\Domains($this->c);
+
+        try {
+            $result = $domains->updateSlave($domainId, $master);
+
+            $this->logger->debug('Update master', ['id' => $domainId]);
+            return $res->withStatus(204);
+        } catch (\Exceptions\NotFoundException $e) {
+            $this->logger->debug('Trying to update non existing slave zone', ['id' => $domainId]);
+            return $res->withJson(['error' => 'No domain found for id ' . $domainId], 404);
+        } catch (\Exceptions\SemanticException $e) {
+            $this->logger->debug('Trying to update non slave zone', ['id' => $domainId]);
+            return $res->withJson(['error' => 'Domain is not a slave zone'], 405);
+        }
+    }
+
+    public function putSoa(Request $req, Response $res, array $args)
+    {
+        $userId = $req->getAttribute('userId');
+        $domainId = $args['domainId'];
+
+        $ac = new \Operations\AccessControl($this->c);
+        if (!$ac->canAccessDomain($userId, $domainId)) {
+            $this->logger->info('Non admin user tries to get domain without permission.');
+            return $res->withJson(['error' => 'You have no permissions for this domain.'], 403);
+        }
+
+        $body = $req->getParsedBody();
+
+        if (!array_key_exists('primary', $body) ||
+            !array_key_exists('email', $body) ||
+            !array_key_exists('refresh', $body) ||
+            !array_key_exists('retry', $body) ||
+            !array_key_exists('expire', $body) ||
+            !array_key_exists('ttl', $body)) {
+            $this->logger->debug('One of the required fields is missing');
+            return $res->withJson(['error' => 'One of the required fields is missing'], 422);
+        }
+
+        $soa = new \Operations\Soa($this->c);
+
+        try {
+            $soa->setSoa(
+                intval($domainId),
+                $body['email'],
+                $body['primary'],
+                intval($body['refresh']),
+                intval($body['retry']),
+                intval($body['expire']),
+                intval($body['ttl'])
+            );
+
+            return $res->withStatus(204);
+        } catch (\Exceptions\NotFoundException $e) {
+            $this->logger->warning('Trying to set soa for not existing domain.', ['domainId' => $domainId]);
+            return $res->withJson(['error' => 'No domain found for id ' . $domainId], 404);
+        } catch (\Exceptions\SemanticException $e) {
+            $this->logger->warning('Trying to set soa for slave domain.', ['domainId' => $domainId]);
+            return $res->withJson(['error' => 'SOA can not be set for slave domains'], 405);
+        }
+    }
+
+    public function getSoa(Request $req, Response $res, array $args)
+    {
+        $userId = $req->getAttribute('userId');
+        $domainId = $args['domainId'];
+
+        $ac = new \Operations\AccessControl($this->c);
+        if (!$ac->canAccessDomain($userId, $domainId)) {
+            $this->logger->info('Non admin user tries to get domain without permission.');
+            return $res->withJson(['error' => 'You have no permissions for this domain.'], 403);
+        }
+
+        $soa = new \Operations\Soa($this->c);
+
+        try {
+            $soaArray = $soa->getSoa($domainId);
+
+            return $res->withJson($soaArray, 200);
+        } catch (\Exceptions\NotFoundException $e) {
+            $this->logger->debug('User tried to get non existing soa.', ['domainId' => $domainId]);
+            return $res->withJson(['error' => 'This domain has no soa record.'], 404);
+        }
+    }
+}

+ 298 - 0
backend/src/operations/Users.php

@@ -0,0 +1,298 @@
+<?php
+
+namespace Operations;
+
+require '../vendor/autoload.php';
+
+/**
+ * This class provides functions for retrieving and modifying users.
+ */
+class Users
+{
+    /** @var \Monolog\Logger */
+    private $logger;
+
+    /** @var \PDO */
+    private $db;
+
+    /** @var \Slim\Container */
+    private $c;
+
+    public function __construct(\Slim\Container $c)
+    {
+        $this->logger = $c->logger;
+        $this->db = $c->db;
+        $this->c = $c;
+    }
+
+    /**
+     * Get a list of users according to filter criteria
+     * 
+     * @param   $pi         PageInfo object, which is also updated with total page number
+     * @param   $nameQuery  Search query, may be null
+     * @param   $type       Type of the user, comma separated, null for no filter
+     * @param   $sorting    Sort string in format 'field-asc,field2-desc', null for default
+     * 
+     * @return  array       Array with matching users
+     */
+    public function getUsers(\Utils\PagingInfo &$pi, ? string $nameQuery, ? string $type, ? string $sorting) : array
+    {
+        $config = $this->c['config']['authentication'];
+
+        $this->db->beginTransaction();
+
+        $nameQuery = $nameQuery !== null ? '%' . $nameQuery . '%' : '%';
+
+        //Count elements
+        if ($pi->pageSize === null) {
+            $pi->totalPages = 1;
+        } else {
+            $query = $this->db->prepare('
+                SELECT COUNT(*) AS total
+                FROM users U
+                WHERE (U.name LIKE :nameQuery) AND
+                (U.type IN ' . \Services\Database::makeSetString($this->db, $type) . ' OR :noTypeFilter)
+            ');
+
+            $query->bindValue(':nameQuery', $nameQuery, \PDO::PARAM_STR);
+            $query->bindValue(':noTypeFilter', intval($type === null), \PDO::PARAM_INT);
+
+            $query->execute();
+            $record = $query->fetch();
+
+            $pi->totalPages = ceil($record['total'] / $pi->pageSize);
+        }
+        
+        //Query and return result
+        $ordStr = \Services\Database::makeSortingString($sorting, [
+            'id' => 'U.id',
+            'name' => 'U.name',
+            'type' => 'U.type'
+        ]);
+        $pageStr = \Services\Database::makePagingString($pi);
+
+        $query = $this->db->prepare('
+            SELECT id, name, type, backend
+            FROM users U
+            WHERE (U.name LIKE :nameQuery) AND
+            (U.type IN ' . \Services\Database::makeSetString($this->db, $type) . ' OR :noTypeFilter)'
+            . $ordStr . $pageStr);
+
+        $query->bindValue(':nameQuery', $nameQuery, \PDO::PARAM_STR);
+        $query->bindValue(':noTypeFilter', intval($type === null), \PDO::PARAM_INT);
+
+        $query->execute();
+
+        $data = $query->fetchAll();
+
+        $this->db->commit();
+
+        $dataTransformed = array_map(
+            function ($item) use ($config) {
+                if (!array_key_exists($item['backend'], $config)) {
+                    return null;
+                }
+                if (!array_key_exists('prefix', $config[$item['backend']])) {
+                    return null;
+                }
+
+                $prefix = $config[$item['backend']]['prefix'];
+
+                if ($prefix === 'default') {
+                    $name = $item['name'];
+                } else {
+                    $name = $prefix . '/' . $item['name'];
+                }
+
+                return [
+                    'id' => intval($item['id']),
+                    'name' => $name,
+                    'type' => $item['type'],
+                    'native' => $item['backend'] === 'native'
+                ];
+            },
+            $data
+        );
+
+        return array_filter($dataTransformed, function ($v) {
+            return $v !== null;
+        });
+    }
+
+    /**
+     * Add new domain
+     * 
+     * @param   $name       Name of the new zone
+     * @param   $type       Type of the new zone
+     * @param   $master     Master for slave zones, otherwise null
+     * 
+     * @return  array       New domain entry
+     * 
+     * @throws  AlreadyExistenException it the domain exists already
+     */
+    public function addDomain(string $name, string $type, ? string $master) : array
+    {
+        if (!in_array($type, [' MASTER ', ' SLAVE ', ' NATIVE '])) {
+            throw new \Exceptions\SemanticException();
+        }
+
+        $this->db->beginTransaction();
+
+        $query = $this->db->prepare(' SELECT id FROM domains WHERE name = : name ');
+        $query->bindValue(' : name ', $name, \PDO::PARAM_STR);
+        $query->execute();
+
+        $record = $query->fetch();
+
+        if ($record !== false) { // Domain already exists
+            $this->db->rollBack();
+            throw new \Exceptions\AlreadyExistentException();
+        }
+
+        if ($type === ' SLAVE ') {
+            $query = $this->db->prepare(' INSERT INTO domains (name, type, master) VALUES(: name, : type, : master) ');
+            $query->bindValue(' : master ', $master, \PDO::PARAM_STR);
+        } else {
+            $query = $this->db->prepare(' INSERT INTO domains (name, type) VALUES (: name, : type) ');
+        }
+        $query->bindValue(' : name ', $name, \PDO::PARAM_STR);
+        $query->bindValue(' : type ', $type, \PDO::PARAM_STR);
+        $query->execute();
+
+
+        $query = $this->db->prepare(' SELECT id, name, type, master FROM domains WHERE name = : name ');
+        $query->bindValue(' : name ', $name, \PDO::PARAM_STR);
+        $query->execute();
+
+        $record = $query->fetch();
+        $record[' id '] = intval($record[' id ']);
+        if ($type !== ' SLAVE ') {
+            unset($record[' master ']);
+        }
+
+        $this->db->commit();
+
+        return $record;
+    }
+
+    /**
+     * Delete domain
+     * 
+     * @param   $id     Id of the domain to delete
+     * 
+     * @return  void
+     * 
+     * @throws  NotFoundException   if domain does not exist
+     */
+    public function deleteDomain(int $id) : void
+    {
+        $this->db->beginTransaction();
+
+        $query = $this->db->prepare(' SELECT id FROM domains WHERE id = : id ');
+        $query->bindValue(' : id ', $id, \PDO::PARAM_INT);
+        $query->execute();
+
+        if ($query->fetch() === false) { //Domain does not exist
+            $this->db->rollBack();
+            throw new \Exceptions\NotFoundException();
+        }
+
+        $query = $this->db->prepare('
+            DELETE E FROM remote E
+            LEFT OUTER JOIN records R ON R . id = E . record
+            WHERE R . domain_id = : id ');
+        $query->bindValue(' : id ', $id, \PDO::PARAM_INT);
+        $query->execute();
+
+        $query = $this->db->prepare(' DELETE FROM records WHERE domain_id = : id ');
+        $query->bindValue(' : id ', $id, \PDO::PARAM_INT);
+        $query->execute();
+
+        $query = $this->db->prepare(' DELETE FROM domains WHERE id = : id ');
+        $query->bindValue(' : id ', $id, \PDO::PARAM_INT);
+        $query->execute();
+
+        $this->db->commit();
+    }
+
+    /**
+     * Get domain
+     * 
+     * @param   $id     Id of the domain to get
+     * 
+     * @return  array   Domain data
+     * 
+     * @throws  NotFoundException   if domain does not exist
+     */
+    public function getDomain(int $id) : array
+    {
+        $query = $this->db->prepare('
+            SELECT D . id, D . name, D . type, D . master, COUNT (R . domain_id) as records FROM domains D
+            LEFT OUTER JOIN records R ON D . id = R . domain_id
+            WHERE D . id = : id
+            GROUP BY D . id, D . name, D . type, D . master
+            ');
+        $query->bindValue(' : id ', $id, \PDO::PARAM_INT);
+        $query->execute();
+
+        $record = $query->fetch();
+
+        if ($record === false) {
+            throw new \Exceptions\NotFoundException();
+        }
+
+        $record[' id '] = intval($record[' id ']);
+        $record[' records '] = intval($record[' records ']);
+        if ($record[' type '] !== ' SLAVE ') {
+            unset($record[' master ']);
+        }
+
+        return $record;
+    }
+
+    /**
+     * Get type of given domain
+     * 
+     * @param   int     Domain id
+     * 
+     * @return  string  Domain type
+     * 
+     * @throws  NotFoundException   if domain does not exist
+     */
+    public function getDomainType(int $id) : string
+    {
+        $query = $this->db->prepare(' SELECT type FROM domains WHERE id = : id ');
+        $query->bindValue(' : id ', $id, \PDO::PARAM_INT);
+        $query->execute();
+        $record = $query->fetch();
+
+        if ($record === false) {
+            throw new \Exceptions\NotFoundException();
+        }
+
+        return $record[' type '];
+    }
+
+    /**
+     * Update master for slave zone
+     * 
+     * @param   int     Domain id
+     * @param   string  New master
+     * 
+     * @return  void
+     * 
+     * @throws  NotFoundException   if domain does not exist
+     * @throws  SemanticException   if domain is no slave zone
+     */
+    public function updateSlave(int $id, string $master)
+    {
+        if ($this->getDomainType($id) !== ' SLAVE ') {
+            throw new \Exceptions\SemanticException();
+        }
+
+        $query = $this->db->prepare(' UPDATE domains SET master = : master WHERE id = : id ');
+        $query->bindValue(' : id ', $id, \PDO::PARAM_INT);
+        $query->bindValue(' : master', $master, \PDO::PARAM_STR);
+        $query->execute();
+    }
+}

+ 2 - 0
backend/src/public/index.php

@@ -48,6 +48,8 @@ $app->group('/v1', function () {
         $this->get('/records/{recordId}/credentials/{credentialId}', '\Controllers\Credentials:getSingle');
         $this->put('/records/{recordId}/credentials/{credentialId}', '\Controllers\Credentials:put');
 
+        $this->get('/users', '\Controllers\Users:getList');
+
         $this->get('/users/{user}/permissions', '\Controllers\Permissions:getList');
         $this->post('/users/{user}/permissions', '\Controllers\Permissions:postNew');
         $this->delete('/users/{user}/permissions/{domainId}', '\Controllers\Permissions:delete');

+ 137 - 0
backend/test/tests/users-get.js

@@ -0,0 +1,137 @@
+const test = require('../testlib');
+const cartesianProduct = require('cartesian-product');
+
+test.run(async function () {
+    await test('admin', async function (assert, req) {
+        //Test sorting in all combinations
+        const sortCombinations = cartesianProduct([
+            ['', 'id-asc', 'id-desc'],
+            ['', 'name-asc', 'name-desc'],
+            ['', 'type-asc', 'type-desc'],
+        ]);
+
+        for (list of sortCombinations) {
+            list = list.filter((str) => str.length > 0);
+            var sortQuery = list.join(',');
+
+            var res = await req({
+                url: '/users?sort=' + sortQuery,
+                method: 'get'
+            });
+
+            assert.equal(res.status, 200);
+
+            var sortedData = res.data.results.slice();
+            sortedData.sort(function (a, b) {
+                for (sort of list) {
+                    var spec = sort.split('-');
+                    if (a[spec[0]] < b[spec[0]]) {
+                        return spec[1] == 'asc' ? -1 : 1;
+                    } else if (a[spec[0]] > b[spec[0]]) {
+                        return spec[1] == 'asc' ? 1 : -1;
+                    }
+                }
+                return 0;
+            });
+
+            assert.equal(res.data.results, sortedData, 'Sort failed for ' + res.config.url);
+        }
+
+        //Test paging
+        var res = await req({
+            url: '/users?pagesize=2',
+            method: 'get'
+        });
+
+        assert.equal(res.status, 200, 'Status should be OK');
+        assert.equal(res.data.paging, {
+            page: 1,
+            total: 2,
+            pagesize: 2
+        }, 'Paging data fail for ' + res.config.url);
+        assert.equal(res.data.results.length, 2, "Should be 2 results.");
+
+        var res = await req({
+            url: '/users?pagesize=2&page=2',
+            method: 'get'
+        });
+
+        assert.equal(res.status, 200, 'Status should be OK');
+        assert.equal(res.data.paging, {
+            page: 2,
+            total: 2,
+            pagesize: 2
+        }, 'Paging data fail for ' + res.config.url);
+        assert.equal(res.data.results.length, 1, "Should be 2 results.");
+
+        //Test query name
+        var res = await req({
+            url: '/users?query=user&sort=id-asc',
+            method: 'get'
+        });
+
+        assert.equal(res.status, 200, 'Status should be OK');
+        assert.equal(res.data.results, [
+            {
+                id: 2,
+                name: 'user',
+                type: 'user',
+                native: true
+            },
+            {
+                id: 3,
+                name: 'config/configuser',
+                type: 'user',
+                native: false
+            }
+        ], 'Result fail for ' + res.config.url);
+
+        //Type filter
+        var res = await req({
+            url: '/users?type=admin,user',
+            method: 'get'
+        });
+
+        assert.equal(res.status, 200, 'Status should be OK');
+        assert.equal(res.data.results.length, 3, 'Result fail for ' + res.config.url);
+
+        //Type filter
+        var res = await req({
+            url: '/users?type=admin',
+            method: 'get'
+        });
+
+        assert.equal(res.status, 200, 'Status should be OK');
+        assert.equal(res.data.results, [
+            {
+                id: 1,
+                name: 'admin',
+                type: 'admin',
+                native: true
+            }
+        ], 'Result fail for ' + res.config.url);
+
+        //Query all check for format
+        var res = await req({
+            url: '/users?sort=id-asc',
+            method: 'get'
+        });
+
+        assert.equal(res.status, 200, 'Status should be OK');
+        assert.equal(res.data.results, [
+            { id: 1, name: 'admin', type: 'admin', native: true },
+            { id: 2, name: 'user', type: 'user', native: true },
+            { id: 3, name: 'config/configuser', type: 'user', native: false }
+        ], 'Result fail for ' + res.config.url);
+    });
+
+    await test('user', async function (assert, req) {
+        //Type filter
+        var res = await req({
+            url: '/users',
+            method: 'get'
+        });
+
+        assert.equal(res.status, 403, 'Get should fail for user');
+    });
+});