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