1   package groovy.security;
2   
3   import groovy.lang.Binding;
4   import groovy.lang.GroovyClassLoader;
5   import groovy.lang.GroovyCodeSource;
6   import groovy.lang.Script;
7   import groovy.util.GroovyTestCase;
8   
9   import java.io.File;
10  import java.io.FileNotFoundException;
11  import java.io.PrintStream;
12  import java.security.AccessControlException;
13  import java.security.AccessController;
14  import java.security.Permission;
15  import java.security.Policy;
16  import java.security.PrivilegedAction;
17  import java.util.Enumeration;
18  
19  import junit.framework.TestCase;
20  import junit.framework.TestFailure;
21  import junit.framework.TestResult;
22  import junit.framework.TestSuite;
23  import junit.textui.ResultPrinter;
24  
25  import org.codehaus.groovy.runtime.InvokerHelper;
26  
27  /***
28   * @author Steve Goetze
29   */
30  public class SecurityTestSupport extends GroovyTestCase {
31  
32  	private static int counter = 0;
33  	private static boolean securityDisabled; 
34  	private static boolean securityAvailable;
35  	private static boolean securityChecked = false;
36  
37  	static {
38  		if (System.getProperty("groovy.security.disabled") != null) {
39  			securityAvailable = false;
40  			securityDisabled = true;
41  		} else {
42  			securityDisabled = false;
43  			String groovyLibDir = System.getProperty("groovy.lib");
44  			if (groovyLibDir == null) {
45  				//Try to find maven repository in the default user.home location
46  				groovyLibDir = System.getProperty("user.home") + "/" + ".maven/repository";
47  			}
48  			if (groovyLibDir == null) {
49  				//Try at user.dir/lib
50  				groovyLibDir = "lib";
51  			}
52  			if (new File(groovyLibDir).exists()) {
53  				securityAvailable = true;
54  				System.setProperty("groovy.lib", groovyLibDir);
55  				System.setProperty("java.security.policy", "=security/groovy.policy");
56  			} else {
57  				securityAvailable = false;
58  			}
59  		}
60  	}
61  
62  	public static boolean isSecurityAvailable() {
63  		return securityAvailable;
64  	}
65  
66  	public static boolean isSecurityDisabled() {
67  		return securityDisabled;
68  	}
69  
70  	public static void resetSecurityPolicy(String policyFileURL) {
71  		System.setProperty("java.security.policy", policyFileURL);
72  		Policy.getPolicy().refresh();
73  	}
74  	
75  	protected class SecurityTestResultPrinter extends ResultPrinter {
76  		
77  		public SecurityTestResultPrinter(PrintStream stream) {
78  			super(stream);
79  		}
80  		public void print(TestResult result) {
81  			getWriter().println("Security testing on a groovy test failed:");
82  		    printErrors(result);
83  		    printFailures(result);
84  		    printFooter(result);
85  		}
86  	}
87  
88      protected GroovyClassLoader loader = (GroovyClassLoader) AccessController.doPrivileged(new PrivilegedAction() {
89  		public Object run() {
90  			return new GroovyClassLoader(SecurityTestSupport.class.getClassLoader()); 
91  		}
92  	});
93      
94  	private SecurityManager securityManager;
95      private ClassLoader currentClassLoader;
96  
97  	public SecurityTestSupport() {
98  	}
99  	
100 	/*
101 	 * Check SecuritySupport to see if security is properly configured.  If not, fail the first
102 	 * test that runs.  All remaining tests will run, but not do any security checking.
103 	 */
104 	private boolean checkSecurity() {
105 		if (!securityChecked) {
106 			securityChecked = true;
107 			if (!isSecurityAvailable()) {
108 				fail("Security is not available - skipping security tests.  Ensure that groovy.lib is set and points to the groovy dependency jars.");
109 			}
110 		}
111 		return isSecurityAvailable();
112 	}
113 
114 	//Prepare for each security test.  First, check to see if groovy.lib can be determined via
115 	//a call to checkSecurity().  If not, fail() the first test.  Establish a security manager
116 	//and make the GroovyClassLoader the initiating class loader (ala GroovyShell) to compile AND
117 	//invoke the test scripts.  This handles cases where multiple .groovy scripts are involved in a
118 	//test case: a.groovy depends on b.groovy; a.groovy is parsed (and in the process the gcl
119 	//loads b.groovy via findClass).  Note that b.groovy is only available in the groovy class loader.
120 	//See 
121 	protected void setUp() {
122 		if (checkSecurity()) {
123 			securityManager = System.getSecurityManager();
124 			if (securityManager == null) {
125 	    		System.setSecurityManager(new SecurityManager());
126 			}
127 		}
128 		currentClassLoader = Thread.currentThread().getContextClassLoader();
129 		AccessController.doPrivileged(new PrivilegedAction() {
130             public Object run() {
131                 Thread.currentThread().setContextClassLoader(loader);
132                 return null;
133             }
134         });
135     }
136 
137 	protected void tearDown() {
138         AccessController.doPrivileged(new PrivilegedAction() {
139             public Object run() {
140                 System.setSecurityManager(securityManager);
141                 Thread.currentThread().setContextClassLoader(currentClassLoader);
142                 return null;
143             }
144         });
145 	}
146 
147     protected synchronized String generateClassName() {
148         return "testSecurity" + (++counter);
149     }
150 
151 	/*
152 	 * Execute the groovy script contained in file.  If missingPermission
153 	 * is non-null, then this invocation expects an AccessControlException with missingPermission
154 	 * as the reason.  If missingPermission is null, the script is expected to execute successfully.
155 	 */
156 	protected Class parseClass(File file) {
157 		GroovyCodeSource gcs = null;
158 		try {
159 			gcs = new GroovyCodeSource(file);
160 		} catch (FileNotFoundException fnfe) {
161 			fail(fnfe.toString());
162 		}
163 		return parseClass(gcs);
164 	}
165 
166     /*
167 	 * Parse the Groovy code contained in the GroovyCodeSource as a privileged operation (i.e. do not
168 	 * require the code source to have specific compile time permissions) and return the resulting class.
169 	 */
170 	protected Class parseClass(final GroovyCodeSource gcs) {
171 		Class clazz = null;
172 		try {
173 			clazz = loader.parseClass(gcs);
174 		} catch (Exception e) {
175 			fail(e.toString());
176 		}
177 		return clazz;
178 	}
179     
180     /*
181 	 * Parse the script contained in the GroovyCodeSource as a privileged operation (i.e. do not
182 	 * require the code source to have specific compile time permissions).  If the class produced is a
183 	 * TestCase, run the test in a suite and evaluate against the missingPermission.
184 	 * Otherwise, run the class as a groovy script and evaluate against the missingPermission.
185 	 */
186 	private void parseAndExecute(final GroovyCodeSource gcs, Permission missingPermission) {
187 		Class clazz = null;
188 		try {
189 			clazz = loader.parseClass(gcs);
190 		} catch (Exception e) {
191 			fail(e.toString());
192 		}
193 		if (TestCase.class.isAssignableFrom(clazz)) {
194 			executeTest(clazz, missingPermission);
195 		} else {
196 			executeScript(clazz, missingPermission);
197 		}
198 	}
199 
200 	protected void executeTest(Class test, Permission missingPermission) {
201 		TestSuite suite = new TestSuite();
202 		suite.addTestSuite(test);
203         TestResult result = new TestResult();
204         suite.run(result);
205         if (result.wasSuccessful()) {
206         	if (missingPermission == null) {
207         		return;
208         	} else {
209         		fail("Security test expected an AccessControlException on " + missingPermission + ", but did not receive one");
210         	}
211         } else {
212         	if (missingPermission == null) {
213         		new SecurityTestResultPrinter(System.out).print(result);
214         		fail("Security test was expected to run successfully, but failed (results on System.out)");
215         	} else {
216         		//There may be more than 1 failure:  iterate to ensure that they all match the missingPermission.
217         		boolean otherFailure = false;
218         		for (Enumeration e = result.errors(); e.hasMoreElements(); ) {
219         			TestFailure failure = (TestFailure) e.nextElement();
220         			if (failure.thrownException() instanceof AccessControlException) {
221         				AccessControlException ace = (AccessControlException) failure.thrownException();
222         				if (missingPermission.implies(ace.getPermission())) {
223         					continue;
224         				}
225         			}
226         			otherFailure = true;        			
227         		}
228         		if (otherFailure) {
229         			new SecurityTestResultPrinter(System.out).print(result);
230         			fail("Security test expected an AccessControlException on " + missingPermission + ", but  failed for other reasons (results on System.out)");
231         		}
232         	}
233         }
234     }
235 
236 	protected void executeScript(Class scriptClass, Permission missingPermission) {
237 		try {
238 			Script script = InvokerHelper.createScript(scriptClass, new Binding());
239 			script.run();
240 			//InvokerHelper.runScript(scriptClass, null);
241 		} catch (AccessControlException ace) {
242 			if (missingPermission != null && missingPermission.implies(ace.getPermission())) {
243 				return;
244 			} else {
245 				fail(ace.toString());
246 			}
247 		}
248 		if (missingPermission != null) {
249 			fail("Should catch an AccessControlException");
250 		}
251 	}
252 	
253 	/*
254 	 * Execute the groovy script contained in file.  If missingPermission
255 	 * is non-null, then this invocation expects an AccessControlException with missingPermission
256 	 * as the reason.  If missingPermission is null, the script is expected to execute successfully.
257 	 */
258 	protected void assertExecute(File file, Permission missingPermission) {
259 		if (!isSecurityAvailable()) {
260 			return;
261 		}
262 		GroovyCodeSource gcs = null;
263 		try {
264 			gcs = new GroovyCodeSource(file);
265 		} catch (FileNotFoundException fnfe) {
266 			fail(fnfe.toString());
267 		}
268 		parseAndExecute(gcs, missingPermission);
269 	}
270 	
271 	/*
272 	 * Execute the script represented by scriptStr using the supplied codebase.  If missingPermission
273 	 * is non-null, then this invocation expects an AccessControlException with missingPermission
274 	 * as the reason.  If missingPermission is null, the script is expected to execute successfully.
275 	 */
276 	protected void assertExecute(String scriptStr, String codeBase, Permission missingPermission) {
277 		if (!isSecurityAvailable()) {
278 			return;
279 		}
280 		if (codeBase == null) {
281 			codeBase = "/groovy/security/test";
282 		}
283 		parseAndExecute(new GroovyCodeSource(scriptStr, generateClassName(), codeBase), missingPermission);
284 	}
285 }