1 package org
.argeo
.cms
.internal
.kernel
;
3 import static org
.argeo
.cms
.internal
.kernel
.KernelUtils
.getFrameworkProp
;
4 import static org
.argeo
.cms
.internal
.kernel
.KernelUtils
.getOsgiInstanceDir
;
7 import java
.io
.IOException
;
9 import java
.util
.ArrayList
;
10 import java
.util
.Arrays
;
11 import java
.util
.Dictionary
;
12 import java
.util
.HashMap
;
13 import java
.util
.HashSet
;
14 import java
.util
.Hashtable
;
15 import java
.util
.List
;
19 import javax
.jcr
.Node
;
20 import javax
.jcr
.Repository
;
21 import javax
.jcr
.RepositoryException
;
22 import javax
.jcr
.Session
;
23 import javax
.jcr
.Value
;
24 import javax
.jcr
.security
.Privilege
;
25 import javax
.naming
.InvalidNameException
;
26 import javax
.naming
.ldap
.LdapName
;
27 import javax
.transaction
.TransactionManager
;
29 import org
.apache
.commons
.io
.FileUtils
;
30 import org
.apache
.commons
.logging
.Log
;
31 import org
.apache
.commons
.logging
.LogFactory
;
32 import org
.argeo
.ArgeoException
;
33 import org
.argeo
.cms
.CmsException
;
34 import org
.argeo
.cms
.auth
.AuthConstants
;
35 import org
.argeo
.jcr
.ArgeoJcrConstants
;
36 import org
.argeo
.jcr
.ArgeoNames
;
37 import org
.argeo
.jcr
.ArgeoTypes
;
38 import org
.argeo
.jcr
.JcrUtils
;
39 import org
.argeo
.jcr
.UserJcrUtils
;
40 import org
.argeo
.osgi
.useradmin
.LdapUserAdmin
;
41 import org
.argeo
.osgi
.useradmin
.LdifUserAdmin
;
42 import org
.argeo
.osgi
.useradmin
.UserAdminConf
;
43 import org
.argeo
.osgi
.useradmin
.UserDirectory
;
44 import org
.argeo
.osgi
.useradmin
.UserDirectoryException
;
45 import org
.osgi
.framework
.InvalidSyntaxException
;
46 import org
.osgi
.service
.useradmin
.Authorization
;
47 import org
.osgi
.service
.useradmin
.Role
;
48 import org
.osgi
.service
.useradmin
.User
;
49 import org
.osgi
.service
.useradmin
.UserAdmin
;
51 import bitronix
.tm
.resource
.ehcache
.EhCacheXAResourceProducer
;
54 * Aggregates multiple {@link UserDirectory} and integrates them with this node
57 public class NodeUserAdmin
implements UserAdmin
, KernelConstants
{
58 private final static Log log
= LogFactory
.getLog(NodeUserAdmin
.class);
59 final static LdapName ROLES_BASE
;
62 ROLES_BASE
= new LdapName(AuthConstants
.ROLES_BASEDN
);
63 } catch (InvalidNameException e
) {
64 throw new UserDirectoryException("Cannot initialize "
65 + NodeUserAdmin
.class, e
);
70 private UserAdmin nodeRoles
= null;
71 private Map
<LdapName
, UserAdmin
> userAdmins
= new HashMap
<LdapName
, UserAdmin
>();
74 /** The home base path. */
75 private String homeBasePath
= "/home";
76 private String peopleBasePath
= ArgeoJcrConstants
.PEOPLE_BASE_PATH
;
77 private Repository repository
;
78 private Session adminSession
;
80 private final String cacheName
= UserDirectory
.class.getName();
82 public NodeUserAdmin(TransactionManager transactionManager
,
83 Repository repository
) {
84 this.repository
= repository
;
86 this.adminSession
= this.repository
.login();
87 } catch (RepositoryException e
) {
88 throw new CmsException("Cannot log-in", e
);
92 File nodeBaseDir
= new File(getOsgiInstanceDir(), DIR_NODE
);
94 String userAdminUri
= getFrameworkProp(USERADMIN_URIS
);
95 initUserAdmins(userAdminUri
, nodeBaseDir
);
96 String nodeRolesUri
= getFrameworkProp(ROLES_URI
);
97 initNodeRoles(nodeRolesUri
, nodeBaseDir
);
99 // Transaction manager
100 ((UserDirectory
) nodeRoles
).setTransactionManager(transactionManager
);
101 for (UserAdmin userAdmin
: userAdmins
.values()) {
102 if (userAdmin
instanceof UserDirectory
)
103 ((UserDirectory
) userAdmin
)
104 .setTransactionManager(transactionManager
);
108 initJcr(adminSession
);
111 Dictionary
<String
, ?
> currentState() {
112 Dictionary
<String
, Object
> res
= new Hashtable
<String
, Object
>();
113 for (LdapName name
: userAdmins
.keySet()) {
114 StringBuilder buf
= new StringBuilder();
115 if (userAdmins
.get(name
) instanceof UserDirectory
) {
116 UserDirectory userDirectory
= (UserDirectory
) userAdmins
118 String uri
= UserAdminConf
.propertiesAsUri(
119 userDirectory
.getProperties()).toString();
122 buf
.append('/').append(name
.toString())
123 .append("?readOnly=true");
129 public void destroy() {
130 for (LdapName name
: userAdmins
.keySet()) {
131 if (userAdmins
.get(name
) instanceof UserDirectory
) {
132 UserDirectory userDirectory
= (UserDirectory
) userAdmins
135 // FIXME Make it less bitronix dependant
136 EhCacheXAResourceProducer
.unregisterXAResource(cacheName
,
137 userDirectory
.getXaResource());
138 } catch (Exception e
) {
139 log
.error("Cannot unregister resource from Bitronix", e
);
141 userDirectory
.destroy();
148 public Role
createRole(String name
, int type
) {
149 return findUserAdmin(name
).createRole(name
, type
);
153 public boolean removeRole(String name
) {
154 boolean actuallyDeleted
= findUserAdmin(name
).removeRole(name
);
155 nodeRoles
.removeRole(name
);
156 return actuallyDeleted
;
160 public Role
getRole(String name
) {
161 return findUserAdmin(name
).getRole(name
);
165 public Role
[] getRoles(String filter
) throws InvalidSyntaxException
{
166 List
<Role
> res
= new ArrayList
<Role
>();
167 for (UserAdmin userAdmin
: userAdmins
.values()) {
168 res
.addAll(Arrays
.asList(userAdmin
.getRoles(filter
)));
170 res
.addAll(Arrays
.asList(nodeRoles
.getRoles(filter
)));
171 return res
.toArray(new Role
[res
.size()]);
175 public User
getUser(String key
, String value
) {
176 List
<User
> res
= new ArrayList
<User
>();
177 for (UserAdmin userAdmin
: userAdmins
.values()) {
178 User u
= userAdmin
.getUser(key
, value
);
182 // Note: node roles cannot contain users, so it is not searched
183 return res
.size() == 1 ? res
.get(0) : null;
187 public Authorization
getAuthorization(User user
) {
188 if (user
== null) {// anonymous
189 return nodeRoles
.getAuthorization(null);
191 UserAdmin userAdmin
= findUserAdmin(user
.getName());
192 Authorization rawAuthorization
= userAdmin
.getAuthorization(user
);
193 // gather system roles
194 Set
<String
> systemRoles
= new HashSet
<String
>();
195 for (String role
: rawAuthorization
.getRoles()) {
196 Authorization auth
= nodeRoles
.getAuthorization((User
) userAdmin
198 systemRoles
.addAll(Arrays
.asList(auth
.getRoles()));
200 Authorization authorization
= new NodeAuthorization(
201 rawAuthorization
.getName(), rawAuthorization
.toString(),
202 systemRoles
, rawAuthorization
.getRoles());
203 syncJcr(adminSession
, authorization
);
204 return authorization
;
208 // USER ADMIN AGGREGATOR
210 public void addUserAdmin(String baseDn
, UserAdmin userAdmin
) {
211 if (userAdmins
.containsKey(baseDn
))
212 throw new UserDirectoryException(
213 "There is already a user admin for " + baseDn
);
215 userAdmins
.put(new LdapName(baseDn
), userAdmin
);
216 } catch (InvalidNameException e
) {
217 throw new UserDirectoryException("Badly formatted base DN "
220 if (userAdmin
instanceof UserDirectory
) {
222 // FIXME Make it less bitronix dependant
223 EhCacheXAResourceProducer
.registerXAResource(cacheName
,
224 ((UserDirectory
) userAdmin
).getXaResource());
225 } catch (Exception e
) {
226 log
.error("Cannot register resource to Bitronix", e
);
231 private UserAdmin
findUserAdmin(String name
) {
233 return findUserAdmin(new LdapName(name
));
234 } catch (InvalidNameException e
) {
235 throw new UserDirectoryException("Badly formatted name " + name
, e
);
239 private UserAdmin
findUserAdmin(LdapName name
) {
240 if (name
.startsWith(ROLES_BASE
))
242 List
<UserAdmin
> res
= new ArrayList
<UserAdmin
>(1);
243 for (LdapName baseDn
: userAdmins
.keySet()) {
244 if (name
.startsWith(baseDn
))
245 res
.add(userAdmins
.get(baseDn
));
248 throw new UserDirectoryException("Cannot find user admin for "
251 throw new UserDirectoryException("Multiple user admin found for "
256 public void setTransactionManager(TransactionManager transactionManager
) {
257 if (nodeRoles
instanceof UserDirectory
)
258 ((UserDirectory
) nodeRoles
)
259 .setTransactionManager(transactionManager
);
260 for (UserAdmin userAdmin
: userAdmins
.values()) {
261 if (userAdmin
instanceof UserDirectory
)
262 ((UserDirectory
) userAdmin
)
263 .setTransactionManager(transactionManager
);
267 private void initUserAdmins(String userAdminUri
, File nodeBaseDir
) {
268 if (userAdminUri
== null) {
269 String demoBaseDn
= "dc=example,dc=com";
270 File businessRolesFile
= new File(nodeBaseDir
, demoBaseDn
+ ".ldif");
271 if (!businessRolesFile
.exists())
273 FileUtils
.copyInputStreamToFile(getClass()
274 .getResourceAsStream(demoBaseDn
+ ".ldif"),
276 } catch (IOException e
) {
277 throw new CmsException("Cannot copy demo resource", e
);
279 userAdminUri
= businessRolesFile
.toURI().toString();
281 String
[] uris
= userAdminUri
.split(" ");
282 for (String uri
: uris
) {
286 if (u
.getPath() == null)
287 throw new CmsException("URI " + uri
288 + " must have a path in order to determine base DN");
289 if (u
.getScheme() == null) {
290 if (uri
.startsWith("/") || uri
.startsWith("./")
291 || uri
.startsWith("../"))
292 u
= new File(uri
).getCanonicalFile().toURI();
293 else if (!uri
.contains("/"))
294 u
= new File(nodeBaseDir
, uri
).getCanonicalFile()
297 throw new CmsException("Cannot interpret " + uri
299 } else if (u
.getScheme().equals("file")) {
300 u
= new File(u
).getCanonicalFile().toURI();
302 } catch (Exception e
) {
303 throw new CmsException(
304 "Cannot interpret " + uri
+ " as an uri", e
);
306 Dictionary
<String
, ?
> properties
= UserAdminConf
.uriAsProperties(u
308 UserDirectory businessRoles
;
309 if (u
.getScheme().startsWith("ldap")) {
310 businessRoles
= new LdapUserAdmin(properties
);
312 businessRoles
= new LdifUserAdmin(properties
);
314 businessRoles
.init();
315 String baseDn
= businessRoles
.getBaseDn();
316 if (userAdmins
.containsKey(baseDn
))
317 throw new UserDirectoryException(
318 "There is already a user admin for " + baseDn
);
320 userAdmins
.put(new LdapName(baseDn
), (UserAdmin
) businessRoles
);
321 } catch (InvalidNameException e
) {
322 throw new UserDirectoryException("Badly formatted base DN "
325 addUserAdmin(businessRoles
.getBaseDn(), (UserAdmin
) businessRoles
);
326 if (log
.isDebugEnabled())
327 log
.debug("User directory " + businessRoles
.getBaseDn() + " ["
328 + u
.getScheme() + "] enabled.");
333 private void initNodeRoles(String nodeRolesUri
, File nodeBaseDir
) {
334 String baseNodeRoleDn
= AuthConstants
.ROLES_BASEDN
;
335 if (nodeRolesUri
== null) {
336 File nodeRolesFile
= new File(nodeBaseDir
, baseNodeRoleDn
+ ".ldif");
337 if (!nodeRolesFile
.exists())
339 FileUtils
.copyInputStreamToFile(getClass()
340 .getResourceAsStream(baseNodeRoleDn
+ ".ldif"),
342 } catch (IOException e
) {
343 throw new CmsException("Cannot copy demo resource", e
);
345 nodeRolesUri
= nodeRolesFile
.toURI().toString();
348 Dictionary
<String
, ?
> nodeRolesProperties
= UserAdminConf
349 .uriAsProperties(nodeRolesUri
);
350 if (!nodeRolesProperties
.get(UserAdminConf
.baseDn
.property()).equals(
352 throw new CmsException("Invalid base dn for node roles");
353 // TODO deal with "mounted" roles with a different baseDN
355 if (nodeRolesUri
.startsWith("ldap")) {
356 nodeRoles
= new LdapUserAdmin(nodeRolesProperties
);
358 nodeRoles
= new LdifUserAdmin(nodeRolesProperties
);
360 ((UserDirectory
) nodeRoles
).setExternalRoles(this);
361 ((UserDirectory
) nodeRoles
).init();
362 addUserAdmin(baseNodeRoleDn
, (UserAdmin
) nodeRoles
);
363 if (log
.isTraceEnabled())
364 log
.trace("Node roles enabled.");
371 private void initJcr(Session adminSession
) {
373 JcrUtils
.mkdirs(adminSession
, homeBasePath
);
374 JcrUtils
.mkdirs(adminSession
, peopleBasePath
);
377 JcrUtils
.addPrivilege(adminSession
, homeBasePath
,
378 AuthConstants
.ROLE_USER_ADMIN
, Privilege
.JCR_READ
);
379 JcrUtils
.addPrivilege(adminSession
, peopleBasePath
,
380 AuthConstants
.ROLE_USER_ADMIN
, Privilege
.JCR_ALL
);
382 } catch (RepositoryException e
) {
383 throw new CmsException("Cannot initialize node user admin", e
);
387 private Node
syncJcr(Session session
, Authorization authorization
) {
388 // TODO check user name validity (e.g. should not start by ROLE_)
389 String username
= authorization
.getName();
390 String
[] roles
= authorization
.getRoles();
392 Node userHome
= UserJcrUtils
.getUserHome(session
, username
);
393 if (userHome
== null) {
394 String homePath
= generateUserPath(homeBasePath
, username
);
395 if (session
.itemExists(homePath
))// duplicate user id
396 userHome
= session
.getNode(homePath
).getParent()
397 .addNode(JcrUtils
.lastPathElement(homePath
));
399 userHome
= JcrUtils
.mkdirs(session
, homePath
);
400 // userHome = JcrUtils.mkfolders(session, homePath);
401 userHome
.addMixin(ArgeoTypes
.ARGEO_USER_HOME
);
402 userHome
.setProperty(ArgeoNames
.ARGEO_USER_ID
, username
);
405 JcrUtils
.clearAccessControList(session
, homePath
, username
);
406 JcrUtils
.addPrivilege(session
, homePath
, username
,
410 Node userProfile
= UserJcrUtils
.getUserProfile(session
, username
);
412 if (userProfile
== null) {
413 String personPath
= generateUserPath(peopleBasePath
, username
);
415 if (session
.itemExists(personPath
))// duplicate user id
416 personBase
= session
.getNode(personPath
).getParent()
417 .addNode(JcrUtils
.lastPathElement(personPath
));
419 personBase
= JcrUtils
.mkdirs(session
, personPath
);
420 userProfile
= personBase
.addNode(ArgeoNames
.ARGEO_PROFILE
);
421 userProfile
.addMixin(ArgeoTypes
.ARGEO_USER_PROFILE
);
422 userProfile
.setProperty(ArgeoNames
.ARGEO_USER_ID
, username
);
423 userProfile
.setProperty(ArgeoNames
.ARGEO_ENABLED
, true);
424 userProfile
.setProperty(ArgeoNames
.ARGEO_ACCOUNT_NON_EXPIRED
,
426 userProfile
.setProperty(ArgeoNames
.ARGEO_ACCOUNT_NON_LOCKED
,
428 userProfile
.setProperty(
429 ArgeoNames
.ARGEO_CREDENTIALS_NON_EXPIRED
, true);
432 JcrUtils
.clearAccessControList(session
, userProfile
.getPath(),
434 JcrUtils
.addPrivilege(session
, userProfile
.getPath(), username
,
440 writeRemoteRoles(userProfile
, roles
);
444 } catch (RepositoryException e
) {
445 JcrUtils
.discardQuietly(session
);
446 throw new ArgeoException("Cannot sync node security model for "
451 /** Generate path for a new user home */
452 private String
generateUserPath(String base
, String username
) {
455 dn
= new LdapName(username
);
456 } catch (InvalidNameException e
) {
457 throw new ArgeoException("Invalid name " + username
, e
);
459 String userId
= dn
.getRdn(dn
.size() - 1).getValue().toString();
460 int atIndex
= userId
.indexOf('@');
462 String domain
= userId
.substring(0, atIndex
);
463 String name
= userId
.substring(atIndex
+ 1);
464 return base
+ '/' + JcrUtils
.firstCharsToPath(domain
, 2) + '/'
465 + domain
+ '/' + JcrUtils
.firstCharsToPath(name
, 2) + '/'
467 } else if (atIndex
== 0 || atIndex
== (userId
.length() - 1)) {
468 throw new ArgeoException("Unsupported username " + userId
);
470 return base
+ '/' + JcrUtils
.firstCharsToPath(userId
, 2) + '/'
475 /** Write remote roles used by remote access in the home directory */
476 private void writeRemoteRoles(Node userHome
, String
[] roles
)
477 throws RepositoryException
{
478 boolean writeRoles
= false;
479 if (userHome
.hasProperty(ArgeoNames
.ARGEO_REMOTE_ROLES
)) {
480 Value
[] remoteRoles
= userHome
.getProperty(
481 ArgeoNames
.ARGEO_REMOTE_ROLES
).getValues();
482 if (remoteRoles
.length
!= roles
.length
)
485 for (int i
= 0; i
< remoteRoles
.length
; i
++)
486 if (!remoteRoles
[i
].getString().equals(roles
[i
]))
492 userHome
.getSession().getWorkspace().getVersionManager()
493 .checkout(userHome
.getPath());
494 userHome
.setProperty(ArgeoNames
.ARGEO_REMOTE_ROLES
, roles
);
495 JcrUtils
.updateLastModified(userHome
);
496 userHome
.getSession().save();
497 userHome
.getSession().getWorkspace().getVersionManager()
498 .checkin(userHome
.getPath());
499 if (log
.isDebugEnabled())
500 log
.debug("Wrote remote roles " + roles
+ " for "
501 + userHome
.getProperty(ArgeoNames
.ARGEO_USER_ID
));