Skip to content

Commit

Permalink
Merge pull request #5186 from PetrOndrusek/api3-readonly-documents
Browse files Browse the repository at this point in the history
API3 read-only documents
  • Loading branch information
sulkaharo authored Dec 6, 2019
2 parents c153a96 + 2050ddc commit 57a3f2d
Show file tree
Hide file tree
Showing 5 changed files with 97 additions and 1 deletion.
2 changes: 2 additions & 0 deletions lib/api3/const.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"NOT_FOUND": 404,
"GONE": 410,
"PRECONDITION_FAILED": 412,
"UNPROCESSABLE_ENTITY": 422,
"INTERNAL_ERROR": 500
},

Expand All @@ -39,6 +40,7 @@
"HTTP_401_MISSING_OR_BAD_TOKEN": "Missing or bad access token or JWT",
"HTTP_403_MISSING_PERMISSION": "Missing permission {0}",
"HTTP_403_NOT_USING_HTTPS": "Not using SSL/TLS",
"HTTP_422_READONLY_MODIFICATION": "Trying to modify read-only document",
"HTTP_500_INTERNAL_ERROR": "Internal Server Error",
"STORAGE_ERROR": "Database error",
"SOCKET_MISSING_OR_BAD_ACCESS_TOKEN": "Missing or bad accessToken",
Expand Down
29 changes: 29 additions & 0 deletions lib/api3/generic/delete/operation.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ async function doDelete (opCtx) {

await security.demandPermission(opCtx, `api:${col.colName}:delete`);

if (await validateDelete(opCtx) !== true)
return;

if (req.query.permanent && req.query.permanent === "true") {
await deletePermanently(opCtx);
} else {
Expand All @@ -22,6 +25,32 @@ async function doDelete (opCtx) {
}


async function validateDelete (opCtx) {

const { col, req, res } = opCtx;

const identifier = req.params.identifier;
const result = await col.storage.findOne(identifier);

if (!result)
throw new Error('empty result');

if (result.length === 0) {
return res.status(apiConst.HTTP.NOT_FOUND).end();
}
else {
const storageDoc = result[0];

if (storageDoc.isReadOnly === true || storageDoc.readOnly === true || storageDoc.readonly === true) {
return opTools.sendJSONStatus(res, apiConst.HTTP.UNPROCESSABLE_ENTITY,
apiConst.MSG.HTTP_422_READONLY_MODIFICATION);
}
}

return true;
}


async function deletePermanently (opCtx) {

const { ctx, col, req, res } = opCtx;
Expand Down
5 changes: 5 additions & 0 deletions lib/api3/generic/update/validate.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ function validate (opCtx, doc, storageDoc, options) {
const immutable = ['identifier', 'date', 'utcOffset', 'eventType', 'device', 'app',
'srvCreated', 'subject', 'srvModified', 'modifiedBy', 'isValid'];

if (storageDoc.isReadOnly === true || storageDoc.readOnly === true || storageDoc.readonly === true) {
return opTools.sendJSONStatus(res, apiConst.HTTP.UNPROCESSABLE_ENTITY,
apiConst.MSG.HTTP_422_READONLY_MODIFICATION);
}

for (const field of immutable) {

// change of identifier is allowed in deduplication (for APIv1 documents)
Expand Down
24 changes: 23 additions & 1 deletion lib/api3/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ openapi: 3.0.0
servers:
- url: '/api/v3'
info:
version: '3.0.0'
version: '3.0.1'
title: Nightscout API
contact:
name: NS development discussion channel
Expand Down Expand Up @@ -178,6 +178,8 @@ paths:
$ref: '#/components/responses/403Forbidden'
404:
$ref: '#/components/responses/404NotFound'
422:
$ref: '#/components/responses/422UnprocessableEntity'


#return HTTP STATUS 400 for all other verbs (PUT, PATCH, DELETE,...)
Expand Down Expand Up @@ -296,6 +298,8 @@ paths:
$ref: '#/components/responses/412PreconditionFailed'
410:
$ref: '#/components/responses/410Gone'
422:
$ref: '#/components/responses/422UnprocessableEntity'


######################################################################################
Expand Down Expand Up @@ -356,6 +360,8 @@ paths:
$ref: '#/components/responses/412PreconditionFailed'
410:
$ref: '#/components/responses/410Gone'
422:
$ref: '#/components/responses/422UnprocessableEntity'


######################################################################################
Expand Down Expand Up @@ -388,6 +394,8 @@ paths:
$ref: '#/components/responses/403Forbidden'
404:
$ref: '#/components/responses/404NotFound'
422:
$ref: '#/components/responses/422UnprocessableEntity'


######################################################################################
Expand Down Expand Up @@ -886,6 +894,9 @@ components:
410Gone:
description: The requested document has already been deleted.

422UnprocessableEntity:
description: The client request is well formed but a server validation error occured. Eg. when trying to modify or delete a read-only document (having `isReadOnly=true`).

search200:
description: Successful operation returning array of documents matching the filtering criteria
content:
Expand Down Expand Up @@ -1129,6 +1140,17 @@ components:
example: false


isReadOnly:
type: boolean
description:
A flag set by client that locks the document from any changes. Every document marked with `isReadOnly=true` is forever immutable and cannot even be deleted.


Any attempt to modify the read-only document will end with status 422 UNPROCESSABLE ENTITY.


example: true

required:
- date
- app
Expand Down
38 changes: 38 additions & 0 deletions tests/api3.generic.workflow.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -253,5 +253,43 @@ describe('Generic REST API3', function() {
}
});


it('should not modify read-only document', async () => {
await self.instance.post(`${self.urlCol}?token=${self.token.create}`)
.send(Object.assign({}, self.docOriginal, { isReadOnly: true }))
.expect(201);

let res = await self.instance.get(`${self.urlResource}?token=${self.token.read}`)
.expect(200);

self.docActual = res.body;
delete self.docActual.srvModified;
const readOnlyMessage = 'Trying to modify read-only document';

res = await self.instance.post(`${self.urlCol}?token=${self.token.update}`)
.send(Object.assign({}, self.docActual, { insulin: 0.41 }))
.expect(422);
res.body.message.should.equal(readOnlyMessage);

res = await self.instance.put(`${self.urlResource}?token=${self.token.update}`)
.send(Object.assign({}, self.docActual, { insulin: 0.42 }))
.expect(422);
res.body.message.should.equal(readOnlyMessage);

res = await self.instance.patch(`${self.urlResource}?token=${self.token.update}`)
.send({ insulin: 0.43 })
.expect(422);
res.body.message.should.equal(readOnlyMessage);

res = await self.instance.delete(`${self.urlResource}?token=${self.token.delete}`)
.query({ 'permanent': 'true' })
.expect(422);
res.body.message.should.equal(readOnlyMessage);

res = await self.instance.get(`${self.urlResource}?token=${self.token.read}`)
.expect(200);
res.body.should.containEql(self.docOriginal);
});

});

0 comments on commit 57a3f2d

Please sign in to comment.