solid-permissions
Version:
Web Access Control based permissions library
718 lines (635 loc) • 21.7 kB
JavaScript
'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;