UNPKG

solid-ui

Version:

UI library for Solid applications

495 lines • 18.8 kB
/** * Non-UI functions for access control. * See https://github.com/solidos/web-access-control-spec * for the spec that defines how ACL documents work. * @packageDocumentation */ import ns from '../ns'; import { solidLogicSingleton, ACL_LINK } from 'solid-logic'; import * as utils from '../utils'; import * as debug from '../debug'; import { graph, serialize, st, sym } from 'rdflib'; const kb = solidLogicSingleton.store; /** * Take the "default" ACL and convert it into the equivalent ACL * which the resource would have had. Return it as a new separate store. * The "defaultForNew" predicate is also accepted, as a deprecated * synonym for "default". */ export function adoptACLDefault(doc, aclDoc, defaultResource, defaultACLDoc) { const ACL = ns.acl; const isContainer = doc.uri.slice(-1) === '/'; // Give default for all directories const defaults = kb .each(undefined, ACL('default'), defaultResource, defaultACLDoc) .concat(kb.each(undefined, ACL('defaultForNew'), defaultResource, defaultACLDoc)); const proposed = defaults.reduce((accumulatedStatements, da) => accumulatedStatements .concat(kb.statementsMatching(da, ns.rdf('type'), ACL('Authorization'), defaultACLDoc)) .concat(kb.statementsMatching(da, ACL('agent'), undefined, defaultACLDoc)) .concat(kb.statementsMatching(da, ACL('agentClass'), undefined, defaultACLDoc)) .concat(kb.statementsMatching(da, ACL('agentGroup'), undefined, defaultACLDoc)) .concat(kb.statementsMatching(da, ACL('origin'), undefined, defaultACLDoc)) .concat(kb.statementsMatching(da, ACL('originClass'), undefined, defaultACLDoc)) .concat(kb.statementsMatching(da, ACL('mode'), undefined, defaultACLDoc)) .concat(st(da, ACL('accessTo'), doc, defaultACLDoc)) .concat(isContainer ? st(da, ACL('default'), doc, defaultACLDoc) : []), []); const kb2 = graph(); // Potential - derived is kept apart proposed.forEach(st => kb2.add(move(st.subject), move(st.predicate), move(st.object), sym(aclDoc.uri))); return kb2; function move(symbol) { const y = defaultACLDoc.uri.length; // The default ACL file return sym(symbol.uri.slice(0, y) === defaultACLDoc.uri ? aclDoc.uri + symbol.uri.slice(y) : symbol.uri); } } /** * Read and canonicalize the ACL for x in aclDoc * * Accumulate the access rights which each agent or class has */ export function readACL(doc, aclDoc, kb2 = kb, getDefaults = false) { const auths = getDefaults ? getDefaultsFallback(kb2, ns) : kb2.each(undefined, ns.acl('accessTo'), doc); const ACL = ns.acl; const ac = { agent: {}, agentClass: {}, agentGroup: {}, origin: {}, originClass: {} }; Object.keys(ac).forEach(pred => { auths.forEach(function (a) { kb2.each(a, ACL('mode')).forEach(function (mode) { kb2.each(a, ACL(pred)).forEach(function (agent) { ac[pred][agent.uri] = ac[pred][agent.uri] || {}; ac[pred][agent.uri][mode.uri] = a; // could be "true" but leave pointer just in case }); }); }); }); return ac; function getDefaultsFallback(kb, ns) { return kb .each(undefined, ns.acl('default'), doc) .concat(kb.each(undefined, ns.acl('defaultForNew'), doc)); } } /** * Compare two ACLs */ export function sameACL(a, b) { const contains = function (a, b) { for (const pred in { agent: true, agentClass: true, agentGroup: true, origin: true, originClass: true }) { if (a[pred]) { for (const agent in a[pred]) { for (const mode in a[pred][agent]) { if (!b[pred][agent] || !b[pred][agent][mode]) { return false; } } } } } return true; }; return contains(a, b) && contains(b, a); } /** * Union N ACLs */ export function ACLunion(list) { const b = list[0]; let a, ag; for (let k = 1; k < list.length; k++) { ; ['agent', 'agentClass', 'agentGroup', 'origin', 'originClass'].forEach(function (pred) { a = list[k]; if (a[pred]) { for (ag in a[pred]) { for (const mode in a[pred][ag]) { if (!b[pred][ag]) b[pred][ag] = []; b[pred][ag][mode] = true; } } } }); } return b; } /** * Merge ACLs lists from things to form union */ export function loadUnionACL(subjectList, callbackFunction) { const aclList = []; const doList = function (list) { if (list.length) { const doc = list.shift().doc(); getACLorDefault(doc, function (ok, p2, targetDoc, targetACLDoc, defaultHolder, defaultACLDoc) { const defa = !p2; if (!ok || !defaultHolder || !defaultACLDoc) return callbackFunction(ok, targetACLDoc); const acl = defa ? readACL(defaultHolder, defaultACLDoc) : readACL(targetDoc, targetACLDoc); aclList.push(acl); doList(list.slice(1)); }); } else { // all gone callbackFunction(true, ACLunion(aclList)); } }; doList(subjectList); } /** * Represents these as an RDF graph by combination of modes * * Each agent can only be in one place in this model, one combination of modes. * Combos are like full control, read append, read only etc. */ export function ACLbyCombination(ac) { const byCombo = {}; ['agent', 'agentClass', 'agentGroup', 'origin', 'originClass'].forEach(function (pred) { for (const agent in ac[pred]) { const combo = []; for (const mode in ac[pred][agent]) { combo.push(mode); } combo.sort(); const combo2 = combo.join('\n'); if (!byCombo[combo2]) byCombo[combo2] = []; byCombo[combo2].push([pred, agent]); } }); return byCombo; } /** * Write ACL graph to store from AC */ export function makeACLGraph(kb, x, ac, aclDoc) { const byCombo = ACLbyCombination(ac); return makeACLGraphbyCombo(kb, x, byCombo, aclDoc); } /** * Write ACL graph to store from combo */ export function makeACLGraphbyCombo(kb, x, byCombo, aclDoc, main, defa) { const ACL = ns.acl; for (const combo in byCombo) { const pairs = byCombo[combo]; if (!pairs.length) continue; // do not add to store when no agent const modeURIs = combo.split('\n'); let short = modeURIs .map(function (u) { return u.split('#')[1]; }) .join(''); if (defa && !main) short += 'Default'; // don't muddle authorizations const a = kb.sym(aclDoc.uri + '#' + short); kb.add(a, ns.rdf('type'), ACL('Authorization'), aclDoc); if (main) { kb.add(a, ACL('accessTo'), x, aclDoc); } if (defa) { kb.add(a, ACL('default'), x, aclDoc); } for (let i = 0; i < modeURIs.length; i++) { kb.add(a, ACL('mode'), kb.sym(modeURIs[i]), aclDoc); } for (let i = 0; i < pairs.length; i++) { const pred = pairs[i][0]; const ag = pairs[i][1]; kb.add(a, ACL(pred), kb.sym(ag), aclDoc); } } } /** * Debugging short strings for dumping ACL * and possibly in the UI */ export function ACLToString(ac) { return comboToString(ACLbyCombination(ac)); } /** * Convert a [[ComboList]] to a string */ export function comboToString(byCombo) { let str = ''; for (const combo in byCombo) { const modeURIs = combo.split('\n'); const initials = modeURIs .map(function (u) { return u.split('#')[1][0]; }) .join(''); str += initials + ':'; const pairs = byCombo[combo]; for (let i = 0; i < pairs.length; i++) { const pred = pairs[i][0]; const ag = sym(pairs[i][1]); str += pred === 'agent' ? '@' : ''; str += ag.sameTerm(ns.foaf('Agent')) ? '*' : utils.label(ag); if (i < pairs.length - 1) str += ','; } str += ';'; } return '{' + str.slice(0, -1) + '}'; // drop extra semicolon } /** * Write ACL graph as Turtle */ export function makeACLString(x, ac, aclDoc) { const kb2 = graph(); makeACLGraph(kb2, x, ac, aclDoc); return serialize(aclDoc, kb2, aclDoc.uri, 'text/turtle') || ''; } /** * Write ACL graph to web */ export function putACLObject(kb, x, ac, aclDoc, callbackFunction) { const byCombo = ACLbyCombination(ac); return putACLbyCombo(kb, x, byCombo, aclDoc, callbackFunction); } /** * Write ACL graph to web from a [[ComboList]] */ export function putACLbyCombo(kb, x, byCombo, aclDoc, callbackFunction) { var _a; const kb2 = graph(); makeACLGraphbyCombo(kb2, x, byCombo, aclDoc, true); // const str = makeACLString = function(x, ac, aclDoc) (_a = kb.updater) === null || _a === void 0 ? void 0 : _a.put(aclDoc, kb2.statementsMatching(undefined, undefined, undefined, aclDoc), 'text/turtle', function (uri, ok, message) { var _a; if (!ok) { callbackFunction(ok, message); } else { (_a = kb.fetcher) === null || _a === void 0 ? void 0 : _a.unload(aclDoc); makeACLGraphbyCombo(kb, x, byCombo, aclDoc, true); kb.fetcher.requested[aclDoc.uri] = 'done'; // missing: save headers callbackFunction(ok); } }); } /** * Fix the ACl for an individual card as a function of the groups it is in * * All group files must be loaded first */ export function fixIndividualCardACL(person, log, callbackFunction) { const groups = kb.each(undefined, ns.vcard('hasMember'), person); // const doc = person.doc() if (groups) { fixIndividualACL(person, groups, log, callbackFunction); } else { log('This card is in no groups'); callbackFunction(true); // fine, no requirements to access. default should be ok } // @@ if no groups, then use default for People container or the book top container.? } /** * This function is used by [[fixIndividualCardACL]] */ export function fixIndividualACL(item, subjects, log, callbackFunction) { log = log || debug.log; const doc = item.doc(); getACLorDefault(doc, function (ok, exists, targetDoc, targetACLDoc, defaultHolder, defaultACLDoc) { if (!ok || !defaultHolder || !defaultACLDoc) return callbackFunction(false, targetACLDoc); // ie message const ac = exists ? readACL(targetDoc, targetACLDoc) : readACL(defaultHolder, defaultACLDoc); loadUnionACL(subjects, function (ok, union) { if (!ok) return callbackFunction(false, union); if (sameACL(union, ac)) { log('Nice - same ACL. no change ' + utils.label(item) + ' ' + doc); } else { log('Group ACLs differ for ' + utils.label(item) + ' ' + doc); // log("Group ACLs: " + makeACLString(targetDoc, union, targetACLDoc)) // log((exists ? "Previous set" : "Default") + " ACLs: " + // makeACLString(targetDoc, ac, targetACLDoc)) putACLObject(kb, targetDoc, union, targetACLDoc, callbackFunction); } }); }); } /** * Set an ACL */ export function setACL(docURI, aclText, callbackFunction) { const aclDoc = kb.any(docURI, ACL_LINK); // @@ check that this get set by web.js if (!kb.fetcher) { throw new Error('Store has no fetcher'); } if (aclDoc) { // Great we already know where it is kb.fetcher .webOperation('PUT', aclDoc.value, { data: aclText, contentType: 'text/turtle' }) .then((res) => { callbackFunction(res.ok, res.error || ''); }); // @@@ check params } else { kb.fetcher.nowOrWhenFetched(docURI, undefined, function (ok, body) { if (!ok) return callbackFunction(ok, 'Gettting headers for ACL: ' + body); const aclDoc = kb.any(docURI, ACL_LINK); // @@ check that this get set by web.js if (!aclDoc) { // complainIfBad(false, "No Link rel=ACL header for " + docURI) callbackFunction(false, 'No Link rel=ACL header for ' + docURI); } else { if (!kb.fetcher) { throw new Error('Store has no fetcher'); } kb.fetcher .webOperation('PUT', aclDoc.value, { data: aclText, contentType: 'text/turtle' }) .then((res) => { callbackFunction(res.ok, res.error || ''); }); } }); } } /** * Get ACL file or default if necessary * * @param callbackFunction Will be called in the following ways, in the following cases: * * `callbackFunction(true, true, doc, aclDoc)` if the ACL did exist * * `callbackFunction(true, false, doc, aclDoc, defaultHolder, defaultACLDoc)` if the ACL file did not exist but a default did * * `callbackFunction(false, false, status, message)` when there was an error getting the original * * `callbackFunction(false, true, status, message)` when there was an error getting the default */ export function getACLorDefault(doc, callbackFunction) { getACL(doc, function (ok, status, aclDoc, message) { const ACL = ns.acl; if (!ok) return callbackFunction(false, false, status, message); // Recursively search for the ACL file which gives default access const tryParent = function (uri) { if (uri.slice(-1) === '/') { uri = uri.slice(0, -1); } const right = uri.lastIndexOf('/'); const left = uri.indexOf('/', uri.indexOf('//') + 2); if (left > right) { return callbackFunction(false, true, 404, 'Found no ACL resource'); } uri = uri.slice(0, right + 1); const doc2 = sym(uri); getACL(doc2, function (ok, status, defaultACLDoc) { if (!ok) { return callbackFunction(false, true, status, `( No ACL pointer ${uri} ${status})${defaultACLDoc}`); } else if (status === 403) { return callbackFunction(false, true, status, `( default ACL file FORBIDDEN. Stop.${uri})`); } else if (status === 404) { return tryParent(uri); } else if (status !== 200) { return callbackFunction(false, true, status, `Error status '${status}' searching for default for ${doc2}`); } // 200 // statusBlock.textContent += (" ACCESS set at " + uri + ". End search.") const defaults = kb .each(undefined, ACL('default'), kb.sym(uri), defaultACLDoc) .concat(kb.each(undefined, ACL('defaultForNew'), kb.sym(uri), defaultACLDoc)); if (!defaults.length) { return tryParent(uri); // Keep searching } const defaultHolder = kb.sym(uri); return callbackFunction(true, false, doc, aclDoc, defaultHolder, defaultACLDoc); }); }; // tryParent if (!ok) { return callbackFunction(false, false, status, `Error accessing Access Control information for ${doc}) ${message}`); } else if (status === 404) { tryParent(doc.uri); // @@ construct default one - the server should do that } else if (status === 403) { return callbackFunction(false, false, status, `(Sharing not available to you)${message}`); } else if (status !== 200) { return callbackFunction(false, false, status, `Error ${status} accessing Access Control information for ${doc}: ${message}`); } else { // 200 return callbackFunction(true, true, doc, aclDoc); } }); // Call to getACL } /** * Calls back `(ok, status, acldoc, message)` as follows * * * `(false, 900, errormessage)` if no link header * * `(true, 403, documentSymbol, fileaccesserror)` if not authorized * * `(true, 404, documentSymbol, fileaccesserror)` if does not exist * * `(true, 200, documentSymbol)` if file exists and read OK */ export function getACL(doc, callbackFunction) { if (!kb.fetcher) { throw new Error('kb has no fetcher'); } kb.fetcher.nowOrWhenFetched(doc, undefined, function (ok, body) { if (!ok) { return callbackFunction(ok, `Can't get headers to find ACL for ${doc}: ${body}`); } const aclDoc = kb.any(doc, ACL_LINK); // @@ check that this get set by web.js if (!aclDoc) { callbackFunction(false, 900, `No Link rel=ACL header for ${doc}`); } else { if (!kb.fetcher) { throw new Error('kb has no fetcher'); } if (kb.fetcher.nonexistent[aclDoc.value]) { return callbackFunction(true, 404, aclDoc, `ACL file ${aclDoc} does not exist.`); } kb.fetcher.nowOrWhenFetched(aclDoc, undefined, function (ok, message, response) { if (!ok) { callbackFunction(true, response.status, aclDoc, `Can't read Access Control File ${aclDoc}: ${message}`); } else { callbackFunction(true, 200, aclDoc); } }); } }); } /** * Calls [[getACLorDefault]] and then (?) */ export async function getProspectiveHolder(targetDirectory) { return new Promise((resolve, reject) => getACLorDefault(sym(targetDirectory), (ok, isDirectACL, targetDoc, targetACLDoc, defaultHolder) => { if (ok) { return resolve((isDirectACL ? targetDoc : defaultHolder)); } return reject(new Error(`Error loading ${targetDirectory}`)); })); } //# sourceMappingURL=acl.js.map