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

5.7.9 beta #1750

Merged
merged 40 commits into from
Feb 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
2dd0789
allow PUT Append for new resource
bourgeoa Nov 28, 2023
208c67b
Merge branch 'main' into appendPutNewDocument
bourgeoa Dec 18, 2023
47100fc
Append with PUT
bourgeoa Dec 19, 2023
990eff0
update check itemName
bourgeoa Dec 23, 2023
9fe8003
mocha is not exiting
bourgeoa Dec 26, 2023
dabea82
Merge branch 'issue#1743' into alain-global
bourgeoa Dec 26, 2023
0185d3c
5.7.9-alpha
bourgeoa Dec 26, 2023
e17beb8
409 itemName and contentTypen in PATCH
bourgeoa Dec 26, 2023
3e6db72
Merge branch 'issue#1743' into 5.7.9-alpha
bourgeoa Dec 26, 2023
0bbb3d3
Merge branch 'appendPutNewDocument' into issue#1743
bourgeoa Dec 27, 2023
d0153fc
Update lib/handlers/put.js
bourgeoa Dec 27, 2023
b348b4b
Update lib/ldp.js
bourgeoa Dec 27, 2023
8018f1f
Update lib/ldp.js
bourgeoa Dec 27, 2023
057157b
Update lib/ldp.js
bourgeoa Dec 27, 2023
a99473d
Update lib/ldp.js
bourgeoa Dec 27, 2023
9e14a1d
failing test in CI
bourgeoa Dec 27, 2023
4900dc7
Merge branch 'issue#1743' of https://github.com/solid/node-solid-serv…
bourgeoa Dec 27, 2023
4860cbb
update solid-crud-tests
bourgeoa Dec 27, 2023
a5d6011
Merge branch 'issue#1743' into 5.7.9-alpha
bourgeoa Dec 27, 2023
b4e062b
update new slug tests
bourgeoa Dec 31, 2023
eca3418
run 18 only
bourgeoa Dec 31, 2023
81f6d10
run 18 only
bourgeoa Dec 31, 2023
1f0277a
Merge branch '5.7.9-alpha' of https://github.com/solid/node-solid-ser…
bourgeoa Dec 31, 2023
a7b7df9
acl-checker.js rewrite parentAcl
bourgeoa Jan 6, 2024
bc592e8
5.7.9-beta
bourgeoa Jan 6, 2024
1593dd2
update DELETE
bourgeoa Jan 7, 2024
667b3a5
update DELETE
bourgeoa Jan 7, 2024
6b6257f
update DELETE
bourgeoa Jan 9, 2024
c91b7bc
cleaning an DELETE
bourgeoa Jan 10, 2024
6c7fe98
cleaning
bourgeoa Jan 10, 2024
95dd7c0
Update README.md
bourgeoa Jan 11, 2024
809e0ac
404 --> 403/401 with DELETE
bourgeoa Jan 15, 2024
33f7354
405 not allowed method
bourgeoa Jan 16, 2024
16e36a6
Update lib/acl-checker.js
bourgeoa Jan 17, 2024
897207c
Update lib/acl-checker.js
bourgeoa Jan 17, 2024
fbd30b0
text/turtle defaultContainerContentType
bourgeoa Jan 17, 2024
5a42a14
Merge branch '5.7.9-beta' of https://github.com/solid/node-solid-serv…
bourgeoa Jan 17, 2024
59f94fb
precondition if-none-match asterisk
bourgeoa Jan 28, 2024
3784e55
redirect http
bourgeoa Jan 28, 2024
9acda7d
cleaning
bourgeoa Jan 29, 2024
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: 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

bourgeoa marked this conversation as resolved.
Show resolved Hide resolved
```

### 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)
}
}
9 changes: 8 additions & 1 deletion lib/handlers/patch.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,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 @@ -65,6 +69,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
Loading