Alfresco unfortunately does not provide an API to transfer permissions from one user to another. There are strategies to avoid this case, but what if you still need to do it? In this post we will show you how it can work and how to avoid this situation.
Username and user permissions
It is always a bad idea to give users permissions to folders or files directly, unless you really mean the individual person you are giving permissions to (e.g. for private shares). Most of the time, however, you mean a role such as "secretarial staff member" or "project manager for Project X". Even if it seems cumbersome at first glance, it is usually better to create a corresponding group in AD or Alfresco and assign the rights to this group. This has the advantage that you can easily transfer this permission by simply changing the members of this group. However, if you have already granted numerous permissions to individual users in Alfresco on folders or even files, you will be surprised if you ever want to change personnel or if the role of a user in the company changes.
The same applies if you need to change usernames in Alfresco, for example, because you want to integrate an SSO solution that requires the consolidation of different usernames. Since usernames cannot be changed in Alfresco, new users are created instead, but then they have no permissions. This special case can be avoided by using our Alfresco module Smart Logins, which can separate the username from the login to allow users to log into Alfresco with a changeable login across all access paths by performing a dynamic lookup against a configurable LDAP attribute.
Now what if you still run into the situation of transferring permissions to another user? The following is a proof-of-concept using JavaScript:
Transferring direct permissions
Unfortunately, Alfresco does not provide an API to query all of a user's direct permissions (ACLs) in the system. A workaround could be to iterate over all nodes in Alfresco by routine to check which permissions are set. However, this approach is discarded here as it would take far too long in a real system with potentially many millions of nodes and also cannot be implemented without batch processing.
It is much more effective to query the underlying database directly to determine all ACL entries in which permissions of a particular user are set.
Basically, for the user testuser
the SQL could looks like this:
select ace.id as ace_id, n.id as node_id, n.acl_id, a.authority, concat(s.protocol,'://', s.identifier,'/', n.uuid) as node_ref, nqn.local_name as node_type, p.name
from alf_access_control_entry ace
join alf_authority a on (ace.authority_id = a.id and a.authority = 'testuser')
join alf_acl_member acm on (ace.id = acm.ace_id and acm.pos=0)
join alf_node n on (acm.acl_id=n.acl_id)
join alf_permission p on (ace.permission_id=p.id and p.type_qname_id in (select id from alf_qname where local_name in ('cmobject','site')))
join alf_store s on (n.store_id=s.id)
join alf_qname nqn on (n.type_qname_id=nqn.id)
Note: We are only interested in permissions of type cmobject
and site
here. For performance reasons, we do not restrict the namespace by an additional join.
The query returns a list of all permissions for the user testuser
together with the affected node. With this list it is easy to iterate over the nodes by script to set the same permissions to another user.
But how do you get this list from JavaScript if you don't have direct access to the database via Alfresco's JavaScript API? We use an extension published by Jens Goldhammer: alfresco-jscript-extensions. The extension contains among other things a root object database
, which can be used to execute SQL from JavaScript.
A corresponding JavaScript function can then look like this:
function copyAuthorityPermissions(fromAuthority, toAuthority){
var aceSqlQuery = `select ace.id as ace_id, a.authority, n.id as node_id, concat(s.protocol,'://', s.identifier,'/', n.uuid) as node_ref, nqn.local_name as node_type, p.name as permission
from alf_access_control_entry ace
join alf_authority a on (ace.authority_id = a.id and a.authority in (?))
join alf_acl_member acm on (ace.id = acm.ace_id and acm.pos=0)
join alf_node n on (acm.acl_id=n.acl_id)
join alf_permission p on (ace.permission_id=p.id and p.type_qname_id in (select id from alf_qname where local_name in ('cmobject','site')))
join alf_store s on (n.store_id=s.id)
join alf_qname nqn on (n.type_qname_id=nqn.id)`
var aceEntries = database.query("dataSource", aceSqlQuery, fromAuthority);
// permission, node_ref, node_type , acl_id, authority, node_id
for each (var aceEntry in aceEntries){
var node = utils.getNodeFromString(aceEntry.node_ref );
if (node.exists()){
logger.log("authority: " + aceEntry.authority + " permission: " + aceEntry.permission + " path: " + node.displayPath + '/' + node.name);
logger.log("nodeRef: " + aceEntry.node_ref);
node.setPermission(aceEntry.permission, toAuthority)
}
}
}
Transfer of the previous userhome
Assuming the new user is to inherit the previous userhome, the following function can be used to set the folder of another user in the new user's profile and gain ownership so it becomes your own userhome:
function switchUserHome(oldUserName,newUserName){
var oldUserNode = people.getPerson(oldUserName);
var newUserNode = people.getPerson(newUserName);
if (oldUserNode){
if (newUserNode){
var newUserHomeFolder = newUserNode.properties["cm:homeFolder"];
var oldUserHomeFolder = oldUserNode.properties["cm:homeFolder"];
if (newUserHomeFolder.exists() && newUserHomeFolder.name != oldUserHomeFolder.name){
if (newUserHomeFolder.name == newUserName){
newUserHomeFolder.name = newUserHomeFolder.name + '-old'
newUserHomeFolder.save();
}
newUserNode.properties["cm:homeFolder"] = oldUserHomeFolder;
newUserNode.save();
newUserHomeFolder.remove();
if (oldUserHomeFolder.name != newUserName){
logger.log("renaming user folder " + oldUserHomeFolder.name + " to " + newUserName);
oldUserHomeFolder.name = newUserName;
oldUserHomeFolder.properties["cm:owner"] = newUserName;
oldUserHomeFolder.save();
}
} else {
logger.error("user home for user " + newUserName + " does not exist or is already moved!");
}
} else {
logger.error("user "+newUserName+" not found!")
}
} else {
logger.error("user "+oldUserName+" not found!")
}
}
Note: In this case switchUserHome
assumes that the userhome of newUser is empty and should be deleted. To merge both homes, one would still have to synchronize the children from the userhome of newUser to that of oldUser.
Copy group memberships
Since users also have permissions through group memberships (such as in sites), the following function can be used to copy the group memberships of a user:
function copyUserGroupMemberships(fromUserName,toUserName){
var fromUser = people.getPerson(fromUserName);
var toUser = people.getPerson(toUserName);
if (fromUser && toUser){
var memberships = fromUser.parentAssociations["cm:member"];
for each (var groupAssoc in memberships){
try {
people.addAuthority(groupAssoc,toUser);
} catch(ex ) {
// unfortunatele the people.addAuthority method does not check if the authority is not already a member
// so we need to catch the exception in case the user is already a member
logger.error("ABORT: Exception occurred: "+ex);
}
}
}
}
All in One
That should be it now. In order execute all the functions in one call, you can pack the above functions into a separate function and execute that one by a one-liner:
function switchUser(oldUserName,newUserName){
switchUserHome(oldUserName,newUserName);
copyUserGroupMemberships(oldUserName,newUserName);
copyAuthorityPermissions(oldUserName,newUserName);
}
switchUser("old.user","new.user");
/*
people.deletePerson("old.user")
*/
If you have any Feedback: Let us know!
Picture credits title picture: aitoff (Andrew Martin) licensed under the pixabay license (free for commercial use)