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