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; 020 021import org.apache.commons.lang3.StringUtils; 022import org.apache.log4j.Logger; 023import org.apache.wiki.api.core.Engine; 024import org.apache.wiki.api.core.Session; 025import org.apache.wiki.auth.AuthenticationManager; 026import org.apache.wiki.auth.GroupPrincipal; 027import org.apache.wiki.auth.NoSuchPrincipalException; 028import org.apache.wiki.auth.SessionMonitor; 029import org.apache.wiki.auth.UserManager; 030import org.apache.wiki.auth.WikiPrincipal; 031import org.apache.wiki.auth.authorize.Group; 032import org.apache.wiki.auth.authorize.GroupManager; 033import org.apache.wiki.auth.authorize.Role; 034import org.apache.wiki.auth.user.UserDatabase; 035import org.apache.wiki.auth.user.UserProfile; 036import org.apache.wiki.event.WikiEvent; 037import org.apache.wiki.event.WikiSecurityEvent; 038import org.apache.wiki.util.HttpUtil; 039 040import javax.security.auth.Subject; 041import javax.servlet.http.HttpServletRequest; 042import javax.servlet.http.HttpSession; 043import java.security.Principal; 044import java.util.ArrayList; 045import java.util.Arrays; 046import java.util.HashSet; 047import java.util.LinkedHashSet; 048import java.util.Locale; 049import java.util.Map; 050import java.util.Set; 051import java.util.concurrent.ConcurrentHashMap; 052 053 054/** 055 * <p>Default implementation for {@link Session}.</p> 056 * <p>In addition to methods for examining individual <code>WikiSession</code> objects, this class also contains a number of static 057 * methods for managing WikiSessions for an entire wiki. These methods allow callers to find, query and remove WikiSession objects, and 058 * to obtain a list of the current wiki session users.</p> 059 */ 060public final class WikiSession implements Session { 061 062 private static final Logger log = Logger.getLogger( WikiSession.class ); 063 064 private static final String ALL = "*"; 065 066 private static ThreadLocal< Session > c_guestSession = new ThreadLocal<>(); 067 068 private final Subject m_subject = new Subject(); 069 070 private final Map< String, Set< String > > m_messages = new ConcurrentHashMap<>(); 071 072 /** The Engine that created this session. */ 073 private Engine m_engine = null; 074 075 private String m_status = ANONYMOUS; 076 077 private Principal m_userPrincipal = WikiPrincipal.GUEST; 078 079 private Principal m_loginPrincipal = WikiPrincipal.GUEST; 080 081 private Locale m_cachedLocale = Locale.getDefault(); 082 083 /** 084 * Returns <code>true</code> if one of this WikiSession's user Principals can be shown to belong to a particular wiki group. If 085 * the user is not authenticated, this method will always return <code>false</code>. 086 * 087 * @param group the group to test 088 * @return the result 089 */ 090 protected boolean isInGroup( final Group group ) { 091 for( final Principal principal : getPrincipals() ) { 092 if( isAuthenticated() && group.isMember( principal ) ) { 093 return true; 094 } 095 } 096 return false; 097 } 098 099 /** 100 * Private constructor to prevent WikiSession from being instantiated directly. 101 */ 102 private WikiSession() { 103 } 104 105 /** {@inheritDoc} */ 106 @Override 107 public boolean isAsserted() { 108 return m_subject.getPrincipals().contains( Role.ASSERTED ); 109 } 110 111 /** {@inheritDoc} */ 112 @Override 113 public boolean isAuthenticated() { 114 // If Role.AUTHENTICATED is in principals set, always return true. 115 if ( m_subject.getPrincipals().contains( Role.AUTHENTICATED ) ) { 116 return true; 117 } 118 119 // With non-JSPWiki LoginModules, the role may not be there, so we need to add it if the user really is authenticated. 120 if ( !isAnonymous() && !isAsserted() ) { 121 m_subject.getPrincipals().add( Role.AUTHENTICATED ); 122 return true; 123 } 124 125 return false; 126 } 127 128 /** {@inheritDoc} */ 129 @Override 130 public boolean isAnonymous() { 131 final Set< Principal > principals = m_subject.getPrincipals(); 132 return principals.contains( Role.ANONYMOUS ) || 133 principals.contains( WikiPrincipal.GUEST ) || 134 HttpUtil.isIPV4Address( getUserPrincipal().getName() ); 135 } 136 137 /** {@inheritDoc} */ 138 @Override 139 public Principal getLoginPrincipal() { 140 return m_loginPrincipal; 141 } 142 143 /** {@inheritDoc} */ 144 @Override 145 public Principal getUserPrincipal() { 146 return m_userPrincipal; 147 } 148 149 /** {@inheritDoc} */ 150 @Override 151 public Locale getLocale() { 152 return m_cachedLocale; 153 } 154 155 /** {@inheritDoc} */ 156 @Override 157 public void addMessage( final String message ) { 158 addMessage( ALL, message ); 159 } 160 161 /** {@inheritDoc} */ 162 @Override 163 public void addMessage( final String topic, final String message ) { 164 if ( topic == null ) { 165 throw new IllegalArgumentException( "addMessage: topic cannot be null." ); 166 } 167 final Set< String > messages = m_messages.computeIfAbsent( topic, k -> new LinkedHashSet<>() ); 168 messages.add( StringUtils.defaultString( message ) ); 169 } 170 171 /** {@inheritDoc} */ 172 @Override 173 public void clearMessages() { 174 m_messages.clear(); 175 } 176 177 /** {@inheritDoc} */ 178 @Override 179 public void clearMessages( final String topic ) { 180 final Set< String > messages = m_messages.get( topic ); 181 if ( messages != null ) { 182 m_messages.clear(); 183 } 184 } 185 186 /** {@inheritDoc} */ 187 @Override 188 public String[] getMessages() { 189 return getMessages( ALL ); 190 } 191 192 /** {@inheritDoc} */ 193 @Override 194 public String[] getMessages( final String topic ) { 195 final Set< String > messages = m_messages.get( topic ); 196 if( messages == null || messages.size() == 0 ) { 197 return new String[ 0 ]; 198 } 199 return messages.toArray( new String[ messages.size() ] ); 200 } 201 202 /** {@inheritDoc} */ 203 @Override 204 public Principal[] getPrincipals() { 205 final ArrayList< Principal > principals = new ArrayList<>(); 206 207 // Take the first non Role as the main Principal 208 for( final Principal principal : m_subject.getPrincipals() ) { 209 if ( AuthenticationManager.isUserPrincipal( principal ) ) { 210 principals.add( principal ); 211 } 212 } 213 214 return principals.toArray( new Principal[ principals.size() ] ); 215 } 216 217 /** {@inheritDoc} */ 218 @Override 219 public Principal[] getRoles() { 220 final Set< Principal > roles = new HashSet<>(); 221 222 // Add all of the Roles possessed by the Subject directly 223 roles.addAll( m_subject.getPrincipals( Role.class ) ); 224 225 // Add all of the GroupPrincipals possessed by the Subject directly 226 roles.addAll( m_subject.getPrincipals( GroupPrincipal.class ) ); 227 228 // Return a defensive copy 229 final Principal[] roleArray = roles.toArray( new Principal[ roles.size() ] ); 230 Arrays.sort( roleArray, WikiPrincipal.COMPARATOR ); 231 return roleArray; 232 } 233 234 /** {@inheritDoc} */ 235 @Override 236 public boolean hasPrincipal( final Principal principal ) { 237 return m_subject.getPrincipals().contains( principal ); 238 } 239 240 /** 241 * Listens for WikiEvents generated by source objects such as the GroupManager, UserManager or AuthenticationManager. This method adds 242 * Principals to the private Subject managed by the WikiSession. 243 * 244 * @see org.apache.wiki.event.WikiEventListener#actionPerformed(WikiEvent) 245 */ 246 @Override 247 public void actionPerformed( final WikiEvent event ) { 248 if ( event instanceof WikiSecurityEvent ) { 249 final WikiSecurityEvent e = (WikiSecurityEvent)event; 250 if ( e.getTarget() != null ) { 251 switch( e.getType() ) { 252 case WikiSecurityEvent.GROUP_ADD: 253 final Group groupAdd = ( Group )e.getTarget(); 254 if( isInGroup( groupAdd ) ) { 255 m_subject.getPrincipals().add( groupAdd.getPrincipal() ); 256 } 257 break; 258 case WikiSecurityEvent.GROUP_REMOVE: 259 final Group group = ( Group )e.getTarget(); 260 m_subject.getPrincipals().remove( group.getPrincipal() ); 261 break; 262 case WikiSecurityEvent.GROUP_CLEAR_GROUPS: 263 m_subject.getPrincipals().removeAll( m_subject.getPrincipals( GroupPrincipal.class ) ); 264 break; 265 case WikiSecurityEvent.LOGIN_INITIATED: 266 // Do nothing 267 break; 268 case WikiSecurityEvent.PRINCIPAL_ADD: 269 final WikiSession targetPA = ( WikiSession )e.getTarget(); 270 if( this.equals( targetPA ) && m_status.equals( AUTHENTICATED ) ) { 271 final Set< Principal > principals = m_subject.getPrincipals(); 272 principals.add( ( Principal )e.getPrincipal() ); 273 } 274 break; 275 case WikiSecurityEvent.LOGIN_ANONYMOUS: 276 final WikiSession targetLAN = ( WikiSession )e.getTarget(); 277 if( this.equals( targetLAN ) ) { 278 m_status = ANONYMOUS; 279 280 // Set the login/user principals and login status 281 final Set< Principal > principals = m_subject.getPrincipals(); 282 m_loginPrincipal = ( Principal )e.getPrincipal(); 283 m_userPrincipal = m_loginPrincipal; 284 285 // Add the login principal to the Subject, and set the built-in roles 286 principals.clear(); 287 principals.add( m_loginPrincipal ); 288 principals.add( Role.ALL ); 289 principals.add( Role.ANONYMOUS ); 290 } 291 break; 292 case WikiSecurityEvent.LOGIN_ASSERTED: 293 final WikiSession targetLAS = ( WikiSession )e.getTarget(); 294 if( this.equals( targetLAS ) ) { 295 m_status = ASSERTED; 296 297 // Set the login/user principals and login status 298 final Set< Principal > principals = m_subject.getPrincipals(); 299 m_loginPrincipal = ( Principal )e.getPrincipal(); 300 m_userPrincipal = m_loginPrincipal; 301 302 // Add the login principal to the Subject, and set the built-in roles 303 principals.clear(); 304 principals.add( m_loginPrincipal ); 305 principals.add( Role.ALL ); 306 principals.add( Role.ASSERTED ); 307 } 308 break; 309 case WikiSecurityEvent.LOGIN_AUTHENTICATED: 310 final WikiSession targetLAU = ( WikiSession )e.getTarget(); 311 if( this.equals( targetLAU ) ) { 312 m_status = AUTHENTICATED; 313 314 // Set the login/user principals and login status 315 final Set< Principal > principals = m_subject.getPrincipals(); 316 m_loginPrincipal = ( Principal )e.getPrincipal(); 317 m_userPrincipal = m_loginPrincipal; 318 319 // Add the login principal to the Subject, and set the built-in roles 320 principals.clear(); 321 principals.add( m_loginPrincipal ); 322 principals.add( Role.ALL ); 323 principals.add( Role.AUTHENTICATED ); 324 325 // Add the user and group principals 326 injectUserProfilePrincipals(); // Add principals for the user profile 327 injectGroupPrincipals(); // Inject group principals 328 } 329 break; 330 case WikiSecurityEvent.PROFILE_SAVE: 331 final WikiSession sourcePS = e.getSrc(); 332 if( this.equals( sourcePS ) ) { 333 injectUserProfilePrincipals(); // Add principals for the user profile 334 injectGroupPrincipals(); // Inject group principals 335 } 336 break; 337 case WikiSecurityEvent.PROFILE_NAME_CHANGED: 338 // Refresh user principals based on new user profile 339 final WikiSession sourcePNC = e.getSrc(); 340 if( this.equals( sourcePNC ) && m_status.equals( AUTHENTICATED ) ) { 341 // To prepare for refresh, set the new full name as the primary principal 342 final UserProfile[] profiles = ( UserProfile[] )e.getTarget(); 343 final UserProfile newProfile = profiles[ 1 ]; 344 if( newProfile.getFullname() == null ) { 345 throw new IllegalStateException( "User profile FullName cannot be null." ); 346 } 347 348 final Set< Principal > principals = m_subject.getPrincipals(); 349 m_loginPrincipal = new WikiPrincipal( newProfile.getLoginName() ); 350 351 // Add the login principal to the Subject, and set the built-in roles 352 principals.clear(); 353 principals.add( m_loginPrincipal ); 354 principals.add( Role.ALL ); 355 principals.add( Role.AUTHENTICATED ); 356 357 // Add the user and group principals 358 injectUserProfilePrincipals(); // Add principals for the user profile 359 injectGroupPrincipals(); // Inject group principals 360 } 361 break; 362 363 // No action, if the event is not recognized. 364 default: 365 break; 366 } 367 } 368 } 369 } 370 371 /** {@inheritDoc} */ 372 @Override 373 public void invalidate() { 374 m_subject.getPrincipals().clear(); 375 m_subject.getPrincipals().add( WikiPrincipal.GUEST ); 376 m_subject.getPrincipals().add( Role.ANONYMOUS ); 377 m_subject.getPrincipals().add( Role.ALL ); 378 m_userPrincipal = WikiPrincipal.GUEST; 379 m_loginPrincipal = WikiPrincipal.GUEST; 380 } 381 382 /** 383 * Injects GroupPrincipal objects into the user's Principal set based on the groups the user belongs to. For Groups, the algorithm 384 * first calls the {@link GroupManager#getRoles()} to obtain the array of GroupPrincipals the authorizer knows about. Then, the 385 * method {@link GroupManager#isUserInRole(Session, Principal)} is called for each Principal. If the user is a member of the 386 * group, an equivalent GroupPrincipal is injected into the user's principal set. Existing GroupPrincipals are flushed and replaced. 387 * This method should generally be called after a user's {@link org.apache.wiki.auth.user.UserProfile} is saved. If the wiki session 388 * is null, or there is no matching user profile, the method returns silently. 389 */ 390 protected void injectGroupPrincipals() { 391 // Flush the existing GroupPrincipals 392 m_subject.getPrincipals().removeAll( m_subject.getPrincipals(GroupPrincipal.class) ); 393 394 // Get the GroupManager and test for each Group 395 final GroupManager manager = m_engine.getManager( GroupManager.class ); 396 for( final Principal group : manager.getRoles() ) { 397 if ( manager.isUserInRole( this, group ) ) { 398 m_subject.getPrincipals().add( group ); 399 } 400 } 401 } 402 403 /** 404 * Adds Principal objects to the Subject that correspond to the logged-in user's profile attributes for the wiki name, full name 405 * and login name. These Principals will be WikiPrincipals, and they will replace all other WikiPrincipals in the Subject. <em>Note: 406 * this method is never called during anonymous or asserted sessions.</em> 407 */ 408 protected void injectUserProfilePrincipals() { 409 // Search for the user profile 410 final String searchId = m_loginPrincipal.getName(); 411 if ( searchId == null ) { 412 // Oh dear, this wasn't an authenticated user after all 413 log.info("Refresh principals failed because WikiSession had no user Principal; maybe not logged in?"); 414 return; 415 } 416 417 // Look up the user and go get the new Principals 418 final UserDatabase database = m_engine.getManager( UserManager.class ).getUserDatabase(); 419 if( database == null ) { 420 throw new IllegalStateException( "User database cannot be null." ); 421 } 422 try { 423 final UserProfile profile = database.find( searchId ); 424 final Principal[] principals = database.getPrincipals( profile.getLoginName() ); 425 for( final Principal principal : principals ) { 426 // Add the Principal to the Subject 427 m_subject.getPrincipals().add( principal ); 428 429 // Set the user principal if needed; we prefer FullName, but the WikiName will also work 430 final boolean isFullNamePrincipal = ( principal instanceof WikiPrincipal && 431 ( ( WikiPrincipal )principal ).getType().equals( WikiPrincipal.FULL_NAME ) ); 432 if ( isFullNamePrincipal ) { 433 m_userPrincipal = principal; 434 } else if ( !( m_userPrincipal instanceof WikiPrincipal ) ) { 435 m_userPrincipal = principal; 436 } 437 } 438 } catch ( final NoSuchPrincipalException e ) { 439 // We will get here if the user has a principal but not a profile 440 // For example, it's a container-managed user who hasn't set up a profile yet 441 log.warn("User profile '" + searchId + "' not found. This is normal for container-auth users who haven't set up a profile yet."); 442 } 443 } 444 445 /** {@inheritDoc} */ 446 @Override 447 public String getStatus() { 448 return m_status; 449 } 450 451 /** {@inheritDoc} */ 452 @Override 453 public Subject getSubject() { 454 return m_subject; 455 } 456 457 /** 458 * Removes the wiki session associated with the user's HTTP request from the cache of wiki sessions, typically as part of a 459 * logout process. 460 * 461 * @param engine the wiki engine 462 * @param request the users's HTTP request 463 */ 464 public static void removeWikiSession( final Engine engine, final HttpServletRequest request ) { 465 if ( engine == null || request == null ) { 466 throw new IllegalArgumentException( "Request or engine cannot be null." ); 467 } 468 final SessionMonitor monitor = SessionMonitor.getInstance( engine ); 469 monitor.remove( request.getSession() ); 470 c_guestSession.remove(); 471 } 472 473 /** 474 * <p>Static factory method that returns the Session object associated with the current HTTP request. This method looks up 475 * the associated HttpSession in an internal WeakHashMap and attempts to retrieve the WikiSession. If not found, one is created. 476 * This method is guaranteed to always return a Session, although the authentication status is unpredictable until the user 477 * attempts to log in. If the servlet request parameter is <code>null</code>, a synthetic {@link #guestSession(Engine)} is 478 * returned.</p> 479 * <p>When a session is created, this method attaches a WikiEventListener to the GroupManager, UserManager and AuthenticationManager, 480 * so that changes to users, groups, logins, etc. are detected automatically.</p> 481 * 482 * @param engine the engine 483 * @param request the servlet request object 484 * @return the existing (or newly created) session 485 */ 486 public static Session getWikiSession( final Engine engine, final HttpServletRequest request ) { 487 if ( request == null ) { 488 if ( log.isDebugEnabled() ) { 489 log.debug( "Looking up WikiSession for NULL HttpRequest: returning guestSession()" ); 490 } 491 return staticGuestSession( engine ); 492 } 493 494 // Look for a WikiSession associated with the user's Http Session and create one if it isn't there yet. 495 final HttpSession session = request.getSession(); 496 final SessionMonitor monitor = SessionMonitor.getInstance( engine ); 497 final WikiSession wikiSession = ( WikiSession )monitor.find( session ); 498 499 // Attach reference to wiki engine 500 wikiSession.m_engine = engine; 501 wikiSession.m_cachedLocale = request.getLocale(); 502 return wikiSession; 503 } 504 505 /** 506 * Static factory method that creates a new "guest" session containing a single user Principal 507 * {@link org.apache.wiki.auth.WikiPrincipal#GUEST}, plus the role principals {@link Role#ALL} and {@link Role#ANONYMOUS}. This 508 * method also adds the session as a listener for GroupManager, AuthenticationManager and UserManager events. 509 * 510 * @param engine the wiki engine 511 * @return the guest wiki session 512 */ 513 public static Session guestSession( final Engine engine ) { 514 final WikiSession session = new WikiSession(); 515 session.m_engine = engine; 516 session.invalidate(); 517 518 // Add the session as listener for GroupManager, AuthManager, UserManager events 519 final GroupManager groupMgr = engine.getManager( GroupManager.class ); 520 final AuthenticationManager authMgr = engine.getManager( AuthenticationManager.class ); 521 final UserManager userMgr = engine.getManager( UserManager.class ); 522 groupMgr.addWikiEventListener( session ); 523 authMgr.addWikiEventListener( session ); 524 userMgr.addWikiEventListener( session ); 525 526 return session; 527 } 528 529 /** 530 * Returns a static guest session, which is available for this thread only. This guest session is used internally whenever 531 * there is no HttpServletRequest involved, but the request is done e.g. when embedding JSPWiki code. 532 * 533 * @param engine Engine for this session 534 * @return A static WikiSession which is shared by all in this same Thread. 535 */ 536 // FIXME: Should really use WeakReferences to clean away unused sessions. 537 private static Session staticGuestSession( final Engine engine ) { 538 Session session = c_guestSession.get(); 539 if( session == null ) { 540 session = guestSession( engine ); 541 c_guestSession.set( session ); 542 } 543 544 return session; 545 } 546 547 /** 548 * Returns the total number of active wiki sessions for a particular wiki. This method delegates to the wiki's 549 * {@link SessionMonitor#sessions()} method. 550 * 551 * @param engine the wiki session 552 * @return the number of sessions 553 * @deprecated use {@link SessionMonitor#sessions()} instead 554 * @see SessionMonitor#sessions() 555 */ 556 @Deprecated 557 public static int sessions( final Engine engine ) { 558 final SessionMonitor monitor = SessionMonitor.getInstance( engine ); 559 return monitor.sessions(); 560 } 561 562 /** 563 * Returns Principals representing the current users known to a particular wiki. Each Principal will correspond to the 564 * value returned by each WikiSession's {@link #getUserPrincipal()} method. This method delegates to 565 * {@link SessionMonitor#userPrincipals()}. 566 * 567 * @param engine the wiki engine 568 * @return an array of Principal objects, sorted by name 569 * @deprecated use {@link SessionMonitor#userPrincipals()} instead 570 * @see SessionMonitor#userPrincipals() 571 */ 572 @Deprecated 573 public static Principal[] userPrincipals( final Engine engine ) { 574 final SessionMonitor monitor = SessionMonitor.getInstance( engine ); 575 return monitor.userPrincipals(); 576 } 577 578}