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 java.io.PrintWriter;
21  import java.io.StringWriter;
22  import java.lang.reflect.Field;
23  import java.lang.reflect.Method;
24  import java.security.AllPermission;
25  import java.util.Hashtable;
26  
27  import junit.framework.Test;
28  import junit.framework.TestCase;
29  
30  import org.apache.commons.lang3.JavaVersion;
31  import org.apache.commons.lang3.SystemUtils;
32  import org.apache.commons.logging.Log;
33  import org.apache.commons.logging.LogFactory;
34  import org.apache.commons.logging.PathableClassLoader;
35  import org.apache.commons.logging.PathableTestSuite;
36  
37  /**
38   * Tests for logging with a security policy that allows JCL access to everything.
39   * <p>
40   * This test cannot run on Java 21 and up: {@code java.lang.UnsupportedOperationException: The Security Manager is deprecated and will be removed in a future release}.
41   * </p>
42   * This class has only one unit test, as we are (in part) checking behavior in the static block of the LogFactory class. As that class cannot be unloaded after
43   * being loaded into a class loader, the only workaround is to use the PathableClassLoader approach to ensure each test is run in its own class loader, and use
44   * a separate test class for each test.
45   * </p>
46   */
47  public class SecurityAllowedTestCase extends TestCase {
48  
49      /**
50       * Dummy special Hashtable, so we can tell JCL to use this instead of the standard one.
51       */
52      public static class CustomHashtable<K, V> extends Hashtable<K, V> {
53  
54          /**
55           * Generated serial version ID.
56           */
57          private static final long serialVersionUID = 8941017300059246720L;
58      }
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.addLogicalLib("commons-logging");
67          parent.addLogicalLib("testclasses");
68          parent.addLogicalLib("commons-lang3");
69  
70          final Class<?> testClass = parent.loadClass("org.apache.commons.logging.security.SecurityAllowedTestCase");
71          return new PathableTestSuite(testClass, parent);
72      }
73  
74      private SecurityManager oldSecMgr;
75  
76      @Override
77      public void setUp() {
78          // Ignore on Java 21 and up
79          // TODO Port tests to JUnit 5
80          if (SystemUtils.isJavaVersionAtLeast(JavaVersion.JAVA_21)) {
81              return;
82          }
83          // save security manager so it can be restored in tearDown
84          oldSecMgr = System.getSecurityManager();
85      }
86  
87      @Override
88      public void tearDown() {
89          // Ignore on Java 21 and up
90          // TODO Port tests to JUnit 5
91          if (SystemUtils.isJavaVersionAtLeast(JavaVersion.JAVA_21)) {
92              return;
93          }
94          // Restore, so other tests don't get stuffed up if a test
95          // sets a custom security manager.
96          // Java 21 and up: java.lang.UnsupportedOperationException: The Security Manager is deprecated and will be removed in a future release
97          System.setSecurityManager(oldSecMgr);
98      }
99  
100     /**
101      * Test what happens when JCL is run with all permissions enabled. Custom overrides should take effect.
102      */
103     public void testAllAllowed() {
104         // Ignore on Java 21 and up
105         // TODO Port tests to JUnit 5
106         if (SystemUtils.isJavaVersionAtLeast(JavaVersion.JAVA_21)) {
107             return;
108         }
109         System.setProperty(LogFactory.HASHTABLE_IMPLEMENTATION_PROPERTY, CustomHashtable.class.getName());
110         final MockSecurityManager mySecurityManager = new MockSecurityManager();
111         mySecurityManager.addPermission(new AllPermission());
112         // Java 22: java.lang.UnsupportedOperationException: The Security Manager is deprecated and will be removed in a future release
113         System.setSecurityManager(mySecurityManager);
114 
115         try {
116             // Use reflection so that we can control exactly when the static
117             // initializer for the LogFactory class is executed.
118             final Class<?> c = this.getClass().getClassLoader().loadClass("org.apache.commons.logging.LogFactory");
119             final Method m = c.getMethod("getLog", Class.class);
120             final Log log = (Log) m.invoke(null, this.getClass());
121 
122             // Check whether we had any security exceptions so far (which were
123             // caught by the code). We should not, as every secure operation
124             // should be wrapped in an AccessController. Any security exceptions
125             // indicate a path that is missing an appropriate AccessController.
126             //
127             // We don't wait until after the log.info call to get this count
128             // because java.util.logging tries to load a resource bundle, which
129             // requires permission accessClassInPackage. JCL explicitly does not
130             // wrap calls to log methods in AccessControllers because writes to
131             // a log file *should* only be permitted if the original caller is
132             // trusted to access that file.
133             final int untrustedCodeCount = mySecurityManager.getUntrustedCodeCount();
134             log.info("testing");
135 
136             // check that the default map implementation was loaded, as JCL was
137             // forbidden from reading the HASHTABLE_IMPLEMENTATION_PROPERTY property.
138             System.setSecurityManager(null);
139             final Field factoryField = c.getDeclaredField("factories");
140             factoryField.setAccessible(true);
141             final Object factoryTable = factoryField.get(null);
142             assertNotNull(factoryTable);
143             assertEquals(CustomHashtable.class.getName(), factoryTable.getClass().getName());
144 
145             // we better compare that we have no security exception during the call to log
146             // IBM JVM tries to load bundles during the invoke call, which increase the count
147             assertEquals("Untrusted code count", untrustedCodeCount, mySecurityManager.getUntrustedCodeCount());
148         } catch (final Throwable t) {
149             // Restore original security manager so output can be generated; the
150             // PrintWriter constructor tries to read the line.separator
151             // system property.
152             System.setSecurityManager(oldSecMgr);
153             final StringWriter sw = new StringWriter();
154             final PrintWriter pw = new PrintWriter(sw);
155             t.printStackTrace(pw);
156             fail("Unexpected exception:" + t.getMessage() + ":" + sw.toString());
157         }
158     }
159 }