001 /*
002 Licensed to the Apache Software Foundation (ASF) under one
003 or more contributor license agreements. See the NOTICE file
004 distributed with this work for additional information
005 regarding copyright ownership. The ASF licenses this file
006 to you under the Apache License, Version 2.0 (the
007 "License"); you may not use this file except in compliance
008 with the License. You may obtain a copy of the License at
009
010 http://www.apache.org/licenses/LICENSE-2.0
011
012 Unless required by applicable law or agreed to in writing,
013 software distributed under the License is distributed on an
014 "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 KIND, either express or implied. See the License for the
016 specific language governing permissions and limitations
017 under the License.
018 */
019 package org.apache.wiki.auth;
020
021 import java.io.File;
022 import java.io.IOException;
023 import java.net.MalformedURLException;
024 import java.net.URL;
025 import java.security.*;
026 import java.util.LinkedHashSet;
027 import java.util.List;
028 import java.util.Set;
029
030 import javax.security.auth.Subject;
031 import javax.security.auth.spi.LoginModule;
032
033 import org.apache.commons.lang.ArrayUtils;
034 import org.apache.log4j.Logger;
035 import org.apache.wiki.InternalWikiException;
036 import org.apache.wiki.WikiEngine;
037 import org.apache.wiki.WikiSession;
038 import org.apache.wiki.api.exceptions.WikiException;
039 import org.apache.wiki.auth.authorize.*;
040 import org.apache.wiki.auth.permissions.AllPermission;
041 import org.apache.wiki.auth.permissions.GroupPermission;
042 import org.apache.wiki.auth.permissions.PermissionFactory;
043 import org.apache.wiki.auth.permissions.WikiPermission;
044 import org.apache.wiki.auth.user.UserDatabase;
045 import org.apache.wiki.auth.user.UserProfile;
046 import org.freshcookies.security.policy.PolicyReader;
047 import org.jdom2.JDOMException;
048
049 /**
050 * Helper class for verifying JSPWiki's security configuration. Invoked by
051 * <code>admin/SecurityConfig.jsp</code>.
052 * @since 2.4
053 */
054 public final class SecurityVerifier
055 {
056 private static final long serialVersionUID = -3859563355089169941L;
057
058 private WikiEngine m_engine;
059
060 private boolean m_isSecurityPolicyConfigured = false;
061
062 private Principal[] m_policyPrincipals = new Principal[0];
063
064 private WikiSession m_session;
065
066 /** Message prefix for errors. */
067 public static final String ERROR = "Error.";
068
069 /** Message prefix for warnings. */
070 public static final String WARNING = "Warning.";
071
072 /** Message prefix for information messages. */
073 public static final String INFO = "Info.";
074
075 /** Message topic for policy errors. */
076 public static final String ERROR_POLICY = "Error.Policy";
077
078 /** Message topic for policy warnings. */
079 public static final String WARNING_POLICY = "Warning.Policy";
080
081 /** Message topic for policy information messages. */
082 public static final String INFO_POLICY = "Info.Policy";
083
084 /** Message topic for JAAS errors. */
085 public static final String ERROR_JAAS = "Error.Jaas";
086
087 /** Message topic for JAAS warnings. */
088 public static final String WARNING_JAAS = "Warning.Jaas";
089
090 /** Message topic for role-checking errors. */
091 public static final String ERROR_ROLES = "Error.Roles";
092
093 /** Message topic for role-checking information messages. */
094 public static final String INFO_ROLES = "Info.Roles";
095
096 /** Message topic for user database errors. */
097 public static final String ERROR_DB = "Error.UserDatabase";
098
099 /** Message topic for user database warnings. */
100 public static final String WARNING_DB = "Warning.UserDatabase";
101
102 /** Message topic for user database information messages. */
103 public static final String INFO_DB = "Info.UserDatabase";
104
105 /** Message topic for group database errors. */
106 public static final String ERROR_GROUPS = "Error.GroupDatabase";
107
108 /** Message topic for group database warnings. */
109 public static final String WARNING_GROUPS = "Warning.GroupDatabase";
110
111 /** Message topic for group database information messages. */
112 public static final String INFO_GROUPS = "Info.GroupDatabase";
113
114 /** Message topic for JAAS information messages. */
115 public static final String INFO_JAAS = "Info.Jaas";
116
117 private static final String[] CONTAINER_ACTIONS = new String[]
118 { "View pages", "Comment on existing pages",
119 "Edit pages", "Upload attachments", "Create a new group", "Rename an existing page", "Delete pages" };
120
121 private static final String[] CONTAINER_JSPS = new String[]
122 { "/Wiki.jsp", "/Comment.jsp", "/Edit.jsp",
123 "/Upload.jsp", "/NewGroup.jsp", "/Rename.jsp", "/Delete.jsp" };
124
125 private static final String BG_GREEN = "bgcolor=\"#c0ffc0\"";
126
127 private static final String BG_RED = "bgcolor=\"#ffc0c0\"";
128
129 private static final Logger LOG = Logger.getLogger( SecurityVerifier.class.getName() );
130
131 /**
132 * Constructs a new SecurityVerifier for a supplied WikiEngine and WikiSession.
133 * @param engine the wiki engine
134 * @param session the wiki session (typically, that of an administrator)
135 */
136 public SecurityVerifier( WikiEngine engine, WikiSession session )
137 {
138 super();
139 m_engine = engine;
140 m_session = session;
141 m_session.clearMessages();
142 verifyJaas();
143 verifyPolicy();
144 try
145 {
146 verifyPolicyAndContainerRoles();
147 }
148 catch ( WikiException e )
149 {
150 m_session.addMessage( ERROR_ROLES, e.getMessage() );
151 }
152 verifyGroupDatabase();
153 verifyUserDatabase();
154 }
155
156 /**
157 * Returns an array of unique Principals from the JSPWIki security policy
158 * file. This array will be zero-length if the policy file was not
159 * successfully located, or if the file did not specify any Principals in
160 * the policy.
161 * @return the array of principals
162 */
163 public Principal[] policyPrincipals()
164 {
165 return m_policyPrincipals;
166 }
167
168 /**
169 * Formats and returns an HTML table containing sample permissions and what
170 * roles are allowed to have them. This method will throw an
171 * {@link IllegalStateException} if the authorizer is not of type
172 * {@link org.apache.wiki.auth.authorize.WebContainerAuthorizer}
173 * @return the formatted HTML table containing the result of the tests
174 */
175 public String policyRoleTable()
176 {
177 Principal[] roles = m_policyPrincipals;
178 String wiki = m_engine.getApplicationName();
179
180 String[] pages = new String[]
181 { "Main", "Index", "GroupTest", "GroupAdmin" };
182 String[] pageActions = new String[]
183 { "view", "edit", "modify", "rename", "delete" };
184
185 String[] groups = new String[]
186 { "Admin", "TestGroup", "Foo" };
187 String[] groupActions = new String[]
188 { "view", "edit", null, null, "delete" };
189
190 // Calculate column widths
191 String colWidth;
192 if ( pageActions.length > 0 && roles.length > 0 )
193 {
194 colWidth = (67f / ( pageActions.length * roles.length )) + "%";
195 }
196 else
197 {
198 colWidth = "67%";
199 }
200
201 StringBuffer s = new StringBuffer();
202
203 // Write the table header
204 s.append( "<table class=\"wikitable\" border=\"1\">\n" );
205 s.append( " <colgroup span=\"1\" width=\"33%\"/>\n" );
206 s.append( " <colgroup span=\"" + pageActions.length * roles.length + "\" width=\"" + colWidth
207 + "\" align=\"center\"/>\n" );
208 s.append( " <tr>\n" );
209 s.append( " <th rowspan=\"2\" valign=\"bottom\">Permission</th>\n" );
210 for( int i = 0; i < roles.length; i++ )
211 {
212 s.append( " <th colspan=\"" + pageActions.length + "\" title=\"" + roles[i].getClass().getName() + "\">"
213 + roles[i].getName() + "</th>\n" );
214 }
215 s.append( " </tr>\n" );
216
217 // Print a column for each role
218 s.append( " <tr>\n" );
219 for( int i = 0; i < roles.length; i++ )
220 {
221 for( String pageAction : pageActions )
222 {
223 String action = pageAction.substring( 0, 1 );
224 s.append( " <th title=\"" + pageAction + "\">" + action + "</th>\n" );
225 }
226 }
227 s.append( " </tr>\n" );
228
229 // Write page permission tests first
230 for( String page : pages )
231 {
232 s.append( " <tr>\n" );
233 s.append( " <td>PagePermission \"" + wiki + ":" + page + "\"</td>\n" );
234 for( Principal role : roles )
235 {
236 for( String pageAction : pageActions )
237 {
238 Permission permission = PermissionFactory.getPagePermission( wiki + ":" + page, pageAction );
239 s.append( printPermissionTest( permission, role, 1 ) );
240 }
241 }
242 s.append( " </tr>\n" );
243 }
244
245 // Now do the group tests
246 for( String group : groups )
247 {
248 s.append( " <tr>\n" );
249 s.append( " <td>GroupPermission \"" + wiki + ":" + group + "\"</td>\n" );
250 for( Principal role : roles )
251 {
252 for( String groupAction : groupActions )
253 {
254 Permission permission = null;
255 if ( groupAction != null)
256 {
257 permission = new GroupPermission( wiki + ":" + group, groupAction );
258 }
259 s.append( printPermissionTest( permission, role, 1 ) );
260 }
261 }
262 s.append( " </tr>\n" );
263 }
264
265
266 // Now check the wiki-wide permissions
267 String[] wikiPerms = new String[]
268 { "createGroups", "createPages", "login", "editPreferences", "editProfile" };
269 for( String wikiPerm : wikiPerms )
270 {
271 s.append( " <tr>\n" );
272 s.append( " <td>WikiPermission \"" + wiki + "\",\"" + wikiPerm + "\"</td>\n" );
273 for( Principal role : roles )
274 {
275 Permission permission = new WikiPermission( wiki, wikiPerm );
276 s.append( printPermissionTest( permission, role, pageActions.length ) );
277 }
278 s.append( " </tr>\n" );
279 }
280
281 // Lastly, check for AllPermission
282 s.append( " <tr>\n" );
283 s.append( " <td>AllPermission \"" + wiki + "\"</td>\n" );
284 for( Principal role : roles )
285 {
286 Permission permission = new AllPermission( wiki );
287 s.append( printPermissionTest( permission, role, pageActions.length ) );
288 }
289 s.append( " </tr>\n" );
290
291 // We're done!
292 s.append( "</table>" );
293 return s.toString();
294 }
295
296 /**
297 * Prints a <td> HTML element with the results of a permission test.
298 * @param permission the permission to format
299 * @param principal
300 * @param cols
301 */
302 private String printPermissionTest( Permission permission, Principal principal, int cols )
303 {
304 StringBuffer s = new StringBuffer();
305 if ( permission == null )
306 {
307 s.append( " <td colspan=\"" + cols + "\" align=\"center\" title=\"N/A\">" );
308 s.append( " </td>\n" );
309 }
310 else
311 {
312 boolean allowed = verifyStaticPermission( principal, permission );
313 s.append( " <td colspan=\"" + cols + "\" align=\"center\" title=\"" );
314 s.append( allowed ? "ALLOW: " : "DENY: " );
315 s.append( permission.getClass().getName() );
316 s.append( " "" );
317 s.append( permission.getName() );
318 s.append( """ );
319 if ( permission.getName() != null )
320 {
321 s.append( ","" );
322 s.append( permission.getActions() );
323 s.append( """ );
324 }
325 s.append( " " );
326 s.append( principal.getClass().getName() );
327 s.append( " "" );
328 s.append( principal.getName() );
329 s.append( """ );
330 s.append( "\"" );
331 s.append( allowed ? BG_GREEN + ">" : BG_RED + ">" );
332 s.append( " </td>\n" );
333 }
334 return s.toString();
335 }
336
337 /**
338 * Formats and returns an HTML table containing the roles the web container
339 * is aware of, and whether each role maps to particular JSPs. This method
340 * throws an {@link IllegalStateException} if the authorizer is not of type
341 * {@link org.apache.wiki.auth.authorize.WebContainerAuthorizer}
342 * @return the formatted HTML table containing the result of the tests
343 * @throws WikiException if tests fail for unexpected reasons
344 */
345 public String containerRoleTable() throws WikiException
346 {
347
348 AuthorizationManager authorizationManager = m_engine.getAuthorizationManager();
349 Authorizer authorizer = authorizationManager.getAuthorizer();
350
351 // If authorizer not WebContainerAuthorizer, print error message
352 if ( !( authorizer instanceof WebContainerAuthorizer ) )
353 {
354 throw new IllegalStateException( "Authorizer should be WebContainerAuthorizer" );
355 }
356
357 // Now, print a table with JSP pages listed on the left, and
358 // an evaluation of each pages' constraints for each role
359 // we discovered
360 StringBuffer s = new StringBuffer();
361 Principal[] roles = authorizer.getRoles();
362 s.append( "<table class=\"wikitable\" border=\"1\">\n" );
363 s.append( "<thead>\n" );
364 s.append( " <tr>\n" );
365 s.append( " <th rowspan=\"2\">Action</th>\n" );
366 s.append( " <th rowspan=\"2\">Page</th>\n" );
367 s.append( " <th colspan=\"" + roles.length + 1 + "\">Roles</th>\n" );
368 s.append( " </tr>\n" );
369 s.append( " <tr>\n" );
370 s.append( " <th>Anonymous</th>\n" );
371 for( Principal role : roles )
372 {
373 s.append( " <th>" + role.getName() + "</th>\n" );
374 }
375 s.append( "</tr>\n" );
376 s.append( "</thead>\n" );
377 s.append( "<tbody>\n" );
378
379 try
380 {
381 WebContainerAuthorizer wca = (WebContainerAuthorizer) authorizer;
382 for( int i = 0; i < CONTAINER_ACTIONS.length; i++ )
383 {
384 String action = CONTAINER_ACTIONS[i];
385 String jsp = CONTAINER_JSPS[i];
386
387 // Print whether the page is constrained for each role
388 boolean allowsAnonymous = !wca.isConstrained( jsp, Role.ALL );
389 s.append( " <tr>\n" );
390 s.append( " <td>" + action + "</td>\n" );
391 s.append( " <td>" + jsp + "</td>\n" );
392 s.append( " <td title=\"" );
393 s.append( allowsAnonymous ? "ALLOW: " : "DENY: " );
394 s.append( jsp );
395 s.append( " Anonymous" );
396 s.append( "\"" );
397 s.append( allowsAnonymous ? BG_GREEN + ">" : BG_RED + ">" );
398 s.append( " </td>\n" );
399 for( Principal role : roles )
400 {
401 boolean allowed = allowsAnonymous || wca.isConstrained( jsp, (Role)role );
402 s.append( " <td title=\"" );
403 s.append( allowed ? "ALLOW: " : "DENY: " );
404 s.append( jsp );
405 s.append( " " );
406 s.append( role.getClass().getName() );
407 s.append( " "" );
408 s.append( role.getName() );
409 s.append( """ );
410 s.append( "\"" );
411 s.append( allowed ? BG_GREEN + ">" : BG_RED + ">" );
412 s.append( " </td>\n" );
413 }
414 s.append( " </tr>\n" );
415 }
416 }
417 catch( JDOMException e )
418 {
419 // If we couldn't evaluate constraints it means
420 // there's some sort of IO mess or parsing issue
421 LOG.error( "Malformed XML in web.xml", e );
422 throw new InternalWikiException( e.getClass().getName() + ": " + e.getMessage() );
423 }
424
425 s.append( "</tbody>\n" );
426 s.append( "</table>\n" );
427 return s.toString();
428 }
429
430 /**
431 * Returns <code>true</code> if the Java security policy is configured
432 * correctly, and it verifies as valid.
433 * @return the result of the configuration check
434 */
435 public boolean isSecurityPolicyConfigured()
436 {
437 return m_isSecurityPolicyConfigured;
438 }
439
440 /**
441 * If the active Authorizer is the WebContainerAuthorizer, returns the roles
442 * it knows about; otherwise, a zero-length array.
443 * @return the roles parsed from <code>web.xml</code>, or a zero-length array
444 * @throws WikiException if the web authorizer cannot obtain the list of roles
445 */
446 public Principal[] webContainerRoles() throws WikiException
447 {
448 Authorizer authorizer = m_engine.getAuthorizationManager().getAuthorizer();
449 if ( authorizer instanceof WebContainerAuthorizer )
450 {
451 return ( (WebContainerAuthorizer) authorizer ).getRoles();
452 }
453 return new Principal[0];
454 }
455
456 /**
457 * Verifies that the roles given in the security policy are reflected by the
458 * container <code>web.xml</code> file.
459 * @throws WikiException if the web authorizer cannot verify the roles
460 */
461 protected void verifyPolicyAndContainerRoles() throws WikiException
462 {
463 Authorizer authorizer = m_engine.getAuthorizationManager().getAuthorizer();
464 Principal[] containerRoles = authorizer.getRoles();
465 boolean missing = false;
466 for( Principal principal : m_policyPrincipals )
467 {
468 if ( principal instanceof Role )
469 {
470 Role role = (Role) principal;
471 boolean isContainerRole = ArrayUtils.contains( containerRoles, role );
472 if ( !Role.isBuiltInRole( role ) && !isContainerRole )
473 {
474 m_session.addMessage( ERROR_ROLES, "Role '" + role.getName() + "' is defined in security policy but not in web.xml." );
475 missing = true;
476 }
477 }
478 }
479 if ( !missing )
480 {
481 m_session.addMessage( INFO_ROLES, "Every non-standard role defined in the security policy was also found in web.xml." );
482 }
483 }
484
485 /**
486 * Verifies that the group datbase was initialized properly, and that
487 * user add and delete operations work as they should.
488 */
489 protected void verifyGroupDatabase()
490 {
491 GroupManager mgr = m_engine.getGroupManager();
492 GroupDatabase db = null;
493 try
494 {
495 db = m_engine.getGroupManager().getGroupDatabase();
496 }
497 catch ( WikiSecurityException e )
498 {
499 m_session.addMessage( ERROR_GROUPS, "Could not retrieve GroupManager: " + e.getMessage() );
500 }
501
502 // Check for obvious error conditions
503 if ( mgr == null || db == null )
504 {
505 if ( mgr == null )
506 {
507 m_session.addMessage( ERROR_GROUPS, "GroupManager is null; JSPWiki could not " +
508 "initialize it. Check the error logs." );
509 }
510 if ( db == null )
511 {
512 m_session.addMessage( ERROR_GROUPS, "GroupDatabase is null; JSPWiki could not " +
513 "initialize it. Check the error logs." );
514 }
515 return;
516 }
517
518 // Everything initialized OK...
519
520 // Tell user what class of database this is.
521 m_session.addMessage( INFO_GROUPS, "GroupDatabase is of type '" + db.getClass().getName() +
522 "'. It appears to be initialized properly." );
523
524 // Now, see how many groups we have.
525 int oldGroupCount = 0;
526 try
527 {
528 Group[] groups = db.groups();
529 oldGroupCount = groups.length;
530 m_session.addMessage( INFO_GROUPS, "The group database contains " + oldGroupCount + " groups." );
531 }
532 catch ( WikiSecurityException e )
533 {
534 m_session.addMessage( ERROR_GROUPS, "Could not obtain a list of current groups: " + e.getMessage() );
535 return;
536 }
537
538 // Try adding a bogus group with random name
539 String name = "TestGroup" + System.currentTimeMillis();
540 Group group = null;
541 try
542 {
543 // Create dummy test group
544 group = mgr.parseGroup( name, "", true );
545 Principal user = new WikiPrincipal( "TestUser" );
546 group.add( user );
547 db.save( group, new WikiPrincipal("SecurityVerifier") );
548
549 // Make sure the group saved successfully
550 if ( db.groups().length == oldGroupCount )
551 {
552 m_session.addMessage( ERROR_GROUPS, "Could not add a test group to the database." );
553 return;
554 }
555 m_session.addMessage( INFO_GROUPS, "The group database allows new groups to be created, as it should." );
556 }
557 catch ( WikiSecurityException e )
558 {
559 m_session.addMessage( ERROR_GROUPS, "Could not add a group to the database: " + e.getMessage() );
560 return;
561 }
562
563 // Now delete the group; should be back to old count
564 try
565 {
566 db.delete( group );
567 if ( db.groups().length != oldGroupCount )
568 {
569 m_session.addMessage( ERROR_GROUPS, "Could not delete a test group from the database." );
570 return;
571 }
572 m_session.addMessage( INFO_GROUPS, "The group database allows groups to be deleted, as it should." );
573 }
574 catch ( WikiSecurityException e )
575 {
576 m_session.addMessage( ERROR_GROUPS, "Could not delete a test group from the database: " + e.getMessage() );
577 return;
578 }
579
580 m_session.addMessage( INFO_GROUPS, "The group database configuration looks fine." );
581 }
582
583 /**
584 * Verfies the JAAS configuration. The configuration is valid if value of the
585 * <code>jspwiki.properties<code> property
586 * {@value org.apache.wiki.auth.AuthenticationManager#PROP_LOGIN_MODULE}
587 * resolves to a valid class on the classpath.
588 */
589 protected void verifyJaas()
590 {
591 // See if JAAS is on
592 AuthorizationManager authMgr = m_engine.getAuthorizationManager();
593 if ( !authMgr.isJAASAuthorized() )
594 {
595 m_session.addMessage( ERROR_JAAS, "JSPWiki's JAAS-based authentication " +
596 "and authorization system is turned off (your jspwiki.properties file " +
597 "contains the setting 'jspwiki.security = container'. This " +
598 "setting disables authorization checks and is meant for testing " +
599 "and troubleshooting only. The test results on this page will not " +
600 "be reliable as a result. You should set this to 'jaas' " +
601 "so that security works properly." );
602 }
603
604 // Verify that the specified JAAS moduie corresponds to a class we can load successfully.
605 String jaasClass = m_engine.getWikiProperties().getProperty( AuthenticationManager.PROP_LOGIN_MODULE );
606 if ( jaasClass == null || jaasClass.length() == 0 )
607 {
608 m_session.addMessage( ERROR_JAAS, "The value of the '" + AuthenticationManager.PROP_LOGIN_MODULE +
609 "' property was null or blank. This is a fatal error. This value should be set to a valid LoginModule implementation " +
610 "on the classpath." );
611 return;
612 }
613
614 // See if we can find the LoginModule on the classpath
615 Class c = null;
616 try
617 {
618 m_session.addMessage( INFO_JAAS, "The property '" + AuthenticationManager.PROP_LOGIN_MODULE +
619 "' specified the class '" + jaasClass + ".'" );
620 c = Class.forName( jaasClass );
621 }
622 catch( ClassNotFoundException e )
623 {
624 m_session.addMessage( ERROR_JAAS, "We could not find the the class '" + jaasClass + "' on the " +
625 "classpath. This is fatal error." );
626 }
627
628 // Is the specified class actually a LoginModule?
629 if ( LoginModule.class.isAssignableFrom( c ) )
630 {
631 m_session.addMessage( INFO_JAAS, "We found the the class '" + jaasClass + "' on the " +
632 "classpath, and it is a LoginModule implementation. Good!" );
633 }
634 else
635 {
636 m_session.addMessage( ERROR_JAAS, "We found the the class '" + jaasClass + "' on the " +
637 "classpath, but it does not seem to be LoginModule implementation! This is fatal error." );
638 }
639 }
640
641 /**
642 * Looks up a file name based on a JRE system property and returns the associated
643 * File object if it exists. This method adds messages with the topic prefix
644 * {@link #ERROR} and {@link #INFO} as appropriate, with the suffix matching the
645 * supplied property.
646 * @param property the system property to look up
647 * @return the file object, or <code>null</code> if not found
648 */
649 protected File getFileFromProperty( String property )
650 {
651 String propertyValue = null;
652 try
653 {
654 propertyValue = System.getProperty( property );
655 if ( propertyValue == null )
656 {
657 m_session.addMessage( "Error." + property, "The system property '" + property + "' is null." );
658 return null;
659 }
660
661 //
662 // It's also possible to use "==" to mark a property. We remove that
663 // here so that we can actually find the property file, then.
664 //
665 if( propertyValue.startsWith("=") )
666 {
667 propertyValue = propertyValue.substring(1);
668 }
669
670 try
671 {
672 m_session.addMessage( "Info." + property, "The system property '" + property + "' is set to: "
673 + propertyValue + "." );
674
675 // Prepend a file: prefix if not there already
676 if ( !propertyValue.startsWith( "file:" ) )
677 {
678 propertyValue = "file:" + propertyValue;
679 }
680 URL url = new URL( propertyValue );
681 File file = new File( url.getPath() );
682 if ( file.exists() )
683 {
684 m_session.addMessage( "Info." + property, "File '" + propertyValue + "' exists in the filesystem." );
685 return file;
686 }
687 }
688 catch( MalformedURLException e )
689 {
690 // Swallow exception because we can't find it anyway
691 }
692 m_session.addMessage( "Error." + property, "File '" + propertyValue
693 + "' doesn't seem to exist. This might be a problem." );
694 return null;
695 }
696 catch( SecurityException e )
697 {
698 m_session.addMessage( "Error." + property, "We could not read system property '" + property
699 + "'. This is probably because you are running with a security manager." );
700 return null;
701 }
702 }
703
704 /**
705 * Verfies the Java security policy configuration. The configuration is
706 * valid if value of the local policy (at <code>WEB-INF/jspwiki.policy</code>
707 * resolves to an existing file, and the policy file contained therein
708 * represents a valid policy.
709 */
710 @SuppressWarnings("unchecked")
711 protected void verifyPolicy()
712 {
713 // Look up the policy file and set the status text.
714 URL policyURL = AuthenticationManager.findConfigFile( m_engine, AuthorizationManager.DEFAULT_POLICY );
715 String path = policyURL.getPath();
716 if ( path.startsWith("file:") )
717 {
718 path = path.substring( 5 );
719 }
720 File policyFile = new File( path );
721
722 // Next, verify the policy
723 try
724 {
725 // Get the file
726 PolicyReader policy = new PolicyReader( policyFile );
727 m_session.addMessage( INFO_POLICY, "The security policy '" + policy.getFile() + "' exists." );
728
729 // See if there is a keystore that's valid
730 KeyStore ks = policy.getKeyStore();
731 if ( ks == null )
732 {
733 m_session.addMessage( WARNING_POLICY,
734 "Policy file does not have a keystore... at least not one that we can locate. If your policy file " +
735 "does not contain any 'signedBy' blocks, this is probably ok." );
736 }
737 else
738 {
739 m_session.addMessage( INFO_POLICY,
740 "The security policy specifies a keystore, and we were able to locate it in the filesystem." );
741 }
742
743 // Verify the file
744 policy.read();
745 List<Exception> errors = policy.getMessages();
746 if ( errors.size() > 0 )
747 {
748 for( Exception e : errors )
749 {
750 m_session.addMessage( ERROR_POLICY, e.getMessage() );
751 }
752 }
753 else
754 {
755 m_session.addMessage( INFO_POLICY, "The security policy looks fine." );
756 m_isSecurityPolicyConfigured = true;
757 }
758
759 // Stash the unique principals mentioned in the file,
760 // plus our standard roles.
761 Set<Principal> principals = new LinkedHashSet<Principal>();
762 principals.add( Role.ALL );
763 principals.add( Role.ANONYMOUS );
764 principals.add( Role.ASSERTED );
765 principals.add( Role.AUTHENTICATED );
766 ProtectionDomain[] domains = policy.getProtectionDomains();
767 for ( ProtectionDomain domain : domains )
768 {
769 for( Principal principal : domain.getPrincipals() )
770 {
771 principals.add( principal );
772 }
773 }
774 m_policyPrincipals = principals.toArray( new Principal[principals.size()] );
775 }
776 catch( IOException e )
777 {
778 m_session.addMessage( ERROR_POLICY, e.getMessage() );
779 }
780 }
781
782 /**
783 * Verifies that a particular Principal possesses a Permission, as defined
784 * in the security policy file.
785 * @param principal the principal
786 * @param permission the permission
787 * @return the result, based on consultation with the active Java security
788 * policy
789 */
790 protected boolean verifyStaticPermission( Principal principal, final Permission permission )
791 {
792 Subject subject = new Subject();
793 subject.getPrincipals().add( principal );
794 boolean allowedByGlobalPolicy = ((Boolean)
795 Subject.doAsPrivileged( subject, new PrivilegedAction<Object>()
796 {
797 public Object run()
798 {
799 try
800 {
801 AccessController.checkPermission( permission );
802 return Boolean.TRUE;
803 }
804 catch ( AccessControlException e )
805 {
806 return Boolean.FALSE;
807 }
808 }
809 }, null )).booleanValue();
810
811 if ( allowedByGlobalPolicy )
812 {
813 return true;
814 }
815
816 // Check local policy
817 Principal[] principals = new Principal[]{ principal };
818 return m_engine.getAuthorizationManager().allowedByLocalPolicy( principals, permission );
819 }
820
821 /**
822 * Verifies that the user datbase was initialized properly, and that
823 * user add and delete operations work as they should.
824 */
825 protected void verifyUserDatabase()
826 {
827 UserDatabase db = m_engine.getUserManager().getUserDatabase();
828
829 // Check for obvious error conditions
830 if ( db == null )
831 {
832 m_session.addMessage( ERROR_DB, "UserDatabase is null; JSPWiki could not " +
833 "initialize it. Check the error logs." );
834 return;
835 }
836
837 if ( db instanceof UserManager.DummyUserDatabase )
838 {
839 m_session.addMessage( ERROR_DB, "UserDatabase is DummyUserDatabase; JSPWiki " +
840 "may not have been able to initialize the database you supplied in " +
841 "jspwiki.properties, or you left the 'jspwiki.userdatabase' property " +
842 "blank. Check the error logs." );
843 }
844
845 // Tell user what class of database this is.
846 m_session.addMessage( INFO_DB, "UserDatabase is of type '" + db.getClass().getName() +
847 "'. It appears to be initialized properly." );
848
849 // Now, see how many users we have.
850 int oldUserCount = 0;
851 try
852 {
853 Principal[] users = db.getWikiNames();
854 oldUserCount = users.length;
855 m_session.addMessage( INFO_DB, "The user database contains " + oldUserCount + " users." );
856 }
857 catch ( WikiSecurityException e )
858 {
859 m_session.addMessage( ERROR_DB, "Could not obtain a list of current users: " + e.getMessage() );
860 return;
861 }
862
863 // Try adding a bogus user with random name
864 String loginName = "TestUser" + System.currentTimeMillis();
865 try
866 {
867 UserProfile profile = db.newProfile();
868 profile.setEmail( "jspwiki.tests@mailinator.com" );
869 profile.setLoginName( loginName );
870 profile.setFullname( "FullName"+loginName );
871 profile.setPassword( "password" );
872 db.save(profile);
873
874 // Make sure the profile saved successfully
875 if ( db.getWikiNames().length == oldUserCount )
876 {
877 m_session.addMessage( ERROR_DB, "Could not add a test user to the database." );
878 return;
879 }
880 m_session.addMessage( INFO_DB, "The user database allows new users to be created, as it should." );
881 }
882 catch ( WikiSecurityException e )
883 {
884 m_session.addMessage( ERROR_DB, "Could not add a test user to the database: " + e.getMessage() );
885 return;
886 }
887
888 // Now delete the profile; should be back to old count
889 try
890 {
891 db.deleteByLoginName( loginName );
892 if ( db.getWikiNames().length != oldUserCount )
893 {
894 m_session.addMessage( ERROR_DB, "Could not delete a test user from the database." );
895 return;
896 }
897 m_session.addMessage( INFO_DB, "The user database allows users to be deleted, as it should." );
898 }
899 catch ( WikiSecurityException e )
900 {
901 m_session.addMessage( ERROR_DB, "Could not delete a test user to the database: " + e.getMessage() );
902 return;
903 }
904
905 m_session.addMessage( INFO_DB, "The user database configuration looks fine." );
906 }
907 }