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