UNPKG

solid-permissions

Version:

Web Access Control based permissions library

718 lines (635 loc) 21.7 kB
'use strict'; /** * Models a single Authorization, as part of a PermissionSet. * @see https://github.com/solid/web-access-control-spec for details. * @module authorization */ 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 vocab = require('solid-namespace'); var _require = require('./modes'), acl = _require.acl; var GroupListing = require('./group-listing'); /** * Models an individual authorization object, for a single resource and for * a single webId (either agent or group). See the comments at the top * of the PermissionSet module for design assumptions. * Low-level, not really meant to be instantiated directly. Use * `permissionSet.addPermission()` instead. * @class Authorization */ var Authorization = function () { /** * @param resourceUrl {String} URL of the resource (`acl:accessTo`) for which * this authorization is intended. * @param [inherited=false] {Boolean} Should this authorization be inherited (contain * `acl:default`). Used for container ACLs. * @constructor */ function Authorization(resourceUrl) { var inherited = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; _classCallCheck(this, Authorization); /** * Hashmap of all of the access modes (`acl:Write` etc) granted to an agent * or group in this authorization. Modified via `addMode()` and `removeMode()` * @property accessModes * @type {Object} */ this.accessModes = {}; /** * Type of authorization, either for a specific resource ('accessTo'), * or to be inherited by all downstream resources ('default') * @property accessType * @type {String} Either 'accessTo' or 'default' */ this.accessType = inherited ? acl.DEFAULT : acl.ACCESS_TO; /** * URL of an agent's WebID (`acl:agent`). Inside an authorization, mutually * exclusive with the `group` property. Set via `setAgent()`. * @property agent * @type {String} */ this.agent = null; /** * URL of a group resource (`acl:agentGroup` or `acl:agentClass`). Inside an * authorization, mutually exclusive with the `agent` property. * Set via `setGroup()`. * @property group * @type {String} */ this.group = null; /** * Does this authorization apply to the contents of a container? * (`acl:default`). Not used with non-container resources. * @property inherited * @type {Boolean} */ this.inherited = inherited; /** * Stores the `mailto:` aliases for a given agent. Semi-unofficial * functionality, used to store a user's email in the root storage .acl, * to use for account recovery etc. * @property mailTo * @type {Array<String>} */ this.mailTo = []; /** * Hashmap of which origins (http Origin: header) are allowed access to this * resource. * @property originsAllowed * @type {Object} */ this.originsAllowed = {}; /** * URL of the resource for which this authorization applies. (`acl:accessTo`) * @property resourceUrl * @type {String} */ this.resourceUrl = resourceUrl; /** * Should this authorization be serialized? (When writing back to an ACL * resource, for example.) Used for implied (rather than explicit) * authorization, such as ones that are derived from acl:Control statements. * @property virtual * @type {Boolean} */ this.virtual = false; } /** * Adds a given `mailto:` alias to this authorization. * @method addMailTo * @param agent {String|Statement} Agent URL (or RDF `acl:agent` statement). */ _createClass(Authorization, [{ key: 'addMailTo', value: function addMailTo(agent) { if (typeof agent !== 'string') { agent = agent.object.value; } if (agent.startsWith('mailto:')) { agent = agent.split(':')[1]; } this.mailTo.push(agent); this.mailTo.sort(); } /** * Adds one or more access modes (`acl:mode` statements) to this authorization. * @method addMode * @param accessMode {String|Statement|Array<String>|Array<Statement>} One or * more access modes, each as either a uri, or an RDF statement. * @return {Authorization} Returns self, chainable. */ }, { key: 'addMode', value: function addMode(accessMode) { var _this = this; if (Array.isArray(accessMode)) { accessMode.forEach(function (ea) { _this.addModeSingle(ea); }); } else { this.addModeSingle(accessMode); } return this; } /** * Adds a single access mode. Internal function, used by `addMode()`. * @method addModeSingle * @private * @param accessMode {String|Statement} Access mode as either a uri, or an RDF * statement. */ }, { key: 'addModeSingle', value: function addModeSingle(accessMode) { if (typeof accessMode !== 'string') { accessMode = accessMode.object.value; } this.accessModes[accessMode] = true; return this; } /** * Adds one or more allowed origins (`acl:origin` statements) to this * authorization. * @method addOrigin * @param origin {String|Statement|Array<String>|Array<Statement>} One or * more origins, each as either a uri, or an RDF statement. * @return {Authorization} Returns self, chainable. */ }, { key: 'addOrigin', value: function addOrigin(origin) { var _this2 = this; if (Array.isArray(origin)) { origin.forEach(function (ea) { _this2.addOriginSingle(ea); }); } else { this.addOriginSingle(origin); } return this; } /** * Adds a single allowed origin. Internal function, used by `addOrigin()`. * @method addOriginSingle * @private * @param origin {String|Statement} Allowed origin as either a uri, or an RDF * statement. */ }, { key: 'addOriginSingle', value: function addOriginSingle(origin) { if (typeof origin !== 'string') { origin = origin.object.value; } this.originsAllowed[origin] = true; return this; } /** * Returns a list of all access modes for this authorization. * @method allModes * @return {Array<String>} */ }, { key: 'allModes', value: function allModes() { return Object.keys(this.accessModes); } /** * Returns a list of all allowed origins for this authorization. * @method allOrigins * @return {Array<String>} */ }, { key: 'allOrigins', value: function allOrigins() { return Object.keys(this.originsAllowed); } /** * Tests whether this authorization grant the specified access mode * @param accessMode {String|NamedNode} Either a named node for the access * mode or a string key ('write', 'read' etc) that maps to that mode. * @return {Boolean} */ }, { key: 'allowsMode', value: function allowsMode(accessMode) { // Normalize the access mode accessMode = acl[accessMode.toUpperCase()] || accessMode; if (accessMode === acl.APPEND) { return this.allowsAppend(); // Handle the Append special case } return this.accessModes[accessMode]; } /** * Does this authorization grant access to requests coming from given origin? * @method allowsOrigin * @param origin {String} * @return {Boolean} */ }, { key: 'allowsOrigin', value: function allowsOrigin(origin) { return origin in this.originsAllowed; } /** * Does this authorization grant `acl:Read` access mode? * @method allowsRead * @return {Boolean} */ }, { key: 'allowsRead', value: function allowsRead() { return this.accessModes[acl.READ]; } /** * Does this authorization grant `acl:Write` access mode? * @method allowsWrite * @return {Boolean} */ }, { key: 'allowsWrite', value: function allowsWrite() { return this.accessModes[acl.WRITE]; } /** * Does this authorization grant `acl:Append` access mode? * @method allowsAppend * @return {Boolean} */ }, { key: 'allowsAppend', value: function allowsAppend() { return this.accessModes[acl.APPEND] || this.accessModes[acl.WRITE]; } /** * Does this authorization grant `acl:Control` access mode? * @method allowsControl * @return {Boolean} */ }, { key: 'allowsControl', value: function allowsControl() { return this.accessModes[acl.CONTROL]; } /** * Returns a deep copy of this authorization. * @return {Authorization} */ }, { key: 'clone', value: function clone() { var auth = new Authorization(); Object.assign(auth, JSON.parse(JSON.stringify(this))); return auth; } /** * Compares this authorization with another one. * Authorizations are equal iff they: * - Are for the same agent or group * - Are intended for the same resourceUrl * - Grant the same access modes * - Have the same `inherit`/`acl:default` flag * - Contain the same `mailto:` agent aliases. * - Has the same allowed origins * @method equals * @param auth {Authorization} * @return {Boolean} */ }, { key: 'equals', value: function equals(auth) { var sameAgent = this.agent === auth.agent; var sameGroup = this.group === auth.group; var sameUrl = this.resourceUrl === auth.resourceUrl; var myModeKeys = Object.keys(this.accessModes); var authModeKeys = Object.keys(auth.accessModes); var sameNumberModes = myModeKeys.length === authModeKeys.length; var sameInherit = JSON.stringify(this.inherited) === JSON.stringify(auth.inherited); var sameModes = true; myModeKeys.forEach(function (key) { if (!auth.accessModes[key]) { sameModes = false; } }); var sameMailTos = JSON.stringify(this.mailTo) === JSON.stringify(auth.mailTo); var sameOrigins = JSON.stringify(this.originsAllowed) === JSON.stringify(auth.originsAllowed); return sameAgent && sameGroup && sameUrl && sameNumberModes && sameModes && sameInherit && sameMailTos && sameOrigins; } /** * Returns a hashed combination of agent/group webId and resourceUrl. Used * internally as a key to store this authorization in a PermissionSet. * @method hashFragment * @private * @throws {Error} Errors if either the webId or the resourceUrl are not set. * @return {String} hash({webId}-{resourceUrl}) */ }, { key: 'hashFragment', value: function hashFragment() { if (!this.webId || !this.resourceUrl) { throw new Error('Cannot call hashFragment() on an incomplete authorization'); } var hashFragment = hashFragmentFor(this.webId(), this.resourceUrl, this.accessType); return hashFragment; } /** * Returns whether or not this authorization is for an agent (vs a group). * @method isAgent * @return {Boolean} Truthy value if agent is set */ }, { key: 'isAgent', value: function isAgent() { return this.agent; } /** * Returns whether or not this authorization is empty (that is, whether it has * any access modes like Read, Write, etc, set on it) * @method isEmpty * @return {Boolean} */ }, { key: 'isEmpty', value: function isEmpty() { return Object.keys(this.accessModes).length === 0; } /** * Is this authorization intended for the foaf:Agent group (that is, everyone)? * @method isPublic * @return {Boolean} */ }, { key: 'isPublic', value: function isPublic() { return this.group === acl.EVERYONE; } /** * Returns whether or not this authorization is for a group (vs an agent). * @method isGroup * @return {Boolean} Truthy value if group is set */ }, { key: 'isGroup', value: function isGroup() { return this.group; } /** * Returns whether this authorization is for a container and should be inherited * (that is, contain `acl:default`). * This is a helper function (instead of the raw attribute) to match the rest * of the api. * @method isInherited * @return {Boolean} */ }, { key: 'isInherited', value: function isInherited() { return this.inherited; } /** * Returns whether this authorization is valid (ready to be serialized into * an RDF graph ACL resource). This requires all three of the following: * 1. Either an agent or an agentClass/group (returned by `webId()`) * 2. A resource URL (`acl:accessTo`) * 3. At least one access mode (read, write, etc) (returned by `isEmpty()`) * @method isValid * @return {Boolean} */ }, { key: 'isValid', value: function isValid() { return this.webId() && this.resourceUrl && !this.isEmpty(); } /** * Merges the access modes of a given authorization with the access modes of * this one (Set union). * @method mergeWith * @param auth * @throws {Error} Error if the other authorization is for a different webId * or resourceUrl (`acl:accessTo`) */ }, { key: 'mergeWith', value: function mergeWith(auth) { if (this.hashFragment() !== auth.hashFragment()) { throw new Error('Cannot merge authorizations with different agent id or resource url (accessTo)'); } for (var accessMode in auth.accessModes) { this.addMode(accessMode); } } /** * Returns an array of RDF statements representing this authorization. * Used by `PermissionSet.serialize()`. * @method rdfStatements * @param rdf {RDF} RDF Library * @return {Array<Triple>} List of RDF statements representing this Auth, * or an empty array if this authorization is invalid. */ }, { key: 'rdfStatements', value: function rdfStatements(rdf) { // Make sure the authorization has at least one agent/group and `accessTo` if (!this.webId() || !this.resourceUrl) { return []; // This Authorization is invalid, return empty array } // Virtual / implied authorizations are not serialized if (this.virtual) { return []; } var statement = void 0; var fragment = rdf.namedNode('#' + this.hashFragment()); var ns = vocab(rdf); var statements = [rdf.triple(fragment, ns.rdf('type'), ns.acl('Authorization'))]; if (this.isAgent()) { statement = rdf.triple(fragment, ns.acl('agent'), rdf.namedNode(this.agent)); statements.push(statement); } if (this.mailTo.length > 0) { this.mailTo.forEach(function (agentMailto) { statement = rdf.triple(fragment, ns.acl('agent'), rdf.namedNode('mailto:' + agentMailto)); statements.push(statement); }); } if (this.isPublic()) { statement = rdf.triple(fragment, ns.acl('agentClass'), ns.foaf('Agent')); statements.push(statement); } else if (this.isGroup()) { statement = rdf.triple(fragment, ns.acl('agentGroup'), rdf.namedNode(this.group)); statements.push(statement); } statement = rdf.triple(fragment, ns.acl('accessTo'), rdf.namedNode(this.resourceUrl)); statements.push(statement); var modes = Object.keys(this.accessModes); modes.forEach(function (accessMode) { statement = rdf.triple(fragment, ns.acl('mode'), rdf.namedNode(accessMode)); statements.push(statement); }); if (this.inherited) { statement = rdf.triple(fragment, ns.acl('defaultForNew'), rdf.namedNode(this.resourceUrl)); statements.push(statement); } this.allOrigins().forEach(function (origin) { statement = rdf.triple(fragment, ns.acl('origin'), rdf.namedNode(origin)); statements.push(statement); }); return statements; } /** * Removes one or more access modes from this authorization. * @method removeMode * @param accessMode {String|Statement|Array<String>|Array<Statement>} URL * representation of the access mode, or an RDF `acl:mode` triple. * @returns {removeMode} */ }, { key: 'removeMode', value: function removeMode(accessMode) { var _this3 = this; if (Array.isArray(accessMode)) { accessMode.forEach(function (ea) { _this3.removeModeSingle(ea); }); } else { this.removeModeSingle(accessMode); } return this; } /** * Removes a single access mode from this authorization. Internal use only * (used by `removeMode()`). * @method removeModeSingle * @private * @param accessMode {String|Statement} URI or RDF statement */ }, { key: 'removeModeSingle', value: function removeModeSingle(accessMode) { if (typeof accessMode !== 'string') { accessMode = accessMode.object.value; } delete this.accessModes[accessMode]; } /** * Removes one or more allowed origins from this authorization. * @method removeOrigin * @param origin {String|Statement|Array<String>|Array<Statement>} URL * representation of the access mode, or an RDF `acl:mode` triple. * @returns {removeMode} */ }, { key: 'removeOrigin', value: function removeOrigin(origin) { var _this4 = this; if (Array.isArray(origin)) { origin.forEach(function (ea) { _this4.removeOriginSingle(ea); }); } else { this.removeOriginSingle(origin); } return this; } /** * Removes a single allowed origin from this authorization. Internal use only * (used by `removeOrigin()`). * @method removeOriginSingle * @private * @param origin {String|Statement} URI or RDF statement */ }, { key: 'removeOriginSingle', value: function removeOriginSingle(origin) { if (typeof origin !== 'string') { origin = origin.object.value; } delete this.originsAllowed[origin]; } /** * Sets the agent WebID for this authorization. * @method setAgent * @param agent {string|Quad|GroupListing} Agent URL (or `acl:agent` RDF triple). */ }, { key: 'setAgent', value: function setAgent(agent) { if (agent instanceof GroupListing) { return this.setGroup(agent); } if (typeof agent !== 'string') { // This is an RDF statement agent = agent.object.value; } if (agent === acl.EVERYONE) { this.setPublic(); } else if (this.group) { throw new Error('Cannot set agent, authorization already has a group set'); } if (agent.startsWith('mailto:')) { this.addMailTo(agent); } else { this.agent = agent; } } /** * Sets the group WebID for this authorization. * @method setGroup * @param group {string|Triple|GroupListing} Group URL (or `acl:agentClass` RDF * triple). */ }, { key: 'setGroup', value: function setGroup(group) { if (this.agent) { throw new Error('Cannot set group, authorization already has an agent set'); } if (group instanceof GroupListing) { group = group.listing; } if (typeof group !== 'string') { // This is an RDF statement group = group.object.value; } this.group = group; } /** * Sets the authorization's group to `foaf:Agent`. Convenience method. * @method setPublic */ }, { key: 'setPublic', value: function setPublic() { this.setGroup(acl.EVERYONE); } /** * Returns the agent or group's WebID for this authorization. * @method webId * @return {String} */ }, { key: 'webId', value: function webId() { return this.agent || this.group; } }]); return Authorization; }(); // --- Standalone (non-instance) functions -- /** * Utility method that creates a hash fragment key for this authorization. * Used with graph serialization to RDF, and as a key to store authorizations * in a PermissionSet. Exported (mainly for use in PermissionSet). * @method hashFragmentFor * @param webId {String} Agent or group web id * @param resourceUrl {String} Resource or container URL for this authorization * @param [authType='accessTo'] {String} Either 'accessTo' or 'default' * @return {String} */ function hashFragmentFor(webId, resourceUrl) { var authType = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : acl.ACCESS_TO; var hashKey = webId + '-' + resourceUrl + '-' + authType; return hashKey; } Authorization.hashFragmentFor = hashFragmentFor; module.exports = Authorization;