1 /****************************************************************
2 * Licensed to the Apache Software Foundation (ASF) under one *
3 * or more contributor license agreements. See the NOTICE file *
4 * distributed with this work for additional information *
5 * regarding copyright ownership. The ASF licenses this file *
6 * to you under the Apache License, Version 2.0 (the *
7 * "License"); you may not use this file except in compliance *
8 * with the License. You may obtain a copy of the License at *
9 * *
10 * http://www.apache.org/licenses/LICENSE-2.0 *
11 * *
12 * Unless required by applicable law or agreed to in writing, *
13 * software distributed under the License is distributed on an *
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
15 * KIND, either express or implied. See the License for the *
16 * specific language governing permissions and limitations *
17 * under the License. *
18 ****************************************************************/
19
20 package org.apache.james.jcr;
21
22 import java.util.ArrayList;
23 import java.util.Collection;
24 import java.util.Iterator;
25
26 import javax.jcr.Credentials;
27 import javax.jcr.Node;
28 import javax.jcr.NodeIterator;
29 import javax.jcr.PathNotFoundException;
30 import javax.jcr.Repository;
31 import javax.jcr.RepositoryException;
32 import javax.jcr.Session;
33
34 import org.apache.commons.logging.Log;
35 import org.apache.commons.logging.LogFactory;
36 import org.apache.james.api.user.User;
37 import org.apache.james.api.user.UsersRepository;
38
39
40 public class JCRUsersRepository extends AbstractJCRRepository implements UsersRepository {
41
42 //TODO: Add namespacing
43 private static final String PASSWD_PROPERTY = "passwd";
44
45 private static final String USERNAME_PROPERTY = "username";
46
47 private static final Log LOGGER = LogFactory.getLog(JCRMailRepository.class);
48
49 /**
50 * For setter injection.
51 */
52 public JCRUsersRepository() {
53 super(LOGGER);
54 this.path = "users";
55 }
56
57 /**
58 * Maximal constructor for injection.
59 * @param repository not null
60 * @param credentials login credentials for accessing the repository
61 * or null to use default credentials
62 * @param workspace name of the workspace used as the mail repository.
63 * or null to use default workspace
64 * @param path path (relative to root) of the user node within the workspace,
65 * or null to use default.
66 */
67 public JCRUsersRepository(Repository repository, Credentials credentials, String workspace, String path, Log logger) {
68 super(repository, credentials, workspace, path, logger);
69 }
70
71 /**
72 * Minimal constructor for injection.
73 * @param repository not null
74 */
75 public JCRUsersRepository(Repository repository) {
76 super(repository, LOGGER);
77 this.path = "users";
78 }
79
80 /**
81 * Adds a user to the repository with the specified User object.
82 *
83 * @param user the user to be added
84 *
85 * @return true if succesful, false otherwise
86 * @since James 1.2.2
87 *
88 * @deprecated James 2.4 user should be added using username/password
89 * because specific implementations of UsersRepository will support specific
90 * implementations of users object.
91 */
92 public boolean addUser(User user) {
93 throw new UnsupportedOperationException("Unsupported by JCR");
94 }
95
96 /**
97 * Adds a user to the repository with the specified attributes. In current
98 * implementations, the Object attributes is generally a String password.
99 *
100 * @param name the name of the user to be added
101 * @param attributes see decription
102 *
103 * @deprecated James 2.4 user is always added using username/password and
104 * eventually modified by retrieving it later.
105 */
106 public void addUser(String name, Object attributes) {
107 if (attributes instanceof String) {
108 addUser(name, (String) attributes);
109 } else {
110 throw new IllegalArgumentException("Expected password string");
111 }
112 }
113
114 /**
115 * Adds a user to the repository with the specified password
116 *
117 * @param username the username of the user to be added
118 * @param password the password of the user to add
119 * @return true if succesful, false otherwise
120 *
121 * @since James 2.3.0
122 */
123 public boolean addUser(String username, String password) {
124
125 try {
126 final Session session = login();
127 try {
128 final String name = toSafeName(username);
129 final String path = this.path + "/" + name;
130 final Node rootNode = session.getRootNode();
131 try {
132 rootNode.getNode(path);
133 logger.info("User already exists");
134 return false;
135 } catch (PathNotFoundException e) {
136 // user does not exist
137 }
138 Node parent;
139 try {
140 parent = rootNode.getNode(this.path);
141 } catch (PathNotFoundException e) {
142 // TODO: Need to consider whether should insist that parent
143 // TODO: path exists.
144 parent = rootNode.addNode(this.path);
145 }
146
147 Node node = parent.addNode(name);
148 node.setProperty(USERNAME_PROPERTY, username);
149 final String hashedPassword;
150 if (password == null)
151 {
152 // Support easy password reset
153 hashedPassword = "";
154 }
155 else
156 {
157 hashedPassword = JCRUser.hashPassword(username, password);
158 }
159 node.setProperty(PASSWD_PROPERTY, hashedPassword);
160 session.save();
161 return true;
162 } finally {
163 session.logout();
164 }
165
166 } catch (RepositoryException e) {
167 if (logger.isInfoEnabled()) {
168 logger.info("Failed to add user: " + username, e);
169 }
170 }
171
172 return false;
173 }
174
175 /**
176 * Get the user object with the specified user name. Return null if no
177 * such user.
178 *
179 * @param name the name of the user to retrieve
180 * @return the user being retrieved, null if the user doesn't exist
181 *
182 * @since James 1.2.2
183 */
184 public User getUserByName(String username) {
185 User user;
186 try {
187 final Session session = login();
188 try {
189 final String name = toSafeName(username);
190 final String path = this.path + "/" + name;
191 final Node rootNode = session.getRootNode();
192
193 try {
194 final Node node = rootNode.getNode(path);
195 user = new JCRUser(node.getProperty(USERNAME_PROPERTY).getString(),
196 node.getProperty(PASSWD_PROPERTY).getString());
197 } catch (PathNotFoundException e) {
198 // user not found
199 user = null;
200 }
201 } finally {
202 session.logout();
203 }
204
205 } catch (RepositoryException e) {
206 if (logger.isInfoEnabled()) {
207 logger.info("Failed to add user: " + username, e);
208 }
209 user = null;
210 }
211 return user;
212 }
213
214 /**
215 * Get the user object with the specified user name. Match user naems on
216 * a case insensitive basis. Return null if no such user.
217 *
218 * @param name the name of the user to retrieve
219 * @return the user being retrieved, null if the user doesn't exist
220 *
221 * @since James 1.2.2
222 * @deprecated James 2.4 now caseSensitive is a property of the repository
223 * implementations and the getUserByName will search according to this property.
224 */
225 public User getUserByNameCaseInsensitive(String name) {
226 throw new UnsupportedOperationException();
227 }
228
229 /**
230 * Returns the user name of the user matching name on an equalsIgnoreCase
231 * basis. Returns null if no match.
232 *
233 * @param name the name to case-correct
234 * @return the case-correct name of the user, null if the user doesn't exist
235 */
236 public String getRealName(String name) {
237 return null;
238 }
239
240 /**
241 * Update the repository with the specified user object. A user object
242 * with this username must already exist.
243 *
244 * @return true if successful.
245 */
246 public boolean updateUser(final User user) {
247 if (user != null && user instanceof JCRUser)
248 {
249 final JCRUser jcrUser = (JCRUser) user;
250 final String userName = jcrUser.getUserName();
251 try {
252 final Session session = login();
253 try {
254 final String name = toSafeName(userName);
255 final String path = this.path + "/" + name;
256 final Node rootNode = session.getRootNode();
257
258 try {
259 final String hashedSaltedPassword = jcrUser.getHashedSaltedPassword();
260 rootNode.getNode(path).setProperty(PASSWD_PROPERTY, hashedSaltedPassword);
261 session.save();
262 return true;
263 } catch (PathNotFoundException e) {
264 // user not found
265 logger.debug("User not found");
266 }
267 } finally {
268 session.logout();
269 }
270
271 } catch (RepositoryException e) {
272 if (logger.isInfoEnabled()) {
273 logger.info("Failed to add user: " + userName, e);
274 }
275 }
276 }
277 return false;
278 }
279
280 /**
281 * Removes a user from the repository
282 *
283 * @param name the user to remove from the repository
284 */
285 public void removeUser(String username) {
286 try {
287 final Session session = login();
288 try {
289 final String name = toSafeName(username);
290 final String path = this.path + "/" + name;
291 try {
292 session.getRootNode().getNode(path).remove();
293 session.save();
294 } catch (PathNotFoundException e) {
295 // user not found
296 }
297 } finally {
298 session.logout();
299 }
300
301 } catch (RepositoryException e) {
302 if (logger.isInfoEnabled()) {
303 logger.info("Failed to add user: " + username, e);
304 }
305 }
306 }
307
308 /**
309 * Returns whether or not this user is in the repository
310 *
311 * @param name the name to check in the repository
312 * @return whether the user is in the repository
313 */
314 public boolean contains(String name) {
315 try {
316 final Session session = login();
317 try {
318 final Node rootNode = session.getRootNode();
319 final String path = this.path + "/" + toSafeName(name);
320 rootNode.getNode(path);
321 return true;
322 } finally {
323 session.logout();
324 }
325
326 } catch (RepositoryException e) {
327 if (logger.isDebugEnabled()) {
328 logger.debug("User not found: " + name, e);
329 }
330 }
331
332 return false;
333 }
334
335 /**
336 * Returns whether or not this user is in the repository. Names are
337 * matched on a case insensitive basis.
338 *
339 * @param name the name to check in the repository
340 * @return whether the user is in the repository
341 *
342 * @deprecated James 2.4 now caseSensitive is a property of the repository
343 * implementations and the contains will search according to this property.
344 */
345 public boolean containsCaseInsensitive(String name) {
346 throw new UnsupportedOperationException();
347 }
348
349 /**
350 * Test if user with name 'name' has password 'password'.
351 *
352 * @param name the name of the user to be tested
353 * @param password the password to be tested
354 *
355 * @return true if the test is successful, false if the user
356 * doesn't exist or if the password is incorrect
357 *
358 * @since James 1.2.2
359 */
360 public boolean test(String username, String password) {
361 try {
362 final Session session = login();
363 try {
364 final String name = toSafeName(username);
365 final String path = this.path + "/" + name;
366 final Node rootNode = session.getRootNode();
367
368 try {
369 final Node node = rootNode.getNode(path);
370 final String current = node.getProperty(PASSWD_PROPERTY).getString();
371 if (current == null || current == "")
372 {
373 return password == null || password == "";
374 }
375 final String hashPassword = JCRUser.hashPassword(username, password);
376 return current.equals(hashPassword);
377 } catch (PathNotFoundException e) {
378 // user not found
379 logger.debug("User not found");
380 return false;
381 }
382 } finally {
383 session.logout();
384 }
385
386 } catch (RepositoryException e) {
387 if (logger.isInfoEnabled()) {
388 logger.info("Failed to add user: " + username, e);
389 }
390 return false;
391 }
392
393 }
394
395 /**
396 * Returns a count of the users in the repository.
397 *
398 * @return the number of users in the repository
399 */
400 public int countUsers() {
401 try {
402 final Session session = login();
403 try {
404 final Node rootNode = session.getRootNode();
405 try {
406 final Node node = rootNode.getNode(path);
407 //TODO: Use query
408 //TODO: Use namespacing to avoid unwanted nodes
409 NodeIterator it = node.getNodes();
410 return (int) it.getSize();
411 } catch (PathNotFoundException e) {
412 return 0;
413 }
414 } finally {
415 session.logout();
416 }
417 } catch (RepositoryException e) {
418 if (logger.isInfoEnabled()) {
419 logger.info("Failed to count user", e);
420 }
421 return 0;
422 }
423 }
424
425 /**
426 * List users in repository.
427 *
428 * @return Iterator over a collection of Strings, each being one user in the repository.
429 */
430 public Iterator list() {
431 final Collection userNames = new ArrayList();
432 try {
433 final Session session = login();
434 try {
435 final Node rootNode = session.getRootNode();
436 try {
437 final Node baseNode = rootNode.getNode(path);
438 //TODO: Use query
439 final NodeIterator it = baseNode.getNodes();
440 while(it.hasNext()) {
441 final Node node = it.nextNode();
442 try {
443 final String userName = node.getProperty(USERNAME_PROPERTY).getString();
444 userNames.add(userName);
445 } catch (PathNotFoundException e) {
446 logger.info("Node missing user name. Ignoring.");
447 }
448 }
449 } catch (PathNotFoundException e) {
450 logger.info("Path not found. Forgotten to setup the repository?");
451 }
452 } finally {
453 session.logout();
454 }
455 } catch (RepositoryException e) {
456 if (logger.isInfoEnabled()) {
457 logger.info("Failed to count user", e);
458 }
459 }
460 return userNames.iterator();
461 }
462 }