diff --git a/lib/handlers/put.js b/lib/handlers/put.js index 5224cc216..168278a46 100644 --- a/lib/handlers/put.js +++ b/lib/handlers/put.js @@ -19,7 +19,7 @@ async function handler (req, res, next) { return next(e) } // check for valid rdf content for auxiliary resource and /profile/card - // in future we may check that /profile/card is a minimal valid WebID card + // TODO check that /profile/card is a minimal valid WebID card if (isAuxiliary(req) || req.originalUrl === '/profile/card') { if (contentType === 'text/turtle') { return bodyParser.text({ type: () => true })(req, res, () => putValidRdf(req, res, next)) @@ -28,17 +28,51 @@ async function handler (req, res, next) { return putStream(req, res, next) } +// Verifies whether the user is allowed to perform Append PUT on the target +async function checkPermission (request, resourceExists) { + // If no ACL object was passed down, assume permissions are okay. + if (!request.acl) return Promise.resolve() + // At this point, we already assume append access, + // we might need to perform additional checks. + let modes = [] + // acl:default Write is required for PUT when Resource Exists + if (resourceExists) modes = ['Write'] + // if (resourceExists && request.originalUrl.endsWith('.acl')) modes = ['Control'] + const { acl, session: { userId } } = request + + const allowed = await Promise.all(modes.map(mode => acl.can(userId, mode, request.method, resourceExists))) + const allAllowed = allowed.reduce((memo, allowed) => memo && allowed, true) + if (!allAllowed) { + // check owner with Control + // const ldp = request.app.locals.ldp + // if (request.path.endsWith('.acl') && userId === await ldp.getOwner(request.hostname)) return Promise.resolve() + + const errors = await Promise.all(modes.map(mode => acl.getError(userId, mode))) + const error = errors.filter(error => !!error) + .reduce((prevErr, err) => prevErr.status > err.status ? prevErr : err, { status: 0 }) + return Promise.reject(error) + } + return Promise.resolve() +} + // TODO could be renamed as putResource (it now covers container and non-container) async function putStream (req, res, next, stream = req) { const ldp = req.app.locals.ldp + // try { + // Obtain details of the target resource + let resourceExists = true + try { + // First check if the file already exists + await ldp.resourceMapper.mapUrlToFile({ url: req }) + } catch (err) { + resourceExists = false + } try { - debug('test ' + req.get('content-type') + getContentType(req.headers)) + if (!req.originalUrl.endsWith('.acl')) await checkPermission(req, resourceExists) await ldp.put(req, stream, getContentType(req.headers)) - debug('succeded putting the file/folder') res.sendStatus(201) return next() } catch (err) { - debug('error putting the file/folder:' + err.message) err.message = 'Can\'t write file/folder: ' + err.message return next(err) } diff --git a/lib/ldp-middleware.js b/lib/ldp-middleware.js index 1b35d56ad..07b1a82d4 100644 --- a/lib/ldp-middleware.js +++ b/lib/ldp-middleware.js @@ -25,7 +25,7 @@ function LdpMiddleware (corsSettings) { router.get('/*', index, allow('Read'), header.addPermissions, get) router.post('/*', allow('Append'), post) router.patch('/*', allow('Append'), patch) - router.put('/*', allow('Write'), put) + router.put('/*', allow('Append'), put) router.delete('/*', allow('Write'), del) return router diff --git a/test/integration/acl-oidc-test.js b/test/integration/acl-oidc-test.js index 37728724e..e482703da 100644 --- a/test/integration/acl-oidc-test.js +++ b/test/integration/acl-oidc-test.js @@ -138,7 +138,7 @@ describe('ACL with WebID+OIDC over HTTP', function () { done() }) }) - it('user1 as solid:owner should let edit the .acl', function (done) { + it('user1 as solid:owner should let edit the .acl', function (done) { // alain const options = createOptions('/empty-acl/.acl', 'user1', 'text/turtle') options.body = '' request.put(options, function (error, response, body) { @@ -209,7 +209,7 @@ describe('ACL with WebID+OIDC over HTTP', function () { done() }) }) - it('Should not create empty acl file', function (done) { + it('Should not create empty acl file', function (done) { // alain const options = createOptions('/write-acl/empty-acl/another-empty-folder/.acl', 'user1', 'text/turtle') options.body = '' request.put(options, function (error, response, body) { @@ -273,7 +273,7 @@ describe('ACL with WebID+OIDC over HTTP', function () { }) describe('no-control', function () { - it('user1 as owner should edit acl file', function (done) { + it('user1 as owner should edit acl file', function (done) { // alain const options = createOptions('/no-control/.acl', 'user1', 'text/turtle') options.body = '<#0>' + '\n a ;' + @@ -571,6 +571,27 @@ describe('ACL with WebID+OIDC over HTTP', function () { done() }) }) + it('user1 should be able to PUT (which CREATEs) (non existent resource)', function (done) { + const options = createOptions('/append-inherited/test1.ttl', 'user1') + options.body = ' .\n' + options.headers['content-type'] = 'text/turtle' + request.put(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 201) + done() + }) + }) + it('user2 should not be able to PUT with Append (existing resource)', function (done) { + const options = createOptions('/append-inherited/test1.ttl', 'user2') + options.body = ' .\n' + options.headers['content-type'] = 'text/turtle' + request.put(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 403) + assert.include(response.statusMessage, 'User Unauthorized') + done() + }) + }) it('user1 should be able to access test file', function (done) { const options = createOptions('/append-acl/abc.ttl', 'user1') request.head(options, function (error, response, body) { @@ -599,6 +620,16 @@ describe('ACL with WebID+OIDC over HTTP', function () { done() }) }) + it('user2 should be able to PUT to (which CREATEs) a non existent resource', function (done) { // alain + const options = createOptions('/append-inherited/new1.ttl', 'user1') + options.body = ' .\n' + options.headers['content-type'] = 'text/turtle' + request.put(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 201) + done() + }) + }) it('user2 should not be able to access test file\'s ACL file', function (done) { const options = createOptions('/append-acl/abc.ttl.acl', 'user2', 'text/turtle') request.head(options, function (error, response, body) { @@ -627,13 +658,13 @@ describe('ACL with WebID+OIDC over HTTP', function () { done() }) }) - it('user2 (with append permission) cannot use PUT to append', function (done) { + it('user2 (with append permission) cannot use PUT on an existing resource', function (done) { const options = createOptions('/append-acl/abc.ttl', 'user2', 'text/turtle') options.body = ' .\n' request.put(options, function (error, response, body) { assert.equal(error, null) assert.equal(response.statusCode, 403) - assert.equal(response.statusMessage, 'User Unauthorized') + assert.include(response.statusMessage, 'Can\'t write file/folder: User Unauthorized') done() }) }) @@ -652,13 +683,15 @@ describe('ACL with WebID+OIDC over HTTP', function () { request.put(options, function (error, response, body) { assert.equal(error, null) assert.equal(response.statusCode, 401) - assert.equal(response.statusMessage, 'Unauthenticated') + assert.include(response.statusMessage, 'Can\'t write file/folder: Unauthenticated') done() }) }) after(function () { rm('/accounts-acl/tim.localhost/append-inherited/test.ttl') + rm('/accounts-acl/tim.localhost/append-inherited/test1.ttl') rm('/accounts-acl/tim.localhost/append-inherited/new.ttl') + rm('/accounts-acl/tim.localhost/append-inherited/new1.ttl') }) })