Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ addons:
- nicola.localhost

before_install:
- echo "No GitHub dependencies allowed" &&
! grep '"github:' package-lock.json
#- echo "No GitHub dependencies allowed" &&
# ! grep '"github:' package-lock.json
- npm install -g npm@latest

install:
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,17 @@
- The browser-reported `Origin` header will now be checked by
default, and the ACL system can be used to restrict access
to applications for added security.
- Users can add `trustedApp` entries to their profile using a new databrowser pane.
You will see an 'A' icon added while you view a Person's profile URL
with the data browser (might have to hit refresh in your browser and make sure you
are viewing a WebId URL like https://localhost:8443/profile/card#me).
- Logging is now verbose by default so the `-v` option has been
removed and a `--quiet` option has been added to mute the log.
- To be bug compliant with 4.x releases, if a rule for public readable
root / does not exist, it will check in /index.html.acl (see issue #1063)
- Command line options are now kebab-cased rather than camelCased,
config options may be both.
- Resource with no extension now have '$.ttl' appended in the filename (see upgrades notes below).
- Many smaller fixes.

#### 5.0.0 Upgrade Notes
Expand Down
217 changes: 217 additions & 0 deletions hard_link/acl-check.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
// Access control logic

const $rdf = require('rdflib')

const ACL = $rdf.Namespace('http://www.w3.org/ns/auth/acl#')
const FOAF = $rdf.Namespace('http://xmlns.com/foaf/0.1/')
const VCARD = $rdf.Namespace('http://www.w3.org/2006/vcard/ns#')

let _logger

function publisherTrustedApp (kb, doc, aclDoc, modesRequired, origin, docAuths) {
let app = $rdf.sym(origin)
let appAuths = docAuths.filter(auth => kb.holds(auth, ACL('mode'), ACL('Control'), aclDoc))
let owners = appAuths.map(auth => kb.each(auth, ACL('agent'))).flat() // owners
let relevant = owners.map(owner => kb.each(owner, ACL('trust'), null, owner.doc()).filter(
ta => kb.holds(ta, ACL('trustedApp'), app, owner.doc()))).flat() // ta's
let modesOK = relevant.map(ta => kb.each(ta, ACL('mode'))).flat().map(m => m.uri)
let modesRequiredURIs = modesRequired.map(m => m.uri)
modesRequiredURIs.every(uri => modesOK.includes(uri))
// modesRequired.every(mode => appAuths.some(auth => kb.holds(auth, ACL('mode'), mode, aclDoc)))
}

function accessDenied (kb, doc, directory, aclDoc, agent, modesRequired, origin, trustedOrigins, originTrustedModes = []) {
log(`accessDenied: checking access to ${doc} by ${agent} and origin ${origin}`)
const modeURIorReasons = modesAllowed(kb, doc, directory, aclDoc, agent, origin, trustedOrigins, originTrustedModes)
let ok = false
log('accessDenied: modeURIorReasons: ' + JSON.stringify(Array.from(modeURIorReasons)))
modesRequired.forEach(mode => {
log(` checking ` + mode)
if (modeURIorReasons.has(mode.uri)) {
log(' Mode required and allowed:' + mode)
} else if (mode.sameTerm(ACL('Append')) && modeURIorReasons.has(ACL('Write').uri)) {
log(' Append required and Write allowed. OK')
} else {
ok = modeURIorReasons.values().next().value || 'Forbidden'
if (ok.startsWith('http')) {
// Then, the situation is that one mode has failed, the other
// has passed, and we get URI of the one that passed, but that's not a good error
ok = 'All Required Access Modes Not Granted'
}
log(' MODE REQUIRED NOT ALLOWED: ' + mode + ' Denying with ' + ok)
}
})
return ok
}

async function getTrustedModesForOrigin (kb, doc, directory, aclDoc, origin, fetch) {
// FIXME: this is duplicate code from the modesAllowed function, will refactor,
// see https://github.com/solid/acl-check/issues/22
var auths
if (!directory) { // Normal case, ACL for a file
auths = kb.each(null, ACL('accessTo'), doc, aclDoc)
log(` ${auths.length} direct authentications about ${doc}`)
} else {
auths = kb.each(null, ACL('default'), directory, null)
auths = auths.concat(kb.each(null, ACL('defaultForNew'), directory, null)) // Deprecated but keep for ages
log(` ${auths.length} default authentications about ${directory} in ${aclDoc}`)
}
const ownerAuths = auths.filter(auth => kb.holds(auth, ACL('mode'), ACL('Control'), aclDoc))
const owners = ownerAuths.reduce((acc, auth) => acc.concat(kb.each(auth, ACL('agent'))), []) // owners
let result
try {
result = await Promise.all(owners.map(owner => {
return fetch(owner).then(() => {
const q = `
SELECT ?mode WHERE {
${owner} ${ACL('trustedApp')} ?trustedOrigin.
?trustedOrigin ${ACL('origin')} ${origin};
${ACL('mode')} ?mode .
}`
return query(q, kb)
}).catch(e => {
log('could not fetch owner doc', owner, e.message)
})
}))
} catch (e) {
log('error checking owner profiles', e.message)
}
let trustedModes = []
result.map(ownerResults => ownerResults.map(entry => {
trustedModes.push(entry['?mode'])
}))
return Promise.resolve(trustedModes)
}

async function query (queryString, store) {
return new Promise((resolve, reject) => {
try {
const query = $rdf.SPARQLToQuery(queryString, true, store)
const results = []
store.query(query, (result) => {
results.push(result)
}, null, () => {
resolve(results)
})
} catch (err) {
reject(err)
}
})
}

/* Function checkAccess
** @param kb A quadstore
** @param doc the resource (A named node) or directory for which ACL applies
*/
function checkAccess (kb, doc, directory, aclDoc, agent, modesRequired, origin, trustedOrigins, originTrustedModes) {
return !accessDenied(kb, doc, directory, aclDoc, agent, modesRequired, origin, trustedOrigins, originTrustedModes)
}

function modesAllowed (kb, doc, directory, aclDoc, agent, origin, trustedOrigins, originTrustedModes = []) {
var auths
if (!directory) { // Normal case, ACL for a file
auths = kb.each(null, ACL('accessTo'), doc, aclDoc)
log(` ${auths.length} direct authentications about ${doc}`)
} else {
auths = kb.each(null, ACL('default'), directory, null)
auths = auths.concat(kb.each(null, ACL('defaultForNew'), directory, null)) // Deprecated but keep for ages
log(` ${auths.length} default authentications about ${directory} in ${aclDoc}`)
}
if (origin && trustedOrigins && nodesIncludeNode(trustedOrigins, origin)) {
log('Origin ' + origin + ' is trusted')
origin = null // stop worrying about origin
log(` modesAllowed: Origin ${origin} is trusted.`)
}

function agentOrGroupOK (auth, agent) {
log(` Checking auth ${auth} with agent ${agent}`)
if (!agent) {
log(` Agent or group: Fail: not public and not logged on.`)
return false
}
if (kb.holds(auth, ACL('agentClass'), ACL('AuthenticatedAgent'), aclDoc)) {
log(' AuthenticatedAgent: logged in, looks good')
return true
}
if (kb.holds(auth, ACL('agent'), agent, aclDoc)) {
log(' Agent explicitly authenticated.')
return true
}
if (kb.each(auth, ACL('agentGroup'), null, aclDoc).some(
group => kb.holds(group, VCARD('hasMember'), agent, group.doc()))) {
log(' Agent is member of group which has access.')
return true
}
log(' Agent or group access fails for this authentication.')
return false
} // Agent or group

function originOK (auth, origin) {
return kb.holds(auth, ACL('origin'), origin, aclDoc)
}

function agentAndAppFail (auth) {
if (kb.holds(auth, ACL('agentClass'), FOAF('Agent'), aclDoc)) {
log(` Agent or group: Ok, its public.`)
return false
}
if (!agentOrGroupOK(auth, agent)) {
log(' The agent/group check fails')
return 'User Unauthorized'
}
if (!origin) {
log(' Origin check not needed: no origin.')
return false
}
if (originTrustedModes && originTrustedModes.length > 0) {
log(` Origin might have access (${originTrustedModes.join(', ')})`)
return false
}
if (originOK(auth, origin)) {
log(' Origin check succeeded.')
return false
}
log(' Origin check FAILED. Origin not trusted.')
return 'Origin Unauthorized' // @@ look for other trusted apps
}

var modeURIorReasons = new Set()

auths.forEach(auth => {
let agentAndAppStatus = agentAndAppFail(auth)
if (agentAndAppStatus) {
log(' Check failed: ' + agentAndAppStatus)
modeURIorReasons.add(agentAndAppStatus)
} else {
let modes = kb.each(auth, ACL('mode'), null, aclDoc)
if (originTrustedModes && originTrustedModes.length > 0) {
modes = modes.filter(mode => nodesIncludeNode(originTrustedModes, mode))
}
modes.forEach(mode => {
log(' Mode allowed: ' + mode)
modeURIorReasons.add(mode.uri)
})
}
})
return modeURIorReasons
}

function nodesIncludeNode (nodes, node) {
return nodes.some(trustedOrigin => trustedOrigin.termType === node.termType && trustedOrigin.value === node.value)
}

function configureLogger (logger) {
_logger = logger
}

function log (...msgs) {
return (_logger || console.log).apply(_logger, msgs)
}

module.exports.accessDenied = accessDenied
module.exports.checkAccess = checkAccess
module.exports.configureLogger = configureLogger
module.exports.getTrustedModesForOrigin = getTrustedModesForOrigin
module.exports.log = log
module.exports.modesAllowed = modesAllowed
module.exports.publisherTrustedApp = publisherTrustedApp
14 changes: 11 additions & 3 deletions lib/acl-checker.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
const rdf = require('rdflib')
const debug = require('./debug').ACL
const HTTPError = require('./http-error')
const aclCheck = require('@solid/acl-check')
const aclCheck = require('../hard_link/acl-check')
const { URL } = require('url')
const { promisify } = require('util')
const fs = require('fs')
Expand Down Expand Up @@ -57,8 +57,16 @@ class ACLChecker {
const modes = [ACL(mode)]
const agentOrigin = this.agentOrigin
const trustedOrigins = this.trustedOrigins
const accessDenied = aclCheck.accessDenied(acl.graph, resource, directory, aclFile, agent, modes, agentOrigin, 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)
})
} catch (e) {
console.error(e.message)
}
const accessDenied = aclCheck.accessDenied(acl.graph, resource, directory, aclFile, agent, modes, agentOrigin, trustedOrigins, originTrustedModes)
if (accessDenied && user) {
this.messagesCached[cacheKey].push(HTTPError(403, accessDenied))
} else if (accessDenied) {
Expand Down
2 changes: 1 addition & 1 deletion lib/create-app.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const debug = require('./debug')
const path = require('path')
const { routeResolvedFile } = require('./utils')
const ResourceMapper = require('./resource-mapper')
const aclCheck = require('@solid/acl-check')
const aclCheck = require('../hard_link/acl-check')
const { version } = require('../package.json')

const corsSettings = cors({
Expand Down
Loading