1 /*
2  * Licensed to the Apache Software Foundation (ASF) under one or more
3  * contributor license agreements.  See the NOTICE file distributed with
4  * this work for additional information regarding copyright ownership.
5  * The ASF licenses this file to You under the Apache License, Version 2.0
6  * (the "License"); you may not use this file except in compliance with
7  * the License.  You may obtain a copy of the License at
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */

17 package org.apache.catalina.users;
18
19 import java.io.File;
20 import java.io.FileNotFoundException;
21 import java.io.FileOutputStream;
22 import java.io.IOException;
23 import java.io.OutputStreamWriter;
24 import java.io.PrintWriter;
25 import java.net.URI;
26 import java.net.URL;
27 import java.net.URLConnection;
28 import java.nio.charset.StandardCharsets;
29 import java.util.ArrayList;
30 import java.util.Iterator;
31 import java.util.Map;
32 import java.util.concurrent.ConcurrentHashMap;
33 import java.util.concurrent.locks.Lock;
34 import java.util.concurrent.locks.ReentrantReadWriteLock;
35
36 import org.apache.catalina.Globals;
37 import org.apache.catalina.Group;
38 import org.apache.catalina.Role;
39 import org.apache.catalina.User;
40 import org.apache.catalina.UserDatabase;
41 import org.apache.juli.logging.Log;
42 import org.apache.juli.logging.LogFactory;
43 import org.apache.tomcat.util.digester.AbstractObjectCreationFactory;
44 import org.apache.tomcat.util.digester.Digester;
45 import org.apache.tomcat.util.file.ConfigFileLoader;
46 import org.apache.tomcat.util.file.ConfigurationSource;
47 import org.apache.tomcat.util.res.StringManager;
48 import org.xml.sax.Attributes;
49
50 /**
51  * Concrete implementation of {@link UserDatabase} that loads all defined users,
52  * groups, and roles into an in-memory data structure, and uses a specified XML
53  * file for its persistent storage.
54  * <p>
55  * This class is thread-safe.
56  * <p>
57  * This class does not enforce what, in an RDBMS, would be called referential
58  * integrity. Concurrent modifications may result in inconsistent data such as
59  * a User retaining a reference to a Role that has been removed from the
60  * database.
61  *
62  * @author Craig R. McClanahan
63  * @since 4.1
64  */

65 /*
66  * Implementation notes:
67  *
68  * Any operation that acts on a single element of the database (e.g. operations
69  * that create, read, update or delete a user, role or group) must first obtain
70  * the read lock. Operations that return iterators for users, roles or groups
71  * also fall into this category.
72  *
73  * Iterators must always be created from copies of the data to prevent possible
74  * corruption of the iterator due to the remove of all elements from the
75  * underlying Map that would occur during a subsequent re-loading of the
76  * database.
77  *
78  * Any operation that acts on multiple elements and expects the database to
79  * remain consistent during the operation (e.g. saving or loading the database)
80  * must first obtain the write lock.
81  */

82 public class MemoryUserDatabase implements UserDatabase {
83
84     private static final Log log = LogFactory.getLog(MemoryUserDatabase.class);
85     private static final StringManager sm = StringManager.getManager(MemoryUserDatabase.class);
86
87
88     // ----------------------------------------------------------- Constructors
89
90     /**
91      * Create a new instance with default values.
92      */

93     public MemoryUserDatabase() {
94         this(null);
95     }
96
97
98     /**
99      * Create a new instance with the specified values.
100      *
101      * @param id Unique global identifier of this user database
102      */

103     public MemoryUserDatabase(String id) {
104         this.id = id;
105     }
106
107     // ----------------------------------------------------- Instance Variables
108
109     /**
110      * The set of {@link Group}s defined in this database, keyed by group name.
111      */

112     protected final Map<String, Group> groups = new ConcurrentHashMap<>();
113
114     /**
115      * The unique global identifier of this user database.
116      */

117     protected final String id;
118
119     /**
120      * The relative (to <code>catalina.base</code>) or absolute pathname to the
121      * XML file in which we will save our persistent information.
122      */

123     protected String pathname = "conf/tomcat-users.xml";
124
125     /**
126      * The relative or absolute pathname to the file in which our old
127      * information is stored while renaming is in progress.
128      */

129     protected String pathnameOld = pathname + ".old";
130
131     /**
132      * The relative or absolute pathname of the file in which we write our new
133      * information prior to renaming.
134      */

135     protected String pathnameNew = pathname + ".new";
136
137     /**
138      * A flag, indicating if the user database is read only.
139      */

140     protected boolean readonly = true;
141
142     /**
143      * The set of {@link Role}s defined in this database, keyed by role name.
144      */

145     protected final Map<String, Role> roles = new ConcurrentHashMap<>();
146
147     /**
148      * The set of {@link User}s defined in this database, keyed by user name.
149      */

150     protected final Map<String, User> users = new ConcurrentHashMap<>();
151
152     private final ReentrantReadWriteLock dbLock = new ReentrantReadWriteLock();
153     private final Lock readLock = dbLock.readLock();
154     private final Lock writeLock = dbLock.writeLock();
155
156     private volatile long lastModified = 0;
157     private boolean watchSource = true;
158
159
160     // ------------------------------------------------------------- Properties
161
162     /**
163      * @return the set of {@link Group}s defined in this user database.
164      */

165     @Override
166     public Iterator<Group> getGroups() {
167         readLock.lock();
168         try {
169             return new ArrayList<>(groups.values()).iterator();
170         } finally {
171             readLock.unlock();
172         }
173     }
174
175
176     /**
177      * @return the unique global identifier of this user database.
178      */

179     @Override
180     public String getId() {
181         return this.id;
182     }
183
184
185     /**
186      * @return the relative or absolute pathname to the persistent storage file.
187      */

188     public String getPathname() {
189         return this.pathname;
190     }
191
192
193     /**
194      * Set the relative or absolute pathname to the persistent storage file.
195      *
196      * @param pathname The new pathname
197      */

198     public void setPathname(String pathname) {
199         this.pathname = pathname;
200         this.pathnameOld = pathname + ".old";
201         this.pathnameNew = pathname + ".new";
202     }
203
204
205     /**
206      * @return the readonly status of the user database
207      */

208     public boolean getReadonly() {
209         return this.readonly;
210     }
211
212
213     /**
214      * Setting the readonly status of the user database
215      *
216      * @param readonly the new status
217      */

218     public void setReadonly(boolean readonly) {
219         this.readonly = readonly;
220     }
221
222
223     public boolean getWatchSource() {
224         return watchSource;
225     }
226
227
228
229     public void setWatchSource(boolean watchSource) {
230         this.watchSource = watchSource;
231     }
232
233
234     /**
235      * @return the set of {@link Role}s defined in this user database.
236      */

237     @Override
238     public Iterator<Role> getRoles() {
239         readLock.lock();
240         try {
241             return new ArrayList<>(roles.values()).iterator();
242         } finally {
243             readLock.unlock();
244         }
245     }
246
247
248     /**
249      * @return the set of {@link User}s defined in this user database.
250      */

251     @Override
252     public Iterator<User> getUsers() {
253         readLock.lock();
254         try {
255             return new ArrayList<>(users.values()).iterator();
256         } finally {
257             readLock.unlock();
258         }
259     }
260
261
262     // --------------------------------------------------------- Public Methods
263
264     /**
265      * Finalize access to this user database.
266      *
267      * @exception Exception if any exception is thrown during closing
268      */

269     @Override
270     public void close() throws Exception {
271
272         writeLock.lock();
273         try {
274             save();
275             users.clear();
276             groups.clear();
277             roles.clear();
278         } finally {
279             writeLock.unlock();
280         }
281     }
282
283
284     /**
285      * Create and return a new {@link Group} defined in this user database.
286      *
287      * @param groupname The group name of the new group (must be unique)
288      * @param description The description of this group
289      */

290     @Override
291     public Group createGroup(String groupname, String description) {
292         if (groupname == null || groupname.length() == 0) {
293             String msg = sm.getString("memoryUserDatabase.nullGroup");
294             log.warn(msg);
295             throw new IllegalArgumentException(msg);
296         }
297
298         MemoryGroup group = new MemoryGroup(this, groupname, description);
299         readLock.lock();
300         try {
301             groups.put(group.getGroupname(), group);
302         } finally {
303             readLock.unlock();
304         }
305         return group;
306     }
307
308
309     /**
310      * Create and return a new {@link Role} defined in this user database.
311      *
312      * @param rolename The role name of the new group (must be unique)
313      * @param description The description of this group
314      */

315     @Override
316     public Role createRole(String rolename, String description) {
317         if (rolename == null || rolename.length() == 0) {
318             String msg = sm.getString("memoryUserDatabase.nullRole");
319             log.warn(msg);
320             throw new IllegalArgumentException(msg);
321         }
322
323         MemoryRole role = new MemoryRole(this, rolename, description);
324         readLock.lock();
325         try {
326             roles.put(role.getRolename(), role);
327         } finally {
328             readLock.unlock();
329         }
330         return role;
331     }
332
333
334     /**
335      * Create and return a new {@link User} defined in this user database.
336      *
337      * @param username The logon username of the new user (must be unique)
338      * @param password The logon password of the new user
339      * @param fullName The full name of the new user
340      */

341     @Override
342     public User createUser(String username, String password, String fullName) {
343
344         if (username == null || username.length() == 0) {
345             String msg = sm.getString("memoryUserDatabase.nullUser");
346             log.warn(msg);
347             throw new IllegalArgumentException(msg);
348         }
349
350         MemoryUser user = new MemoryUser(this, username, password, fullName);
351         readLock.lock();
352         try {
353             users.put(user.getUsername(), user);
354         } finally {
355             readLock.unlock();
356         }
357         return user;
358     }
359
360
361     /**
362      * Return the {@link Group} with the specified group name, if any; otherwise
363      * return <code>null</code>.
364      *
365      * @param groupname Name of the group to return
366      */

367     @Override
368     public Group findGroup(String groupname) {
369         readLock.lock();
370         try {
371             return groups.get(groupname);
372         } finally {
373             readLock.unlock();
374         }
375     }
376
377
378     /**
379      * Return the {@link Role} with the specified role name, if any; otherwise
380      * return <code>null</code>.
381      *
382      * @param rolename Name of the role to return
383      */

384     @Override
385     public Role findRole(String rolename) {
386         readLock.lock();
387         try {
388             return roles.get(rolename);
389         } finally {
390             readLock.unlock();
391         }
392     }
393
394
395     /**
396      * Return the {@link User} with the specified user name, if any; otherwise
397      * return <code>null</code>.
398      *
399      * @param username Name of the user to return
400      */

401     @Override
402     public User findUser(String username) {
403         readLock.lock();
404         try {
405             return users.get(username);
406         } finally {
407             readLock.unlock();
408         }
409     }
410
411
412     /**
413      * Initialize access to this user database.
414      *
415      * @exception Exception if any exception is thrown during opening
416      */

417     @Override
418     public void open() throws Exception {
419         writeLock.lock();
420         try {
421             // Erase any previous groups and users
422             users.clear();
423             groups.clear();
424             roles.clear();
425
426             String pathName = getPathname();
427             try (ConfigurationSource.Resource resource = ConfigFileLoader.getSource().getResource(pathName)) {
428                 this.lastModified = resource.getURI().toURL().openConnection().getLastModified();
429
430                 // Construct a digester to read the XML input file
431                 Digester digester = new Digester();
432                 try {
433                     digester.setFeature(
434                             "http://apache.org/xml/features/allow-java-encodings"true);
435                 } catch (Exception e) {
436                     log.warn(sm.getString("memoryUserDatabase.xmlFeatureEncoding"), e);
437                 }
438                 digester.addFactoryCreate("tomcat-users/group",
439                         new MemoryGroupCreationFactory(this), true);
440                 digester.addFactoryCreate("tomcat-users/role",
441                         new MemoryRoleCreationFactory(this), true);
442                 digester.addFactoryCreate("tomcat-users/user",
443                         new MemoryUserCreationFactory(this), true);
444
445                 // Parse the XML input to load this database
446                 digester.parse(resource.getInputStream());
447             } catch (IOException ioe) {
448                 log.error(sm.getString("memoryUserDatabase.fileNotFound", pathName));
449             } catch (Exception e) {
450                 // Fail safe on error
451                 users.clear();
452                 groups.clear();
453                 roles.clear();
454                 throw e;
455             }
456         } finally {
457             writeLock.unlock();
458         }
459     }
460
461
462     /**
463      * Remove the specified {@link Group} from this user database.
464      *
465      * @param group The group to be removed
466      */

467     @Override
468     public void removeGroup(Group group) {
469         readLock.lock();
470         try {
471             Iterator<User> users = getUsers();
472             while (users.hasNext()) {
473                 User user = users.next();
474                 user.removeGroup(group);
475             }
476             groups.remove(group.getGroupname());
477         } finally {
478             readLock.unlock();
479         }
480     }
481
482
483     /**
484      * Remove the specified {@link Role} from this user database.
485      *
486      * @param role The role to be removed
487      */

488     @Override
489     public void removeRole(Role role) {
490         readLock.lock();
491         try {
492             Iterator<Group> groups = getGroups();
493             while (groups.hasNext()) {
494                 Group group = groups.next();
495                 group.removeRole(role);
496             }
497             Iterator<User> users = getUsers();
498             while (users.hasNext()) {
499                 User user = users.next();
500                 user.removeRole(role);
501             }
502             roles.remove(role.getRolename());
503         } finally {
504             readLock.unlock();
505         }
506     }
507
508
509     /**
510      * Remove the specified {@link User} from this user database.
511      *
512      * @param user The user to be removed
513      */

514     @Override
515     public void removeUser(User user) {
516         readLock.lock();
517         try {
518             users.remove(user.getUsername());
519         } finally {
520             readLock.unlock();
521         }
522     }
523
524
525     /**
526      * Check for permissions to save this user database to persistent storage
527      * location.
528      *
529      * @return <code>true</code> if the database is writable
530      */

531     public boolean isWriteable() {
532
533         File file = new File(pathname);
534         if (!file.isAbsolute()) {
535             file = new File(System.getProperty(Globals.CATALINA_BASE_PROP), pathname);
536         }
537         File dir = file.getParentFile();
538         return dir.exists() && dir.isDirectory() && dir.canWrite();
539     }
540
541
542     /**
543      * Save any updated information to the persistent storage location for this
544      * user database.
545      *
546      * @exception Exception if any exception is thrown during saving
547      */

548     @Override
549     public void save() throws Exception {
550
551         if (getReadonly()) {
552             log.error(sm.getString("memoryUserDatabase.readOnly"));
553             return;
554         }
555
556         if (!isWriteable()) {
557             log.warn(sm.getString("memoryUserDatabase.notPersistable"));
558             return;
559         }
560
561         // Write out contents to a temporary file
562         File fileNew = new File(pathnameNew);
563         if (!fileNew.isAbsolute()) {
564             fileNew = new File(System.getProperty(Globals.CATALINA_BASE_PROP), pathnameNew);
565         }
566
567         writeLock.lock();
568         try {
569             try (FileOutputStream fos = new FileOutputStream(fileNew);
570                     OutputStreamWriter osw = new OutputStreamWriter(fos, StandardCharsets.UTF_8);
571                     PrintWriter writer = new PrintWriter(osw)) {
572
573                 // Print the file prolog
574                 writer.println("<?xml version='1.0' encoding='utf-8'?>");
575                 writer.println("<tomcat-users xmlns=\"http://tomcat.apache.org/xml\"");
576                 writer.print("              ");
577                 writer.println("xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"");
578                 writer.print("              ");
579                 writer.println("xsi:schemaLocation=\"http://tomcat.apache.org/xml tomcat-users.xsd\"");
580                 writer.println("              version=\"1.0\">");
581
582                 // Print entries for each defined role, group, and user
583                 Iterator<?> values = null;
584                 values = getRoles();
585                 while (values.hasNext()) {
586                     writer.print("  ");
587                     writer.println(values.next());
588                 }
589                 values = getGroups();
590                 while (values.hasNext()) {
591                     writer.print("  ");
592                     writer.println(values.next());
593                 }
594                 values = getUsers();
595                 while (values.hasNext()) {
596                     writer.print("  ");
597                     writer.println(((MemoryUser) values.next()).toXml());
598                 }
599
600                 // Print the file epilog
601                 writer.println("</tomcat-users>");
602
603                 // Check for errors that occurred while printing
604                 if (writer.checkError()) {
605                     throw new IOException(sm.getString("memoryUserDatabase.writeException",
606                             fileNew.getAbsolutePath()));
607                 }
608             } catch (IOException e) {
609                 if (fileNew.exists() && !fileNew.delete()) {
610                     log.warn(sm.getString("memoryUserDatabase.fileDelete", fileNew));
611                 }
612                 throw e;
613             }
614             this.lastModified = fileNew.lastModified();
615         } finally {
616             writeLock.unlock();
617         }
618
619         // Perform the required renames to permanently save this file
620         File fileOld = new File(pathnameOld);
621         if (!fileOld.isAbsolute()) {
622             fileOld = new File(System.getProperty(Globals.CATALINA_BASE_PROP), pathnameOld);
623         }
624         if (fileOld.exists() && !fileOld.delete()) {
625             throw new IOException(sm.getString("memoryUserDatabase.fileDelete", fileOld));
626         }
627         File fileOrig = new File(pathname);
628         if (!fileOrig.isAbsolute()) {
629             fileOrig = new File(System.getProperty(Globals.CATALINA_BASE_PROP), pathname);
630         }
631         if (fileOrig.exists()) {
632             if (!fileOrig.renameTo(fileOld)) {
633                 throw new IOException(sm.getString("memoryUserDatabase.renameOld",
634                         fileOld.getAbsolutePath()));
635             }
636         }
637         if (!fileNew.renameTo(fileOrig)) {
638             if (fileOld.exists()) {
639                 if (!fileOld.renameTo(fileOrig)) {
640                     log.warn(sm.getString("memoryUserDatabase.restoreOrig", fileOld));
641                 }
642             }
643             throw new IOException(sm.getString("memoryUserDatabase.renameNew",
644                     fileOrig.getAbsolutePath()));
645         }
646         if (fileOld.exists() && !fileOld.delete()) {
647             throw new IOException(sm.getString("memoryUserDatabase.fileDelete", fileOld));
648         }
649     }
650
651
652     @Override
653     public void backgroundProcess() {
654         if (!watchSource) {
655             return;
656         }
657
658         URI uri = ConfigFileLoader.getSource().getURI(getPathname());
659         URLConnection uConn = null;
660         try {
661             URL url = uri.toURL();
662             uConn = url.openConnection();
663
664             if (this.lastModified != uConn.getLastModified()) {
665                 writeLock.lock();
666                 try {
667                     long detectedLastModified = uConn.getLastModified();
668                     // Last modified as a resolution of 1s. Ensure that a write
669                     // to the file is not in progress by ensuring that the last
670                     // modified time is at least 2 seconds ago.
671                     if (this.lastModified != detectedLastModified &&
672                             detectedLastModified + 2000 < System.currentTimeMillis()) {
673                         log.info(sm.getString("memoryUserDatabase.reload", id, uri));
674                         open();
675                     }
676                 } finally {
677                     writeLock.unlock();
678                 }
679             }
680         } catch (Exception ioe) {
681             log.error(sm.getString("memoryUserDatabase.reloadError", id, uri), ioe);
682         } finally {
683             if (uConn != null) {
684                 try {
685                     // Can't close a uConn directly. Have to do it like this.
686                     uConn.getInputStream().close();
687                 } catch (FileNotFoundException fnfe) {
688                     // The file doesn't exist.
689                     // This has been logged above. No need to log again.
690                     // Set the last modified time to avoid repeated log messages
691                     this.lastModified = 0;
692                 } catch (IOException ioe) {
693                     log.warn(sm.getString("memoryUserDatabase.fileClose", pathname), ioe);
694                 }
695             }
696         }
697     }
698
699
700     /**
701      * Return a String representation of this UserDatabase.
702      */

703     @Override
704     public String toString() {
705         StringBuilder sb = new StringBuilder("MemoryUserDatabase[id=");
706         sb.append(this.id);
707         sb.append(",pathname=");
708         sb.append(pathname);
709         sb.append(",groupCount=");
710         sb.append(this.groups.size());
711         sb.append(",roleCount=");
712         sb.append(this.roles.size());
713         sb.append(",userCount=");
714         sb.append(this.users.size());
715         sb.append("]");
716         return sb.toString();
717     }
718 }
719
720
721 /**
722  * Digester object creation factory for group instances.
723  */

724 class MemoryGroupCreationFactory extends AbstractObjectCreationFactory {
725
726     public MemoryGroupCreationFactory(MemoryUserDatabase database) {
727         this.database = database;
728     }
729
730
731     @Override
732     public Object createObject(Attributes attributes) {
733         String groupname = attributes.getValue("groupname");
734         if (groupname == null) {
735             groupname = attributes.getValue("name");
736         }
737         String description = attributes.getValue("description");
738         String roles = attributes.getValue("roles");
739         Group group = database.createGroup(groupname, description);
740         if (roles != null) {
741             while (roles.length() > 0) {
742                 String rolename = null;
743                 int comma = roles.indexOf(',');
744                 if (comma >= 0) {
745                     rolename = roles.substring(0, comma).trim();
746                     roles = roles.substring(comma + 1);
747                 } else {
748                     rolename = roles.trim();
749                     roles = "";
750                 }
751                 if (rolename.length() > 0) {
752                     Role role = database.findRole(rolename);
753                     if (role == null) {
754                         role = database.createRole(rolename, null);
755                     }
756                     group.addRole(role);
757                 }
758             }
759         }
760         return group;
761     }
762
763     private final MemoryUserDatabase database;
764 }
765
766
767 /**
768  * Digester object creation factory for role instances.
769  */

770 class MemoryRoleCreationFactory extends AbstractObjectCreationFactory {
771
772     public MemoryRoleCreationFactory(MemoryUserDatabase database) {
773         this.database = database;
774     }
775
776
777     @Override
778     public Object createObject(Attributes attributes) {
779         String rolename = attributes.getValue("rolename");
780         if (rolename == null) {
781             rolename = attributes.getValue("name");
782         }
783         String description = attributes.getValue("description");
784         Role role = database.createRole(rolename, description);
785         return role;
786     }
787
788     private final MemoryUserDatabase database;
789 }
790
791
792 /**
793  * Digester object creation factory for user instances.
794  */

795 class MemoryUserCreationFactory extends AbstractObjectCreationFactory {
796
797     public MemoryUserCreationFactory(MemoryUserDatabase database) {
798         this.database = database;
799     }
800
801
802     @Override
803     public Object createObject(Attributes attributes) {
804         String username = attributes.getValue("username");
805         if (username == null) {
806             username = attributes.getValue("name");
807         }
808         String password = attributes.getValue("password");
809         String fullName = attributes.getValue("fullName");
810         if (fullName == null) {
811             fullName = attributes.getValue("fullname");
812         }
813         String groups = attributes.getValue("groups");
814         String roles = attributes.getValue("roles");
815         User user = database.createUser(username, password, fullName);
816         if (groups != null) {
817             while (groups.length() > 0) {
818                 String groupname = null;
819                 int comma = groups.indexOf(',');
820                 if (comma >= 0) {
821                     groupname = groups.substring(0, comma).trim();
822                     groups = groups.substring(comma + 1);
823                 } else {
824                     groupname = groups.trim();
825                     groups = "";
826                 }
827                 if (groupname.length() > 0) {
828                     Group group = database.findGroup(groupname);
829                     if (group == null) {
830                         group = database.createGroup(groupname, null);
831                     }
832                     user.addGroup(group);
833                 }
834             }
835         }
836         if (roles != null) {
837             while (roles.length() > 0) {
838                 String rolename = null;
839                 int comma = roles.indexOf(',');
840                 if (comma >= 0) {
841                     rolename = roles.substring(0, comma).trim();
842                     roles = roles.substring(comma + 1);
843                 } else {
844                     rolename = roles.trim();
845                     roles = "";
846                 }
847                 if (rolename.length() > 0) {
848                     Role role = database.findRole(rolename);
849                     if (role == null) {
850                         role = database.createRole(rolename, null);
851                     }
852                     user.addRole(role);
853                 }
854             }
855         }
856         return user;
857     }
858
859     private final MemoryUserDatabase database;
860 }
861