Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

API3 read-only documents #5186

Merged
merged 3 commits into from
Dec 6, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
});

});