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