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 }