UNPKG

@solid/acl-check

Version:

Web Access Control check access function

222 lines (204 loc) 8.54 kB
// 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) { const app = $rdf.sym(origin); const appAuths = docAuths.filter(auth => kb.holds(auth, ACL('mode'), ACL('Control'), aclDoc)); const owners = appAuths.map(auth => kb.each(auth, ACL('agent'))).flat(); // owners const 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 const modesOK = relevant.map(ta => kb.each(ta, ACL('mode'))).flat().map(m => m.uri); const 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 let 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); } const trustedModes = []; try { result.forEach(ownerResults => ownerResults.forEach(entry => { trustedModes.push(entry['?mode']); })); } catch (e) { log('error processing owner results'); } 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 = []) { let 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 } const modeURIorReasons = new Set(); auths.forEach(auth => { const agentAndAppStatus = agentAndAppFail(auth); if (agentAndAppStatus) { log(' Check failed: ' + agentAndAppStatus); modeURIorReasons.add(agentAndAppStatus); } else { let modes = kb.each(auth, ACL('mode'), null, aclDoc); // If there IS an origin, check that those modes are allowed to it too if (origin && 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;