Skip to content

Commit 032df34

Browse files
authored
Merge pull request #1750 from nodeSolidServer/5.7.9-beta
5.7.9 beta
2 parents 83dad06 + 9acda7d commit 032df34

File tree

17 files changed

+5513
-24555
lines changed

17 files changed

+5513
-24555
lines changed

.github/workflows/ci.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ jobs:
1717

1818
strategy:
1919
matrix:
20-
node-version: [16.x, 18.x]
20+
node-version: [18.x]
2121
os: [ubuntu-latest]
2222

2323
steps:

README.md

+9
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,15 @@ $ solid start --root path/to/folder --port 8443 --ssl-key path/to/ssl-key.pem --
6969
# Solid server (solid v0.2.24) running on https://localhost:8443/
7070
```
7171

72+
By default, `solid` runs in `debug all` mode. To stop the debug logs, use `-q`, the quiet parameter.
73+
74+
```bash
75+
$ DEBUG="solid:*" solid start -q
76+
# use quiet mode and set debug to all
77+
# DEBUG="solid:ACL" logs only debug.ACL's
78+
79+
```
80+
7281
### Running in development environments
7382

7483
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.

lib/acl-checker.js

+99-38
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
const { dirname } = require('path')
55
const rdf = require('rdflib')
66
const debug = require('./debug').ACL
7-
const debugCache = require('./debug').cache
7+
// const debugCache = require('./debug').cache
88
const HTTPError = require('./http-error')
99
const aclCheck = require('@solid/acl-check')
1010
const { URL } = require('url')
@@ -55,68 +55,98 @@ class ACLChecker {
5555
}
5656
this.messagesCached[cacheKey] = this.messagesCached[cacheKey] || []
5757

58-
const acl = await this.getNearestACL().catch(err => {
58+
// for method DELETE nearestACL and ACL from parent resource
59+
const acl = await this.getNearestACL(method).catch(err => {
5960
this.messagesCached[cacheKey].push(new HTTPError(err.status || 500, err.message || err))
6061
})
6162
if (!acl) {
6263
this.aclCached[cacheKey] = Promise.resolve(false)
6364
return this.aclCached[cacheKey]
6465
}
6566
let resource = rdf.sym(this.resource)
67+
let parentResource = resource
68+
if (!this.resource.endsWith('/')) { parentResource = rdf.sym(ACLChecker.getDirectory(this.resource)) }
6669
if (this.resource.endsWith('/' + this.suffix)) {
6770
resource = rdf.sym(ACLChecker.getDirectory(this.resource))
71+
parentResource = resource
6872
}
6973
// If this is an ACL, Control mode must be present for any operations
7074
if (this.isAcl(this.resource)) {
7175
mode = 'Control'
72-
resource = rdf.sym(this.resource.substring(0, this.resource.length - this.suffix.length))
76+
const thisResource = this.resource.substring(0, this.resource.length - this.suffix.length)
77+
resource = rdf.sym(thisResource)
78+
parentResource = resource
79+
if (!thisResource.endsWith('/')) parentResource = rdf.sym(ACLChecker.getDirectory(thisResource))
7380
}
74-
// If the slug is an acl, reject
75-
/* if (this.isAcl(this.slug)) {
76-
this.aclCached[cacheKey] = Promise.resolve(false)
77-
return this.aclCached[cacheKey]
78-
} */
79-
const directory = acl.isContainer ? rdf.sym(ACLChecker.getDirectory(acl.acl)) : null
80-
const aclFile = rdf.sym(acl.acl)
81+
const directory = acl.isContainer ? rdf.sym(ACLChecker.getDirectory(acl.docAcl)) : null
82+
const aclFile = rdf.sym(acl.docAcl)
83+
const aclGraph = acl.docGraph
8184
const agent = user ? rdf.sym(user) : null
8285
const modes = [ACL(mode)]
8386
const agentOrigin = this.agentOrigin
8487
const trustedOrigins = this.trustedOrigins
8588
let originTrustedModes = []
8689
try {
8790
this.fetch(aclFile.doc().value)
88-
originTrustedModes = await aclCheck.getTrustedModesForOrigin(acl.graph, resource, directory, aclFile, agentOrigin, (uriNode) => {
89-
return this.fetch(uriNode.doc().value, acl.graph)
91+
originTrustedModes = await aclCheck.getTrustedModesForOrigin(aclGraph, resource, directory, aclFile, agentOrigin, (uriNode) => {
92+
return this.fetch(uriNode.doc().value, aclGraph)
9093
})
9194
} catch (e) {
9295
// FIXME: https://github.com/solid/acl-check/issues/23
9396
// console.error(e.message)
9497
}
95-
let accessDenied = aclCheck.accessDenied(acl.graph, resource, directory, aclFile, agent, modes, agentOrigin, trustedOrigins, originTrustedModes)
9698

97-
function accessDeniedForAccessTo (mode) {
98-
const accessDeniedAccessTo = aclCheck.accessDenied(acl.graph, directory, null, aclFile, agent, [ACL(mode)], agentOrigin, trustedOrigins, originTrustedModes)
99+
function resourceAccessDenied (modes) {
100+
return aclCheck.accessDenied(aclGraph, resource, directory, aclFile, agent, modes, agentOrigin, trustedOrigins, originTrustedModes)
101+
}
102+
function accessDeniedForAccessTo (modes) {
103+
const accessDeniedAccessTo = aclCheck.accessDenied(aclGraph, directory, null, aclFile, agent, modes, agentOrigin, trustedOrigins, originTrustedModes)
99104
const accessResult = !accessDenied && !accessDeniedAccessTo
100-
accessDenied = accessResult ? false : accessDenied || accessDeniedAccessTo
101-
// debugCache('accessDenied result ' + accessDenied)
105+
return accessResult ? false : accessDenied || accessDeniedAccessTo
106+
}
107+
async function accessdeniedFromParent (modes) {
108+
const parentAclDirectory = ACLChecker.getDirectory(acl.parentAcl)
109+
const parentDirectory = parentResource === parentAclDirectory ? null : rdf.sym(parentAclDirectory)
110+
const accessDeniedParent = aclCheck.accessDenied(acl.parentGraph, parentResource, parentDirectory, rdf.sym(acl.parentAcl), agent, modes, agentOrigin, trustedOrigins, originTrustedModes)
111+
const accessResult = !accessDenied && !accessDeniedParent
112+
return accessResult ? false : accessDenied || accessDeniedParent
102113
}
114+
115+
let accessDenied = resourceAccessDenied(modes)
116+
// debugCache('accessDenied resource ' + accessDenied)
117+
103118
// For create and update HTTP methods
104-
if ((method === 'PUT' || method === 'PATCH' || method === 'COPY') && directory) {
119+
if ((method === 'PUT' || method === 'PATCH' || method === 'COPY')) {
105120
// if resource and acl have same parent container,
106121
// and resource does not exist, then accessTo Append from parent is required
107-
if (directory.value === dirname(aclFile.value) + '/' && !resourceExists) {
108-
accessDeniedForAccessTo('Append')
122+
if (directory && directory.value === dirname(aclFile.value) + '/' && !resourceExists) {
123+
accessDenied = accessDeniedForAccessTo([ACL('Append')])
109124
}
125+
// debugCache('accessDenied PUT/PATCH ' + accessDenied)
110126
}
111127

112128
// For delete HTTP method
113-
if ((method === 'DELETE') && directory) {
114-
// if resource and acl have same parent container,
115-
// then accessTo Write from parent is required
116-
if (directory.value === dirname(aclFile.value) + '/') {
117-
accessDeniedForAccessTo('Write')
118-
}
129+
if ((method === 'DELETE')) {
130+
if (resourceExists) {
131+
// deleting a Container
132+
// without Read, the response code will reveal whether a Container is empty or not
133+
if (directory && this.resource.endsWith('/')) accessDenied = resourceAccessDenied([ACL('Read'), ACL('Write')])
134+
// if resource and acl have same parent container,
135+
// then both Read and Write on parent is required
136+
else if (!directory && aclFile.value.endsWith(`/${this.suffix}`)) accessDenied = await accessdeniedFromParent([ACL('Read'), ACL('Write')])
137+
138+
// deleting a Document
139+
else if (directory && directory.value === dirname(aclFile.value) + '/') {
140+
accessDenied = accessDeniedForAccessTo([ACL('Write')])
141+
} else {
142+
accessDenied = await accessdeniedFromParent([ACL('Write')])
143+
}
144+
145+
// https://github.com/solid/specification/issues/14#issuecomment-1712773516
146+
} else { accessDenied = true }
147+
// debugCache('accessDenied DELETE ' + accessDenied)
119148
}
149+
120150
if (accessDenied && user) {
121151
this.messagesCached[cacheKey].push(HTTPError(403, accessDenied))
122152
} else if (accessDenied) {
@@ -140,43 +170,74 @@ class ACLChecker {
140170
return `${parts.join('/')}/`
141171
}
142172

143-
// Gets the ACL that applies to the resource
144-
async getNearestACL () {
173+
// Gets any ACLs that apply to the resource
174+
// DELETE uses docAcl when docAcl is parent to the resource
175+
// or docAcl and parentAcl when docAcl is the ACL of the Resource
176+
async getNearestACL (method) {
145177
const { resource } = this
146178
let isContainer = false
147179
const possibleACLs = this.getPossibleACLs()
148180
const acls = [...possibleACLs]
149181
let returnAcl = null
150-
while (possibleACLs.length > 0 && !returnAcl) {
182+
let returnParentAcl = null
183+
let parentAcl = null
184+
let parentGraph = null
185+
let docAcl = null
186+
let docGraph = null
187+
while (possibleACLs.length > 0 && !returnParentAcl) {
151188
const acl = possibleACLs.shift()
152189
let graph
153190
try {
154191
this.requests[acl] = this.requests[acl] || this.fetch(acl)
155192
graph = await this.requests[acl]
156193
} catch (err) {
157194
if (err && (err.code === 'ENOENT' || err.status === 404)) {
158-
isContainer = true
195+
// only set isContainer before docAcl
196+
if (!docAcl) isContainer = true
159197
continue
160198
}
161199
debug(err)
162200
throw err
163201
}
164-
const relative = resource.replace(acl.replace(/[^/]+$/, ''), './')
165-
debug(`Using ACL ${acl} for ${relative}`)
166-
returnAcl = { acl, graph, isContainer }
202+
// const relative = resource.replace(acl.replace(/[^/]+$/, ''), './')
203+
// debug(`Using ACL ${acl} for ${relative}`)
204+
if (!docAcl) {
205+
docAcl = acl
206+
docGraph = graph
207+
// parentAcl is only needed for DELETE
208+
if (method !== 'DELETE') returnParentAcl = true
209+
} else {
210+
parentAcl = acl
211+
parentGraph = graph
212+
returnParentAcl = true
213+
}
214+
215+
returnAcl = { docAcl, docGraph, isContainer, parentAcl, parentGraph }
167216
}
168217
if (!returnAcl) {
169218
throw new HTTPError(500, `No ACL found for ${resource}, searched in \n- ${acls.join('\n- ')}`)
170219
}
171-
const groupNodes = returnAcl.graph.statementsMatching(null, ACL('agentGroup'), null)
172-
const groupUrls = groupNodes.map(node => node.object.value.split('#')[0])
220+
// fetch group
221+
let groupNodes = returnAcl.docGraph.statementsMatching(null, ACL('agentGroup'), null)
222+
let groupUrls = groupNodes.map(node => node.object.value.split('#')[0])
173223
await Promise.all(groupUrls.map(async groupUrl => {
174224
try {
175-
const graph = await this.fetch(groupUrl, returnAcl.graph)
176-
this.requests[groupUrl] = this.requests[groupUrl] || graph
225+
const docGraph = await this.fetch(groupUrl, returnAcl.docGraph)
226+
this.requests[groupUrl] = this.requests[groupUrl] || docGraph
177227
} catch (e) {} // failed to fetch groupUrl
178228
}))
229+
if (parentAcl) {
230+
groupNodes = returnAcl.parentGraph.statementsMatching(null, ACL('agentGroup'), null)
231+
groupUrls = groupNodes.map(node => node.object.value.split('#')[0])
232+
await Promise.all(groupUrls.map(async groupUrl => {
233+
try {
234+
const docGraph = await this.fetch(groupUrl, returnAcl.parentGraph)
235+
this.requests[groupUrl] = this.requests[groupUrl] || docGraph
236+
} catch (e) {} // failed to fetch groupUrl
237+
}))
238+
}
179239

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

@@ -264,7 +325,7 @@ function fetchLocalOrRemote (mapper, serverUri) {
264325
// debugCache('Expunging from cache', url)
265326
delete temporaryCache[url]
266327
if (Object.keys(temporaryCache).length === 0) {
267-
debugCache('Cache is empty again')
328+
// debugCache('Cache is empty again')
268329
}
269330
}, EXPIRY_MS),
270331
promise: doFetch(url)

lib/create-app.js

+27
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,33 @@ function createApp (argv = {}) {
117117
// Attach the LDP middleware
118118
app.use('/', LdpMiddleware(corsSettings))
119119

120+
// https://stackoverflow.com/questions/51741383/nodejs-express-return-405-for-un-supported-method
121+
app.use(function (req, res, next) {
122+
const AllLayers = app._router.stack
123+
const Layers = AllLayers.filter(x => x.name === 'bound dispatch' && x.regexp.test(req.path))
124+
125+
const Methods = []
126+
Layers.forEach(layer => {
127+
for (const method in layer.route.methods) {
128+
if (layer.route.methods[method] === true) {
129+
Methods.push(method.toUpperCase())
130+
}
131+
}
132+
})
133+
134+
if (Layers.length !== 0 && !Methods.includes(req.method)) {
135+
// res.setHeader('Allow', Methods.join(','))
136+
137+
if (req.method === 'OPTIONS') {
138+
return res.send(Methods.join(', '))
139+
} else {
140+
return res.status(405).send()
141+
}
142+
} else {
143+
next()
144+
}
145+
})
146+
120147
// Errors
121148
app.use(errorPages.handler)
122149

lib/handlers/allow.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ module.exports = allow
22

33
// const path = require('path')
44
const ACL = require('../acl-checker')
5-
const debug = require('../debug.js').ACL
5+
// const debug = require('../debug.js').ACL
66
// const error = require('../http-error')
77

88
function allow (mode) {
@@ -77,7 +77,7 @@ function allow (mode) {
7777
if (resourceUrl.endsWith('.acl') && (await ldp.isOwner(userId, req.hostname))) return next()
7878
} catch (err) {}
7979
const error = req.authError || await req.acl.getError(userId, mode)
80-
debug(`${mode} access denied to ${userId || '(none)'}: ${error.status} - ${error.message}`)
80+
// debug(`${mode} access denied to ${userId || '(none)'}: ${error.status} - ${error.message}`)
8181
next(error)
8282
}
8383
}

lib/handlers/patch.js

+8-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,11 @@ async function patchHandler (req, res, next) {
5353
({ path, contentType } = await ldp.resourceMapper.mapUrlToFile(
5454
{ url: req, createIfNotExists: true, contentType: contentTypeForNew(req) }))
5555
// check if a folder with same name exists
56-
await ldp.checkItemName(req)
56+
try {
57+
await ldp.checkItemName(req)
58+
} catch (err) {
59+
return next(err)
60+
}
5761
resourceExists = false
5862
}
5963
const { url } = await ldp.resourceMapper.mapFileToUrl({ path, hostname: req.hostname })
@@ -65,6 +69,9 @@ async function patchHandler (req, res, next) {
6569
patch.text = req.body ? req.body.toString() : ''
6670
patch.uri = `${url}#patch-${hash(patch.text)}`
6771
patch.contentType = getContentType(req.headers)
72+
if (!patch.contentType) {
73+
throw error(400, 'PATCH request requires a content-type via the Content-Type header')
74+
}
6875
debug('PATCH -- Received patch (%d bytes, %s)', patch.text.length, patch.contentType)
6976
const parsePatch = PATCH_PARSERS[patch.contentType]
7077
if (!parsePatch) {

0 commit comments

Comments
 (0)