jsdav-ext
Version:
jsDAV allows you to easily add WebDAV support to a NodeJS application. jsDAV is meant to cover the entire standard, and attempts to allow integration using an easy to understand API.
1,311 lines (1,171 loc) • 57.5 kB
JavaScript
/*
* @package jsDAV
* @subpackage DAVACL
* @copyright Copyright(c) 2013 Mike de Boer. <info AT mikedeboer DOT nl>
* @author Mike de Boer <info AT mikedeboer DOT nl>
* @license http://github.com/mikedeboer/jsDAV/blob/master/LICENSE MIT License
*/
"use strict";
var jsDAV_ServerPlugin = require("./../DAV/plugin");
var jsDAV_Property_Href = require("./../DAV/property/href");
var jsDAV_Property_HrefList = require("./../DAV/property/hrefList");
var jsDAV_Property_Response = require("./../DAV/property/response");
var jsDAV_Property_ResponseList = require("./../DAV/property/responseList");
var jsDAV_iHref = require("./../DAV/interfaces/iHref");
var jsDAVACL_iPrincipal = require("./interfaces/iPrincipal");
var jsDAVACL_iACL = require("./interfaces/iAcl");
var jsDAVACL_iPrincipalCollection = require("./interfaces/iPrincipalCollection");
var jsDAVACL_Property_Principal = require("./property/principal");
var jsDAVACL_Property_SupportedPrivilegeSet = require("./property/supportedPrivilegeSet");
var jsDAVACL_Property_CurrentUserPrivilegeSet = require("./property/currentUserPrivilegeSet");
var jsDAVACL_Property_Acl = require("./property/acl");
var jsDAVACL_Property_AclRestrictions = require("./property/aclRestrictions");
var AsyncEventEmitter = require("./../shared/asyncEvents").EventEmitter;
var Exc = require("./../shared/exceptions");
var Util = require("./../shared/util");
var Xml = require("./../shared/xml");
var Async = require("asyncjs");
/**
* jsDAV ACL Plugin
*
* This plugin provides functionality to enforce ACL permissions.
* ACL is defined in RFC3744.
*
* In addition it also provides support for the {DAV:}current-user-principal
* property, defined in RFC5397 and the {DAV:}expand-property report, as
* defined in RFC3253.
*/
var jsDAVACL_Plugin = module.exports = jsDAV_ServerPlugin.extend({
/**
* Plugin name
*
* @var String
*/
name: "acl",
/**
* Recursion constants
*
* This only checks the base node
*/
R_PARENT: 1,
/**
* Recursion constants
*
* This checks every node in the tree
*/
R_RECURSIVE: 2,
/**
* Recursion constants
*
* This checks every parentnode in the tree, but not leaf-nodes.
*/
R_RECURSIVEPARENTS: 3,
/**
* Reference to server object.
*
* @var jsDAV_Handler
*/
handler: null,
/**
* List of urls containing principal collections.
* Modify this if your principals are located elsewhere.
*
* @var array
*/
principalCollectionSet: [
"principals"
],
/**
* By default ACL is only enforced for nodes that have ACL support (the
* ones that implement IACL). For any other node, access is
* always granted.
*
* To override this behaviour you can turn this setting off. This is useful
* if you plan to fully support ACL in the entire tree.
*
* @var bool
*/
allowAccessToNodesWithoutACL: true,
/**
* By default nodes that are inaccessible by the user, can still be seen
* in directory listings (PROPFIND on parent with Depth: 1)
*
* In certain cases it's desirable to hide inaccessible nodes. Setting this
* to true will cause these nodes to be hidden from directory listings.
*
* @var bool
*/
hideNodesFromListings: false,
/**
* This {String} is prepended to the username of the currently logged in
* user. This allows the plugin to determine the principal path based on
* the username.
*
* @var string
*/
defaultUsernamePath: "principals",
/**
* This list of properties are the properties a client can search on using
* the {DAV:}principal-property-search report.
*
* The keys are the property names, values are descriptions.
*
* @var Object
*/
principalSearchPropertySet: {
"{DAV:}displayname": "Display name",
"{http://ajax.org/2005/aml}email-address": "Email address"
},
/**
* Any principal uri's added here, will automatically be added to the list
* of ACL's. They will effectively receive {DAV:}all privileges, as a
* protected privilege.
*
* @var array
*/
adminPrincipals: [],
/**
* Sets up the plugin
*
* This method is automatically called by the server class.
*
* @param jsDAV_Handler handler
* @return void
*/
initialize: function(handler) {
this.handler = handler;
var options = this.handler.server.options;
if (options.allowAccessToNodesWithoutACL)
this.allowAccessToNodesWithoutACL = options.allowAccessToNodesWithoutACL;
if (options.hideNodesFromListings)
this.hideNodesFromListings = options.hideNodesFromListings;
if (options.defaultUsernamePath)
this.defaultUsernamePath = options.defaultUsernamePath;
if (options.adminPrincipals)
this.adminPrincipals = Util.makeUnique(this.adminPrincipals.concat(options.adminPrincipals));
handler.addEventListener("beforeGetProperties", this.beforeGetProperties.bind(this));
handler.addEventListener("beforeMethod", this.beforeMethod.bind(this), AsyncEventEmitter.PRIO_HIGH);
handler.addEventListener("beforeBind", this.beforeBind.bind(this), AsyncEventEmitter.PRIO_HIGH);
handler.addEventListener("beforeUnbind", this.beforeUnbind.bind(this), AsyncEventEmitter.PRIO_HIGH);
handler.addEventListener("updateProperties",this.updateProperties.bind(this));
handler.addEventListener("beforeUnlock", this.beforeUnlock.bind(this), AsyncEventEmitter.PRIO_HIGH);
handler.addEventListener("report",this.report.bind(this));
handler.addEventListener("unknownMethod", this.unknownMethod.bind(this));
handler.protectedProperties.push(
"{DAV:}alternate-URI-set",
"{DAV:}principal-URL",
"{DAV:}group-membership",
"{DAV:}principal-collection-set",
"{DAV:}current-user-principal",
"{DAV:}supported-privilege-set",
"{DAV:}current-user-privilege-set",
"{DAV:}acl",
"{DAV:}acl-restrictions",
"{DAV:}inherited-acl-set",
"{DAV:}owner",
"{DAV:}group"
);
handler.protectedProperties = Util.makeUnique(handler.protectedProperties);
// Automatically mapping nodes implementing IPrincipal to the
// {DAV:}principal resourcetype.
handler.resourceTypeMapping["{DAV:}principal"] = jsDAVACL_iPrincipal;
// Mapping the group-member-set property to the HrefList property
// class.
handler.propertyMap["{DAV:}group-member-set"] = jsDAV_Property_HrefList;
},
/**
* Returns a list of features added by this plugin.
*
* This list is used in the response of a HTTP OPTIONS request.
*
* @return array
*/
getFeatures: function() {
return ["access-control", "calendarserver-principal-property-search"];
},
/**
* Returns a list of available methods for a given url
*
* @param {String} uri
* @return array
*/
getHTTPMethods: function(uri) {
return ["ACL"];
},
/**
* Returns a list of reports this plugin supports.
*
* This will be used in the {DAV:}supported-report-set property.
* Note that you still need to subscribe to the 'report' event to actually
* implement them
*
* @param {String} uri
* @return array
*/
getSupportedReportSet: function(uri, callback) {
callback(null, [
"{DAV:}expand-property",
"{DAV:}principal-property-search",
"{DAV:}principal-search-property-set",
]);
},
/**
* Checks if the current user has the specified privilege(s).
*
* You can specify a single privilege, or a list of privileges.
* This method will throw an exception if the privilege is not available
* and return true otherwise.
*
* @param {String} uri
* @param array|string privileges
* @param number recursion
* @throws jsDAV_Exception_NeedPrivileges
* @return bool
*/
checkPrivileges: function(uri, privileges, recursion, callback) {
if (!Array.isArray(privileges))
privileges = [privileges];
recursion = recursion || this.R_PARENT;
var self = this;
this.getCurrentUserPrivilegeSet(uri, function(err, acl) {
if (err)
return callback(err);
if (!acl) {
if (self.allowAccessToNodesWithoutACL)
return callback(null, true);
else
return callback(new Exc.NeedPrivileges(uri, privileges), false);
}
var failed = privileges.filter(function(priv) {
return acl.indexOf(priv) === -1;
});
if (failed.length)
return callback(new Exc.NeedPrivileges(uri, failed), false);
callback(null, true);
});
},
/**
* Returns the standard users' principal.
*
* This is one authorative principal url for the current user.
* This method will return null if the user wasn't logged in.
*
* @return string|null
*/
getCurrentUserPrincipal: function(callback) {
var authPlugin = this.handler.plugins.auth;
if (!authPlugin)
return callback();
/** @var authPlugin jsDAV_Auth_Plugin */
var self = this;
authPlugin.getCurrentUser(function(err, userName) {
if (err)
return callback(err);
if (!userName)
return callback();
callback(null, self.defaultUsernamePath + "/" + userName);
});
},
/**
* Returns a list of principals that's associated to the current
* user, either directly or through group membership.
*
* @return array
*/
getCurrentUserPrincipals: function(callback) {
var self = this;
this.getCurrentUserPrincipal(function(err, currentUser) {
if (err)
return callback(err);
if (!currentUser)
return callback(null, []);
self.getPrincipalMembership(currentUser, function(err, membership) {
if (err)
return callback(err);
callback(null, [currentUser].concat(membership));
});
});
},
/**
* This object holds a cache for all the principals that are associated with
* a single principal.
*
* @var object
*/
principalMembershipCache: {},
/**
* Returns all the principal groups the specified principal is a member of.
*
* @param {String} principal
* @return array
*/
getPrincipalMembership: function(mainPrincipal, callback) {
// First check our cache
if (this.principalMembershipCache[mainPrincipal])
return callback(null, this.principalMembershipCache[mainPrincipal]);
var check = [mainPrincipal];
var principals = [];
var self = this;
function checkNext() {
var principal = check.shift();
if (!principal)
return checkedAll();
self.handler.getNodeForPath(principal, function(err, node) {
if (err)
return checkedAll(err);
if (node.hasFeature(jsDAVACL_iPrincipal)) {
node.getGroupMembership(function(err, memberships) {
if (err)
return checkedAll(err);
memberships.forEach(function(groupMember) {
if (principals.indexOf(groupMember) === -1) {
check.push(groupMember);
principals.push(groupMember);
}
});
checkNext();
});
}
else
checkNext();
});
}
function checkedAll(err) {
if (err)
return callback(err, []);
// Store the result in the cache
self.principalMembershipCache[mainPrincipal] = principals;
callback(err, principals);
}
checkNext();
},
/**
* Returns the supported privilege structure for this ACL plugin.
*
* See RFC3744 for more details. Currently we default on a simple,
* standard structure.
*
* You can either get the list of privileges by a uri (path) or by
* specifying a Node.
*
* @param string|DAV\INode node
* @return array
*/
getSupportedPrivilegeSet: function(node, callback) {
var self = this;
if (!node.hasFeature) {
this.handler.getNodeForPath(node, function(err, n) {
if (err)
return callback(err);
node = n;
gotNodePrivSet();
});
}
else
gotNodePrivSet();
function gotNodePrivSet() {
if (node.hasFeature(jsDAVACL_iACL))
return callback(null, node.getSupportedPrivilegeSet() || self.getDefaultSupportedPrivilegeSet());
callback(null, self.getDefaultSupportedPrivilegeSet());
}
},
/**
* Returns a fairly standard set of privileges, which may be useful for
* other systems to use as a basis.
*
* @return array
*/
getDefaultSupportedPrivilegeSet: function() {
return {
"privilege" : "{DAV:}all",
"abstract" : true,
"aggregates" : [
{
"privilege" : "{DAV:}read",
"aggregates" : [
{
"privilege" : "{DAV:}read-acl",
"abstract" : true
},
{
"privilege" : "{DAV:}read-current-user-privilege-set",
"abstract" : true
}
]
}, // {DAV:}read
{
"privilege" : "{DAV:}write",
"aggregates" : [
{
"privilege" : "{DAV:}write-acl",
"abstract" : true
},
{
"privilege" : "{DAV:}write-properties",
"abstract" : true
},
{
"privilege" : "{DAV:}write-content",
"abstract" : true
},
{
"privilege" : "{DAV:}bind",
"abstract" : true
},
{
"privilege" : "{DAV:}unbind",
"abstract" : true
},
{
"privilege" : "{DAV:}unlock",
"abstract" : true
}
]
} // {DAV:}write
]
}; // {DAV:}all
},
/**
* Returns the supported privilege set as a flat list
*
* This is much easier to parse.
*
* The returned list will be index by privilege name.
* The value is a struct containing the following properties:
* - aggregates
* - abstract
* - concrete
*
* @param string|DAV\INode node
* @return array
*/
getFlatPrivilegeSet: function(node, callback) {
this.getSupportedPrivilegeSet(node, function(err, privs) {
if (err)
return callback(err);
var flat = {};
// Traverses the privilege set tree for reordering
function getFPSTraverse(priv, isConcrete) {
var myPriv = {
"privilege" : priv.privilege,
"abstract" : !!priv.abstract && priv.abstract,
"aggregates" : [],
"concrete" : !!priv.abstract && priv.abstract ? isConcrete : priv.privilege
};
if (priv.aggregates) {
priv.aggregates.forEach(function(subPriv) {
myPriv.aggregates.push(subPriv.privilege);
});
}
flat[priv.privilege] = myPriv;
if (priv.aggregates) {
priv.aggregates.forEach(function(subPriv) {
getFPSTraverse(subPriv, myPriv.concrete);
});
}
}
getFPSTraverse(privs, null);
callback(null, flat);
});
},
/**
* Returns the full ACL list.
*
* Either a uri or a DAV\INode may be passed.
*
* null will be returned if the node doesn't support ACLs.
*
* @param string|DAV\INode node
* @return array
*/
getACL: function(node, callback) {
var self = this;
if (typeof node == "string") {
this.handler.getNodeForPath(node, function(err, n) {
if (err)
return callback(err);
node = n;
gotNodeACL();
});
}
else
gotNodeACL();
function gotNodeACL() {
if (!node.hasFeature(jsDAVACL_iACL))
return callback();
var acl = node.getACL();
self.adminPrincipals.forEach(function(adminPrincipal) {
acl.push({
"principal" : adminPrincipal,
"privilege" : "{DAV:}all",
"protected" : true
});
});
callback(null, acl);
}
},
/**
* Returns a list of privileges the current user has
* on a particular node.
*
* Either a uri or a jsDAV_iNode may be passed.
*
* null will be returned if the node doesn't support ACLs.
*
* @param string|jsDAV_iNode node
* @return array
*/
getCurrentUserPrivilegeSet: function(node, callback) {
var self = this;
if (typeof node == "string") {
this.handler.getNodeForPath(node, function(err, n) {
if (err)
return callback(err);
node = n;
gotNode();
});
}
else
gotNode();
function gotNode() {
self.getACL(node, function(err, acl) {
if (err)
return callback(err);
if (!acl)
return callback();
self.getCurrentUserPrincipals(function(err, principals) {
if (err)
return callback(err);
var collected = [];
acl.forEach(function(ace) {
var principal = ace.principal;
switch (principal) {
case "{DAV:}owner" :
var owner = node.getOwner();
if (owner && principals.indexOf(owner) > -1)
collected.push(ace);
break;
// 'all' matches for every user
case "{DAV:}all" :
// 'authenticated' matched for every user that's logged in.
// Since it's not possible to use ACL while not being logged
// in, this is also always true.
case "{DAV:}authenticated" :
collected.push(ace);
break;
// 'unauthenticated' can never occur either, so we simply
// ignore these.
case "{DAV:}unauthenticated" :
break;
default :
if (principals.indexOf(ace.principal) > -1)
collected.push(ace);
break;
}
});
// Now we deduct all aggregated privileges.
self.getFlatPrivilegeSet(node, function(err, flat) {
if (err)
return callback(err);
var current;
var collected2 = [];
while (collected.length) {
current = collected.pop();
collected2.push(current.privilege);
flat[current.privilege].aggregates.forEach(function(subPriv) {
collected2.push(subPriv);
collected.push(flat[subPriv]);
});
}
callback(null, Util.makeUnique(collected2));
});
});
});
}
},
/**
* Principal property search
*
* This method can search for principals matching certain values in
* properties.
*
* This method will return a list of properties for the matched properties.
*
* @param {Array} searchProperties The properties to search on. This is a
* key-value list. The keys are property
* names, and the values the strings to
* match them on.
* @param {Array} requestedProperties This is the list of properties to
* return for every match.
* @param {String} collectionUri The principal collection to search on.
* If this is ommitted, the standard
* principal collection-set will be used.
* @return {Array} This method returns an array structure similar to
* jsDAV_Handler.getPropertiesForPath. Returned
* properties are index by a HTTP status code.
*
*/
principalSearch: function(searchProperties, requestedProperties, collectionUri, callback) {
var uris = collectionUri ? [collectionUri] : this.principalCollectionSet;
var lookupResults = [];
var self = this;
Async.list(uris)
.each(function(uri, next) {
self.handler.getNodeForPath(uri, function(err, principalCollection) {
if (err)
return next(err);
if (!principalCollection.hasFeature(jsDAVACL_iPrincipalCollection)) {
// Not a principal collection, we're simply going to ignore
// this.
return next();
}
principalCollection.searchPrincipals(searchProperties, function(err, results) {
if (err)
return next(err);
results.forEach(function(result) {
lookupResults.push(Util.rtrim(uri, "/") + "/" + result);
});
next();
});
});
})
.end(function(err) {
if (err)
return callback(err);
var matches = [];
Async.list(lookupResults)
.each(function(lookupResult, next) {
self.handler.getPropertiesForPath(lookupResult, requestedProperties, 0, function(err, props) {
if (err)
return next(err);
matches.push(props);
next();
});
})
.end(function(err) {
callback(err, matches)
});
});
},
/**
* Triggered before any method is handled
*
* @param {String} method
* @param {String} uri
* @return void
*/
beforeMethod: function(e, method, uri) {
var self = this;
this.handler.getNodeForPath(uri, function(err, node) {
// do not yield errors:
// If the node doesn't exists, none of these checks apply
if (err)
return e.next();
function cont(err) {
e.next(err);
}
switch(method) {
case "GET" :
case "HEAD" :
case "OPTIONS" :
// For these 3 we only need to know if the node is readable.
self.checkPrivileges(uri, "{DAV:}read", null, cont);
break;
case "PUT" :
case "LOCK" :
case "UNLOCK" :
// This method requires the write-content priv if the node
// already exists, and bind on the parent if the node is being
// created.
// The bind privilege is handled in the beforeBind event.
self.checkPrivileges(uri, "{DAV:}write-content", null, cont);
break;
case "PROPPATCH" :
self.checkPrivileges(uri, "{DAV:}write-properties", null, cont);
break;
case "ACL" :
self.checkPrivileges(uri, "{DAV:}write-acl", null, cont);
break;
case "COPY" :
case "MOVE" :
// Copy requires read privileges on the entire source tree.
// If the target exists write-content normally needs to be
// checked, however, we're deleting the node beforehand and
// creating a new one after, so this is handled by the
// beforeUnbind event.
//
// The creation of the new node is handled by the beforeBind
// event.
//
// If MOVE is used beforeUnbind will also be used to check if
// the sourcenode can be deleted.
self.checkPrivileges(uri, "{DAV:}read", self.R_RECURSIVE, cont);
break;
default:
e.next();
break;
}
});
},
/**
* Triggered before a new node is created.
*
* This allows us to check permissions for any operation that creates a
* new node, such as PUT, MKCOL, MKCALENDAR, LOCK, COPY and MOVE.
*
* @param {String} uri
* @return void
*/
beforeBind: function(e, uri) {
var parentUri = Util.splitPath(uri)[0];
this.checkPrivileges(parentUri, "{DAV:}bind", null, e.next.bind(e));
},
/**
* Triggered before a node is deleted
*
* This allows us to check permissions for any operation that will delete
* an existing node.
*
* @param {String} uri
* @return void
*/
beforeUnbind: function(e, uri) {
var parentUri = Util.splitPath(uri)[0];
this.checkPrivileges(parentUri, "{DAV:}unbind", this.R_RECURSIVEPARENTS, e.next.bind(e));
},
/**
* Triggered before a node is unlocked.
*
* @param {String} uri
* @param DAV\Locks\LockInfo lock
* @TODO: not yet implemented
* @return void
*/
beforeUnlock: function(e, uri, lock) {
e.next();
},
/**
* Triggered before properties are looked up in specific nodes.
*
* @param {String} uri
* @param jsDAV_iNode node
* @param {Array} requestedProperties
* @param {Object} returnedProperties
* @TODO really should be broken into multiple methods, or even a class.
* @return bool
*/
beforeGetProperties: function(e, uri, node, requestedProperties, returnedProperties) {
var self = this;
// Checking the read permission
this.checkPrivileges(uri,"{DAV:}read", this.R_PARENT, function(err, hasPriv) {
if (!hasPriv) {
// User is not allowed to read properties
if (self.hideNodesFromListings)
return e.stop();
// Marking all requested properties as '403'.
Object.keys(requestedProperties).forEach(function(prop) {
returnedProperties["403"][prop] = null;
delete requestedProperties[prop];
});
return e.next();
}
var propHandlers = {
"{DAV:}alternate-uri-set": function(prop, next) {
delete requestedProperties[prop];
returnedProperties["200"]["{DAV:}alternate-URI-set"] = jsDAV_Property_HrefList.new(node.getAlternateUriSet());
next();
},
"{DAV:}principal-url": function(prop, next) {
delete requestedProperties[prop];
returnedProperties["200"]["{DAV:}principal-URL"] = jsDAV_Property_Href.new(node.getPrincipalUrl() + "/");
next();
},
"{DAV:}group-member-set": function(prop, next) {
delete requestedProperties[prop];
node.getGroupMemberSet(function(err, memberSet) {
if (err)
return next(err);
returnedProperties["200"]["{DAV:}group-member-set"] = jsDAV_Property_HrefList.new(memberSet);
next();
});
},
"{DAV:}group-membership": function(prop, next) {
delete requestedProperties[prop];
node.getGroupMembership(function(err, membership) {
if (err)
return next(err);
returnedProperties["200"]["{DAV:}group-membership"] = jsDAV_Property_HrefList.new(membership);
next();
});
},
"{DAV:}displayname": function(prop, next) {
returnedProperties["200"]["{DAV:}displayname"] = node.getDisplayName();
next();
},
"{DAV:}principal-collection-set": function(prop, next) {
delete requestedProperties[prop];
var val = [].concat(self.principalCollectionSet);
// Ensuring all collections end with a slash
for (var i = 0, l = val.length; i > l; ++i)
val[i] = val[i] + "/";
returnedProperties["200"]["{DAV:}principal-collection-set"] = jsDAV_Property_HrefList.new(val);
next();
},
"{DAV:}current-user-principal": function(prop, next) {
delete requestedProperties[prop];
self.getCurrentUserPrincipal(function(err, url) {
if (err)
return next(err);
if (url)
returnedProperties["200"]["{DAV:}current-user-principal"] = jsDAVACL_Property_Principal.new(jsDAVACL_Property_Principal.HREF, url + "/");
else
returnedProperties["200"]["{DAV:}current-user-principal"] = jsDAVACL_Property_Principal.new(jsDAVACL_Property_Principal.UNAUTHENTICATED);
next();
});
},
"{DAV:}supported-privilege-set": function(prop, next) {
delete requestedProperties[prop];
self.getSupportedPrivilegeSet(node, function(err, privSet) {
if (err)
return next(err);
returnedProperties["200"]["{DAV:}supported-privilege-set"] = jsDAVACL_Property_SupportedPrivilegeSet.new(privSet);
next();
});
},
"{DAV:}current-user-privilege-set": function(prop, next) {
self.checkPrivileges(uri, "{DAV:}read-current-user-privilege-set", self.R_PARENT, function(err, hasPriv) {
if (!hasPriv) {
returnedProperties["403"]["{DAV:}current-user-privilege-set"] = null;
delete requestedProperties[prop];
next();
}
else {
self.getCurrentUserPrivilegeSet(node, function(err, privSet) {
if (err)
return next(err);
if (privSet) {
delete requestedProperties[prop];
returnedProperties["200"]["{DAV:}current-user-privilege-set"] = jsDAVACL_Property_CurrentUserPrivilegeSet.new(privSet);
}
next();
});
}
});
},
"{DAV:}acl": function(prop, next) {
self.checkPrivileges(uri, "{DAV:}read-acl", self.R_PARENT, function(err, hasPriv) {
if (!hasPriv) {
delete requestedProperties[prop];
returnedProperties["403"]["{DAV:}acl"] = null;
next();
}
else {
self.getACL(node, function(err, acl) {
if (err)
return next(err);
if (acl) {
delete requestedProperties[prop];
returnedProperties["200"]["{DAV:}acl"] = jsDAVACL_Property_Acl.new(acl);
}
next();
});
}
});
},
"{DAV:}acl-restrictions": function(prop, next) {
delete requestedProperties[prop];
returnedProperties["200"]["{DAV:}acl-restrictions"] = jsDAVACL_Property_AclRestrictions.new();
next();
},
"{DAV:}owner": function(prop, next) {
delete requestedProperties[prop];
returnedProperties["200"]["{DAV:}owner"] = jsDAV_Property_Href.new(node.getOwner() + "/");
next();
}
};
var propsToCheck = ["{DAV:}principal-collection-set", "{DAV:}current-user-principal",
"{DAV:}supported-privilege-set", "{DAV:}current-user-privilege-set",
"{DAV:}acl", "{DAV:}acl-restrictions"];
// Adding principal properties
if (node.hasFeature(jsDAVACL_iPrincipal)) {
propsToCheck.push("{DAV:}alternate-uri-set", "{DAV:}principal-url",
"{DAV:}group-member-set", "{DAV:}group-membership", "{DAV:}displayname");
}
// Adding ACL properties
if (node.hasFeature(jsDAVACL_iACL))
propsToCheck.push("{DAV:}owner");
// property must be requested to be processed...
propsToCheck = propsToCheck.filter(function(prop) {
return !!requestedProperties[prop];
});
Async.list(propsToCheck)
.delay(0, 10)
.each(function(prop, next) {
if (propHandlers[prop])
propHandlers[prop](prop, next);
else
next();
})
.end(e.next.bind(e));
});
},
/**
* This method intercepts PROPPATCH methods and make sure the
* group-member-set is updated correctly.
*
* @param {Array} propertyDelta
* @param {Array} result
* @param DAV\INode node
* @return bool
*/
updateProperties: function(e, propertyDelta, result, node) {
if (!propertyDelta["{DAV:}group-member-set"])
return;
var self = this;
var memberSet;
if (!propertyDelta["{DAV:}group-member-set"]) {
memberSet = [];
}
else if (propertyDelta["{DAV:}group-member-set"].hasFeature(jsDAV_Property_HrefList)) {
memberSet = propertyDelta["{DAV:}group-member-set"].getHrefs().map(function(uri) {
return self.handler.calculateUri(uri);
});
}
else
return e.next(new Exc.Exception("The group-member-set property MUST be an instance of jsDAV_Property_HrefList or null"));
if (!(node.hasFeature(jsDAVACL_iPrincipal))) {
result["403"]["{DAV:}group-member-set"] = null;
delete propertyDelta["{DAV:}group-member-set"];
// e.stop() will stop the updateProperties process
return e.stop();
}
node.setGroupMemberSet(memberSet, function(err) {
if (err)
return e.next(err);
// We must also clear our cache, just in case
self.principalMembershipCache = {};
result["200"]["{DAV:}group-member-set"] = null;
delete propertyDelta["{DAV:}group-member-set"];
e.next();
});
},
/**
* This method handles HTTP REPORT requests
*
* @param {String} reportName
* @param \DOMNode dom
* @return bool
*/
report: function(e, reportName, dom) {
switch(reportName) {
case "{DAV:}principal-property-search" :
this.principalPropertySearchReport(e, dom);
break;
case "{DAV:}principal-search-property-set" :
this.principalSearchPropertySetReport(e, dom);
break;
case "{DAV:}expand-property" :
this.expandPropertyReport(e, dom);
break;
default:
e.next();
break;
}
},
/**
* This event is triggered for any HTTP method that is not known by the
* webserver.
*
* @param {String} method
* @param {String} uri
* @return bool
*/
unknownMethod: function(e, method, uri) {
if (method !== "ACL")
return e.next();
this.httpACL(e, uri);
},
/**
* This method is responsible for handling the 'ACL' event.
*
* @param {String} uri
* @return void
*/
httpACL: function(e, uri) {
var self = this;
this.handler.getRequestBody("utf8", null, false, function(err, body) {
if (err)
return e.next(err);
Xml.loadDOMDocument(body, self.handler.server.options.parser, function(err, dom) {
if (err)
return e.next(err);
var newAcl = jsDAVACL_Property_Acl.unserialize(dom.firstChild).getPrivileges();
// Normalizing urls
newAcl.forEach(function(newAce) {
newAce.principal = self.handler.calculateUri(newAce.principal);
});
self.handler.getNodeForPath(uri, function(err, node) {
if (err)
return e.next(err);
if (!node.hasFeature(jsDAVACL_iACL))
return e.next(new Exc.Exception_MethodNotAllowed("This node does not support the ACL method"));
self.getACL(node, function(err, oldAcl) {
if (err)
return e.next(err);
self.getFlatPrivilegeSet(node, function(err, supportedPrivileges) {
if (err)
return e.next(err);
// Checking if protected principals from the existing principal set are
// not overwritten.
var i, l, j, l2, oldAce, newAce, found;
for (i = 0, l = oldAcl.length; i < l; ++i) {
oldAce = oldAcl[i];
if (!oldAce.protected)
continue;
found = false;
for (j = 0, l2 = newAcl.length; j < l2; ++j) {
newAce = newAcl[j];
if (newAce.privilege === oldAce.privilege &&
newAce.principal === oldAce.principal &&
newAce.protected) {
found = true;
}
}
if (!found)
return e.next(new Exc.AceConflict("This resource contained a protected {DAV:}ace, but this privilege did not occur in the ACL request"));
}
Async.list(newAcl)
.each(function(newAce, nextAce) {
// Do we recognize the privilege
if (!supportedPrivileges[newAce.privilege]) {
return nextAce(new Exc.NotSupportedPrivilege("The privilege you specified (" +
newAce.privilege + ") is not recognized by this server"));
}
if (supportedPrivileges[newAce.privilege].abstract) {
return nextAce(new Exc.NoAbstract("The privilege you specified (" +
newAce.privilege + ") is an abstract privilege"));
}
// Looking up the principal
self.handler.getNodeForPath(newAce.principal, function(err, principal) {
if (err) {
if (err instanceof Exc.NotFound) {
return nextAce(new Exc.NotRecognizedPrincipal("The specified principal (" +
newAce.principal + ") does not exist"));
}
else
return nextAce(err);
}
if (!principal.hasFeature(jsDAVACL_iPrincipal)) {
return nextAce(new Exc.NotRecognizedPrincipal("The specified uri (" +
newAce.principal + ") is not a principal"));
}
nextAce();
});
})
.end(function(err) {
if (err)
return e.next(err);
node.setACL(newAcl, function(err) {
if (err)
return e.next(err);
e.stop();
});
});
});
});
});
});
});
},
/**
* The expand-property report is defined in RFC3253 section 3-8.
*
* This report is very similar to a standard PROPFIND. The difference is
* that it has the additional ability to look at properties containing a
* {DAV:}href element, follow that property and grab additional elements
* there.
*
* Other rfc's, such as ACL rely on this report, so it made sense to put
* it in this plugin.
*
* @param DOMElement dom
* @return void
*/
expandPropertyReport: function(e, dom) {
var requestedProperties = this.parseExpandPropertyReportRequest(dom.firstChild.firstChild);
var depth = this.handler.getHTTPDepth(0);
var requestUri = this.handler.getRequestUri();
var self = this;
this.expandProperties(requestUri, requestedProperties, depth, function(err, result) {
if (err)
return e.next(err);
e.stop();
var namespace, prefix, entry, href, response;
var xml = '<?xml version="1.0" encoding="utf-8"?><d:multistatus';
// Adding in default namespaces
for (namespace in Xml.xmlNamespaces) {
prefix = Xml.xmlNamespaces[namespace];
xml += ' xmlns:' + prefix + '="' + namespace + '"';
}
xml += ">";
result.forEach(function(response) {
xml = response.serialize(self.handler, xml);
});
self.handler.httpResponse.writeHead(207, {"content-type": "application/xml; charset=utf-8"});
self.handler.httpResponse.end(xml + "</d:multistatus>");
});
},
/**
* This method is used by expandPropertyReport to parse
* out the entire HTTP request.
*
* @param DOMElement node
* @return array
*/
parseExpandPropertyReportRequest: function(node) {
var requestedProperties = {};
var children, namespace, propName;
do {
if (Xml.toClarkNotation(node) !== "{DAV:}property")
continue;
if (node.firstChild)
children = this.parseExpandPropertyReportRequest(node.firstChild);
else
children = [];
namespace = node.getAttribute("namespace");
if (!namespace)
namespace = "DAV:";
propName = "{" + namespace + "}" + node.getAttribute("name");
requestedProperties[propName] = children;
}
while (node = node.nextSibling)
return requestedProperties;
},
/**
* This method expands all the properties and returns
* a list with property values
*
* @param {Array} path
* @param {Array} requestedProperties the list of required properties
* @param {Number} depth
* @return array
*/
expandProperties: function(path, requestedProperties, depth, callback) {
var self = this;
this.handler.getPropertiesForPath(path, Object.keys(requestedProperties), depth, function(err, foundProperties) {
if (err)
return callback(err);
var result = [];
Async.list(Object.keys(foundProperties))
.each(function(prop, nextProp) {
var node = foundProperties[prop];
Async.list(Object.keys(requestedProperties))
.each(function(propertyName, nextReqProp) {
var childRequestedProperties = requestedProperties[propertyName];
// We're only traversing if sub-properties were requested
if (Object.keys(childRequestedProperties).length === 0)
return nextReqProp();
// We only have to do the expansion if the property was found
// and it contains an href element.
if (!node["200"][propertyName])
return nextReqProp();