diff --git a/backend/src/controllers/Users.php b/backend/src/controllers/Users.php index c7dc1f0..197d3d7 100644 --- a/backend/src/controllers/Users.php +++ b/backend/src/controllers/Users.php @@ -48,35 +48,36 @@ class Users { $ac = new \Operations\AccessControl($this->c); if (!$ac->isAdmin($req->getAttribute('userId'))) { - $this->logger->info('Non admin user tries to add domain'); + $this->logger->info('Non admin user tries to add user'); 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))) { + !array_key_exists('type', $body) || + !array_key_exists('password', $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; + $password = $body['password']; - $domains = new \Operations\Domains($this->c); + $users = new \Operations\Users($this->c); try { - $result = $domains->addDomain($name, $type, $master); + $result = $users->addUser($name, $type, $password); - $this->logger->info('Created domain', $result); + $this->logger->info('Created user', $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); + $this->logger->debug('User with name ' . $name . ' already exists.'); + return $res->withJson(['error' => 'User 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); + $this->logger->info('Invalid type for new user', ['type' => $type]); + return $res->withJson(['error' => 'Invalid type allowed are admin and user'], 400); } } @@ -84,147 +85,81 @@ class Users { $ac = new \Operations\AccessControl($this->c); if (!$ac->isAdmin($req->getAttribute('userId'))) { - $this->logger->info('Non admin user tries to delete domain'); + $this->logger->info('Non admin user tries to delete user'); return $res->withJson(['error' => 'You must be admin to use this feature'], 403); } - $domains = new \Operations\Domains($this->c); + $users = new \Operations\Users($this->c); - $domainId = intval($args['domainId']); + $user = intval($args['user']); try { - $domains->deleteDomain($domainId); + $users->deleteDomain($user); - $this->logger->info('Deleted domain', ['id' => $domainId]); + $this->logger->info('Deleted user', ['id' => $user]); return $res->withStatus(204); } catch (\Exceptions\NotFoundException $e) { - return $res->withJson(['error' => 'No domain found for id ' . $domainId], 404); + return $res->withJson(['error' => 'No user 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); + if ($args['user'] === 'me') { + $user = $req->getAttribute('userId'); + } elseif ($ac->isAdmin($req->getAttribute('userId'))) { + $user = intval($args['user']); + } else { + $this->logger->info('Non admin user tries to get other user'); + return $res->withJson(['error' => 'You must be admin to use this feature'], 403); } - $domains = new \Operations\Domains($this->c); + $users = new \Operations\Users($this->c); try { - $result = $domains->getDomain($domainId); + $result = $users->getUser($user); - $this->logger->debug('Get domain info', ['id' => $domainId]); + $this->logger->debug('Get user info', ['id' => $user]); return $res->withJson($result, 200); } catch (\Exceptions\NotFoundException $e) { - return $res->withJson(['error' => 'No domain found for id ' . $domainId], 404); + return $res->withJson(['error' => 'No user found for id ' . $user], 404); } } public function put(Request $req, Response $res, array $args) { + $body = $req->getParsedBody(); + + $name = array_key_exists('name', $body) ? $body['name'] : null; + $type = array_key_exists('type', $body) ? $body['type'] : null; + $password = array_key_exists('password', $body) ? $body['password'] : null; + $ac = new \Operations\AccessControl($this->c); - if (!$ac->isAdmin($req->getAttribute('userId'))) { - $this->logger->info('Non admin user tries to delete domain'); + if ($args['user'] === 'me') { + $user = $req->getAttribute('userId'); + $name = null; + $type = null; + } elseif ($ac->isAdmin($req->getAttribute('userId'))) { + $user = intval($args['user']); + } else { + $this->logger->info('Non admin user tries to get other user'); 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); + $users = new \Operations\Users($this->c); try { - $result = $domains->updateSlave($domainId, $master); + $result = $users->updateUser($user, $name, $type, $password); - $this->logger->debug('Update master', ['id' => $domainId]); + $this->logger->debug('Update user', ['id' => $user]); 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); + $this->logger->debug('Trying to update non existing user', ['id' => $user]); + return $res->withJson(['error' => 'No user found for id ' . $user], 404); + } catch (\Exceptions\AlreadyExistentException $e) { + $this->logger->debug('Trying to rename user to conflicting name', ['id' => $user]); + return $res->withJson(['error' => 'The new name already exists.'], 409); } } } diff --git a/backend/src/operations/Users.php b/backend/src/operations/Users.php index d24c6f9..7b3e4d1 100644 --- a/backend/src/operations/Users.php +++ b/backend/src/operations/Users.php @@ -120,26 +120,26 @@ class Users } /** - * Add new domain + * Add new user * * @param $name Name of the new zone * @param $type Type of the new zone - * @param $master Master for slave zones, otherwise null + * @param $password Password for the new user * - * @return array New domain entry + * @return array New user entry * - * @throws AlreadyExistenException it the domain exists already + * @throws AlreadyExistenException it the user exists already */ - public function addDomain(string $name, string $type, ? string $master) : array + public function addUser(string $name, string $type, string $password) : array { - if (!in_array($type, [' MASTER ', ' SLAVE ', ' NATIVE '])) { + if (!in_array($type, ['admin', 'user'])) { 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 = $this->db->prepare('SELECT id FROM users WHERE name=:name AND backend=\'native\''); + $query->bindValue(':name', $name, \PDO::PARAM_STR); $query->execute(); $record = $query->fetch(); @@ -149,26 +149,20 @@ class Users 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); + $passwordHash = password_hash($password, PASSWORD_DEFAULT); + + $query = $this->db->prepare('INSERT INTO users (name, backend, type, password) VALUES(:name, \'native\', :type, :password)'); + $query->bindValue(':name', $name, \PDO::PARAM_STR); + $query->bindValue(':type', $type, \PDO::PARAM_STR); + $query->bindValue(':password', $passwordHash, \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 = $this->db->prepare('SELECT id,name,type FROM users WHERE name=:name AND backend=\'native\''); + $query->bindValue(':name', $name, \PDO::PARAM_STR); $query->execute(); $record = $query->fetch(); - $record[' id '] = intval($record[' id ']); - if ($type !== ' SLAVE ') { - unset($record[' master ']); - } + $record['id'] = intval($record['id']); $this->db->commit(); @@ -176,63 +170,53 @@ class Users } /** - * Delete domain + * Delete user * - * @param $id Id of the domain to delete + * @param $id Id of the user to delete * * @return void * - * @throws NotFoundException if domain does not exist + * @throws NotFoundException if user 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 = $this->db->prepare('SELECT id FROM users WHERE id=:id'); + $query->bindValue(':id', $id, \PDO::PARAM_INT); $query->execute(); - if ($query->fetch() === false) { //Domain does not exist + if ($query->fetch() === false) { //User 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 = $this->db->prepare('DELETE FROM permissions WHERE user_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 = $this->db->prepare('DELETE FROM users WHERE id=:id'); + $query->bindValue(':id', $id, \PDO::PARAM_INT); $query->execute(); $this->db->commit(); } /** - * Get domain + * Get user * - * @param $id Id of the domain to get + * @param $id Id of the user to get * - * @return array Domain data + * @return array User data * - * @throws NotFoundException if domain does not exist + * @throws NotFoundException if user does not exist */ - public function getDomain(int $id) : array + public function getUser(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); + $config = $this->c['config']['authentication']; + + $query = $this->db->prepare('SELECT id,name,type,backend FROM users WHERE id=:id'); + $query->bindValue(':id', $id, \PDO::PARAM_INT); $query->execute(); $record = $query->fetch(); @@ -241,58 +225,84 @@ class Users throw new \Exceptions\NotFoundException(); } - $record[' id '] = intval($record[' id ']); - $record[' records '] = intval($record[' records ']); - if ($record[' type '] !== ' SLAVE ') { - unset($record[' master ']); + if (!array_key_exists($record['backend'], $config)) { + throw new \Exceptions\NotFoundException(); } - - 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) { + if (!array_key_exists('prefix', $config[$record['backend']])) { throw new \Exceptions\NotFoundException(); } - return $record[' type ']; + $prefix = $config[$record['backend']]['prefix']; + + if ($prefix === 'default') { + $name = $record['name']; + } else { + $name = $prefix . '/' . $record['name']; + } + + return [ + 'id' => intval($record['id']), + 'name' => $name, + 'type' => $record['type'], + 'native' => $record['backend'] === 'native' + ]; } - /** - * Update master for slave zone + /** Update user * - * @param int Domain id - * @param string New master + * If params are null do not change. If user is not native, name and password are ignored. + * + * @param $userId User to update + * @param $name New name + * @param $type New type + * @param $password New password * * @return void * - * @throws NotFoundException if domain does not exist - * @throws SemanticException if domain is no slave zone + * @throws NotFoundException The given record does not exist + * @throws AlreadyExistentException The given record name does already exist */ - public function updateSlave(int $id, string $master) + public function updateUser(int $userId, ? string $name, ? string $type, ? string $password) { - if ($this->getDomainType($id) !== ' SLAVE ') { - throw new \Exceptions\SemanticException(); + $this->db->beginTransaction(); + + $query = $this->db->prepare('SELECT id,name,type,backend,password FROM users WHERE id=:userId'); + $query->bindValue(':userId', $userId); + $query->execute(); + + $record = $query->fetch(); + + if ($record === false) { + $this->db->rollBack(); + throw new \Exceptions\NotFoundException(); } - $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); + if ($record['backend'] !== 'native') { + $name = null; + $password = null; + } + + if ($record['backend'] === 'native' && $name !== null) { + //Check if user already exists + $query = $this->db->prepare('SELECT id FROM users WHERE name=:name AND backend=\'native\''); + $query->bindValue(':name', $name); + $query->execute(); + if ($query->fetch() !== false) { + throw new \Exceptions\AlreadyExistentException(); + } + } + + $name = $name === null ? $record['name'] : $name; + $type = $type === null ? $record['type'] : $type; + $password = $password === null ? $record['password'] : password_hash($password, PASSWORD_DEFAULT); + + $query = $this->db->prepare('UPDATE users SET name=:name,type=:type,password=:password WHERE id=:userId'); + $query->bindValue(':userId', $userId); + $query->bindValue(':name', $name); + $query->bindValue(':type', $type); + $query->bindValue(':password', $password); $query->execute(); + + $this->db->commit(); } } diff --git a/backend/src/public/index.php b/backend/src/public/index.php index d2ffbda..a8405f8 100644 --- a/backend/src/public/index.php +++ b/backend/src/public/index.php @@ -49,6 +49,10 @@ $app->group('/v1', function () { $this->put('/records/{recordId}/credentials/{credentialId}', '\Controllers\Credentials:put'); $this->get('/users', '\Controllers\Users:getList'); + $this->post('/users', '\Controllers\Users:postNew'); + $this->delete('/users/{user}', '\Controllers\Users:delete'); + $this->get('/users/{user}', '\Controllers\Users:getSingle'); + $this->put('/users/{user}', '\Controllers\Users:put'); $this->get('/users/{user}/permissions', '\Controllers\Permissions:getList'); $this->post('/users/{user}/permissions', '\Controllers\Permissions:postNew'); diff --git a/backend/test/tests/users-crud.js b/backend/test/tests/users-crud.js new file mode 100644 index 0000000..3ee7b7d --- /dev/null +++ b/backend/test/tests/users-crud.js @@ -0,0 +1,209 @@ +const test = require('../testlib'); + +test.run(async function () { + await test('admin', async function (assert, req) { + //Test missing fields + var res = await req({ + url: '/users', + method: 'post', + data: { + name: 'newadmin', + type: 'admin' + } + }); + assert.equal(res.status, 422, 'Missing fields should trigger error.'); + + //Test invalid type + var res = await req({ + url: '/users', + method: 'post', + data: { + name: 'newadmin', + type: 'foo', + password: 'foo' + } + }); + assert.equal(res.status, 400, 'Invalid type should trigger error.'); + + //Test duplicate user + var res = await req({ + url: '/users', + method: 'post', + data: { + name: 'admin', + type: 'admin', + password: 'foo' + } + }); + assert.equal(res.status, 409, 'Duplicate user should trigger error.'); + + //Test user creation + var res = await req({ + url: '/users', + method: 'post', + data: { + name: 'newadmin', + type: 'admin', + password: 'newadmin' + } + }); + assert.equal(res.status, 201, 'User creation should succeed.'); + assert.equal(res.data, { id: 4, name: 'newadmin', type: 'admin' }, 'Add user data fail.'); + + //Test if new user can log in + var res = await req({ + url: '/sessions', + method: 'post', + data: { + username: 'newadmin', + password: 'newadmin' + } + }); + assert.equal(res.status, 201, 'Login with new user should succeed.'); + + //Test user get + var res = await req({ + url: '/users/4', + method: 'get' + }); + assert.equal(res.status, 200, 'New user should be found.'); + assert.equal(res.data, { id: 4, name: 'newadmin', type: 'admin', native: true }, 'New user data fail.'); + + //Test user change without data + var res = await req({ + url: '/users/4', + method: 'put', + data: { dummy: 'foo' } + }); + assert.equal(res.status, 204, 'Update without field should succeed.'); + + //Test user get + var res = await req({ + url: '/users/4', + method: 'get' + }); + assert.equal(res.status, 200, 'New user should be found after update.'); + assert.equal(res.data, { id: 4, name: 'newadmin', type: 'admin', native: true }, 'New user should not change by noop update.'); + + //Test user update + var res = await req({ + url: '/users/4', + method: 'put', + data: { + name: 'foo', + password: 'bar', + type: 'user' + } + }); + assert.equal(res.status, 204, 'Update should succeed.'); + + //Test if updated user can log in + var res = await req({ + url: '/sessions', + method: 'post', + data: { + username: 'foo', + password: 'bar' + } + }); + assert.equal(res.status, 201, 'Login with updated user should succeed.'); + + //Test user get + var res = await req({ + url: '/users/4', + method: 'get' + }); + assert.equal(res.status, 200, 'New user should be found after second update.'); + assert.equal(res.data, { id: 4, name: 'foo', type: 'user', native: true }, 'New user should change by update.'); + + //Test user update conflict + var res = await req({ + url: '/users/4', + method: 'put', + data: { + name: 'admin' + } + }); + assert.equal(res.status, 409, 'Update with existent name should fail.'); + + //Test user delete for not existing user + var res = await req({ + url: '/users/100', + method: 'delete' + }); + assert.equal(res.status, 404, 'Deletion of not existens user should fail.'); + + //Test user delete + var res = await req({ + url: '/users/4', + method: 'delete' + }); + assert.equal(res.status, 204, 'Deletion of user should succeed.'); + + var res = await req({ + url: '/users/4', + method: 'get' + }); + assert.equal(res.status, 404, 'New user should not be found after deletion.'); + + // Test me alias get + var res = await req({ + url: '/users/me', + method: 'get' + }); + assert.equal(res.status, 200, 'Admin should be able to use /me.'); + assert.equal(res.data, { id: 1, name: 'admin', type: 'admin', native: true }, 'Admin /me data fail.'); + + // Test me alias update + var res = await req({ + url: '/users/me', + method: 'put', + data: { + password: 'abc' + } + }); + assert.equal(res.status, 204, 'Admin should be able to update /me.'); + + //Test if updated user can log in + var res = await req({ + url: '/sessions', + method: 'post', + data: { + username: 'admin', + password: 'abc' + } + }); + assert.equal(res.status, 201, 'Login with updated admin should succeed.'); + }); + + await test('user', async function (assert, req) { + // Test me alias get + var res = await req({ + url: '/users/me', + method: 'get' + }); + assert.equal(res.status, 200, 'User should be able to use /me.'); + assert.equal(res.data, { id: 2, name: 'user', type: 'user', native: true }, 'User /me data fail.'); + + // Test me alias update + var res = await req({ + url: '/users/me', + method: 'put', + data: { + password: 'abc' + } + }); + assert.equal(res.status, 204, 'User should be able to update /me.'); + + //Test if updated user can log in + var res = await req({ + url: '/sessions', + method: 'post', + data: { + username: 'user', + password: 'abc' + } + }); + assert.equal(res.status, 201, 'Login with updated user should succeed.'); + }); +}); \ No newline at end of file