Skip to content

Commit

Permalink
Merge branch 'main' into accept-headers
Browse files Browse the repository at this point in the history
  • Loading branch information
bourgeoa authored Feb 6, 2024
2 parents 37173c1 + 7c8ccc6 commit 9f636ea
Show file tree
Hide file tree
Showing 19 changed files with 5,537 additions and 24,560 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:

strategy:
matrix:
node-version: [16.x, 18.x]
node-version: [18.x]
os: [ubuntu-latest]

steps:
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,15 @@ $ solid start --root path/to/folder --port 8443 --ssl-key path/to/ssl-key.pem --
# Solid server (solid v0.2.24) running on https://localhost:8443/
```

By default, `solid` runs in `debug all` mode. To stop the debug logs, use `-q`, the quiet parameter.

```bash
$ DEBUG="solid:*" solid start -q
# use quiet mode and set debug to all
# DEBUG="solid:ACL" logs only debug.ACL's

```

### Running in development environments

Solid requires SSL certificates to be valid, so you cannot use self-signed certificates. To switch off this security feature in development environments, you can use the `bin/solid-test` executable, which unsets the `NODE_TLS_REJECT_UNAUTHORIZED` flag and sets the `rejectUnauthorized` option.
Expand Down
137 changes: 99 additions & 38 deletions lib/acl-checker.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
const { dirname } = require('path')
const rdf = require('rdflib')
const debug = require('./debug').ACL
const debugCache = require('./debug').cache
// const debugCache = require('./debug').cache
const HTTPError = require('./http-error')
const aclCheck = require('@solid/acl-check')
const { URL } = require('url')
Expand Down Expand Up @@ -55,68 +55,98 @@ class ACLChecker {
}
this.messagesCached[cacheKey] = this.messagesCached[cacheKey] || []

const acl = await this.getNearestACL().catch(err => {
// for method DELETE nearestACL and ACL from parent resource
const acl = await this.getNearestACL(method).catch(err => {
this.messagesCached[cacheKey].push(new HTTPError(err.status || 500, err.message || err))
})
if (!acl) {
this.aclCached[cacheKey] = Promise.resolve(false)
return this.aclCached[cacheKey]
}
let resource = rdf.sym(this.resource)
let parentResource = resource
if (!this.resource.endsWith('/')) { parentResource = rdf.sym(ACLChecker.getDirectory(this.resource)) }
if (this.resource.endsWith('/' + this.suffix)) {
resource = rdf.sym(ACLChecker.getDirectory(this.resource))
parentResource = resource
}
// If this is an ACL, Control mode must be present for any operations
if (this.isAcl(this.resource)) {
mode = 'Control'
resource = rdf.sym(this.resource.substring(0, this.resource.length - this.suffix.length))
const thisResource = this.resource.substring(0, this.resource.length - this.suffix.length)
resource = rdf.sym(thisResource)
parentResource = resource
if (!thisResource.endsWith('/')) parentResource = rdf.sym(ACLChecker.getDirectory(thisResource))
}
// If the slug is an acl, reject
/* if (this.isAcl(this.slug)) {
this.aclCached[cacheKey] = Promise.resolve(false)
return this.aclCached[cacheKey]
} */
const directory = acl.isContainer ? rdf.sym(ACLChecker.getDirectory(acl.acl)) : null
const aclFile = rdf.sym(acl.acl)
const directory = acl.isContainer ? rdf.sym(ACLChecker.getDirectory(acl.docAcl)) : null
const aclFile = rdf.sym(acl.docAcl)
const aclGraph = acl.docGraph
const agent = user ? rdf.sym(user) : null
const modes = [ACL(mode)]
const agentOrigin = this.agentOrigin
const trustedOrigins = this.trustedOrigins
let originTrustedModes = []
try {
this.fetch(aclFile.doc().value)
originTrustedModes = await aclCheck.getTrustedModesForOrigin(acl.graph, resource, directory, aclFile, agentOrigin, (uriNode) => {
return this.fetch(uriNode.doc().value, acl.graph)
originTrustedModes = await aclCheck.getTrustedModesForOrigin(aclGraph, resource, directory, aclFile, agentOrigin, (uriNode) => {
return this.fetch(uriNode.doc().value, aclGraph)
})
} catch (e) {
// FIXME: https://github.com/solid/acl-check/issues/23
// console.error(e.message)
}
let accessDenied = aclCheck.accessDenied(acl.graph, resource, directory, aclFile, agent, modes, agentOrigin, trustedOrigins, originTrustedModes)

function accessDeniedForAccessTo (mode) {
const accessDeniedAccessTo = aclCheck.accessDenied(acl.graph, directory, null, aclFile, agent, [ACL(mode)], agentOrigin, trustedOrigins, originTrustedModes)
function resourceAccessDenied (modes) {
return aclCheck.accessDenied(aclGraph, resource, directory, aclFile, agent, modes, agentOrigin, trustedOrigins, originTrustedModes)
}
function accessDeniedForAccessTo (modes) {
const accessDeniedAccessTo = aclCheck.accessDenied(aclGraph, directory, null, aclFile, agent, modes, agentOrigin, trustedOrigins, originTrustedModes)
const accessResult = !accessDenied && !accessDeniedAccessTo
accessDenied = accessResult ? false : accessDenied || accessDeniedAccessTo
// debugCache('accessDenied result ' + accessDenied)
return accessResult ? false : accessDenied || accessDeniedAccessTo
}
async function accessdeniedFromParent (modes) {
const parentAclDirectory = ACLChecker.getDirectory(acl.parentAcl)
const parentDirectory = parentResource === parentAclDirectory ? null : rdf.sym(parentAclDirectory)
const accessDeniedParent = aclCheck.accessDenied(acl.parentGraph, parentResource, parentDirectory, rdf.sym(acl.parentAcl), agent, modes, agentOrigin, trustedOrigins, originTrustedModes)
const accessResult = !accessDenied && !accessDeniedParent
return accessResult ? false : accessDenied || accessDeniedParent
}

let accessDenied = resourceAccessDenied(modes)
// debugCache('accessDenied resource ' + accessDenied)

// For create and update HTTP methods
if ((method === 'PUT' || method === 'PATCH' || method === 'COPY') && directory) {
if ((method === 'PUT' || method === 'PATCH' || method === 'COPY')) {
// if resource and acl have same parent container,
// and resource does not exist, then accessTo Append from parent is required
if (directory.value === dirname(aclFile.value) + '/' && !resourceExists) {
accessDeniedForAccessTo('Append')
if (directory && directory.value === dirname(aclFile.value) + '/' && !resourceExists) {
accessDenied = accessDeniedForAccessTo([ACL('Append')])
}
// debugCache('accessDenied PUT/PATCH ' + accessDenied)
}

// For delete HTTP method
if ((method === 'DELETE') && directory) {
// if resource and acl have same parent container,
// then accessTo Write from parent is required
if (directory.value === dirname(aclFile.value) + '/') {
accessDeniedForAccessTo('Write')
}
if ((method === 'DELETE')) {
if (resourceExists) {
// deleting a Container
// without Read, the response code will reveal whether a Container is empty or not
if (directory && this.resource.endsWith('/')) accessDenied = resourceAccessDenied([ACL('Read'), ACL('Write')])
// if resource and acl have same parent container,
// then both Read and Write on parent is required
else if (!directory && aclFile.value.endsWith(`/${this.suffix}`)) accessDenied = await accessdeniedFromParent([ACL('Read'), ACL('Write')])

// deleting a Document
else if (directory && directory.value === dirname(aclFile.value) + '/') {
accessDenied = accessDeniedForAccessTo([ACL('Write')])
} else {
accessDenied = await accessdeniedFromParent([ACL('Write')])
}

// https://github.com/solid/specification/issues/14#issuecomment-1712773516
} else { accessDenied = true }
// debugCache('accessDenied DELETE ' + accessDenied)
}

if (accessDenied && user) {
this.messagesCached[cacheKey].push(HTTPError(403, accessDenied))
} else if (accessDenied) {
Expand All @@ -140,43 +170,74 @@ class ACLChecker {
return `${parts.join('/')}/`
}

// Gets the ACL that applies to the resource
async getNearestACL () {
// Gets any ACLs that apply to the resource
// DELETE uses docAcl when docAcl is parent to the resource
// or docAcl and parentAcl when docAcl is the ACL of the Resource
async getNearestACL (method) {
const { resource } = this
let isContainer = false
const possibleACLs = this.getPossibleACLs()
const acls = [...possibleACLs]
let returnAcl = null
while (possibleACLs.length > 0 && !returnAcl) {
let returnParentAcl = null
let parentAcl = null
let parentGraph = null
let docAcl = null
let docGraph = null
while (possibleACLs.length > 0 && !returnParentAcl) {
const acl = possibleACLs.shift()
let graph
try {
this.requests[acl] = this.requests[acl] || this.fetch(acl)
graph = await this.requests[acl]
} catch (err) {
if (err && (err.code === 'ENOENT' || err.status === 404)) {
isContainer = true
// only set isContainer before docAcl
if (!docAcl) isContainer = true
continue
}
debug(err)
throw err
}
const relative = resource.replace(acl.replace(/[^/]+$/, ''), './')
debug(`Using ACL ${acl} for ${relative}`)
returnAcl = { acl, graph, isContainer }
// const relative = resource.replace(acl.replace(/[^/]+$/, ''), './')
// debug(`Using ACL ${acl} for ${relative}`)
if (!docAcl) {
docAcl = acl
docGraph = graph
// parentAcl is only needed for DELETE
if (method !== 'DELETE') returnParentAcl = true
} else {
parentAcl = acl
parentGraph = graph
returnParentAcl = true
}

returnAcl = { docAcl, docGraph, isContainer, parentAcl, parentGraph }
}
if (!returnAcl) {
throw new HTTPError(500, `No ACL found for ${resource}, searched in \n- ${acls.join('\n- ')}`)
}
const groupNodes = returnAcl.graph.statementsMatching(null, ACL('agentGroup'), null)
const groupUrls = groupNodes.map(node => node.object.value.split('#')[0])
// fetch group
let groupNodes = returnAcl.docGraph.statementsMatching(null, ACL('agentGroup'), null)
let groupUrls = groupNodes.map(node => node.object.value.split('#')[0])
await Promise.all(groupUrls.map(async groupUrl => {
try {
const graph = await this.fetch(groupUrl, returnAcl.graph)
this.requests[groupUrl] = this.requests[groupUrl] || graph
const docGraph = await this.fetch(groupUrl, returnAcl.docGraph)
this.requests[groupUrl] = this.requests[groupUrl] || docGraph
} catch (e) {} // failed to fetch groupUrl
}))
if (parentAcl) {
groupNodes = returnAcl.parentGraph.statementsMatching(null, ACL('agentGroup'), null)
groupUrls = groupNodes.map(node => node.object.value.split('#')[0])
await Promise.all(groupUrls.map(async groupUrl => {
try {
const docGraph = await this.fetch(groupUrl, returnAcl.parentGraph)
this.requests[groupUrl] = this.requests[groupUrl] || docGraph
} catch (e) {} // failed to fetch groupUrl
}))
}

// debugAccounts('ALAIN returnACl ' + '\ndocAcl ' + returnAcl.docAcl + '\nparentAcl ' + returnAcl.parentAcl)
return returnAcl
}

Expand Down Expand Up @@ -264,7 +325,7 @@ function fetchLocalOrRemote (mapper, serverUri) {
// debugCache('Expunging from cache', url)
delete temporaryCache[url]
if (Object.keys(temporaryCache).length === 0) {
debugCache('Cache is empty again')
// debugCache('Cache is empty again')
}
}, EXPIRY_MS),
promise: doFetch(url)
Expand Down
27 changes: 27 additions & 0 deletions lib/create-app.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,33 @@ function createApp (argv = {}) {
// Attach the LDP middleware
app.use('/', LdpMiddleware(corsSettings))

// https://stackoverflow.com/questions/51741383/nodejs-express-return-405-for-un-supported-method
app.use(function (req, res, next) {
const AllLayers = app._router.stack
const Layers = AllLayers.filter(x => x.name === 'bound dispatch' && x.regexp.test(req.path))

const Methods = []
Layers.forEach(layer => {
for (const method in layer.route.methods) {
if (layer.route.methods[method] === true) {
Methods.push(method.toUpperCase())
}
}
})

if (Layers.length !== 0 && !Methods.includes(req.method)) {
// res.setHeader('Allow', Methods.join(','))

if (req.method === 'OPTIONS') {
return res.send(Methods.join(', '))
} else {
return res.status(405).send()
}
} else {
next()
}
})

// Errors
app.use(errorPages.handler)

Expand Down
4 changes: 2 additions & 2 deletions lib/handlers/allow.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ module.exports = allow

// const path = require('path')
const ACL = require('../acl-checker')
const debug = require('../debug.js').ACL
// const debug = require('../debug.js').ACL
// const error = require('../http-error')

function allow (mode) {
Expand Down Expand Up @@ -77,7 +77,7 @@ function allow (mode) {
if (resourceUrl.endsWith('.acl') && (await ldp.isOwner(userId, req.hostname))) return next()
} catch (err) {}
const error = req.authError || await req.acl.getError(userId, mode)
debug(`${mode} access denied to ${userId || '(none)'}: ${error.status} - ${error.message}`)
// debug(`${mode} access denied to ${userId || '(none)'}: ${error.status} - ${error.message}`)
next(error)
}
}
7 changes: 3 additions & 4 deletions lib/handlers/get.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,9 @@ async function handler (req, res, next) {

// If it is not in our RDFs we can't even translate,
// Sorry, we can't help
if (!possibleRDFType) {
if (!possibleRDFType || !RDFs.includes(contentType)) { // possibleRDFType defaults to text/turtle
return next(error(406, 'Cannot serve requested type: ' + contentType))
}

try {
// Translate from the contentType found to the possibleRDFType desired
const data = await translate(stream, baseUri, contentType, possibleRDFType)
Expand All @@ -135,8 +134,8 @@ async function handler (req, res, next) {
res.send(data)
return next()
} catch (err) {
debug('error translating: ' + req.originalUrl + ' ' + contentType + ' -> ' + possibleRDFType + ' -- ' + 500 + ' ' + err.message)
return next(error(500, 'Error translating between RDF formats'))
debug('error translating: ' + req.originalUrl + ' ' + contentType + ' -> ' + possibleRDFType + ' -- ' + 406 + ' ' + err.message)
return next(error(500, 'Cannot serve requested type: ' + requestedType))
}
}

Expand Down
9 changes: 8 additions & 1 deletion lib/handlers/patch.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,11 @@ async function patchHandler (req, res, next) {
({ path, contentType } = await ldp.resourceMapper.mapUrlToFile(
{ url: req, createIfNotExists: true, contentType: contentTypeForNew(req) }))
// check if a folder with same name exists
await ldp.checkItemName(req)
try {
await ldp.checkItemName(req)
} catch (err) {
return next(err)
}
resourceExists = false
}
const { url } = await ldp.resourceMapper.mapFileToUrl({ path, hostname: req.hostname })
Expand All @@ -64,6 +68,9 @@ async function patchHandler (req, res, next) {
patch.text = req.body ? req.body.toString() : ''
patch.uri = `${url}#patch-${hash(patch.text)}`
patch.contentType = getContentType(req.headers)
if (!patch.contentType) {
throw error(400, 'PATCH request requires a content-type via the Content-Type header')
}
debug('PATCH -- Received patch (%d bytes, %s)', patch.text.length, patch.contentType)
const parsePatch = PATCH_PARSERS[patch.contentType]
if (!parsePatch) {
Expand Down
Loading

0 comments on commit 9f636ea

Please sign in to comment.