View Javadoc
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  
18  package org.apache.commons.logging.security;
19  
20  import static org.junit.Assert.assertNotEquals;
21  
22  import java.io.PrintWriter;
23  import java.io.StringWriter;
24  import java.lang.reflect.Field;
25  import java.lang.reflect.Method;
26  import java.util.Hashtable;
27  
28  import org.apache.commons.logging.Log;
29  import org.apache.commons.logging.LogFactory;
30  import org.apache.commons.logging.PathableClassLoader;
31  import org.apache.commons.logging.PathableTestSuite;
32  
33  import junit.framework.Test;
34  import junit.framework.TestCase;
35  
36  /**
37   * Tests for logging with a security policy that forbids JCL access to anything.
38   * <p>
39   * Performing tests with security permissions disabled is tricky, as building error
40   * messages on failure requires certain security permissions. If the security manager
41   * blocks these, then the test can fail without the error messages being output.
42   * <p>
43   * This class has only one unit test, as we are (in part) checking behavior in
44   * the static block of the LogFactory class. As that class cannot be unloaded after
45   * being loaded into a class loader, the only workaround is to use the
46   * PathableClassLoader approach to ensure each test is run in its own
47   * class loader, and use a separate test class for each test.
48   */
49  public class SecurityForbiddenTestCase extends TestCase {
50  
51      // Dummy special hashtable, so we can tell JCL to use this instead of
52      // the standard one.
53      public static class CustomHashtable extends Hashtable {
54  
55          /**
56           * Generated serial version ID.
57           */
58          private static final long serialVersionUID = 7224652794746236024L;
59      }
60      /**
61       * Return the tests included in this test suite.
62       */
63      public static Test suite() throws Exception {
64          final PathableClassLoader parent = new PathableClassLoader(null);
65          parent.useExplicitLoader("junit.", Test.class.getClassLoader());
66          parent.useExplicitLoader("org.junit.", Test.class.getClassLoader());
67          parent.addLogicalLib("commons-logging");
68          parent.addLogicalLib("testclasses");
69  
70          final Class testClass = parent.loadClass(
71              "org.apache.commons.logging.security.SecurityForbiddenTestCase");
72          return new PathableTestSuite(testClass, parent);
73      }
74  
75      private SecurityManager oldSecMgr;
76  
77      private ClassLoader otherClassLoader;
78  
79      /**
80       * Loads a class with the given class loader.
81       */
82      private Object loadClass(final String name, final ClassLoader classLoader) {
83          try {
84              final Class clazz = classLoader.loadClass(name);
85              return clazz.getConstructor().newInstance();
86          } catch (final Exception e) {
87              final StringWriter sw = new StringWriter();
88              final PrintWriter pw = new PrintWriter(sw);
89              e.printStackTrace(pw);
90              fail("Unexpected exception:" + e.getMessage() + ":" + sw.toString());
91          }
92          return null;
93      }
94  
95      @Override
96      public void setUp() {
97          // save security manager so it can be restored in tearDown
98          oldSecMgr = System.getSecurityManager();
99  
100         final PathableClassLoader classLoader = new PathableClassLoader(null);
101         classLoader.addLogicalLib("commons-logging");
102         classLoader.addLogicalLib("testclasses");
103 
104         otherClassLoader = classLoader;
105     }
106 
107     @Override
108     public void tearDown() {
109         // Restore, so other tests don't get stuffed up if a test
110         // sets a custom security manager.
111         System.setSecurityManager(oldSecMgr);
112     }
113 
114     /**
115      * Test what happens when JCL is run with absolutely no security
116      * privileges at all, including reading system properties. Everything
117      * should fall back to the built-in defaults.
118      */
119     public void testAllForbidden() {
120         // Ignore on Java 21
121         if (System.getProperty("java.version").startsWith("21.")) {
122             return;
123         }
124         System.setProperty(
125                 LogFactory.HASHTABLE_IMPLEMENTATION_PROPERTY,
126                 CustomHashtable.class.getName());
127         final MockSecurityManager mySecurityManager = new MockSecurityManager();
128 
129         System.setSecurityManager(mySecurityManager);
130 
131         try {
132             // Use reflection so that we can control exactly when the static
133             // initializer for the LogFactory class is executed.
134             final Class c = this.getClass().getClassLoader().loadClass(
135                     "org.apache.commons.logging.LogFactory");
136             final Method m = c.getMethod("getLog", Class.class);
137             final Log log = (Log) m.invoke(null, this.getClass());
138             log.info("testing");
139 
140             // check that the default map implementation was loaded, as JCL was
141             // forbidden from reading the HASHTABLE_IMPLEMENTATION_PROPERTY property.
142             //
143             // The default is either the java Hashtable class (java < 1.2) or the
144             // JCL WeakHashtable (java >= 1.3).
145             System.setSecurityManager(oldSecMgr);
146             final Field factoryField = c.getDeclaredField("factories");
147             factoryField.setAccessible(true);
148             final Object factoryTable = factoryField.get(null);
149             assertNotNull(factoryTable);
150             final String ftClassName = factoryTable.getClass().getName();
151             assertNotEquals("Custom hashtable unexpectedly used",
152                     CustomHashtable.class.getName(), ftClassName);
153 
154             assertEquals(0, mySecurityManager.getUntrustedCodeCount());
155         } catch (final Throwable t) {
156             // Restore original security manager so output can be generated; the
157             // PrintWriter constructor tries to read the line.separator
158             // system property.
159             System.setSecurityManager(oldSecMgr);
160             final StringWriter sw = new StringWriter();
161             final PrintWriter pw = new PrintWriter(sw);
162             t.printStackTrace(pw);
163             fail("Unexpected exception:" + t.getMessage() + ":" + sw.toString());
164         }
165     }
166 
167     /**
168      * Test what happens when JCL is run with absolutely no security
169      * privileges at all and a class loaded with a different class loader
170      * than the context class loader of the current thread tries to log something.
171      */
172     public void testContextClassLoader() {
173         // Ignore on Java 21
174         if (System.getProperty("java.version").startsWith("21.")) {
175             return;
176         }
177         System.setProperty(
178                 LogFactory.HASHTABLE_IMPLEMENTATION_PROPERTY,
179                 CustomHashtable.class.getName());
180         final MockSecurityManager mySecurityManager = new MockSecurityManager();
181 
182         System.setSecurityManager(mySecurityManager);
183 
184         try {
185             // load a dummy class with another class loader
186             // to force a SecurityException when the LogFactory calls
187             // Thread.getCurrentThread().getContextClassLoader()
188             loadClass("org.apache.commons.logging.security.DummyClass", otherClassLoader);
189 
190             System.setSecurityManager(oldSecMgr);
191             assertEquals(0, mySecurityManager.getUntrustedCodeCount());
192         } catch (final Throwable t) {
193             // Restore original security manager so output can be generated; the
194             // PrintWriter constructor tries to read the line.separator
195             // system property.
196             System.setSecurityManager(oldSecMgr);
197             final StringWriter sw = new StringWriter();
198             final PrintWriter pw = new PrintWriter(sw);
199             t.printStackTrace(pw);
200             fail("Unexpected exception:" + t.getMessage() + ":" + sw.toString());
201         }
202     }
203 }