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( int i = 0; i < rolesLength; i++ ) 224 { 225 s.append( " <th colspan=\"" ).append( pageActionsLength ).append( "\" title=\"" ).append( roles[i].getClass().getName() ).append( "\">" ).append( roles[i].getName() ).append( "</th>\n" ); 226 } 227 s.append( " </tr>\n" ); 228 229 // Print a column for each role 230 s.append( " <tr>\n" ); 231 for( int i = 0; i < rolesLength; i++ ) 232 { 233 for( final String pageAction : pageActions ) 234 { 235 final String action = pageAction.substring( 0, 1 ); 236 s.append( " <th title=\"" ).append( pageAction ).append( "\">" ).append( action ).append( "</th>\n" ); 237 } 238 } 239 s.append( " </tr>\n" ); 240 241 // Write page permission tests first 242 for( final String page : pages ) { 243 s.append( " <tr>\n" ); 244 s.append( " <td>PagePermission \"" ).append( wiki ).append( ":" ).append( page ).append( "\"</td>\n" ); 245 for( final Principal role : roles ) { 246 for( final String pageAction : pageActions ) { 247 final Permission permission = PermissionFactory.getPagePermission( wiki + ":" + page, pageAction ); 248 s.append( printPermissionTest( permission, role, 1 ) ); 249 } 250 } 251 s.append( " </tr>\n" ); 252 } 253 254 // Now do the group tests 255 for( final String group : groups ) { 256 s.append( " <tr>\n" ); 257 s.append( " <td>GroupPermission \"" ).append( wiki ).append( ":" ).append( group ).append( "\"</td>\n" ); 258 for( final Principal role : roles ) { 259 for( final String groupAction : groupActions ) { 260 Permission permission = null; 261 if( groupAction != null ) { 262 permission = new GroupPermission( wiki + ":" + group, groupAction ); 263 } 264 s.append( printPermissionTest( permission, role, 1 ) ); 265 } 266 } 267 s.append( " </tr>\n" ); 268 } 269 270 271 // Now check the wiki-wide permissions 272 final String[] wikiPerms = new String[] { "createGroups", "createPages", "login", "editPreferences", "editProfile" }; 273 for( final String wikiPerm : wikiPerms ) { 274 s.append( " <tr>\n" ); 275 s.append( " <td>WikiPermission \"" ).append( wiki ).append( "\",\"" ).append( wikiPerm ).append( "\"</td>\n" ); 276 for( final Principal role : roles ) { 277 final Permission permission = new WikiPermission( wiki, wikiPerm ); 278 s.append( printPermissionTest( permission, role, pageActionsLength ) ); 279 } 280 s.append( " </tr>\n" ); 281 } 282 283 // Lastly, check for AllPermission 284 s.append( " <tr>\n" ); 285 s.append( " <td>AllPermission \"" ).append( wiki ).append( "\"</td>\n" ); 286 for( final Principal role : roles ) 287 { 288 final Permission permission = new AllPermission( wiki ); 289 s.append( printPermissionTest( permission, role, pageActionsLength ) ); 290 } 291 s.append( " </tr>\n" ); 292 293 // We're done! 294 s.append( "</table>" ); 295 return s.toString(); 296 } 297 298 /** 299 * Prints a <td> HTML element with the results of a permission test. 300 * @param permission the permission to format 301 * @param principal 302 * @param cols 303 */ 304 private String printPermissionTest( final Permission permission, final Principal principal, final int cols ) { 305 final StringBuilder s = new StringBuilder(); 306 if( permission == null ) { 307 s.append( " <td colspan=\"" ).append( cols ).append( "\" align=\"center\" title=\"N/A\">" ); 308 s.append( " </td>\n" ); 309 } else { 310 final boolean allowed = verifyStaticPermission( principal, permission ); 311 s.append( " <td colspan=\"" ).append( cols ).append( "\" align=\"center\" title=\"" ); 312 s.append( allowed ? "ALLOW: " : "DENY: " ); 313 s.append( permission.getClass().getName() ); 314 s.append( " "" ); 315 s.append( permission.getName() ); 316 s.append( """ ); 317 if ( permission.getName() != null ) 318 { 319 s.append( ","" ); 320 s.append( permission.getActions() ); 321 s.append( """ ); 322 } 323 s.append( " " ); 324 s.append( principal.getClass().getName() ); 325 s.append( " "" ); 326 s.append( principal.getName() ); 327 s.append( """ ); 328 s.append( "\"" ); 329 s.append( allowed ? BG_GREEN + ">" : BG_RED + ">" ); 330 s.append( " </td>\n" ); 331 } 332 return s.toString(); 333 } 334 335 /** 336 * Formats and returns an HTML table containing the roles the web container 337 * is aware of, and whether each role maps to particular JSPs. This method 338 * throws an {@link IllegalStateException} if the authorizer is not of type 339 * {@link org.apache.wiki.auth.authorize.WebContainerAuthorizer} 340 * @return the formatted HTML table containing the result of the tests 341 * @throws WikiException if tests fail for unexpected reasons 342 */ 343 public String containerRoleTable() throws WikiException { 344 final AuthorizationManager authorizationManager = m_engine.getManager( AuthorizationManager.class ); 345 final Authorizer authorizer = authorizationManager.getAuthorizer(); 346 347 // If authorizer not WebContainerAuthorizer, print error message 348 if ( !( authorizer instanceof WebContainerAuthorizer ) ) { 349 throw new IllegalStateException( "Authorizer should be WebContainerAuthorizer" ); 350 } 351 352 // Now, print a table with JSP pages listed on the left, and 353 // an evaluation of each pages' constraints for each role 354 // we discovered 355 final StringBuilder s = new StringBuilder(); 356 final Principal[] roles = authorizer.getRoles(); 357 s.append( "<table class=\"wikitable\" border=\"1\">\n" ); 358 s.append( "<thead>\n" ); 359 s.append( " <tr>\n" ); 360 s.append( " <th rowspan=\"2\">Action</th>\n" ); 361 s.append( " <th rowspan=\"2\">Page</th>\n" ); 362 s.append( " <th colspan=\"" ).append( roles.length ).append( 1 ).append( "\">Roles</th>\n" ); 363 s.append( " </tr>\n" ); 364 s.append( " <tr>\n" ); 365 s.append( " <th>Anonymous</th>\n" ); 366 for( final Principal role : roles ) { 367 s.append( " <th>" ).append( role.getName() ).append( "</th>\n" ); 368 } 369 s.append( "</tr>\n" ); 370 s.append( "</thead>\n" ); 371 s.append( "<tbody>\n" ); 372 373 final WebContainerAuthorizer wca = (WebContainerAuthorizer) authorizer; 374 for( int i = 0; i < CONTAINER_ACTIONS.length; i++ ) { 375 final String action = CONTAINER_ACTIONS[i]; 376 final String jsp = CONTAINER_JSPS[i]; 377 378 // Print whether the page is constrained for each role 379 final boolean allowsAnonymous = !wca.isConstrained( jsp, Role.ALL ); 380 s.append( " <tr>\n" ); 381 s.append( " <td>" ).append( action ).append( "</td>\n" ); 382 s.append( " <td>" ).append( jsp ).append( "</td>\n" ); 383 s.append( " <td title=\"" ); 384 s.append( allowsAnonymous ? "ALLOW: " : "DENY: " ); 385 s.append( jsp ); 386 s.append( " Anonymous" ); 387 s.append( "\"" ); 388 s.append( allowsAnonymous ? BG_GREEN + ">" : BG_RED + ">" ); 389 s.append( " </td>\n" ); 390 for( final Principal role : roles ) 391 { 392 final boolean allowed = allowsAnonymous || wca.isConstrained( jsp, (Role)role ); 393 s.append( " <td title=\"" ); 394 s.append( allowed ? "ALLOW: " : "DENY: " ); 395 s.append( jsp ); 396 s.append( " " ); 397 s.append( role.getClass().getName() ); 398 s.append( " "" ); 399 s.append( role.getName() ); 400 s.append( """ ); 401 s.append( "\"" ); 402 s.append( allowed ? BG_GREEN + ">" : BG_RED + ">" ); 403 s.append( " </td>\n" ); 404 } 405 s.append( " </tr>\n" ); 406 } 407 408 s.append( "</tbody>\n" ); 409 s.append( "</table>\n" ); 410 return s.toString(); 411 } 412 413 /** 414 * Returns <code>true</code> if the Java security policy is configured 415 * correctly, and it verifies as valid. 416 * @return the result of the configuration check 417 */ 418 public boolean isSecurityPolicyConfigured() 419 { 420 return m_isSecurityPolicyConfigured; 421 } 422 423 /** 424 * If the active Authorizer is the WebContainerAuthorizer, returns the roles it knows about; otherwise, a zero-length array. 425 * 426 * @return the roles parsed from <code>web.xml</code>, or a zero-length array 427 * @throws WikiException if the web authorizer cannot obtain the list of roles 428 */ 429 public Principal[] webContainerRoles() throws WikiException { 430 final Authorizer authorizer = m_engine.getManager( AuthorizationManager.class ).getAuthorizer(); 431 if ( authorizer instanceof WebContainerAuthorizer ) { 432 return authorizer.getRoles(); 433 } 434 return new Principal[0]; 435 } 436 437 /** 438 * Verifies that the roles given in the security policy are reflected by the 439 * container <code>web.xml</code> file. 440 * @throws WikiException if the web authorizer cannot verify the roles 441 */ 442 void verifyPolicyAndContainerRoles() throws WikiException 443 { 444 final Authorizer authorizer = m_engine.getManager( AuthorizationManager.class ).getAuthorizer(); 445 final Principal[] containerRoles = authorizer.getRoles(); 446 boolean missing = false; 447 for( final Principal principal : m_policyPrincipals ) 448 { 449 if ( principal instanceof Role ) 450 { 451 final Role role = (Role) principal; 452 final boolean isContainerRole = ArrayUtils.contains( containerRoles, role ); 453 if ( !Role.isBuiltInRole( role ) && !isContainerRole ) 454 { 455 m_session.addMessage( ERROR_ROLES, "Role '" + role.getName() + "' is defined in security policy but not in web.xml." ); 456 missing = true; 457 } 458 } 459 } 460 if ( !missing ) 461 { 462 m_session.addMessage( INFO_ROLES, "Every non-standard role defined in the security policy was also found in web.xml." ); 463 } 464 } 465 466 /** 467 * Verifies that the group datbase was initialized properly, and that 468 * user add and delete operations work as they should. 469 */ 470 void verifyGroupDatabase() 471 { 472 final GroupManager mgr = m_engine.getManager( GroupManager.class ); 473 GroupDatabase db = null; 474 try { 475 db = m_engine.getManager( GroupManager.class ).getGroupDatabase(); 476 } catch ( final WikiSecurityException e ) { 477 m_session.addMessage( ERROR_GROUPS, "Could not retrieve GroupManager: " + e.getMessage() ); 478 } 479 480 // Check for obvious error conditions 481 if ( mgr == null || db == null ) { 482 if ( mgr == null ) { 483 m_session.addMessage( ERROR_GROUPS, "GroupManager is null; JSPWiki could not initialize it. Check the error logs." ); 484 } 485 if ( db == null ) { 486 m_session.addMessage( ERROR_GROUPS, "GroupDatabase is null; JSPWiki could not initialize it. Check the error logs." ); 487 } 488 return; 489 } 490 491 // Everything initialized OK... 492 493 // Tell user what class of database this is. 494 m_session.addMessage( INFO_GROUPS, "GroupDatabase is of type '" + db.getClass().getName() + "'. It appears to be initialized properly." ); 495 496 // Now, see how many groups we have. 497 final int oldGroupCount; 498 try { 499 final Group[] groups = db.groups(); 500 oldGroupCount = groups.length; 501 m_session.addMessage( INFO_GROUPS, "The group database contains " + oldGroupCount + " groups." ); 502 } catch( final WikiSecurityException e ) { 503 m_session.addMessage( ERROR_GROUPS, "Could not obtain a list of current groups: " + e.getMessage() ); 504 return; 505 } 506 507 // Try adding a bogus group with random name 508 final String name = "TestGroup" + System.currentTimeMillis(); 509 final Group group; 510 try { 511 // Create dummy test group 512 group = mgr.parseGroup( name, "", true ); 513 final Principal user = new WikiPrincipal( "TestUser" ); 514 group.add( user ); 515 db.save( group, new WikiPrincipal( "SecurityVerifier" ) ); 516 517 // Make sure the group saved successfully 518 if( db.groups().length == oldGroupCount ) { 519 m_session.addMessage( ERROR_GROUPS, "Could not add a test group to the database." ); 520 return; 521 } 522 m_session.addMessage( INFO_GROUPS, "The group database allows new groups to be created, as it should." ); 523 } catch( final WikiSecurityException e ) { 524 m_session.addMessage( ERROR_GROUPS, "Could not add a group to the database: " + e.getMessage() ); 525 return; 526 } 527 528 // Now delete the group; should be back to old count 529 try { 530 db.delete( group ); 531 if( db.groups().length != oldGroupCount ) { 532 m_session.addMessage( ERROR_GROUPS, "Could not delete a test group from the database." ); 533 return; 534 } 535 m_session.addMessage( INFO_GROUPS, "The group database allows groups to be deleted, as it should." ); 536 } catch( final WikiSecurityException e ) { 537 m_session.addMessage( ERROR_GROUPS, "Could not delete a test group from the database: " + e.getMessage() ); 538 return; 539 } 540 541 m_session.addMessage( INFO_GROUPS, "The group database configuration looks fine." ); 542 } 543 544 /** 545 * Verfies the JAAS configuration. The configuration is valid if value of the 546 * <code>jspwiki.properties<code> property 547 * {@value org.apache.wiki.auth.AuthenticationManager#PROP_LOGIN_MODULE} 548 * resolves to a valid class on the classpath. 549 */ 550 void verifyJaas() { 551 // Verify that the specified JAAS moduie corresponds to a class we can load successfully. 552 final String jaasClass = m_engine.getWikiProperties().getProperty( AuthenticationManager.PROP_LOGIN_MODULE ); 553 if( jaasClass == null || jaasClass.isEmpty() ) { 554 m_session.addMessage( ERROR_JAAS, "The value of the '" + AuthenticationManager.PROP_LOGIN_MODULE 555 + "' property was null or blank. This is a fatal error. This value should be set to a valid LoginModule implementation " 556 + "on the classpath." ); 557 return; 558 } 559 560 // See if we can find the LoginModule on the classpath 561 Class< ? > c = null; 562 try { 563 m_session.addMessage( INFO_JAAS, 564 "The property '" + AuthenticationManager.PROP_LOGIN_MODULE + "' specified the class '" + jaasClass + ".'" ); 565 c = Class.forName( jaasClass ); 566 } catch( final ClassNotFoundException e ) { 567 m_session.addMessage( ERROR_JAAS, "We could not find the the class '" + jaasClass + "' on the " + "classpath. This is fatal error." ); 568 } 569 570 // Is the specified class actually a LoginModule? 571 if( LoginModule.class.isAssignableFrom( c ) ) { 572 m_session.addMessage( INFO_JAAS, "We found the the class '" + jaasClass + "' on the classpath, and it is a LoginModule implementation. Good!" ); 573 } else { 574 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." ); 575 } 576 } 577 578 /** 579 * Looks up a file name based on a JRE system property and returns the associated 580 * File object if it exists. This method adds messages with the topic prefix 581 * {@link #ERROR} and {@link #INFO} as appropriate, with the suffix matching the 582 * supplied property. 583 * @param property the system property to look up 584 * @return the file object, or <code>null</code> if not found 585 */ 586 File getFileFromProperty( final String property ) 587 { 588 String propertyValue = null; 589 try 590 { 591 propertyValue = System.getProperty( property ); 592 if ( propertyValue == null ) 593 { 594 m_session.addMessage( "Error." + property, "The system property '" + property + "' is null." ); 595 return null; 596 } 597 598 // 599 // It's also possible to use "==" to mark a property. We remove that 600 // here so that we can actually find the property file, then. 601 // 602 if( propertyValue.startsWith("=") ) 603 { 604 propertyValue = propertyValue.substring(1); 605 } 606 607 try 608 { 609 m_session.addMessage( "Info." + property, "The system property '" + property + "' is set to: " 610 + propertyValue + "." ); 611 612 // Prepend a file: prefix if not there already 613 if ( !propertyValue.startsWith( "file:" ) ) 614 { 615 propertyValue = "file:" + propertyValue; 616 } 617 final URL url = new URL( propertyValue ); 618 final File file = new File( url.getPath() ); 619 if ( file.exists() ) 620 { 621 m_session.addMessage( "Info." + property, "File '" + propertyValue + "' exists in the filesystem." ); 622 return file; 623 } 624 } 625 catch( final MalformedURLException e ) 626 { 627 // Swallow exception because we can't find it anyway 628 } 629 m_session.addMessage( "Error." + property, "File '" + propertyValue 630 + "' doesn't seem to exist. This might be a problem." ); 631 return null; 632 } 633 catch( final SecurityException e ) 634 { 635 m_session.addMessage( "Error." + property, "We could not read system property '" + property 636 + "'. This is probably because you are running with a security manager." ); 637 return null; 638 } 639 } 640 641 /** 642 * Verfies the Java security policy configuration. The configuration is 643 * valid if value of the local policy (at <code>WEB-INF/jspwiki.policy</code> 644 * resolves to an existing file, and the policy file contained therein 645 * represents a valid policy. 646 */ 647 @SuppressWarnings("unchecked") 648 void verifyPolicy() { 649 // Look up the policy file and set the status text. 650 final URL policyURL = m_engine.findConfigFile( AuthorizationManager.DEFAULT_POLICY ); 651 String path = policyURL.getPath(); 652 if ( path.startsWith("file:") ) { 653 path = path.substring( 5 ); 654 } 655 final File policyFile = new File( path ); 656 657 // Next, verify the policy 658 try { 659 // Get the file 660 final PolicyReader policy = new PolicyReader( policyFile ); 661 m_session.addMessage( INFO_POLICY, "The security policy '" + policy.getFile() + "' exists." ); 662 663 // See if there is a keystore that's valid 664 final KeyStore ks = policy.getKeyStore(); 665 if ( ks == null ) { 666 m_session.addMessage( WARNING_POLICY, 667 "Policy file does not have a keystore... at least not one that we can locate. If your policy file " + 668 "does not contain any 'signedBy' blocks, this is probably ok." ); 669 } else { 670 m_session.addMessage( INFO_POLICY, 671 "The security policy specifies a keystore, and we were able to locate it in the filesystem." ); 672 } 673 674 // Verify the file 675 policy.read(); 676 final List<Exception> errors = policy.getMessages(); 677 if ( errors.size() > 0 ) { 678 for( final Exception e : errors ) { 679 m_session.addMessage( ERROR_POLICY, e.getMessage() ); 680 } 681 } else { 682 m_session.addMessage( INFO_POLICY, "The security policy looks fine." ); 683 m_isSecurityPolicyConfigured = true; 684 } 685 686 // Stash the unique principals mentioned in the file, 687 // plus our standard roles. 688 final Set<Principal> principals = new LinkedHashSet<>(); 689 principals.add( Role.ALL ); 690 principals.add( Role.ANONYMOUS ); 691 principals.add( Role.ASSERTED ); 692 principals.add( Role.AUTHENTICATED ); 693 final ProtectionDomain[] domains = policy.getProtectionDomains(); 694 for ( final ProtectionDomain domain : domains ) { 695 principals.addAll(Arrays.asList(domain.getPrincipals())); 696 } 697 m_policyPrincipals = principals.toArray( new Principal[0] ); 698 } catch( final IOException e ) { 699 m_session.addMessage( ERROR_POLICY, e.getMessage() ); 700 } 701 } 702 703 /** 704 * Verifies that a particular Principal possesses a Permission, as defined 705 * in the security policy file. 706 * @param principal the principal 707 * @param permission the permission 708 * @return the result, based on consultation with the active Java security 709 * policy 710 */ 711 boolean verifyStaticPermission( final Principal principal, final Permission permission ) 712 { 713 final Subject subject = new Subject(); 714 subject.getPrincipals().add( principal ); 715 final boolean allowedByGlobalPolicy = (Boolean) 716 Subject.doAsPrivileged( subject, ( PrivilegedAction< Object > )() -> { 717 try { 718 AccessController.checkPermission( permission ); 719 return Boolean.TRUE; 720 } catch( final AccessControlException e ) { 721 return Boolean.FALSE; 722 } 723 }, null ); 724 725 if ( allowedByGlobalPolicy ) 726 { 727 return true; 728 } 729 730 // Check local policy 731 final Principal[] principals = new Principal[]{ principal }; 732 return m_engine.getManager( AuthorizationManager.class ).allowedByLocalPolicy( principals, permission ); 733 } 734 735 /** 736 * Verifies that the user datbase was initialized properly, and that 737 * user add and delete operations work as they should. 738 */ 739 void verifyUserDatabase() 740 { 741 final UserDatabase db = m_engine.getManager( UserManager.class ).getUserDatabase(); 742 743 // Check for obvious error conditions 744 if ( db == null ) 745 { 746 m_session.addMessage( ERROR_DB, "UserDatabase is null; JSPWiki could not " + 747 "initialize it. Check the error logs." ); 748 return; 749 } 750 751 if ( db instanceof DummyUserDatabase ) 752 { 753 m_session.addMessage( ERROR_DB, "UserDatabase is DummyUserDatabase; JSPWiki " + 754 "may not have been able to initialize the database you supplied in " + 755 "jspwiki.properties, or you left the 'jspwiki.userdatabase' property " + 756 "blank. Check the error logs." ); 757 } 758 759 // Tell user what class of database this is. 760 m_session.addMessage( INFO_DB, "UserDatabase is of type '" + db.getClass().getName() + 761 "'. It appears to be initialized properly." ); 762 763 // Now, see how many users we have. 764 final int oldUserCount; 765 try { 766 final Principal[] users = db.getWikiNames(); 767 oldUserCount = users.length; 768 m_session.addMessage( INFO_DB, "The user database contains " + oldUserCount + " users." ); 769 } catch( final WikiSecurityException e ) { 770 m_session.addMessage( ERROR_DB, "Could not obtain a list of current users: " + e.getMessage() ); 771 return; 772 } 773 774 // Try adding a bogus user with random name 775 final String loginName = "TestUser" + System.currentTimeMillis(); 776 try { 777 final UserProfile profile = db.newProfile(); 778 profile.setEmail( "jspwiki.tests@mailinator.com" ); 779 profile.setLoginName( loginName ); 780 profile.setFullname( "FullName" + loginName ); 781 profile.setPassword( "password" ); 782 db.save( profile ); 783 784 // Make sure the profile saved successfully 785 if( db.getWikiNames().length == oldUserCount ) { 786 m_session.addMessage( ERROR_DB, "Could not add a test user to the database." ); 787 return; 788 } 789 m_session.addMessage( INFO_DB, "The user database allows new users to be created, as it should." ); 790 } catch( final WikiSecurityException e ) { 791 m_session.addMessage( ERROR_DB, "Could not add a test user to the database: " + e.getMessage() ); 792 return; 793 } 794 795 // Now delete the profile; should be back to old count 796 try { 797 db.deleteByLoginName( loginName ); 798 if( db.getWikiNames().length != oldUserCount ) { 799 m_session.addMessage( ERROR_DB, "Could not delete a test user from the database." ); 800 return; 801 } 802 m_session.addMessage( INFO_DB, "The user database allows users to be deleted, as it should." ); 803 } catch( final WikiSecurityException e ) { 804 m_session.addMessage( ERROR_DB, "Could not delete a test user to the database: " + e.getMessage() ); 805 return; 806 } 807 808 m_session.addMessage( INFO_DB, "The user database configuration looks fine." ); 809 } 810}