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