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