UNPKG

solid-permissions

Version:

Web Access Control based permissions library

1,136 lines (1,030 loc) 39 kB
'use strict'; /** * @module permission-set * Models the set of Authorizations in a given .acl resource. * @see https://github.com/solid/web-access-control-spec for details. * The working assumptions here are: * - Model the various permissions in an ACL resource as a set of unique * authorizations, with one agent (or one group), and only * one resource (acl:accessTo or acl:default) per authorization. * - If the source RDF of the ACL resource has multiple agents or multiple * resources in one authorization, separate them into multiple separate * Authorization objects (with one agent/group and one resourceUrl each) * - A single Authorization object can grant access to multiple modes (read, * write, control, etc) * - By default, all the authorizations in a container's ACL will be marked * as 'to be inherited', that is will have `acl:default` set. */ var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } var Authorization = require('./authorization'); var GroupListing = require('./group-listing'); var _require = require('./modes'), acl = _require.acl; var vocab = require('solid-namespace'); var debug = require('debug')('solid:permissions'); var DEFAULT_ACL_SUFFIX = '.acl'; var DEFAULT_CONTENT_TYPE = 'text/turtle'; /** * Resource types, used by PermissionSet objects */ var RESOURCE = 'resource'; var CONTAINER = 'container'; /** * Agent type index names (used by findAuthByAgent() etc) */ var AGENT_INDEX = 'agents'; var GROUP_INDEX = 'groups'; var PermissionSet = function () { /** * @class PermissionSet * @param resourceUrl {String} URL of the resource to which this PS applies * @param aclUrl {String} URL of the ACL corresponding to the resource * @param isContainer {Boolean} Is the resource a container? (Affects usage of * inherit semantics / acl:default) * @param [options={}] {Object} Options hashmap * @param [options.graph] {Graph} Parsed RDF graph of the ACL resource * @param [options.rdf] {RDF} RDF Library * @param [options.strictOrigin] {Boolean} Enforce strict origin? * @param [options.host] {String} Actual request uri * @param [options.origin] {String} Origin URI to enforce, relevant * if strictOrigin is set to true * @param [options.webClient] {SolidWebClient} Used for save() and clear() * @param [options.isAcl] {Function} * @param [options.aclUrlFor] {Function} * @constructor */ function PermissionSet(resourceUrl, aclUrl, isContainer) { var options = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {}; _classCallCheck(this, PermissionSet); /** * Hashmap of all Authorizations in this permission set, keyed by a hashed * combination of an agent's/group's webId and the resourceUrl. * @property authorizations * @type {Object} */ this.authorizations = {}; /** * The URL of the corresponding ACL resource, at which these permissions will * be saved. * @property aclUrl * @type {String} */ this.aclUrl = aclUrl; /** * Optional request host (used by checkOrigin()) * @property host * @type {String} */ this.host = options.host; /** * Initialize the agents / groups indexes. * For each index type (`agents`, `groups`), authorizations are indexed * first by `agentId`, then by access type (direct or inherited), and * lastly by resource. For example: * * ``` * agents: { * 'https://alice.com/#i': { * accessTo: { * 'https://alice.com/file1': authorization1 * }, * default: { * 'https://alice.com/': authorization2 * } * } * } * ``` * @property authsBy * @type {Object} */ this.authsBy = { 'agents': {}, // Auths by agent webId 'groups': {} // Auths by group webId (also includes Public / EVERYONE) /** * Cache of GroupListing objects, by group webId. Populated by `loadGroups()`. * @property groups * @type {Object} */ };this.groups = {}; /** * RDF Library (optionally injected) * @property rdf * @type {RDF} */ this.rdf = options.rdf; /** * Whether this permission set is for a 'container' or a 'resource'. * Determines whether or not the inherit/'acl:default' attribute is set on * all its Authorizations. * @property resourceType * @type {String} */ this.resourceType = isContainer ? CONTAINER : RESOURCE; /** * The URL of the resource for which these permissions apply. * @property resourceUrl * @type {String} */ this.resourceUrl = resourceUrl; /** * Should this permission set enforce "strict origin" policy? * (If true, uses `options.origin` parameter) * @property strictOrigin * @type {Boolean} */ this.strictOrigin = options.strictOrigin; /** * Contents of the request's `Origin:` header. * (used only if `strictOrigin` parameter is set to true) * @property origin * @type {String} */ this.origin = options.origin; /** * Solid REST client (optionally injected), used by save() and clear(). * @type {SolidWebClient} */ this.webClient = options.webClient; // Init the functions for deriving an ACL url for a given resource this.aclUrlFor = options.aclUrlFor ? options.aclUrlFor : defaultAclUrlFor; this.aclUrlFor.bind(this); this.isAcl = options.isAcl ? options.isAcl : defaultIsAcl; this.isAcl.bind(this); // Optionally initialize from a given parsed graph if (options.graph) { this.initFromGraph(options.graph); } } /** * Adds a given Authorization instance to the permission set. * Low-level function, clients should use `addPermission()` instead, in most * cases. * @method addAuthorization * @private * @param auth {Authorization} * @return {PermissionSet} Returns self (chainable) */ _createClass(PermissionSet, [{ key: 'addAuthorization', value: function addAuthorization(auth) { var hashFragment = auth.hashFragment(); if (hashFragment in this.authorizations) { // An authorization for this agent and resource combination already exists // Merge the incoming access modes with its existing ones this.authorizations[hashFragment].mergeWith(auth); } else { this.authorizations[hashFragment] = auth; } if (!auth.virtual && auth.allowsControl()) { // If acl:Control is involved, ensure implicit rules for the .acl resource this.addControlPermissionsFor(auth); } // Create the appropriate indexes this.addToAgentIndex(auth); if (auth.isPublic() || auth.isGroup()) { this.addToGroupIndex(auth); } return this; } /** * Creates an Authorization with the given parameters, and passes it on to * `addAuthorization()` to be added to this PermissionSet. * Essentially a convenience factory method. * @method addAuthorizationFor * @private * @param resourceUrl {String} * @param inherit {Boolean} * @param agent {string|Quad|GroupListing} Agent URL (or `acl:agent` RDF triple). * @param [accessModes=[]] {string|NamedNode|Array} 'READ'/'WRITE' etc. * @param [origins=[]] {Array<String>} List of origins that are allowed access * @param [mailTos=[]] {Array<String>} * @return {PermissionSet} Returns self, chainable */ }, { key: 'addAuthorizationFor', value: function addAuthorizationFor(resourceUrl, inherit, agent) { var accessModes = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : []; var origins = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : []; var mailTos = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : []; var auth = new Authorization(resourceUrl, inherit); if (agent instanceof GroupListing) { auth.setGroup(agent.listing); } else { auth.setAgent(agent); } auth.addMode(accessModes); auth.addOrigin(origins); mailTos.forEach(function (mailTo) { auth.addMailTo(mailTo); }); this.addAuthorization(auth); return this; } /** * Adds a virtual (will not be serialized to RDF) authorization giving * Read/Write/Control access to the corresponding ACL resource if acl:Control * is encountered in the actual source ACL. * @method addControlPermissionsFor * @private * @param auth {Authorization} Authorization containing an acl:Control access * mode. */ }, { key: 'addControlPermissionsFor', value: function addControlPermissionsFor(auth) { var impliedAuth = auth.clone(); impliedAuth.resourceUrl = this.aclUrlFor(auth.resourceUrl); impliedAuth.virtual = true; impliedAuth.addMode(acl.ALL_MODES); this.addAuthorization(impliedAuth); } /** * Adds a group permission for the given access mode and group web id. * @method addGroupPermission * @param webId {String} * @param accessMode {String|Array<String>} * @return {PermissionSet} Returns self (chainable) */ }, { key: 'addGroupPermission', value: function addGroupPermission(webId, accessMode) { if (!this.resourceUrl) { throw new Error('Cannot add a permission to a PermissionSet with no resourceUrl'); } var auth = new Authorization(this.resourceUrl, this.isAuthInherited()); auth.setGroup(webId); auth.addMode(accessMode); this.addAuthorization(auth); return this; } /** * Adds a permission for the given access mode and agent id. * @method addPermission * @param webId {String} URL of an agent for which this permission applies * @param accessMode {String|Array<String>} One or more access modes * @param [origin] {String|Array<String>} One or more allowed origins (optional) * @return {PermissionSet} Returns self (chainable) */ }, { key: 'addPermission', value: function addPermission(webId, accessMode, origin) { if (!webId) { throw new Error('addPermission() requires a valid webId'); } if (!accessMode) { throw new Error('addPermission() requires a valid accessMode'); } if (!this.resourceUrl) { throw new Error('Cannot add a permission to a PermissionSet with no resourceUrl'); } var auth = new Authorization(this.resourceUrl, this.isAuthInherited()); auth.setAgent(webId); auth.addMode(accessMode); if (origin) { auth.addOrigin(origin); } this.addAuthorization(auth); return this; } /** * Adds a given authorization to the "lookup by agent id" index. * Enables lookups via `findAuthByAgent()`. * @method addToAgentIndex * @private * @param authorization {Authorization} */ }, { key: 'addToAgentIndex', value: function addToAgentIndex(authorization) { var webId = authorization.webId(); var accessType = authorization.accessType; var resourceUrl = authorization.resourceUrl; var agents = this.authsBy.agents; if (!agents[webId]) { agents[webId] = {}; } if (!agents[webId][accessType]) { agents[webId][accessType] = {}; } if (!agents[webId][accessType][resourceUrl]) { agents[webId][accessType][resourceUrl] = authorization; } else { agents[webId][accessType][resourceUrl].mergeWith(authorization); } } /** * Adds a given authorization to the "lookup by group id" index. * Enables lookups via `findAuthByAgent()`. * @method addToGroupIndex * @private * @param authorization {Authorization} */ }, { key: 'addToGroupIndex', value: function addToGroupIndex(authorization) { var webId = authorization.webId(); var accessType = authorization.accessType; var resourceUrl = authorization.resourceUrl; var groups = this.authsBy.groups; if (!groups[webId]) { groups[webId] = {}; } if (!groups[webId][accessType]) { groups[webId][accessType] = {}; } if (!groups[webId][accessType][resourceUrl]) { groups[webId][accessType][resourceUrl] = authorization; } else { groups[webId][accessType][resourceUrl].mergeWith(authorization); } } /** * Returns a list of all the Authorizations that belong to this permission set. * Mostly for internal use. * @method allAuthorizations * @return {Array<Authorization>} */ }, { key: 'allAuthorizations', value: function allAuthorizations() { var _this = this; var authList = []; var auth; Object.keys(this.authorizations).forEach(function (authKey) { auth = _this.authorizations[authKey]; authList.push(auth); }); return authList; } /** * Tests whether this PermissionSet gives Public (acl:agentClass foaf:Agent) * access to a given uri. * @method allowsPublic * @param mode {String|NamedNode} Access mode (read/write/control etc) * @param resourceUrl {String} * @return {Boolean} */ }, { key: 'allowsPublic', value: function allowsPublic(mode, resourceUrl) { resourceUrl = resourceUrl || this.resourceUrl; var publicAuth = this.findPublicAuth(resourceUrl); if (!publicAuth) { return false; } return publicAuth.allowsMode(mode); } /** * Returns an RDF graph representation of this permission set and all its * Authorizations. Used by `save()`. * @method buildGraph * @private * @param rdf {RDF} RDF Library * @return {Graph} */ }, { key: 'buildGraph', value: function buildGraph(rdf) { var graph = rdf.graph(); this.allAuthorizations().forEach(function (auth) { graph.add(auth.rdfStatements(rdf)); }); return graph; } /** * Tests whether the given agent has the specified access to a resource. * This is one of the main use cases for this solid-permissions library. * Optionally performs strict origin checking (if `strictOrigin` is enabled * in the constructor's options). * @method checkAccess * @param resourceUrl {String} * @param agentId {String} * @param accessMode {String} Access mode (read/write/control) * @param [options={}] {Object} Passed through to `loadGroups()`. * @param [options.fetchGraph] {Function} Injected, returns a parsed graph of * a remote document (group listing). Required. * @param [options.rdf] {RDF} RDF library * @throws {Error} * @return {Promise<Boolean>} */ }, { key: 'checkAccess', value: function checkAccess(resourceUrl, agentId, accessMode) { var _this2 = this; var options = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {}; debug('Checking access for agent ' + agentId); // First, check to see if there is public access for this mode if (this.allowsPublic(accessMode, resourceUrl)) { debug('Public access allowed for ' + resourceUrl); return Promise.resolve(true); } // Next, see if there is an individual authorization (for a user or a group) if (this.checkAccessForAgent(resourceUrl, agentId, accessMode)) { debug('Individual access granted for ' + resourceUrl); return Promise.resolve(true); } // If there are no group authorizations, no need to proceed if (!this.hasGroups()) { debug('No groups authorizations exist'); return Promise.resolve(false); } // Lastly, load the remote group listings, and check for group auth debug('Check groups authorizations'); return this.loadGroups(options).then(function () { return _this2.checkGroupAccess(resourceUrl, agentId, accessMode, options); }); } /** * @param resourceUrl {String} * @param agentId {String} * @param accessMode {String} Access mode (read/write/control) * @throws {Error} * @return {Boolean} */ }, { key: 'checkAccessForAgent', value: function checkAccessForAgent(resourceUrl, agentId, accessMode) { var auth = this.findAuthByAgent(agentId, resourceUrl); var result = auth && this.checkOrigin(auth) && auth.allowsMode(accessMode); return result; } /** * @param resourceUrl {string} * @param agentId {string} * @param accessMode {string} Access mode (read/write/control) * @param [options={}] {Object} * @param [options.fetchDocument] {Function} * @throws {Error} * @return {boolean} */ }, { key: 'checkGroupAccess', value: function checkGroupAccess(resourceUrl, agentId, accessMode) { var _this3 = this; var options = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {}; var result = false; var membershipMatches = this.groupsForMember(agentId); membershipMatches.find(function (groupWebId) { debug('Looking for access rights for ' + groupWebId); if (_this3.checkAccessForAgent(resourceUrl, groupWebId, accessMode)) { debug('Groups access granted for ' + resourceUrl); result = true; } }); return result; } /** * Tests whether a given authorization allows operations from the current * request's `Origin` header. (The current request's origin and host are * passed in as options to the PermissionSet's constructor.) * @param authorization {Authorization} * @return {Boolean} */ }, { key: 'checkOrigin', value: function checkOrigin(authorization) { if (!this.strictOrigin || // Enforcement turned off in server config !this.origin || // No origin - not a script, do not enforce origin this.origin === this.host) { // same origin is trusted return true; } // If not same origin, check that the origin is in the explicit ACL list return authorization.allowsOrigin(this.origin); } /** * Sends a delete request to a particular ACL resource. Intended to be used for * an existing loaded PermissionSet, but you can also specify a particular * URL to delete. * Usage: * * ``` * // If you have an existing PermissionSet as a result of `getPermissions()`: * solid.getPermissions('https://www.example.com/file1') * .then(function (permissionSet) { * // do stuff * return permissionSet.clear() // deletes that permissionSet * }) * // Otherwise, use the helper function * // solid.clearPermissions(resourceUrl) instead * solid.clearPermissions('https://www.example.com/file1') * .then(function (response) { * // file1.acl is now deleted * }) * ``` * @method clear * @param [webClient] {SolidWebClient} * @throws {Error} Rejects with an error if it doesn't know where to delete, or * with any XHR errors that crop up. * @return {Promise<Request>} */ }, { key: 'clear', value: function clear(webClient) { webClient = webClient || this.webClient; if (!webClient) { return Promise.reject(new Error('Cannot clear - no web client')); } var aclUrl = this.aclUrl; if (!aclUrl) { return Promise.reject(new Error('Cannot clear - unknown target url')); } return webClient.del(aclUrl); } /** * Returns the number of Authorizations in this permission set. * @method count * @return {Number} */ }, { key: 'equals', /** * Returns whether or not this permission set is equal to another one. * A PermissionSet is considered equal to another one iff: * - It has the same number of authorizations, and each of those authorizations * has a corresponding one in the other set * - They are both intended for the same resource (have the same resourceUrl) * - They are both intended to be saved at the same aclUrl * @method equals * @param ps {PermissionSet} The other permission set to compare to * @return {Boolean} */ value: function equals(ps) { var _this4 = this; var sameUrl = this.resourceUrl === ps.resourceUrl; var sameAclUrl = this.aclUrl === ps.aclUrl; var sameResourceType = this.resourceType === ps.resourceType; var myAuthKeys = Object.keys(this.authorizations); var otherAuthKeys = Object.keys(ps.authorizations); if (myAuthKeys.length !== otherAuthKeys.length) { return false; } var sameAuths = true; var myAuth, otherAuth; myAuthKeys.forEach(function (authKey) { myAuth = _this4.authorizations[authKey]; otherAuth = ps.authorizations[authKey]; if (!otherAuth) { sameAuths = false; } if (!myAuth.equals(otherAuth)) { sameAuths = false; } }); return sameUrl && sameAclUrl && sameResourceType && sameAuths; } /** * Finds and returns an authorization (stored in the 'find by agent' index) * for a given agent (web id) and resource. * @method findAuthByAgent * @private * @param webId {String} * @param resourceUrl {String} * @param indexType {String} Either 'default' or 'accessTo' * @return {Authorization} */ }, { key: 'findAuthByAgent', value: function findAuthByAgent(webId, resourceUrl) { var indexType = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : AGENT_INDEX; var index = this.authsBy[indexType]; if (!index[webId]) { // There are no permissions at all for this agent return false; } // first check the accessTo type var accessToAuths = index[webId][acl.ACCESS_TO]; var accessToMatch = void 0; if (accessToAuths) { accessToMatch = accessToAuths[resourceUrl]; } if (accessToMatch) { return accessToMatch; } // then check the default/inherited type permissions var defaultAuths = index[webId][acl.DEFAULT]; var defaultMatch = void 0; if (defaultAuths) { // First try an exact match (resource matches the acl:default object) defaultMatch = defaultAuths[resourceUrl]; if (!defaultMatch) { // Next check to see if resource is in any of the relevant containers var containers = Object.keys(defaultAuths).sort().reverse(); // Loop through the container URLs, sorted in reverse alpha var _iteratorNormalCompletion = true; var _didIteratorError = false; var _iteratorError = undefined; try { for (var _iterator = containers[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { var containerUrl = _step.value; if (resourceUrl.startsWith(containerUrl)) { defaultMatch = defaultAuths[containerUrl]; break; } } } catch (err) { _didIteratorError = true; _iteratorError = err; } finally { try { if (!_iteratorNormalCompletion && _iterator.return) { _iterator.return(); } } finally { if (_didIteratorError) { throw _iteratorError; } } } } } return defaultMatch; } /** * Finds and returns an authorization (stored in the 'find by group' index) * for the "Everyone" group (acl:agentClass foaf:Agent), for a given resource. * @method findAuthByAgent * @private * @param resourceUrl {String} * @return {Authorization} */ }, { key: 'findPublicAuth', value: function findPublicAuth(resourceUrl) { return this.findAuthByAgent(acl.EVERYONE, resourceUrl, GROUP_INDEX); } /** * Iterates over all the authorizations in this permission set. * Convenience method. * Usage: * * ``` * solid.getPermissions(resourceUrl) * .then(function (permissionSet) { * permissionSet.forEach(function (auth) { * // do stuff with auth * }) * }) * ``` * @method forEach * @param callback {Function} Function to apply to each authorization */ }, { key: 'forEach', value: function forEach(callback) { var _this5 = this; this.allAuthorizations().forEach(function (auth) { callback.call(_this5, auth); }); } /** * Returns a list of webIds of groups to which this agent belongs. * Note: Only checks loaded groups (assumes a previous `loadGroups()` call). * @param agentId {string} * @return {Array<string>} */ }, { key: 'groupsForMember', value: function groupsForMember(agentId) { var _this6 = this; var loadedGroupIds = Object.keys(this.groups); return loadedGroupIds.filter(function (groupWebId) { return _this6.groups[groupWebId].hasMember(agentId); }); } /** * Returns a list of URIs of group authorizations in this permission set * (those added via addGroupPermission(), etc). * @param [excludePublic=true] {Boolean} Should agentClass Agent be excluded? * @return {Array<string>} */ }, { key: 'groupUris', value: function groupUris() { var excludePublic = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true; var groupIndex = this.authsBy.groups; var uris = Object.keys(groupIndex); if (excludePublic) { uris = uris.filter(function (uri) { return uri !== acl.EVERYONE; }); } return uris; } /** * Tests whether this permission set has any `acl:agentGroup` authorizations * @return {Boolean} */ }, { key: 'hasGroups', value: function hasGroups() { return this.groupUris().length > 0; } /** * Creates and loads all the authorizations from a given RDF graph. * Used by `getPermissions()` and by the constructor (optionally). * Usage: * * ``` * var acls = new PermissionSet(resourceUri, aclUri, isContainer, {rdf: rdf}) * acls.initFromGraph(graph) * ``` * @method initFromGraph * @param graph {Dataset} RDF Graph (parsed from the source ACL) */ }, { key: 'initFromGraph', value: function initFromGraph(graph) { var _this7 = this; var ns = vocab(this.rdf); var authSections = graph.match(null, null, ns.acl('Authorization')); if (authSections.length) { authSections = authSections.map(function (match) { return match.subject; }); } else { // Attempt to deal with an ACL with no acl:Authorization types present. var subjects = {}; authSections = graph.match(null, ns.acl('mode')); authSections.forEach(function (match) { subjects[match.subject.value] = match.subject; }); authSections = Object.keys(subjects).map(function (section) { return subjects[section]; }); } // Iterate through each grouping of authorizations in the .acl graph authSections.forEach(function (fragment) { // Extract the access modes var accessModes = graph.match(fragment, ns.acl('mode')); // Extract allowed origins var origins = graph.match(fragment, ns.acl('origin')); // Extract all the authorized agents var agentMatches = graph.match(fragment, ns.acl('agent')); // Mailtos only apply to agents (not groups) var mailTos = agentMatches.filter(isMailTo); // Now filter out mailtos agentMatches = agentMatches.filter(function (ea) { return !isMailTo(ea); }); // Extract all 'Public' matches (agentClass foaf:Agent) var publicMatches = graph.match(fragment, ns.acl('agentClass'), ns.foaf('Agent')); // Extract all acl:agentGroup matches var groupMatches = graph.match(fragment, ns.acl('agentGroup')); groupMatches = groupMatches.map(function (ea) { return new GroupListing({ listing: ea }); }); // Create an Authorization object for each group (accessTo and default) var allAgents = agentMatches.concat(publicMatches).concat(groupMatches); // Create an Authorization object for each agent or group // (both individual (acl:accessTo) and inherited (acl:default)) allAgents.forEach(function (agentMatch) { // Extract the acl:accessTo statements. var accessToMatches = graph.match(fragment, ns.acl('accessTo')); accessToMatches.forEach(function (resourceMatch) { var resourceUrl = resourceMatch.object.value; _this7.addAuthorizationFor(resourceUrl, acl.NOT_INHERIT, agentMatch, accessModes, origins, mailTos); }); // Extract inherited / acl:default statements var inheritedMatches = graph.match(fragment, ns.acl('default')).concat(graph.match(fragment, ns.acl('defaultForNew'))); inheritedMatches.forEach(function (containerMatch) { var containerUrl = containerMatch.object.value; _this7.addAuthorizationFor(containerUrl, acl.INHERIT, agentMatch, accessModes, origins, mailTos); }); }); }); } /** * Returns whether or not authorizations added to this permission set be * inherited, by default? (That is, should they have acl:default set on them). * @method isAuthInherited * @return {Boolean} */ }, { key: 'isAuthInherited', value: function isAuthInherited() { return this.resourceType === CONTAINER; } /** * Returns whether or not this permission set has any Authorizations added to it * @method isEmpty * @return {Boolean} */ }, { key: 'isEmpty', value: function isEmpty() { return this.count === 0; } /** * @method loadGroups * @param [options={}] * @param [options.fetchGraph] {Function} Injected, returns a parsed graph of * a remote document (group listing). Required. * @param [options.rdf] {RDF} RDF library * @throws {Error} * @return {Promise<PermissionSet>} Resolves to self, chainable */ }, { key: 'loadGroups', value: function loadGroups() { var _this8 = this; var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; var fetchGraph = options.fetchGraph; debug('Fetching with ' + fetchGraph); var rdf = options.rdf || this.rdf; if (!fetchGraph) { return Promise.reject(new Error('Cannot load groups, fetchGraph() not supplied')); } if (!rdf) { return Promise.reject(new Error('Cannot load groups, rdf library not supplied')); } var uris = this.groupUris(); var loadActions = uris.map(function (uri) { return GroupListing.loadFrom(uri, fetchGraph, rdf, options); }); return Promise.all(loadActions).then(function (groups) { groups.forEach(function (group) { if (group) { _this8.groups[group.uri] = group; } }); return _this8; }); } /** * Returns the corresponding Authorization for a given agent/group webId (and * for a given resourceUrl, although it assumes by default that it's the same * resourceUrl as the PermissionSet). * @method permissionFor * @param webId {String} URL of the agent or group * @param [resourceUrl] {String} * @return {Authorization} Returns the corresponding Authorization, or `null` * if no webId is given, or if no such authorization exists. */ }, { key: 'permissionFor', value: function permissionFor(webId, resourceUrl) { if (!webId) { return null; } resourceUrl = resourceUrl || this.resourceUrl; var hashFragment = Authorization.hashFragmentFor(webId, resourceUrl); return this.authorizations[hashFragment]; } /** * Deletes a given Authorization instance from the permission set. * Low-level function, clients should use `removePermission()` instead, in most * cases. * @method removeAuthorization * @param auth {Authorization} * @return {PermissionSet} Returns self (chainable) */ }, { key: 'removeAuthorization', value: function removeAuthorization(auth) { var hashFragment = auth.hashFragment(); delete this.authorizations[hashFragment]; return this; } /** * Removes one or more access modes from an authorization in this permission set * (defined by a unique combination of agent/group id (webId) and a resourceUrl). * If no more access modes remain for that authorization, it's deleted from the * permission set. * @method removePermission * @param webId * @param accessMode {String|Array<String>} * @return {PermissionSet} Returns self (via a chainable function) */ }, { key: 'removePermission', value: function removePermission(webId, accessMode) { var auth = this.permissionFor(webId, this.resourceUrl); if (!auth) { // No authorization for this webId + resourceUrl exists. Bail. return this; } // Authorization exists, remove the accessMode from it auth.removeMode(accessMode); if (auth.isEmpty()) { // If no more access modes remain, after removing, delete it from this // permission set this.removeAuthorization(auth); } return this; } /** * @method save * @param [options={}] {Object} Options hashmap * @param [options.aclUrl] {String} Optional URL to save the .ACL resource to. * Defaults to its pre-set `aclUrl`, if not explicitly passed in. * @param [options.contentType] {string} Optional content type to serialize as * @throws {Error} Rejects with an error if it doesn't know where to save, or * with any XHR errors that crop up. * @return {Promise<SolidResponse>} */ }, { key: 'save', value: function save() { var _this9 = this; var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; var aclUrl = options.aclUrl || this.aclUrl; var contentType = options.contentType || DEFAULT_CONTENT_TYPE; if (!aclUrl) { return Promise.reject(new Error('Cannot save - unknown target url')); } if (!this.webClient) { return Promise.reject(new Error('Cannot save - no web client')); } return this.serialize({ contentType: contentType }).then(function (graph) { return _this9.webClient.put(aclUrl, graph, contentType); }); } /** * Serializes this permission set (and all its Authorizations) to a string RDF * representation (Turtle by default). * Note: invalid authorizations (ones that don't have at least one agent/group, * at least one resourceUrl and at least one access mode) do not get serialized, * and are instead skipped. * @method serialize * @param [options={}] {Object} Options hashmap * @param [options.contentType='text/turtle'] {string} * @param [options.rdf] {RDF} RDF Library to serialize with * @throws {Error} Rejects with an error if one is encountered during RDF * serialization. * @return {Promise<String>} Graph serialized to contentType RDF syntax */ }, { key: 'serialize', value: function serialize() { var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; var contentType = options.contentType || DEFAULT_CONTENT_TYPE; var rdf = options.rdf || this.rdf; if (!rdf) { return Promise.reject(new Error('Cannot save - no rdf library')); } var graph = this.buildGraph(rdf); var target = null; var base = this.aclUrl; return new Promise(function (resolve, reject) { rdf.serialize(target, graph, base, contentType, function (err, result) { if (err) { return reject(err); } if (!result) { return reject(new Error('Error serializing the graph to ' + contentType)); } resolve(result); }); }); } }, { key: 'count', get: function get() { return Object.keys(this.authorizations).length; } }]); return PermissionSet; }(); /** * Returns the corresponding ACL uri, for a given resource. * This is the default template for the `aclUrlFor()` method that's used by * PermissionSet instances, unless it's overridden in options. * @param resourceUri {String} * @return {String} ACL uri */ function defaultAclUrlFor(resourceUri) { if (defaultIsAcl(resourceUri)) { return resourceUri; // .acl resources are their own ACLs } else { return resourceUri + DEFAULT_ACL_SUFFIX; } } /** * Tests whether a given uri is for an ACL resource. * This is the default template for the `isAcl()` method that's used by * PermissionSet instances, unless it's overridden in options. * @method defaultIsAcl * @param uri {String} * @return {Boolean} */ function defaultIsAcl(uri) { return uri.endsWith(DEFAULT_ACL_SUFFIX); } /** * Returns whether or not a given agent webId is actually a `mailto:` link. * Standalone helper function. * @param agent {String|Statement} URL string (or RDF `acl:agent` triple) * @return {Boolean} */ function isMailTo(agent) { if (typeof agent === 'string') { return agent.startsWith('mailto:'); } else { return agent.object.value.startsWith('mailto:'); } } PermissionSet.RESOURCE = RESOURCE; PermissionSet.CONTAINER = CONTAINER; module.exports = PermissionSet;