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( pageActionsLength > 0 && 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 WebContainerAuthorizer ) ) { 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 final WebContainerAuthorizer wca = (WebContainerAuthorizer) authorizer; 373 for( int i = 0; i < CONTAINER_ACTIONS.length; i++ ) { 374 final String action = CONTAINER_ACTIONS[i]; 375 final String jsp = CONTAINER_JSPS[i]; 376 377 // Print whether the page is constrained for each role 378 final boolean allowsAnonymous = !wca.isConstrained( jsp, Role.ALL ); 379 s.append( " <tr>\n" ); 380 s.append( " <td>" ).append( action ).append( "</td>\n" ); 381 s.append( " <td>" ).append( jsp ).append( "</td>\n" ); 382 s.append( " <td title=\"" ); 383 s.append( allowsAnonymous ? "ALLOW: " : "DENY: " ); 384 s.append( jsp ); 385 s.append( " Anonymous" ); 386 s.append( "\"" ); 387 s.append( allowsAnonymous ? BG_GREEN + ">" : BG_RED + ">" ); 388 s.append( " </td>\n" ); 389 for( final Principal role : roles ) 390 { 391 final boolean allowed = allowsAnonymous || wca.isConstrained( jsp, (Role)role ); 392 s.append( " <td title=\"" ); 393 s.append( allowed ? "ALLOW: " : "DENY: " ); 394 s.append( jsp ); 395 s.append( " " ); 396 s.append( role.getClass().getName() ); 397 s.append( " "" ); 398 s.append( role.getName() ); 399 s.append( """ ); 400 s.append( "\"" ); 401 s.append( allowed ? BG_GREEN + ">" : BG_RED + ">" ); 402 s.append( " </td>\n" ); 403 } 404 s.append( " </tr>\n" ); 405 } 406 407 s.append( "</tbody>\n" ); 408 s.append( "</table>\n" ); 409 return s.toString(); 410 } 411 412 /** 413 * Returns <code>true</code> if the Java security policy is configured 414 * correctly, and it verifies as valid. 415 * @return the result of the configuration check 416 */ 417 public boolean isSecurityPolicyConfigured() 418 { 419 return m_isSecurityPolicyConfigured; 420 } 421 422 /** 423 * If the active Authorizer is the WebContainerAuthorizer, returns the roles it knows about; otherwise, a zero-length array. 424 * 425 * @return the roles parsed from <code>web.xml</code>, or a zero-length array 426 * @throws WikiException if the web authorizer cannot obtain the list of roles 427 */ 428 public Principal[] webContainerRoles() throws WikiException { 429 final Authorizer authorizer = m_engine.getManager( AuthorizationManager.class ).getAuthorizer(); 430 if ( authorizer instanceof WebContainerAuthorizer ) { 431 return authorizer.getRoles(); 432 } 433 return new Principal[0]; 434 } 435 436 /** 437 * Verifies that the roles given in the security policy are reflected by the 438 * container <code>web.xml</code> file. 439 * @throws WikiException if the web authorizer cannot verify the roles 440 */ 441 void verifyPolicyAndContainerRoles() throws WikiException 442 { 443 final Authorizer authorizer = m_engine.getManager( AuthorizationManager.class ).getAuthorizer(); 444 final Principal[] containerRoles = authorizer.getRoles(); 445 boolean missing = false; 446 for( final Principal principal : m_policyPrincipals ) 447 { 448 if ( principal instanceof Role ) 449 { 450 final Role role = (Role) principal; 451 final boolean isContainerRole = ArrayUtils.contains( containerRoles, role ); 452 if ( !Role.isBuiltInRole( role ) && !isContainerRole ) 453 { 454 m_session.addMessage( ERROR_ROLES, "Role '" + role.getName() + "' is defined in security policy but not in web.xml." ); 455 missing = true; 456 } 457 } 458 } 459 if ( !missing ) 460 { 461 m_session.addMessage( INFO_ROLES, "Every non-standard role defined in the security policy was also found in web.xml." ); 462 } 463 } 464 465 /** 466 * Verifies that the group datbase was initialized properly, and that 467 * user add and delete operations work as they should. 468 */ 469 void verifyGroupDatabase() 470 { 471 final GroupManager mgr = m_engine.getManager( GroupManager.class ); 472 GroupDatabase db = null; 473 try { 474 db = m_engine.getManager( GroupManager.class ).getGroupDatabase(); 475 } catch ( final WikiSecurityException e ) { 476 m_session.addMessage( ERROR_GROUPS, "Could not retrieve GroupManager: " + e.getMessage() ); 477 } 478 479 // Check for obvious error conditions 480 if ( mgr == null || db == null ) { 481 if ( mgr == null ) { 482 m_session.addMessage( ERROR_GROUPS, "GroupManager is null; JSPWiki could not initialize it. Check the error logs." ); 483 } 484 if ( db == null ) { 485 m_session.addMessage( ERROR_GROUPS, "GroupDatabase is null; JSPWiki could not initialize it. Check the error logs." ); 486 } 487 return; 488 } 489 490 // Everything initialized OK... 491 492 // Tell user what class of database this is. 493 m_session.addMessage( INFO_GROUPS, "GroupDatabase is of type '" + db.getClass().getName() + "'. It appears to be initialized properly." ); 494 495 // Now, see how many groups we have. 496 final int oldGroupCount; 497 try { 498 final Group[] groups = db.groups(); 499 oldGroupCount = groups.length; 500 m_session.addMessage( INFO_GROUPS, "The group database contains " + oldGroupCount + " groups." ); 501 } catch( final WikiSecurityException e ) { 502 m_session.addMessage( ERROR_GROUPS, "Could not obtain a list of current groups: " + e.getMessage() ); 503 return; 504 } 505 506 // Try adding a bogus group with random name 507 final String name = "TestGroup" + System.currentTimeMillis(); 508 final Group group; 509 try { 510 // Create dummy test group 511 group = mgr.parseGroup( name, "", true ); 512 final Principal user = new WikiPrincipal( "TestUser" ); 513 group.add( user ); 514 db.save( group, new WikiPrincipal( "SecurityVerifier" ) ); 515 516 // Make sure the group saved successfully 517 if( db.groups().length == oldGroupCount ) { 518 m_session.addMessage( ERROR_GROUPS, "Could not add a test group to the database." ); 519 return; 520 } 521 m_session.addMessage( INFO_GROUPS, "The group database allows new groups to be created, as it should." ); 522 } catch( final WikiSecurityException e ) { 523 m_session.addMessage( ERROR_GROUPS, "Could not add a group to the database: " + e.getMessage() ); 524 return; 525 } 526 527 // Now delete the group; should be back to old count 528 try { 529 db.delete( group ); 530 if( db.groups().length != oldGroupCount ) { 531 m_session.addMessage( ERROR_GROUPS, "Could not delete a test group from the database." ); 532 return; 533 } 534 m_session.addMessage( INFO_GROUPS, "The group database allows groups to be deleted, as it should." ); 535 } catch( final WikiSecurityException e ) { 536 m_session.addMessage( ERROR_GROUPS, "Could not delete a test group from the database: " + e.getMessage() ); 537 return; 538 } 539 540 m_session.addMessage( INFO_GROUPS, "The group database configuration looks fine." ); 541 } 542 543 /** 544 * Verfies the JAAS configuration. The configuration is valid if value of the 545 * <code>jspwiki.properties<code> property 546 * {@value org.apache.wiki.auth.AuthenticationManager#PROP_LOGIN_MODULE} 547 * resolves to a valid class on the classpath. 548 */ 549 void verifyJaas() { 550 // Verify that the specified JAAS moduie corresponds to a class we can load successfully. 551 final String jaasClass = m_engine.getWikiProperties().getProperty( AuthenticationManager.PROP_LOGIN_MODULE ); 552 if( jaasClass == null || jaasClass.isEmpty() ) { 553 m_session.addMessage( ERROR_JAAS, "The value of the '" + AuthenticationManager.PROP_LOGIN_MODULE 554 + "' property was null or blank. This is a fatal error. This value should be set to a valid LoginModule implementation " 555 + "on the classpath." ); 556 return; 557 } 558 559 // See if we can find the LoginModule on the classpath 560 Class< ? > c = null; 561 try { 562 m_session.addMessage( INFO_JAAS, 563 "The property '" + AuthenticationManager.PROP_LOGIN_MODULE + "' specified the class '" + jaasClass + ".'" ); 564 c = Class.forName( jaasClass ); 565 } catch( final ClassNotFoundException e ) { 566 m_session.addMessage( ERROR_JAAS, "We could not find the the class '" + jaasClass + "' on the " + "classpath. This is fatal error." ); 567 } 568 569 // Is the specified class actually a LoginModule? 570 if( LoginModule.class.isAssignableFrom( c ) ) { 571 m_session.addMessage( INFO_JAAS, "We found the the class '" + jaasClass + "' on the classpath, and it is a LoginModule implementation. Good!" ); 572 } else { 573 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." ); 574 } 575 } 576 577 /** 578 * Looks up a file name based on a JRE system property and returns the associated 579 * File object if it exists. This method adds messages with the topic prefix 580 * {@link #ERROR} and {@link #INFO} as appropriate, with the suffix matching the 581 * supplied property. 582 * @param property the system property to look up 583 * @return the file object, or <code>null</code> if not found 584 */ 585 File getFileFromProperty( final String property ) 586 { 587 String propertyValue; 588 try 589 { 590 propertyValue = System.getProperty( property ); 591 if ( propertyValue == null ) 592 { 593 m_session.addMessage( "Error." + property, "The system property '" + property + "' is null." ); 594 return null; 595 } 596 597 // 598 // It's also possible to use "==" to mark a property. We remove that 599 // here so that we can actually find the property file, then. 600 // 601 if( propertyValue.startsWith("=") ) 602 { 603 propertyValue = propertyValue.substring(1); 604 } 605 606 try 607 { 608 m_session.addMessage( "Info." + property, "The system property '" + property + "' is set to: " 609 + propertyValue + "." ); 610 611 // Prepend a file: prefix if not there already 612 if ( !propertyValue.startsWith( "file:" ) ) 613 { 614 propertyValue = "file:" + propertyValue; 615 } 616 final URL url = new URL( propertyValue ); 617 final File file = new File( url.getPath() ); 618 if ( file.exists() ) 619 { 620 m_session.addMessage( "Info." + property, "File '" + propertyValue + "' exists in the filesystem." ); 621 return file; 622 } 623 } 624 catch( final MalformedURLException e ) 625 { 626 // Swallow exception because we can't find it anyway 627 } 628 m_session.addMessage( "Error." + property, "File '" + propertyValue 629 + "' doesn't seem to exist. This might be a problem." ); 630 return null; 631 } 632 catch( final SecurityException e ) 633 { 634 m_session.addMessage( "Error." + property, "We could not read system property '" + property 635 + "'. This is probably because you are running with a security manager." ); 636 return null; 637 } 638 } 639 640 /** 641 * Verfies the Java security policy configuration. The configuration is 642 * valid if value of the local policy (at <code>WEB-INF/jspwiki.policy</code> 643 * resolves to an existing file, and the policy file contained therein 644 * represents a valid policy. 645 */ 646 @SuppressWarnings("unchecked") 647 void verifyPolicy() { 648 // Look up the policy file and set the status text. 649 final URL policyURL = m_engine.findConfigFile( AuthorizationManager.DEFAULT_POLICY ); 650 String path = policyURL.getPath(); 651 if ( path.startsWith("file:") ) { 652 path = path.substring( 5 ); 653 } 654 final File policyFile = new File( path ); 655 656 // Next, verify the policy 657 try { 658 // Get the file 659 final PolicyReader policy = new PolicyReader( policyFile ); 660 m_session.addMessage( INFO_POLICY, "The security policy '" + policy.getFile() + "' exists." ); 661 662 // See if there is a keystore that's valid 663 final KeyStore ks = policy.getKeyStore(); 664 if ( ks == null ) { 665 m_session.addMessage( WARNING_POLICY, 666 "Policy file does not have a keystore... at least not one that we can locate. If your policy file " + 667 "does not contain any 'signedBy' blocks, this is probably ok." ); 668 } else { 669 m_session.addMessage( INFO_POLICY, 670 "The security policy specifies a keystore, and we were able to locate it in the filesystem." ); 671 } 672 673 // Verify the file 674 policy.read(); 675 final List<Exception> errors = policy.getMessages(); 676 if ( errors.size() > 0 ) { 677 for( final Exception e : errors ) { 678 m_session.addMessage( ERROR_POLICY, e.getMessage() ); 679 } 680 } else { 681 m_session.addMessage( INFO_POLICY, "The security policy looks fine." ); 682 m_isSecurityPolicyConfigured = true; 683 } 684 685 // Stash the unique principals mentioned in the file, 686 // plus our standard roles. 687 final Set<Principal> principals = new LinkedHashSet<>(); 688 principals.add( Role.ALL ); 689 principals.add( Role.ANONYMOUS ); 690 principals.add( Role.ASSERTED ); 691 principals.add( Role.AUTHENTICATED ); 692 final ProtectionDomain[] domains = policy.getProtectionDomains(); 693 for ( final ProtectionDomain domain : domains ) { 694 principals.addAll(Arrays.asList(domain.getPrincipals())); 695 } 696 m_policyPrincipals = principals.toArray( new Principal[0] ); 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 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 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}