]> git.argeo.org Git - lgpl/argeo-commons.git/blob - NodeUserAdmin.java
d4daef11f04b1f8205c9f1fa299dd9ca2d4dd3aa
[lgpl/argeo-commons.git] / 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.security.Privilege;
24 import javax.naming.InvalidNameException;
25 import javax.naming.ldap.LdapName;
26 import javax.transaction.TransactionManager;
27
28 import org.apache.commons.io.FileUtils;
29 import org.apache.commons.logging.Log;
30 import org.apache.commons.logging.LogFactory;
31 import org.argeo.ArgeoException;
32 import org.argeo.cms.CmsException;
33 import org.argeo.cms.auth.AuthConstants;
34 import org.argeo.jcr.ArgeoJcrConstants;
35 import org.argeo.jcr.ArgeoNames;
36 import org.argeo.jcr.ArgeoTypes;
37 import org.argeo.jcr.JcrUtils;
38 import org.argeo.jcr.UserJcrUtils;
39 import org.argeo.osgi.useradmin.LdapUserAdmin;
40 import org.argeo.osgi.useradmin.LdifUserAdmin;
41 import org.argeo.osgi.useradmin.UserAdminConf;
42 import org.argeo.osgi.useradmin.UserDirectory;
43 import org.argeo.osgi.useradmin.UserDirectoryException;
44 import org.osgi.framework.InvalidSyntaxException;
45 import org.osgi.service.useradmin.Authorization;
46 import org.osgi.service.useradmin.Role;
47 import org.osgi.service.useradmin.User;
48 import org.osgi.service.useradmin.UserAdmin;
49
50 import bitronix.tm.resource.ehcache.EhCacheXAResourceProducer;
51
52 /**
53 * Aggregates multiple {@link UserDirectory} and integrates them with this node
54 * system roles.
55 */
56 public class NodeUserAdmin implements UserAdmin, KernelConstants {
57 private final static Log log = LogFactory.getLog(NodeUserAdmin.class);
58 final static LdapName ROLES_BASE;
59 static {
60 try {
61 ROLES_BASE = new LdapName(AuthConstants.ROLES_BASEDN);
62 } catch (InvalidNameException e) {
63 throw new UserDirectoryException("Cannot initialize " + NodeUserAdmin.class, e);
64 }
65 }
66
67 // DAOs
68 private UserAdmin nodeRoles = null;
69 private Map<LdapName, UserAdmin> userAdmins = new HashMap<LdapName, UserAdmin>();
70
71 // JCR
72 /** The home base path. */
73 private String homeBasePath = "/home";
74 private String peopleBasePath = ArgeoJcrConstants.PEOPLE_BASE_PATH;
75 private Repository repository;
76 private Session adminSession;
77
78 private final String cacheName = UserDirectory.class.getName();
79
80 public NodeUserAdmin(TransactionManager transactionManager, Repository repository) {
81 this.repository = repository;
82 try {
83 this.adminSession = this.repository.login();
84 } catch (RepositoryException e) {
85 throw new CmsException("Cannot log-in", e);
86 }
87
88 // DAOs
89 File nodeBaseDir = new File(getOsgiInstanceDir(), DIR_NODE);
90 nodeBaseDir.mkdirs();
91 String userAdminUri = getFrameworkProp(USERADMIN_URIS);
92 initUserAdmins(userAdminUri, nodeBaseDir);
93 String nodeRolesUri = getFrameworkProp(ROLES_URI);
94 initNodeRoles(nodeRolesUri, nodeBaseDir);
95
96 // Transaction manager
97 ((UserDirectory) nodeRoles).setTransactionManager(transactionManager);
98 for (UserAdmin userAdmin : userAdmins.values()) {
99 if (userAdmin instanceof UserDirectory)
100 ((UserDirectory) userAdmin).setTransactionManager(transactionManager);
101 }
102
103 // JCR
104 initJcr(adminSession);
105 }
106
107 Dictionary<String, ?> currentState() {
108 Dictionary<String, Object> res = new Hashtable<String, Object>();
109 for (LdapName name : userAdmins.keySet()) {
110 StringBuilder buf = new StringBuilder();
111 if (userAdmins.get(name) instanceof UserDirectory) {
112 UserDirectory userDirectory = (UserDirectory) userAdmins.get(name);
113 String uri = UserAdminConf.propertiesAsUri(userDirectory.getProperties()).toString();
114 res.put(uri, "");
115 } else {
116 buf.append('/').append(name.toString()).append("?readOnly=true");
117 }
118 }
119 return res;
120 }
121
122 public void destroy() {
123 for (LdapName name : userAdmins.keySet()) {
124 if (userAdmins.get(name) instanceof UserDirectory) {
125 UserDirectory userDirectory = (UserDirectory) userAdmins.get(name);
126 try {
127 // FIXME Make it less bitronix dependant
128 EhCacheXAResourceProducer.unregisterXAResource(cacheName, userDirectory.getXaResource());
129 } catch (Exception e) {
130 log.error("Cannot unregister resource from Bitronix", e);
131 }
132 userDirectory.destroy();
133
134 }
135 }
136 }
137
138 @Override
139 public Role createRole(String name, int type) {
140 return findUserAdmin(name).createRole(name, type);
141 }
142
143 @Override
144 public boolean removeRole(String name) {
145 boolean actuallyDeleted = findUserAdmin(name).removeRole(name);
146 nodeRoles.removeRole(name);
147 return actuallyDeleted;
148 }
149
150 @Override
151 public Role getRole(String name) {
152 return findUserAdmin(name).getRole(name);
153 }
154
155 @Override
156 public Role[] getRoles(String filter) throws InvalidSyntaxException {
157 List<Role> res = new ArrayList<Role>();
158 for (UserAdmin userAdmin : userAdmins.values()) {
159 res.addAll(Arrays.asList(userAdmin.getRoles(filter)));
160 }
161 res.addAll(Arrays.asList(nodeRoles.getRoles(filter)));
162 return res.toArray(new Role[res.size()]);
163 }
164
165 @Override
166 public User getUser(String key, String value) {
167 List<User> res = new ArrayList<User>();
168 for (UserAdmin userAdmin : userAdmins.values()) {
169 User u = userAdmin.getUser(key, value);
170 if (u != null)
171 res.add(u);
172 }
173 // Note: node roles cannot contain users, so it is not searched
174 return res.size() == 1 ? res.get(0) : null;
175 }
176
177 @Override
178 public Authorization getAuthorization(User user) {
179 if (user == null) {// anonymous
180 return nodeRoles.getAuthorization(null);
181 }
182 UserAdmin userAdmin = findUserAdmin(user.getName());
183 Authorization rawAuthorization = userAdmin.getAuthorization(user);
184 // gather system roles
185 Set<String> systemRoles = new HashSet<String>();
186 for (String role : rawAuthorization.getRoles()) {
187 Authorization auth = nodeRoles.getAuthorization((User) userAdmin.getRole(role));
188 systemRoles.addAll(Arrays.asList(auth.getRoles()));
189 }
190 Authorization authorization = new NodeAuthorization(rawAuthorization.getName(), rawAuthorization.toString(),
191 systemRoles, rawAuthorization.getRoles());
192 syncJcr(adminSession, authorization);
193 return authorization;
194 }
195
196 //
197 // USER ADMIN AGGREGATOR
198 //
199 public void addUserAdmin(String baseDn, UserAdmin userAdmin) {
200 if (userAdmins.containsKey(baseDn))
201 throw new UserDirectoryException("There is already a user admin for " + baseDn);
202 try {
203 userAdmins.put(new LdapName(baseDn), userAdmin);
204 } catch (InvalidNameException e) {
205 throw new UserDirectoryException("Badly formatted base DN " + baseDn, e);
206 }
207 if (userAdmin instanceof UserDirectory) {
208 try {
209 // FIXME Make it less bitronix dependant
210 EhCacheXAResourceProducer.registerXAResource(cacheName, ((UserDirectory) userAdmin).getXaResource());
211 } catch (Exception e) {
212 log.error("Cannot register resource to Bitronix", e);
213 }
214 }
215 }
216
217 private UserAdmin findUserAdmin(String name) {
218 try {
219 return findUserAdmin(new LdapName(name));
220 } catch (InvalidNameException e) {
221 throw new UserDirectoryException("Badly formatted name " + name, e);
222 }
223 }
224
225 private UserAdmin findUserAdmin(LdapName name) {
226 if (name.startsWith(ROLES_BASE))
227 return nodeRoles;
228 List<UserAdmin> res = new ArrayList<UserAdmin>(1);
229 for (LdapName baseDn : userAdmins.keySet()) {
230 if (name.startsWith(baseDn))
231 res.add(userAdmins.get(baseDn));
232 }
233 if (res.size() == 0)
234 throw new UserDirectoryException("Cannot find user admin for " + name);
235 if (res.size() > 1)
236 throw new UserDirectoryException("Multiple user admin found for " + name);
237 return res.get(0);
238 }
239
240 public void setTransactionManager(TransactionManager transactionManager) {
241 if (nodeRoles instanceof UserDirectory)
242 ((UserDirectory) nodeRoles).setTransactionManager(transactionManager);
243 for (UserAdmin userAdmin : userAdmins.values()) {
244 if (userAdmin instanceof UserDirectory)
245 ((UserDirectory) userAdmin).setTransactionManager(transactionManager);
246 }
247 }
248
249 private void initUserAdmins(String userAdminUri, File nodeBaseDir) {
250 if (userAdminUri == null) {
251 String demoBaseDn = "dc=example,dc=com";
252 File businessRolesFile = new File(nodeBaseDir, demoBaseDn + ".ldif");
253 if (!businessRolesFile.exists())
254 try {
255 FileUtils.copyInputStreamToFile(getClass().getResourceAsStream(demoBaseDn + ".ldif"),
256 businessRolesFile);
257 } catch (IOException e) {
258 throw new CmsException("Cannot copy demo resource", e);
259 }
260 userAdminUri = businessRolesFile.toURI().toString();
261 }
262 String[] uris = userAdminUri.split(" ");
263 for (String uri : uris) {
264 URI u;
265 try {
266 u = new URI(uri);
267 if (u.getPath() == null)
268 throw new CmsException("URI " + uri + " must have a path in order to determine base DN");
269 if (u.getScheme() == null) {
270 if (uri.startsWith("/") || uri.startsWith("./") || uri.startsWith("../"))
271 u = new File(uri).getCanonicalFile().toURI();
272 else if (!uri.contains("/")) {
273 u = new URI(nodeBaseDir.toURI() + uri);
274 // u = new File(nodeBaseDir, uri).getCanonicalFile()
275 // .toURI();
276 } else
277 throw new CmsException("Cannot interpret " + uri + " as an uri");
278 } else if (u.getScheme().equals("file")) {
279 u = new File(u).getCanonicalFile().toURI();
280 }
281 } catch (Exception e) {
282 throw new CmsException("Cannot interpret " + uri + " as an uri", e);
283 }
284 Dictionary<String, ?> properties = UserAdminConf.uriAsProperties(u.toString());
285 UserDirectory businessRoles;
286 if (u.getScheme().startsWith("ldap")) {
287 businessRoles = new LdapUserAdmin(properties);
288 } else {
289 businessRoles = new LdifUserAdmin(properties);
290 }
291 businessRoles.init();
292 String baseDn = businessRoles.getBaseDn();
293 if (userAdmins.containsKey(baseDn))
294 throw new UserDirectoryException("There is already a user admin for " + baseDn);
295 try {
296 userAdmins.put(new LdapName(baseDn), (UserAdmin) businessRoles);
297 } catch (InvalidNameException e) {
298 throw new UserDirectoryException("Badly formatted base DN " + baseDn, e);
299 }
300 addUserAdmin(businessRoles.getBaseDn(), (UserAdmin) businessRoles);
301 if (log.isDebugEnabled())
302 log.debug("User directory " + businessRoles.getBaseDn() + " [" + u.getScheme() + "] enabled.");
303 }
304
305 }
306
307 private void initNodeRoles(String nodeRolesUri, File nodeBaseDir) {
308 String baseNodeRoleDn = AuthConstants.ROLES_BASEDN;
309 if (nodeRolesUri == null) {
310 File nodeRolesFile = new File(nodeBaseDir, baseNodeRoleDn + ".ldif");
311 if (!nodeRolesFile.exists())
312 try {
313 FileUtils.copyInputStreamToFile(getClass().getResourceAsStream(baseNodeRoleDn + ".ldif"),
314 nodeRolesFile);
315 } catch (IOException e) {
316 throw new CmsException("Cannot copy demo resource", e);
317 }
318 nodeRolesUri = nodeRolesFile.toURI().toString();
319 }
320
321 Dictionary<String, ?> nodeRolesProperties = UserAdminConf.uriAsProperties(nodeRolesUri);
322 if (!nodeRolesProperties.get(UserAdminConf.baseDn.property()).equals(baseNodeRoleDn)) {
323 throw new CmsException("Invalid base dn for node roles");
324 // TODO deal with "mounted" roles with a different baseDN
325 }
326 if (nodeRolesUri.startsWith("ldap")) {
327 nodeRoles = new LdapUserAdmin(nodeRolesProperties);
328 } else {
329 nodeRoles = new LdifUserAdmin(nodeRolesProperties);
330 }
331 ((UserDirectory) nodeRoles).setExternalRoles(this);
332 ((UserDirectory) nodeRoles).init();
333 addUserAdmin(baseNodeRoleDn, (UserAdmin) nodeRoles);
334 if (log.isTraceEnabled())
335 log.trace("Node roles enabled.");
336
337 }
338
339 /*
340 * JCR
341 */
342 private void initJcr(Session adminSession) {
343 try {
344 JcrUtils.mkdirs(adminSession, homeBasePath);
345 JcrUtils.mkdirs(adminSession, peopleBasePath);
346 adminSession.save();
347
348 JcrUtils.addPrivilege(adminSession, homeBasePath, AuthConstants.ROLE_USER_ADMIN, Privilege.JCR_READ);
349 JcrUtils.addPrivilege(adminSession, peopleBasePath, AuthConstants.ROLE_USER_ADMIN, Privilege.JCR_ALL);
350 adminSession.save();
351 } catch (RepositoryException e) {
352 throw new CmsException("Cannot initialize node user admin", e);
353 }
354 }
355
356 private Node syncJcr(Session session, Authorization authorization) {
357 // TODO check user name validity (e.g. should not start by ROLE_)
358 String username = authorization.getName();
359 // String[] roles = authorization.getRoles();
360 try {
361 Node userHome = UserJcrUtils.getUserHome(session, username);
362 if (userHome == null) {
363 String homePath = generateUserPath(homeBasePath, username);
364 if (session.itemExists(homePath))// duplicate user id
365 userHome = session.getNode(homePath).getParent().addNode(JcrUtils.lastPathElement(homePath));
366 else
367 userHome = JcrUtils.mkdirs(session, homePath);
368 // userHome = JcrUtils.mkfolders(session, homePath);
369 userHome.addMixin(ArgeoTypes.ARGEO_USER_HOME);
370 userHome.setProperty(ArgeoNames.ARGEO_USER_ID, username);
371 session.save();
372
373 JcrUtils.clearAccessControList(session, homePath, username);
374 JcrUtils.addPrivilege(session, homePath, username, Privilege.JCR_ALL);
375 }
376
377 Node userProfile = UserJcrUtils.getUserProfile(session, username);
378 // new user
379 if (userProfile == null) {
380 String personPath = generateUserPath(peopleBasePath, username);
381 Node personBase;
382 if (session.itemExists(personPath))// duplicate user id
383 personBase = session.getNode(personPath).getParent().addNode(JcrUtils.lastPathElement(personPath));
384 else
385 personBase = JcrUtils.mkdirs(session, personPath);
386 userProfile = personBase.addNode(ArgeoNames.ARGEO_PROFILE);
387 userProfile.addMixin(ArgeoTypes.ARGEO_USER_PROFILE);
388 userProfile.setProperty(ArgeoNames.ARGEO_USER_ID, username);
389 userProfile.setProperty(ArgeoNames.ARGEO_ENABLED, true);
390 userProfile.setProperty(ArgeoNames.ARGEO_ACCOUNT_NON_EXPIRED, true);
391 userProfile.setProperty(ArgeoNames.ARGEO_ACCOUNT_NON_LOCKED, true);
392 userProfile.setProperty(ArgeoNames.ARGEO_CREDENTIALS_NON_EXPIRED, true);
393 session.save();
394
395 JcrUtils.clearAccessControList(session, userProfile.getPath(), username);
396 JcrUtils.addPrivilege(session, userProfile.getPath(), username, Privilege.JCR_READ);
397 }
398
399 // Remote roles
400 // if (roles != null) {
401 // writeRemoteRoles(userProfile, roles);
402 // }
403 if (adminSession.hasPendingChanges())
404 adminSession.save();
405 return userProfile;
406 } catch (RepositoryException e) {
407 JcrUtils.discardQuietly(session);
408 throw new ArgeoException("Cannot sync node security model for " + username, e);
409 }
410 }
411
412 /** Generate path for a new user home */
413 private String generateUserPath(String base, String username) {
414 LdapName dn;
415 try {
416 dn = new LdapName(username);
417 } catch (InvalidNameException e) {
418 throw new ArgeoException("Invalid name " + username, e);
419 }
420 String userId = dn.getRdn(dn.size() - 1).getValue().toString();
421 int atIndex = userId.indexOf('@');
422 if (atIndex > 0) {
423 String domain = userId.substring(0, atIndex);
424 String name = userId.substring(atIndex + 1);
425 return base + '/' + JcrUtils.firstCharsToPath(domain, 2) + '/' + domain + '/'
426 + JcrUtils.firstCharsToPath(name, 2) + '/' + name;
427 } else if (atIndex == 0 || atIndex == (userId.length() - 1)) {
428 throw new ArgeoException("Unsupported username " + userId);
429 } else {
430 return base + '/' + JcrUtils.firstCharsToPath(userId, 2) + '/' + userId;
431 }
432 }
433
434 // /** Write remote roles used by remote access in the home directory */
435 // private void writeRemoteRoles(Node userHome, String[] roles)
436 // throws RepositoryException {
437 // boolean writeRoles = false;
438 // if (userHome.hasProperty(ArgeoNames.ARGEO_REMOTE_ROLES)) {
439 // Value[] remoteRoles = userHome.getProperty(
440 // ArgeoNames.ARGEO_REMOTE_ROLES).getValues();
441 // if (remoteRoles.length != roles.length)
442 // writeRoles = true;
443 // else
444 // for (int i = 0; i < remoteRoles.length; i++)
445 // if (!remoteRoles[i].getString().equals(roles[i]))
446 // writeRoles = true;
447 // } else
448 // writeRoles = true;
449 //
450 // if (writeRoles) {
451 // userHome.getSession().getWorkspace().getVersionManager()
452 // .checkout(userHome.getPath());
453 // userHome.setProperty(ArgeoNames.ARGEO_REMOTE_ROLES, roles);
454 // JcrUtils.updateLastModified(userHome);
455 // userHome.getSession().save();
456 // userHome.getSession().getWorkspace().getVersionManager()
457 // .checkin(userHome.getPath());
458 // if (log.isDebugEnabled())
459 // log.debug("Wrote remote roles " + roles + " for "
460 // + userHome.getProperty(ArgeoNames.ARGEO_USER_ID));
461 // }
462 //
463 // }
464 }