UNPKG

solid-permissions

Version:

Web Access Control based permissions library

609 lines (573 loc) 18.2 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 */ const vocab = require('solid-namespace') const { acl } = require('./modes') const 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 */ class Authorization { /** * @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 */ constructor (resourceUrl, inherited = false) { /** * 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). */ 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. */ addMode (accessMode) { if (Array.isArray(accessMode)) { accessMode.forEach(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. */ 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. */ addOrigin (origin) { if (Array.isArray(origin)) { origin.forEach((ea) => { this.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. */ 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>} */ allModes () { return Object.keys(this.accessModes) } /** * Returns a list of all allowed origins for this authorization. * @method allOrigins * @return {Array<String>} */ 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} */ 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} */ allowsOrigin (origin) { return origin in this.originsAllowed } /** * Does this authorization grant `acl:Read` access mode? * @method allowsRead * @return {Boolean} */ allowsRead () { return this.accessModes[ acl.READ ] } /** * Does this authorization grant `acl:Write` access mode? * @method allowsWrite * @return {Boolean} */ allowsWrite () { return this.accessModes[ acl.WRITE ] } /** * Does this authorization grant `acl:Append` access mode? * @method allowsAppend * @return {Boolean} */ allowsAppend () { return this.accessModes[ acl.APPEND ] || this.accessModes[ acl.WRITE ] } /** * Does this authorization grant `acl:Control` access mode? * @method allowsControl * @return {Boolean} */ allowsControl () { return this.accessModes[ acl.CONTROL ] } /** * Returns a deep copy of this authorization. * @return {Authorization} */ clone () { let 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} */ equals (auth) { let sameAgent = this.agent === auth.agent let sameGroup = this.group === auth.group let sameUrl = this.resourceUrl === auth.resourceUrl let myModeKeys = Object.keys(this.accessModes) let authModeKeys = Object.keys(auth.accessModes) let sameNumberModes = myModeKeys.length === authModeKeys.length let sameInherit = JSON.stringify(this.inherited) === JSON.stringify(auth.inherited) let sameModes = true myModeKeys.forEach((key) => { if (!auth.accessModes[ key ]) { sameModes = false } }) let sameMailTos = JSON.stringify(this.mailTo) === JSON.stringify(auth.mailTo) let 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}) */ hashFragment () { if (!this.webId || !this.resourceUrl) { throw new Error('Cannot call hashFragment() on an incomplete authorization') } let 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 */ 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} */ isEmpty () { return Object.keys(this.accessModes).length === 0 } /** * Is this authorization intended for the foaf:Agent group (that is, everyone)? * @method isPublic * @return {Boolean} */ 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 */ 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} */ 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} */ 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`) */ 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. */ 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 [] } let statement let fragment = rdf.namedNode('#' + this.hashFragment()) let ns = vocab(rdf) let 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((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) let modes = Object.keys(this.accessModes) modes.forEach((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((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} */ removeMode (accessMode) { if (Array.isArray(accessMode)) { accessMode.forEach((ea) => { this.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 */ 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} */ removeOrigin (origin) { if (Array.isArray(origin)) { origin.forEach((ea) => { this.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 */ 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). */ 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). */ 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 */ setPublic () { this.setGroup(acl.EVERYONE) } /** * Returns the agent or group's WebID for this authorization. * @method webId * @return {String} */ webId () { return this.agent || this.group } } // --- 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, authType = acl.ACCESS_TO) { let hashKey = webId + '-' + resourceUrl + '-' + authType return hashKey } Authorization.hashFragmentFor = hashFragmentFor module.exports = Authorization