diff --git a/lib/acl-checker.js b/lib/acl-checker.js index 6e15a0d00..53c41f86d 100644 --- a/lib/acl-checker.js +++ b/lib/acl-checker.js @@ -113,9 +113,8 @@ class ACLChecker { if (!returnAcl) { throw new HTTPError(500, `No ACL found for ${resource}, searched in \n- ${acls.join('\n- ')}`) } - const groupUrls = returnAcl.graph - .statementsMatching(null, ACL('agentGroup'), null) - .map(node => node.object.value.split('#')[0]) + const groupNodes = returnAcl.graph.statementsMatching(null, ACL('agentGroup'), null) + const groupUrls = groupNodes.map(node => node.object.value.split('#')[0]) await Promise.all(groupUrls.map(groupUrl => { this.requests[groupUrl] = this.requests[groupUrl] || this.fetch(groupUrl, returnAcl.graph) return this.requests[groupUrl] diff --git a/lib/handlers/allow.js b/lib/handlers/allow.js index b18c2e0a7..851b93fa7 100644 --- a/lib/handlers/allow.js +++ b/lib/handlers/allow.js @@ -44,7 +44,8 @@ function allow (mode, checkPermissionsForDirectory) { trustedOrigins.push(ldp.serverUri) } // Obtain and store the ACL of the requested resource - req.acl = ACL.createFromLDPAndRequest(rootUrl + resourcePath, ldp, req) + const resourceUrl = rootUrl + resourcePath + req.acl = ACL.createFromLDPAndRequest(resourceUrl, ldp, req) // Ensure the user has the required permission const userId = req.session.userId @@ -52,6 +53,19 @@ function allow (mode, checkPermissionsForDirectory) { if (isAllowed) { return next() } + if (mode === 'Read' && (resourcePath === '' || resourcePath === '/')) { + // This is a hack to make NSS check the ACL for representation that is served for root (if any) + // See https://github.com/solid/node-solid-server/issues/1063 for more info + const representationUrl = await ldp.resourceMapper.getRepresentationUrlForResource(resourceUrl) + if (representationUrl.endsWith('index.html')) { + // We ONLY want to do this when the representation we return is a HTML file + req.acl = ACL.createFromLDPAndRequest(representationUrl, ldp, req) + const representationIsAllowed = await req.acl.can(userId, mode) + if (representationIsAllowed) { + return next() + } + } + } const error = await req.acl.getError(userId, mode) debug(`${mode} access denied to ${userId || '(none)'}: ${error.status} - ${error.message}`) next(error) diff --git a/lib/resource-mapper.js b/lib/resource-mapper.js index 241996bf6..d9811400e 100644 --- a/lib/resource-mapper.js +++ b/lib/resource-mapper.js @@ -90,6 +90,25 @@ class ResourceMapper { return { path, contentType: contentType || this._defaultContentType } } + async getRepresentationUrlForResource (resourceUrl) { + let fullPath = this.getFullPath(resourceUrl) + let isIndex = fullPath.endsWith('/') + + // Append index filename if the URL ends with a '/' + if (isIndex) { + fullPath += this._indexFilename + } + + // Read all files in the corresponding folder + const filename = fullPath.substr(fullPath.lastIndexOf('/') + 1) + const folder = fullPath.substr(0, fullPath.length - filename.length) + const files = await this._readdir(folder) + + // Find a file with the same name (minus the dollar extension) + let match = (files.find(f => this._removeDollarExtension(f) === filename || (isIndex && f.startsWith(this._indexFilename + '.')))) + return `${resourceUrl}${match || ''}` + } + // Maps a given server file to a URL async mapFileToUrl ({ path, hostname }) { // Determine the URL by chopping off everything after the dollar sign diff --git a/test/integration/authentication-oidc-test.js b/test/integration/authentication-oidc-test.js index ccc2d7d19..54281eec3 100644 --- a/test/integration/authentication-oidc-test.js +++ b/test/integration/authentication-oidc-test.js @@ -164,7 +164,7 @@ describe('Authentication API (OIDC)', () => { describe('with that cookie and a non-matching origin', () => { let response before(done => { - alice.get('/') + alice.get('/private-for-owner.txt') .set('Cookie', cookie) .set('Origin', bobServerUri) .end((err, res) => { @@ -234,7 +234,7 @@ describe('Authentication API (OIDC)', () => { describe('with that cookie and our origin', () => { let response before(done => { - alice.get('/') + alice.get('/private-for-owner.txt') .set('Cookie', cookie) .set('Origin', 'https://some.other.domain.com') .end((err, res) => { @@ -252,7 +252,7 @@ describe('Authentication API (OIDC)', () => { describe('without that cookie but with our origin', () => { let response before(done => { - alice.get('/') + alice.get('/private-for-owner.txt') .set('Origin', aliceServerUri) .end((err, res) => { response = res @@ -324,7 +324,7 @@ describe('Authentication API (OIDC)', () => { describe('without that cookie and a matching origin', () => { let response before(done => { - alice.get('/') + alice.get('/private-for-owner.txt') .set('Origin', bobServerUri) .end((err, res) => { response = res @@ -341,7 +341,7 @@ describe('Authentication API (OIDC)', () => { describe('with that cookie and a non-matching origin', () => { let response before(done => { - alice.get('/') + alice.get('/private-for-owner.txt') .set('Cookie', cookie) .set('Origin', bobServerUri) .end((err, res) => { @@ -360,7 +360,7 @@ describe('Authentication API (OIDC)', () => { let response before(done => { var malcookie = cookie.replace(/connect\.sid=(\S+)/, 'connect.sid=l33th4x0rzp0wn4g3;') - alice.get('/') + alice.get('/private-for-owner.txt') .set('Cookie', malcookie) .set('Origin', bobServerUri) .end((err, res) => { diff --git a/test/integration/special-root-acl-handling.js b/test/integration/special-root-acl-handling.js new file mode 100644 index 000000000..aea464f78 --- /dev/null +++ b/test/integration/special-root-acl-handling.js @@ -0,0 +1,66 @@ +const assert = require('chai').assert +const request = require('request') +const path = require('path') +const { checkDnsSettings, cleanDir } = require('../utils') + +const ldnode = require('../../index') + +const port = 7777 +const serverUri = `https://localhost:${port}` +const root = path.join(__dirname, '../resources/accounts-acl') +const dbPath = path.join(root, 'db') +const configPath = path.join(root, 'config') + +function createOptions (path = '') { + return { + url: `https://nicola.localhost:${port}${path}` + } +} + +describe('Special handling: Root ACL does not give READ access to root', () => { + let ldp, ldpHttpsServer + + before(checkDnsSettings) + + before(done => { + ldp = ldnode.createServer({ + root, + serverUri, + dbPath, + port, + configPath, + sslKey: path.join(__dirname, '../keys/key.pem'), + sslCert: path.join(__dirname, '../keys/cert.pem'), + webid: true, + multiuser: true, + auth: 'oidc', + strictOrigin: true, + host: { serverUri } + }) + ldpHttpsServer = ldp.listen(port, done) + }) + + after(() => { + if (ldpHttpsServer) ldpHttpsServer.close() + cleanDir(root) + }) + + describe('should still grant READ access to everyone because of index.html.acl', () => { + it('for root with /', function (done) { + var options = createOptions('/') + request.get(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 200) + done() + }) + }) + it('for root without /', function (done) { + var options = createOptions() + request.get(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 200) + done() + }) + }) + }) +}) diff --git a/test/resources/accounts-acl/nicola.localhost/.acl b/test/resources/accounts-acl/nicola.localhost/.acl new file mode 100644 index 000000000..de881069a --- /dev/null +++ b/test/resources/accounts-acl/nicola.localhost/.acl @@ -0,0 +1 @@ +# This ACL does nothing by default diff --git a/test/resources/accounts-acl/nicola.localhost/index.html b/test/resources/accounts-acl/nicola.localhost/index.html new file mode 100644 index 000000000..cb12e071b --- /dev/null +++ b/test/resources/accounts-acl/nicola.localhost/index.html @@ -0,0 +1 @@ +
Everyone should get READ access for this file throughindex.html.acl. diff --git a/test/resources/accounts-acl/nicola.localhost/index.html.acl b/test/resources/accounts-acl/nicola.localhost/index.html.acl new file mode 100644 index 000000000..19f9544f3 --- /dev/null +++ b/test/resources/accounts-acl/nicola.localhost/index.html.acl @@ -0,0 +1,10 @@ +# This file grants everyone READ access to ./index.html + +@prefix acl: