]> git.argeo.org Git - lgpl/argeo-commons.git/blob - org.argeo.cms/src/org/argeo/cms/internal/kernel/NodeUserAdmin.java
Fix XA error codes
[lgpl/argeo-commons.git] / org.argeo.cms / src / org / argeo / cms / internal / kernel / NodeUserAdmin.java
1 package org.argeo.cms.internal.kernel;
2
3 import static org.argeo.cms.internal.kernel.KernelUtils.getFrameworkProp;
4 import static org.argeo.cms.internal.kernel.KernelUtils.getOsgiInstanceDir;
5
6 import java.io.File;
7 import java.io.IOException;
8 import java.net.URI;
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;
16 import java.util.Map;
17 import java.util.Set;
18
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;
28
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;
50
51 import bitronix.tm.resource.ehcache.EhCacheXAResourceProducer;
52
53 /**
54 * Aggregates multiple {@link UserDirectory} and integrates them with this node
55 * system roles.
56 */
57 public class NodeUserAdmin implements UserAdmin, KernelConstants {
58 private final static Log log = LogFactory.getLog(NodeUserAdmin.class);
59 final static LdapName ROLES_BASE;
60 static {
61 try {
62 ROLES_BASE = new LdapName(AuthConstants.ROLES_BASEDN);
63 } catch (InvalidNameException e) {
64 throw new UserDirectoryException("Cannot initialize "
65 + NodeUserAdmin.class, e);
66 }
67 }
68
69 // DAOs
70 private UserAdmin nodeRoles = null;
71 private Map<LdapName, UserAdmin> userAdmins = new HashMap<LdapName, UserAdmin>();
72
73 // JCR
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;
79
80 private final String cacheName = UserDirectory.class.getName();
81
82 public NodeUserAdmin(TransactionManager transactionManager,
83 Repository repository) {
84 this.repository = repository;
85 try {
86 this.adminSession = this.repository.login();
87 } catch (RepositoryException e) {
88 throw new CmsException("Cannot log-in", e);
89 }
90
91 // DAOs
92 File nodeBaseDir = new File(getOsgiInstanceDir(), DIR_NODE);
93 nodeBaseDir.mkdirs();
94 String userAdminUri = getFrameworkProp(USERADMIN_URIS);
95 initUserAdmins(userAdminUri, nodeBaseDir);
96 String nodeRolesUri = getFrameworkProp(ROLES_URI);
97 initNodeRoles(nodeRolesUri, nodeBaseDir);
98
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);
105 }
106
107 // JCR
108 initJcr(adminSession);
109 }
110
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
117 .get(name);
118 String uri = UserAdminConf.propertiesAsUri(
119 userDirectory.getProperties()).toString();
120 res.put(uri, "");
121 } else {
122 buf.append('/').append(name.toString())
123 .append("?readOnly=true");
124 }
125 }
126 return res;
127 }
128
129 public void destroy() {
130 for (LdapName name : userAdmins.keySet()) {
131 if (userAdmins.get(name) instanceof UserDirectory) {
132 UserDirectory userDirectory = (UserDirectory) userAdmins
133 .get(name);
134 try {
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);
140 }
141 userDirectory.destroy();
142
143 }
144 }
145 }
146
147 @Override
148 public Role createRole(String name, int type) {
149 return findUserAdmin(name).createRole(name, type);
150 }
151
152 @Override
153 public boolean removeRole(String name) {
154 boolean actuallyDeleted = findUserAdmin(name).removeRole(name);
155 nodeRoles.removeRole(name);
156 return actuallyDeleted;
157 }
158
159 @Override
160 public Role getRole(String name) {
161 return findUserAdmin(name).getRole(name);
162 }
163
164 @Override
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)));
169 }
170 res.addAll(Arrays.asList(nodeRoles.getRoles(filter)));
171 return res.toArray(new Role[res.size()]);
172 }
173
174 @Override
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);
179 if (u != null)
180 res.add(u);
181 }
182 // Note: node roles cannot contain users, so it is not searched
183 return res.size() == 1 ? res.get(0) : null;
184 }
185
186 @Override
187 public Authorization getAuthorization(User user) {
188 if (user == null) {// anonymous
189 return nodeRoles.getAuthorization(null);
190 }
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
197 .getRole(role));
198 systemRoles.addAll(Arrays.asList(auth.getRoles()));
199 }
200 Authorization authorization = new NodeAuthorization(
201 rawAuthorization.getName(), rawAuthorization.toString(),
202 systemRoles, rawAuthorization.getRoles());
203 syncJcr(adminSession, authorization);
204 return authorization;
205 }
206
207 //
208 // USER ADMIN AGGREGATOR
209 //
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);
214 try {
215 userAdmins.put(new LdapName(baseDn), userAdmin);
216 } catch (InvalidNameException e) {
217 throw new UserDirectoryException("Badly formatted base DN "
218 + baseDn, e);
219 }
220 if (userAdmin instanceof UserDirectory) {
221 try {
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);
227 }
228 }
229 }
230
231 private UserAdmin findUserAdmin(String name) {
232 try {
233 return findUserAdmin(new LdapName(name));
234 } catch (InvalidNameException e) {
235 throw new UserDirectoryException("Badly formatted name " + name, e);
236 }
237 }
238
239 private UserAdmin findUserAdmin(LdapName name) {
240 if (name.startsWith(ROLES_BASE))
241 return nodeRoles;
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));
246 }
247 if (res.size() == 0)
248 throw new UserDirectoryException("Cannot find user admin for "
249 + name);
250 if (res.size() > 1)
251 throw new UserDirectoryException("Multiple user admin found for "
252 + name);
253 return res.get(0);
254 }
255
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);
264 }
265 }
266
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())
272 try {
273 FileUtils.copyInputStreamToFile(getClass()
274 .getResourceAsStream(demoBaseDn + ".ldif"),
275 businessRolesFile);
276 } catch (IOException e) {
277 throw new CmsException("Cannot copy demo resource", e);
278 }
279 userAdminUri = businessRolesFile.toURI().toString();
280 }
281 String[] uris = userAdminUri.split(" ");
282 for (String uri : uris) {
283 URI u;
284 try {
285 u = new URI(uri);
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 URI(nodeBaseDir.toURI()+ uri);
295 // u = new File(nodeBaseDir, uri).getCanonicalFile()
296 // .toURI();
297 } else
298 throw new CmsException("Cannot interpret " + uri
299 + " as an uri");
300 } else if (u.getScheme().equals("file")) {
301 u = new File(u).getCanonicalFile().toURI();
302 }
303 } catch (Exception e) {
304 throw new CmsException(
305 "Cannot interpret " + uri + " as an uri", e);
306 }
307 Dictionary<String, ?> properties = UserAdminConf.uriAsProperties(u
308 .toString());
309 UserDirectory businessRoles;
310 if (u.getScheme().startsWith("ldap")) {
311 businessRoles = new LdapUserAdmin(properties);
312 } else {
313 businessRoles = new LdifUserAdmin(properties);
314 }
315 businessRoles.init();
316 String baseDn = businessRoles.getBaseDn();
317 if (userAdmins.containsKey(baseDn))
318 throw new UserDirectoryException(
319 "There is already a user admin for " + baseDn);
320 try {
321 userAdmins.put(new LdapName(baseDn), (UserAdmin) businessRoles);
322 } catch (InvalidNameException e) {
323 throw new UserDirectoryException("Badly formatted base DN "
324 + baseDn, e);
325 }
326 addUserAdmin(businessRoles.getBaseDn(), (UserAdmin) businessRoles);
327 if (log.isDebugEnabled())
328 log.debug("User directory " + businessRoles.getBaseDn() + " ["
329 + u.getScheme() + "] enabled.");
330 }
331
332 }
333
334 private void initNodeRoles(String nodeRolesUri, File nodeBaseDir) {
335 String baseNodeRoleDn = AuthConstants.ROLES_BASEDN;
336 if (nodeRolesUri == null) {
337 File nodeRolesFile = new File(nodeBaseDir, baseNodeRoleDn + ".ldif");
338 if (!nodeRolesFile.exists())
339 try {
340 FileUtils.copyInputStreamToFile(getClass()
341 .getResourceAsStream(baseNodeRoleDn + ".ldif"),
342 nodeRolesFile);
343 } catch (IOException e) {
344 throw new CmsException("Cannot copy demo resource", e);
345 }
346 nodeRolesUri = nodeRolesFile.toURI().toString();
347 }
348
349 Dictionary<String, ?> nodeRolesProperties = UserAdminConf
350 .uriAsProperties(nodeRolesUri);
351 if (!nodeRolesProperties.get(UserAdminConf.baseDn.property()).equals(
352 baseNodeRoleDn)) {
353 throw new CmsException("Invalid base dn for node roles");
354 // TODO deal with "mounted" roles with a different baseDN
355 }
356 if (nodeRolesUri.startsWith("ldap")) {
357 nodeRoles = new LdapUserAdmin(nodeRolesProperties);
358 } else {
359 nodeRoles = new LdifUserAdmin(nodeRolesProperties);
360 }
361 ((UserDirectory) nodeRoles).setExternalRoles(this);
362 ((UserDirectory) nodeRoles).init();
363 addUserAdmin(baseNodeRoleDn, (UserAdmin) nodeRoles);
364 if (log.isTraceEnabled())
365 log.trace("Node roles enabled.");
366
367 }
368
369 /*
370 * JCR
371 */
372 private void initJcr(Session adminSession) {
373 try {
374 JcrUtils.mkdirs(adminSession, homeBasePath);
375 JcrUtils.mkdirs(adminSession, peopleBasePath);
376 adminSession.save();
377
378 JcrUtils.addPrivilege(adminSession, homeBasePath,
379 AuthConstants.ROLE_USER_ADMIN, Privilege.JCR_READ);
380 JcrUtils.addPrivilege(adminSession, peopleBasePath,
381 AuthConstants.ROLE_USER_ADMIN, Privilege.JCR_ALL);
382 adminSession.save();
383 } catch (RepositoryException e) {
384 throw new CmsException("Cannot initialize node user admin", e);
385 }
386 }
387
388 private Node syncJcr(Session session, Authorization authorization) {
389 // TODO check user name validity (e.g. should not start by ROLE_)
390 String username = authorization.getName();
391 String[] roles = authorization.getRoles();
392 try {
393 Node userHome = UserJcrUtils.getUserHome(session, username);
394 if (userHome == null) {
395 String homePath = generateUserPath(homeBasePath, username);
396 if (session.itemExists(homePath))// duplicate user id
397 userHome = session.getNode(homePath).getParent()
398 .addNode(JcrUtils.lastPathElement(homePath));
399 else
400 userHome = JcrUtils.mkdirs(session, homePath);
401 // userHome = JcrUtils.mkfolders(session, homePath);
402 userHome.addMixin(ArgeoTypes.ARGEO_USER_HOME);
403 userHome.setProperty(ArgeoNames.ARGEO_USER_ID, username);
404 session.save();
405
406 JcrUtils.clearAccessControList(session, homePath, username);
407 JcrUtils.addPrivilege(session, homePath, username,
408 Privilege.JCR_ALL);
409 }
410
411 Node userProfile = UserJcrUtils.getUserProfile(session, username);
412 // new user
413 if (userProfile == null) {
414 String personPath = generateUserPath(peopleBasePath, username);
415 Node personBase;
416 if (session.itemExists(personPath))// duplicate user id
417 personBase = session.getNode(personPath).getParent()
418 .addNode(JcrUtils.lastPathElement(personPath));
419 else
420 personBase = JcrUtils.mkdirs(session, personPath);
421 userProfile = personBase.addNode(ArgeoNames.ARGEO_PROFILE);
422 userProfile.addMixin(ArgeoTypes.ARGEO_USER_PROFILE);
423 userProfile.setProperty(ArgeoNames.ARGEO_USER_ID, username);
424 userProfile.setProperty(ArgeoNames.ARGEO_ENABLED, true);
425 userProfile.setProperty(ArgeoNames.ARGEO_ACCOUNT_NON_EXPIRED,
426 true);
427 userProfile.setProperty(ArgeoNames.ARGEO_ACCOUNT_NON_LOCKED,
428 true);
429 userProfile.setProperty(
430 ArgeoNames.ARGEO_CREDENTIALS_NON_EXPIRED, true);
431 session.save();
432
433 JcrUtils.clearAccessControList(session, userProfile.getPath(),
434 username);
435 JcrUtils.addPrivilege(session, userProfile.getPath(), username,
436 Privilege.JCR_READ);
437 }
438
439 // Remote roles
440 if (roles != null) {
441 writeRemoteRoles(userProfile, roles);
442 }
443 adminSession.save();
444 return userProfile;
445 } catch (RepositoryException e) {
446 JcrUtils.discardQuietly(session);
447 throw new ArgeoException("Cannot sync node security model for "
448 + username, e);
449 }
450 }
451
452 /** Generate path for a new user home */
453 private String generateUserPath(String base, String username) {
454 LdapName dn;
455 try {
456 dn = new LdapName(username);
457 } catch (InvalidNameException e) {
458 throw new ArgeoException("Invalid name " + username, e);
459 }
460 String userId = dn.getRdn(dn.size() - 1).getValue().toString();
461 int atIndex = userId.indexOf('@');
462 if (atIndex > 0) {
463 String domain = userId.substring(0, atIndex);
464 String name = userId.substring(atIndex + 1);
465 return base + '/' + JcrUtils.firstCharsToPath(domain, 2) + '/'
466 + domain + '/' + JcrUtils.firstCharsToPath(name, 2) + '/'
467 + name;
468 } else if (atIndex == 0 || atIndex == (userId.length() - 1)) {
469 throw new ArgeoException("Unsupported username " + userId);
470 } else {
471 return base + '/' + JcrUtils.firstCharsToPath(userId, 2) + '/'
472 + userId;
473 }
474 }
475
476 /** Write remote roles used by remote access in the home directory */
477 private void writeRemoteRoles(Node userHome, String[] roles)
478 throws RepositoryException {
479 boolean writeRoles = false;
480 if (userHome.hasProperty(ArgeoNames.ARGEO_REMOTE_ROLES)) {
481 Value[] remoteRoles = userHome.getProperty(
482 ArgeoNames.ARGEO_REMOTE_ROLES).getValues();
483 if (remoteRoles.length != roles.length)
484 writeRoles = true;
485 else
486 for (int i = 0; i < remoteRoles.length; i++)
487 if (!remoteRoles[i].getString().equals(roles[i]))
488 writeRoles = true;
489 } else
490 writeRoles = true;
491
492 if (writeRoles) {
493 userHome.getSession().getWorkspace().getVersionManager()
494 .checkout(userHome.getPath());
495 userHome.setProperty(ArgeoNames.ARGEO_REMOTE_ROLES, roles);
496 JcrUtils.updateLastModified(userHome);
497 userHome.getSession().save();
498 userHome.getSession().getWorkspace().getVersionManager()
499 .checkin(userHome.getPath());
500 if (log.isDebugEnabled())
501 log.debug("Wrote remote roles " + roles + " for "
502 + userHome.getProperty(ArgeoNames.ARGEO_USER_ID));
503 }
504
505 }
506
507 }