Implemented als CRUD operations for /records

This commit is contained in:
Lukas Metzger 2018-03-30 14:02:32 +02:00
parent 4a7f884fb6
commit 2f41db98e9
7 changed files with 610 additions and 2 deletions

View file

@ -22,6 +22,16 @@ $defaultConfig = [
'plugin' => 'native',
'config' => null
]
],
'records' => [
'allowedTypes' => [
'A', 'A6', 'AAAA', 'AFSDB', 'ALIAS', 'CAA', 'CDNSKEY', 'CDS', 'CERT', 'CNAME', 'DHCID',
'DLV', 'DNAME', 'DNSKEY', 'DS', 'EUI48', 'EUI64', 'HINFO',
'IPSECKEY', 'KEY', 'KX', 'LOC', 'MAILA', 'MAILB', 'MINFO', 'MR',
'MX', 'NAPTR', 'NS', 'NSEC', 'NSEC3', 'NSEC3PARAM', 'OPENPGPKEY',
'OPT', 'PTR', 'RKEY', 'RP', 'RRSIG', 'SIG', 'SPF',
'SRV', 'TKEY', 'SSHFP', 'TLSA', 'TSIG', 'TXT', 'WKS', 'MBOXFW', 'URL'
]
]
];

View file

@ -41,4 +41,117 @@ class Records
'results' => $results
], 200);
}
public function postNew(Request $req, Response $res, array $args)
{
$body = $req->getParsedBody();
if (!array_key_exists('name', $body) ||
!array_key_exists('type', $body) ||
!array_key_exists('content', $body) ||
!array_key_exists('priority', $body) ||
!array_key_exists('ttl', $body) ||
!array_key_exists('domain', $body)) {
$this->logger->debug('One of the required fields is missing');
return $res->withJson(['error' => 'One of the required fields is missing'], 422);
}
$userId = $req->getAttribute('userId');
$ac = new \Operations\AccessControl($this->c);
if (!$ac->canAccessDomain($userId, $body['domain'])) {
$this->logger->info('User tries to add record for domain without permission.');
return $res->withJson(['error' => 'You have no permissions for the given domain.'], 403);
}
$records = new \Operations\Records($this->c);
try {
$result = $records->addRecord($body['name'], $body['type'], $body['content'], $body['priority'], $body['ttl'], $body['domain']);
return $res->withJson($result, 201);
} catch (\Exceptions\NotFoundException $e) {
$this->logger->debug('User tries to add record for invalid domain.');
return $res->withJson(['error' => 'The domain does not exist or is neighter MASTER nor NATIVE.'], 404);
} catch (\Exceptions\SemanticException $e) {
$this->logger->debug('User tries to add record with invalid type.', ['type' => $type]);
return $res->withJson(['error' => 'The provided type is invalid.'], 400);
}
}
public function delete(Request $req, Response $res, array $args)
{
$userId = $req->getAttribute('userId');
$recordId = intval($args['recordId']);
$ac = new \Operations\AccessControl($this->c);
if (!$ac->canAccessRecord($userId, $recordId)) {
$this->logger->info('User tries to delete record without permissions.');
return $res->withJson(['error' => 'You have no permission to delete this record'], 403);
}
$records = new \Operations\Records($this->c);
try {
$records->deleteRecord($recordId);
$this->logger->info('Deleted record', ['id' => $recordId]);
return $res->withStatus(204);
} catch (\Exceptions\NotFoundException $e) {
return $res->withJson(['error' => 'No record found for id ' . $recordId], 404);
}
}
public function getSingle(Request $req, Response $res, array $args)
{
$userId = $req->getAttribute('userId');
$recordId = intval($args['recordId']);
$ac = new \Operations\AccessControl($this->c);
if (!$ac->canAccessRecord($userId, $recordId)) {
$this->logger->info('Non admin user tries to get record without permission.');
return $res->withJson(['error' => 'You have no permissions for this record.'], 403);
}
$records = new \Operations\Records($this->c);
try {
$result = $records->getRecord($recordId);
$this->logger->debug('Get record info', ['id' => $recordId]);
return $res->withJson($result, 200);
} catch (\Exceptions\NotFoundException $e) {
return $res->withJson(['error' => 'No record found for id ' . $domainId], 404);
}
}
public function put(Request $req, Response $res, array $args)
{
$userId = $req->getAttribute('userId');
$recordId = intval($args['recordId']);
$ac = new \Operations\AccessControl($this->c);
if (!$ac->canAccessRecord($userId, $recordId)) {
$this->logger->info('Non admin user tries to update record without permission.');
return $res->withJson(['error' => 'You have no permissions for this record.'], 403);
}
$body = $req->getParsedBody();
$name = array_key_exists('name', $body) ? $body['name'] : null;
$type = array_key_exists('type', $body) ? $body['type'] : null;
$content = array_key_exists('content', $body) ? $body['content'] : null;
$priority = array_key_exists('priority', $body) ? $body['priority'] : null;
$ttl = array_key_exists('ttl', $body) ? $body['ttl'] : null;
$records = new \Operations\Records($this->c);
try {
$records->updateRecord($recordId, $name, $type, $content, $priority, $ttl);
return $res->withStatus(204);
} catch (\Exceptions\NotFoundException $e) {
$this->logger->debug('User tries to update not existing record.');
return $res->withJson(['error' => 'The record does not exist.'], 404);
} catch (\Exceptions\SemanticException $e) {
$this->logger->debug('User tries to update record with invalid type.', ['type' => $type]);
return $res->withJson(['error' => 'The provided type is invalid.'], 400);
}
}
}

View file

@ -71,4 +71,36 @@ class AccessControl
return true;
}
}
/**
* Check if a given user has permissons for a given record.
*
* @param $userId User id of the user
* @param $recordId Record to check
*
* @return bool true if access is granted, false otherwise
*/
public function canAccessRecord(int $userId, int $recordId) : bool
{
if ($this->isAdmin($userId)) {
return true;
}
$query = $this->db->prepare('
SELECT * FROM records R
LEFT OUTER JOIN permissions P ON P.domain_id=R.domain_id
WHERE R.id=:recordId AND P.user_id=:userId
');
$query->bindValue(':userId', $userId, \PDO::PARAM_INT);
$query->bindValue(':recordId', $recordId, \PDO::PARAM_INT);
$query->execute();
$record = $query->fetch();
if ($record === false) {
return false;
} else {
return true;
}
}
}

View file

@ -28,7 +28,7 @@ class Records
}
/**
* Get a list of domains according to filter criteria
* Get a list of records according to filter criteria
*
* @param $pi PageInfo object, which is also updated with total page number
* @param $userId Id of the user for which the table should be retrieved
@ -127,4 +127,174 @@ class Records
return $item;
}, $data);
}
/**
* Add new record
*
* @param $name Name of the new record
* @param $type Type of the new record
* @param $content Content of the new record
* @param $priority Priority of the new record
* @param $ttl TTL of the new record
* @param $domain Domain id of the domain to add the record
*
* @return array New record entry
*
* @throws NotFoundException if the domain does not exist
* @throws SemanticException if the record type is invalid
*/
public function addRecord(string $name, string $type, string $content, int $priority, int $ttl, int $domain) : array
{
if (!in_array($type, $this->c['config']['records']['allowedTypes'])) {
throw new \Exceptions\SemanticException();
}
$this->db->beginTransaction();
$query = $this->db->prepare('SELECT id FROM domains WHERE id=:id AND type IN (\'MASTER\',\'NATIVE\')');
$query->bindValue(':id', $domain, \PDO::PARAM_INT);
$query->execute();
$record = $query->fetch();
if ($record === false) { // Domain does not exist
$this->db->rollBack();
throw new \Exceptions\NotFoundException();
}
$query = $this->db->prepare('INSERT INTO records (domain_id, name, type, content, ttl, prio, change_date)
VALUES (:domainId, :name, :type, :content, :ttl, :prio, :changeDate)');
$query->bindValue(':domainId', $domain, \PDO::PARAM_INT);
$query->bindValue(':name', $name, \PDO::PARAM_STR);
$query->bindValue(':type', $type, \PDO::PARAM_STR);
$query->bindValue(':content', $content, \PDO::PARAM_STR);
$query->bindValue(':ttl', $ttl, \PDO::PARAM_INT);
$query->bindValue(':prio', $priority, \PDO::PARAM_INT);
$query->bindValue(':changeDate', time(), \PDO::PARAM_INT);
$query->execute();
$query = $this->db->prepare('SELECT id,name,type,content,prio AS priority,ttl,domain_id AS domain FROM records
ORDER BY id DESC LIMIT 1');
$query->execute();
$record = $query->fetch();
$record['id'] = intval($record['id']);
$record['priority'] = intval($record['priority']);
$record['ttl'] = intval($record['ttl']);
$record['domain'] = intval($record['domain']);
$this->db->commit();
return $record;
}
/**
* Delete record
*
* @param $id Id of the record to delete
*
* @return void
*
* @throws NotFoundException if record does not exist
*/
public function deleteRecord(int $id) : void
{
$this->db->beginTransaction();
$query = $this->db->prepare('SELECT id FROM records 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 FROM records WHERE id=:id');
$query->bindValue(':id', $id, \PDO::PARAM_INT);
$query->execute();
}
/**
* Get record
*
* @param $recordId Name of the record
*
* @return array Record entry
*
* @throws NotFoundException if the record does not exist
*/
public function getRecord(int $recordId) : array
{
$query = $this->db->prepare('SELECT id,name,type,content,prio AS priority,ttl,domain_id AS domain FROM records
WHERE id=:recordId');
$query->bindValue(':recordId', $recordId, \PDO::PARAM_INT);
$query->execute();
$record = $query->fetch();
if ($record === false) {
throw new \Exceptions\NotFoundException();
}
$record['id'] = intval($record['id']);
$record['priority'] = intval($record['priority']);
$record['ttl'] = intval($record['ttl']);
$record['domain'] = intval($record['domain']);
return $record;
}
/** Update Record
*
* If params are null do not change
*
* @param $recordId Record to update
* @param $name New name
* @param $type New type
* @param $content New content
* @param $priority New priority
* @param $ttl New ttl
*
* @return void
*
* @throws NotFoundException The given record does not exist
* @throws SemanticException The given record type is invalid
*/
public function updateRecord(int $recordId, ? string $name, ? string $type, ? string $content, ? int $priority, ? int $ttl)
{
$this->db->beginTransaction();
$query = $this->db->prepare('SELECT id,name,type,content,prio,ttl FROM records WHERE id=:recordId');
$query->bindValue(':recordId', $recordId);
$query->execute();
$record = $query->fetch();
if ($record === false) {
$this->db->rollBack();
throw new \Exceptions\NotFoundException();
}
if ($type !== null && !in_array($type, $this->c['config']['records']['allowedTypes'])) {
throw new \Exceptions\SemanticException();
}
$name = $name === null ? $record['name'] : $name;
$type = $type === null ? $record['type'] : $type;
$content = $content === null ? $record['content'] : $content;
$priority = $priority === null ? intval($record['prio']) : $priority;
$ttl = $ttl === null ? intval($record['ttl']) : $ttl;
$query = $this->db->prepare('UPDATE records SET name=:name,type=:type,content=:content,prio=:priority,ttl=:ttl');
$query->bindValue(':name', $name);
$query->bindValue(':type', $type);
$query->bindValue(':content', $content);
$query->bindValue(':priority', $priority);
$query->bindValue(':ttl', $ttl);
$query->execute();
$this->db->commit();
}
}

View file

@ -37,6 +37,10 @@ $app->group('/v1', function () {
$this->get('/domains/{domainId}/soa', '\Controllers\Domains:getSoa');
$this->get('/records', '\Controllers\Records:getList');
$this->post('/records', '\Controllers\Records:postNew');
$this->delete('/records/{recordId}', '\Controllers\Records:delete');
$this->get('/records/{recordId}', '\Controllers\Records:getSingle');
$this->put('/records/{recordId}', '\Controllers\Records:put');
})->add('\Middlewares\Authentication');
});

View file

@ -287,7 +287,7 @@ ALTER TABLE `domains`
-- AUTO_INCREMENT for table `records`
--
ALTER TABLE `records`
MODIFY `id` bigint(20) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=2;
MODIFY `id` bigint(20) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=5;
--
-- AUTO_INCREMENT for table `tsigkeys`
--

View file

@ -0,0 +1,279 @@
const test = require('../testlib');
test.run(async function () {
await test('admin', async function (assert, req) {
//Test missing fields
var res = await req({
url: '/records',
method: 'post',
data: {
name: 'foo.abc.de',
type: 'A'
}
});
assert.equal(res.status, 422, 'Missing fields should trigger error.');
//Test invalid record type
var res = await req({
url: '/records',
method: 'post',
data: {
name: "dns.example.com",
type: "FOOBARBAZ",
content: "1.2.3.4",
priority: 0,
ttl: 86400,
domain: 1
}
});
assert.equal(res.status, 400, 'Invalid record type should trigger error.');
//Test adding for slave zone
var res = await req({
url: '/records',
method: 'post',
data: {
name: "dns.example.com",
type: "A",
content: "1.2.3.4",
priority: 0,
ttl: 86400,
domain: 2
}
});
assert.equal(res.status, 404, 'Adding record for slave should trigger error.');
//Test adding for not existing zone
var res = await req({
url: '/records',
method: 'post',
data: {
name: "dns.example.com",
type: "A",
content: "1.2.3.4",
priority: 0,
ttl: 86400,
domain: 100
}
});
assert.equal(res.status, 404, 'Adding record to not existing domain should trigger error.');
//Test adding of record
var res = await req({
url: '/records',
method: 'post',
data: {
name: 'dns.example.com',
type: 'A',
content: '1.2.3.4',
priority: 0,
ttl: 86400,
domain: 1
}
});
assert.equal(res.status, 201, 'Adding of record should succeed.');
assert.equal(res.data, {
id: 5,
name: 'dns.example.com',
type: 'A',
content: '1.2.3.4',
priority: 0,
ttl: 86400,
domain: 1
}, 'Adding record return data fail.');
//Get not existing record
var res = await req({
url: '/records/100',
method: 'get'
});
assert.equal(res.status, 404, 'Get of not existing record should fail.');
//Get created record
var res = await req({
url: '/records/5',
method: 'get'
});
assert.equal(res.status, 200, 'Get of created record should succeed.');
assert.equal(res.data, {
id: 5,
name: 'dns.example.com',
type: 'A',
content: '1.2.3.4',
priority: 0,
ttl: 86400,
domain: 1
}, 'Record data should be the same it was created with.');
//Update record
var res = await req({
url: '/records/5',
method: 'put',
data: {
name: 'foo.example.com'
}
});
assert.equal(res.status, 204, 'Updating record should succeed');
//Get updated record
var res = await req({
url: '/records/5',
method: 'get'
});
assert.equal(res.status, 200, 'Get updated record should succeed.');
assert.equal(res.data, {
id: 5,
name: 'foo.example.com',
type: 'A',
content: '1.2.3.4',
priority: 0,
ttl: 86400,
domain: 1
}, 'Updated record has wrong data.');
//Delete not existing record
var res = await req({
url: '/records/100',
method: 'delete'
});
assert.equal(res.status, 404, 'Deletion of not existing record should fail.');
//Delete existing record
var res = await req({
url: '/records/5',
method: 'delete'
});
assert.equal(res.status, 204, 'Deletion of existing record should succeed.');
});
await test('user', async function (assert, req) {
//Test insufficient privileges for add
var res = await req({
url: '/records',
method: 'post',
data: {
name: 'dns.example.com',
type: 'A',
content: '1.2.3.4',
priority: 0,
ttl: 86400,
domain: 3
}
});
assert.equal(res.status, 403, 'Adding of record should fail for user.');
//Test insufficient privileges for delete
var res = await req({
url: '/records/4',
method: 'delete'
});
assert.equal(res.status, 403, 'Deletion of record should fail for user.');
//Test insufficient privileges for update
var res = await req({
url: '/records/4',
method: 'put',
data: {
name: 'foo.example.com',
ttl: 60
}
});
assert.equal(res.status, 403, 'Updating record should succeed');
//Test adding of record
var res = await req({
url: '/records',
method: 'post',
data: {
name: 'dns.example.com',
type: 'A',
content: '1.2.3.4',
priority: 0,
ttl: 86400,
domain: 1
}
});
assert.equal(res.status, 201, 'Adding of record should succeed.');
assert.equal(res.data, {
id: 6,
name: 'dns.example.com',
type: 'A',
content: '1.2.3.4',
priority: 0,
ttl: 86400,
domain: 1
}, 'Adding record return data fail.');
//Get created record
var res = await req({
url: '/records/6',
method: 'get'
});
assert.equal(res.status, 200, 'Get of created record should succeed.');
assert.equal(res.data, {
id: 6,
name: 'dns.example.com',
type: 'A',
content: '1.2.3.4',
priority: 0,
ttl: 86400,
domain: 1
}, 'Record data should be the same it was created with.');
//Update record
var res = await req({
url: '/records/6',
method: 'put',
data: {
name: 'foo.example.com',
ttl: 60
}
});
assert.equal(res.status, 204, 'Updating record should succeed');
//Get updated record
var res = await req({
url: '/records/6',
method: 'get'
});
assert.equal(res.status, 200, 'Get updated record should succeed.');
assert.equal(res.data, {
id: 6,
name: 'foo.example.com',
type: 'A',
content: '1.2.3.4',
priority: 0,
ttl: 60,
domain: 1
}, 'Updated record has wrong data.');
//Delete existing record
var res = await req({
url: '/records/6',
method: 'delete'
});
assert.equal(res.status, 204, 'Deletion of existing record should succeed.');
});
});