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 30 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.
bourgeoa marked this conversation as resolved.
Show resolved Hide resolved

```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
122 changes: 89 additions & 33 deletions lib/acl-checker.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
const { dirname } = require('path')
const rdf = require('rdflib')
const debug = require('./debug').ACL
const debugCache = require('./debug').cache
// const debugCache = require('./debug').cache
// const debugAccounts = require('./debug').accounts
const HTTPError = require('./http-error')
const aclCheck = require('@solid/acl-check')
const { URL } = require('url')
Expand Down Expand Up @@ -55,68 +56,93 @@ class ACLChecker {
}
this.messagesCached[cacheKey] = this.messagesCached[cacheKey] || []

const acl = await this.getNearestACL().catch(err => {
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 resourceAccessDenied (modes) {
accessDenied = aclCheck.accessDenied(aclGraph, 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 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)
}
async function accessdeniedFromParent (modes) {
const parentAclDirectory = ACLChecker.getDirectory(acl.parentAcl)
const parentDirectory = parentResource === parentAclDirectory ? null : rdf.sym(parentAclDirectory)
// if (acl.parentAcl.endWith('/.acl')) parentDirectory = rdf.sym(parentAclDirectory)
const accessDeniedParent = aclCheck.accessDenied(acl.parentGraph, parentResource, parentDirectory, rdf.sym(acl.parentAcl), agent, modes, agentOrigin, trustedOrigins, originTrustedModes)
const accessResult = !accessDenied && !accessDeniedParent
accessDenied = accessResult ? false : accessDenied || accessDeniedParent
// debugCache('accessDenied result ' + accessDenied)
}

let accessDenied
resourceAccessDenied(modes)
// 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) {
accessDeniedForAccessTo([ACL('Append')])
}
}

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

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

if (accessDenied && user) {
this.messagesCached[cacheKey].push(HTTPError(403, accessDenied))
} else if (accessDenied) {
Expand All @@ -141,42 +167,72 @@ class ACLChecker {
}

// Gets the ACL that applies to the resource
async getNearestACL () {
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) {
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 // alain
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 // alain
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 +320,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
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
51 changes: 46 additions & 5 deletions lib/handlers/put.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,17 @@ const { stringToStream } = require('../utils')
async function handler (req, res, next) {
debug(req.originalUrl)
res.header('MS-Author-Via', 'SPARQL')

const contentType = req.get('content-type')

// check whether a folder or resource with same name exists
try {
const ldp = req.app.locals.ldp
await ldp.checkItemName(req)
} catch (e) {
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))
Expand All @@ -21,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)
}
Expand Down
2 changes: 1 addition & 1 deletion lib/ldp-middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading