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