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