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 jakarta.servlet.http.HttpServletRequest; 043import jakarta.servlet.http.HttpSession; 044import java.net.http.HttpRequest; 045import java.security.Principal; 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 private String remoteAddress; 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.isEmpty()) { 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 227 // Return a defensive copy 228 final Principal[] roleArray = roles.toArray( new Principal[0] ); 229 Arrays.sort( roleArray, WikiPrincipal.COMPARATOR ); 230 return roleArray; 231 } 232 233 /** {@inheritDoc} */ 234 @Override 235 public boolean hasPrincipal( final Principal principal ) { 236 return m_subject.getPrincipals().contains( principal ); 237 } 238 239 /** 240 * Listens for WikiEvents generated by source objects such as the GroupManager, UserManager or AuthenticationManager. This method adds 241 * Principals to the private Subject managed by the WikiSession. 242 * 243 * @see org.apache.wiki.event.WikiEventListener#actionPerformed(WikiEvent) 244 */ 245 @Override 246 public void actionPerformed( final WikiEvent event ) { 247 if ( event instanceof WikiSecurityEvent ) { 248 final WikiSecurityEvent e = (WikiSecurityEvent)event; 249 if ( e.getTarget() != null ) { 250 switch( e.getType() ) { 251 case WikiSecurityEvent.GROUP_ADD: 252 final Group groupAdd = ( Group )e.getTarget(); 253 if( isInGroup( groupAdd ) ) { 254 m_subject.getPrincipals().add( groupAdd.getPrincipal() ); 255 } 256 break; 257 case WikiSecurityEvent.GROUP_REMOVE: 258 final Group group = ( Group )e.getTarget(); 259 m_subject.getPrincipals().remove( group.getPrincipal() ); 260 break; 261 case WikiSecurityEvent.GROUP_CLEAR_GROUPS: 262 m_subject.getPrincipals().removeAll( m_subject.getPrincipals( GroupPrincipal.class ) ); 263 break; 264 case WikiSecurityEvent.LOGIN_INITIATED: 265 // Do nothing 266 break; 267 case WikiSecurityEvent.PRINCIPAL_ADD: 268 final WikiSession targetPA = ( WikiSession )e.getTarget(); 269 if( this.equals( targetPA ) && m_status.equals( AUTHENTICATED ) ) { 270 final Set< Principal > principals = m_subject.getPrincipals(); 271 principals.add( ( Principal )e.getPrincipal() ); 272 } 273 break; 274 case WikiSecurityEvent.LOGIN_ANONYMOUS: 275 final WikiSession targetLAN = ( WikiSession )e.getTarget(); 276 if( this.equals( targetLAN ) ) { 277 m_status = ANONYMOUS; 278 279 // Set the login/user principals and login status 280 final Set< Principal > principals = m_subject.getPrincipals(); 281 m_loginPrincipal = ( Principal )e.getPrincipal(); 282 m_userPrincipal = m_loginPrincipal; 283 284 // Add the login principal to the Subject, and set the built-in roles 285 principals.clear(); 286 principals.add( m_loginPrincipal ); 287 principals.add( Role.ALL ); 288 principals.add( Role.ANONYMOUS ); 289 } 290 break; 291 case WikiSecurityEvent.LOGIN_ASSERTED: 292 final WikiSession targetLAS = ( WikiSession )e.getTarget(); 293 if( this.equals( targetLAS ) ) { 294 m_status = ASSERTED; 295 296 // Set the login/user principals and login status 297 final Set< Principal > principals = m_subject.getPrincipals(); 298 m_loginPrincipal = ( Principal )e.getPrincipal(); 299 m_userPrincipal = m_loginPrincipal; 300 301 // Add the login principal to the Subject, and set the built-in roles 302 principals.clear(); 303 principals.add( m_loginPrincipal ); 304 principals.add( Role.ALL ); 305 principals.add( Role.ASSERTED ); 306 } 307 break; 308 case WikiSecurityEvent.LOGIN_AUTHENTICATED: 309 final WikiSession targetLAU = ( WikiSession )e.getTarget(); 310 if( this.equals( targetLAU ) ) { 311 m_status = AUTHENTICATED; 312 313 // Set the login/user principals and login status 314 final Set< Principal > principals = m_subject.getPrincipals(); 315 m_loginPrincipal = ( Principal )e.getPrincipal(); 316 m_userPrincipal = m_loginPrincipal; 317 318 // Add the login principal to the Subject, and set the built-in roles 319 principals.clear(); 320 principals.add( m_loginPrincipal ); 321 principals.add( Role.ALL ); 322 principals.add( Role.AUTHENTICATED ); 323 324 // Add the user and group principals 325 injectUserProfilePrincipals(); // Add principals for the user profile 326 injectGroupPrincipals(); // Inject group principals 327 } 328 break; 329 case WikiSecurityEvent.PROFILE_SAVE: 330 final WikiSession sourcePS = e.getSrc(); 331 if( this.equals( sourcePS ) ) { 332 injectUserProfilePrincipals(); // Add principals for the user profile 333 injectGroupPrincipals(); // Inject group principals 334 } 335 break; 336 case WikiSecurityEvent.PROFILE_NAME_CHANGED: 337 // Refresh user principals based on new user profile 338 final WikiSession sourcePNC = e.getSrc(); 339 if( this.equals( sourcePNC ) && m_status.equals( AUTHENTICATED ) ) { 340 // To prepare for refresh, set the new full name as the primary principal 341 final UserProfile[] profiles = ( UserProfile[] )e.getTarget(); 342 final UserProfile newProfile = profiles[ 1 ]; 343 if( newProfile.getFullname() == null ) { 344 throw new IllegalStateException( "User profile FullName cannot be null." ); 345 } 346 347 final Set< Principal > principals = m_subject.getPrincipals(); 348 m_loginPrincipal = new WikiPrincipal( newProfile.getLoginName() ); 349 350 // Add the login principal to the Subject, and set the built-in roles 351 principals.clear(); 352 principals.add( m_loginPrincipal ); 353 principals.add( Role.ALL ); 354 principals.add( Role.AUTHENTICATED ); 355 356 // Add the user and group principals 357 injectUserProfilePrincipals(); // Add principals for the user profile 358 injectGroupPrincipals(); // Inject group principals 359 } 360 break; 361 362 // No action, if the event is not recognized. 363 default: 364 break; 365 } 366 } 367 } 368 } 369 370 /** {@inheritDoc} */ 371 @Override 372 public void invalidate() { 373 m_subject.getPrincipals().clear(); 374 m_subject.getPrincipals().add( WikiPrincipal.GUEST ); 375 m_subject.getPrincipals().add( Role.ANONYMOUS ); 376 m_subject.getPrincipals().add( Role.ALL ); 377 m_userPrincipal = WikiPrincipal.GUEST; 378 m_loginPrincipal = WikiPrincipal.GUEST; 379 } 380 381 /** 382 * Injects GroupPrincipal objects into the user's Principal set based on the groups the user belongs to. For Groups, the algorithm 383 * first calls the {@link GroupManager#getRoles()} to obtain the array of GroupPrincipals the authorizer knows about. Then, the 384 * method {@link GroupManager#isUserInRole(Session, Principal)} is called for each Principal. If the user is a member of the 385 * group, an equivalent GroupPrincipal is injected into the user's principal set. Existing GroupPrincipals are flushed and replaced. 386 * This method should generally be called after a user's {@link org.apache.wiki.auth.user.UserProfile} is saved. If the wiki session 387 * is null, or there is no matching user profile, the method returns silently. 388 */ 389 protected void injectGroupPrincipals() { 390 // Flush the existing GroupPrincipals 391 m_subject.getPrincipals().removeAll( m_subject.getPrincipals(GroupPrincipal.class) ); 392 393 // Get the GroupManager and test for each Group 394 final GroupManager manager = m_engine.getManager( GroupManager.class ); 395 for( final Principal group : manager.getRoles() ) { 396 if ( manager.isUserInRole( this, group ) ) { 397 m_subject.getPrincipals().add( group ); 398 } 399 } 400 } 401 402 /** 403 * Adds Principal objects to the Subject that correspond to the logged-in user's profile attributes for the wiki name, full name 404 * and login name. These Principals will be WikiPrincipals, and they will replace all other WikiPrincipals in the Subject. <em>Note: 405 * this method is never called during anonymous or asserted sessions.</em> 406 */ 407 protected void injectUserProfilePrincipals() { 408 // Search for the user profile 409 final String searchId = m_loginPrincipal.getName(); 410 if ( searchId == null ) { 411 // Oh dear, this wasn't an authenticated user after all 412 LOG.info("Refresh principals failed because WikiSession had no user Principal; maybe not logged in?"); 413 return; 414 } 415 416 // Look up the user and go get the new Principals 417 final UserDatabase database = m_engine.getManager( UserManager.class ).getUserDatabase(); 418 if( database == null ) { 419 throw new IllegalStateException( "User database cannot be null." ); 420 } 421 try { 422 final UserProfile profile = database.findByLoginName( searchId ); 423 final Principal[] principals = database.getPrincipals( profile.getLoginName() ); 424 for( final Principal principal : principals ) { 425 // Add the Principal to the Subject 426 m_subject.getPrincipals().add( principal ); 427 428 // Set the user principal if needed; we prefer FullName, but the WikiName will also work 429 final boolean isFullNamePrincipal = ( principal instanceof WikiPrincipal && 430 ( ( WikiPrincipal )principal ).getType().equals( WikiPrincipal.FULL_NAME ) ); 431 if (( principal instanceof WikiPrincipal && 432 ( ( WikiPrincipal )principal ).getType().equals( WikiPrincipal.LOGIN_NAME )) ){ 433 m_loginPrincipal = principal; 434 } 435 436 437 if ( isFullNamePrincipal ) { 438 m_userPrincipal = principal; 439 } else if ( !( m_userPrincipal instanceof WikiPrincipal ) ) { 440 m_userPrincipal = principal; 441 } 442 } 443 } catch ( final NoSuchPrincipalException e ) { 444 // We will get here if the user has a principal but not a profile 445 // For example, it's a container-managed user who hasn't set up a profile yet 446 LOG.warn("User profile '" + searchId + "' not found. This is normal for container-auth users who haven't set up a profile yet."); 447 } 448 } 449 450 /** {@inheritDoc} */ 451 @Override 452 public String getStatus() { 453 return m_status; 454 } 455 456 /** {@inheritDoc} */ 457 @Override 458 public Subject getSubject() { 459 return m_subject; 460 } 461 462 /** 463 * Removes the wiki session associated with the user's HTTP request from the cache of wiki sessions, typically as part of a 464 * logout process. 465 * 466 * @param engine the wiki engine 467 * @param request the user's HTTP request 468 */ 469 public static void removeWikiSession( final Engine engine, final HttpServletRequest request ) { 470 if ( engine == null || request == null ) { 471 throw new IllegalArgumentException( "Request or engine cannot be null." ); 472 } 473 final SessionMonitor monitor = SessionMonitor.getInstance( engine ); 474 monitor.remove( request.getSession() ); 475 c_guestSession.remove(); 476 } 477 478 /** 479 * <p>Static factory method that returns the Session object associated with the current HTTP request. This method looks up 480 * the associated HttpSession in an internal WeakHashMap and attempts to retrieve the WikiSession. If not found, one is created. 481 * This method is guaranteed to always return a Session, although the authentication status is unpredictable until the user 482 * attempts to log in. If the servlet request parameter is <code>null</code>, a synthetic {@link #guestSession(Engine)} is 483 * returned.</p> 484 * <p>When a session is created, this method attaches a WikiEventListener to the GroupManager, UserManager and AuthenticationManager, 485 * so that changes to users, groups, logins, etc. are detected automatically.</p> 486 * 487 * @param engine the engine 488 * @param request the servlet request object 489 * @return the existing (or newly created) session 490 */ 491 public static Session getWikiSession( final Engine engine, final HttpServletRequest request ) { 492 if ( request == null ) { 493 LOG.debug( "Looking up WikiSession for NULL HttpRequest: returning guestSession()" ); 494 return staticGuestSession( engine ); 495 } 496 497 // Look for a WikiSession associated with the user's Http Session and create one if it isn't there yet. 498 final HttpSession session = request.getSession(); 499 final SessionMonitor monitor = SessionMonitor.getInstance( engine ); 500 final WikiSession wikiSession = ( WikiSession )monitor.find( session ); 501 wikiSession.remoteAddress = request.getRemoteAddr(); 502 // Attach reference to wiki engine 503 wikiSession.m_engine = engine; 504 wikiSession.m_cachedLocale = request.getLocale(); 505 506 String v = engine.getWikiProperties().getProperty("jspwiki.role.extraRoles", null); 507 if (v != null) { 508 String[] extraRoles = v.split("\\,"); 509 for (String s : extraRoles) { 510 if (request.isUserInRole(s)) { 511 wikiSession.m_subject.getPrincipals().add(new GroupPrincipal(s)); 512 } 513 } 514 } 515 516 return wikiSession; 517 } 518 519 /** 520 * Static factory method that creates a new "guest" session containing a single user Principal 521 * {@link org.apache.wiki.auth.WikiPrincipal#GUEST}, plus the role principals {@link Role#ALL} and {@link Role#ANONYMOUS}. This 522 * method also adds the session as a listener for GroupManager, AuthenticationManager and UserManager events. 523 * 524 * @param engine the wiki engine 525 * @return the guest wiki session 526 */ 527 public static Session guestSession( final Engine engine ) { 528 final WikiSession session = new WikiSession(); 529 session.m_engine = engine; 530 session.invalidate(); 531 session.antiCsrfToken = UUID.randomUUID().toString(); 532 533 // Add the session as listener for GroupManager, AuthManager, UserManager events 534 final GroupManager groupMgr = engine.getManager( GroupManager.class ); 535 final AuthenticationManager authMgr = engine.getManager( AuthenticationManager.class ); 536 final UserManager userMgr = engine.getManager( UserManager.class ); 537 groupMgr.addWikiEventListener( session ); 538 authMgr.addWikiEventListener( session ); 539 userMgr.addWikiEventListener( session ); 540 541 return session; 542 } 543 544 /** 545 * Returns a static guest session, which is available for this thread only. This guest session is used internally whenever 546 * there is no HttpServletRequest involved, but the request is done e.g. when embedding JSPWiki code. 547 * 548 * @param engine Engine for this session 549 * @return A static WikiSession which is shared by all in this same Thread. 550 */ 551 // FIXME: Should really use WeakReferences to clean away unused sessions. 552 private static Session staticGuestSession( final Engine engine ) { 553 Session session = c_guestSession.get(); 554 if( session == null ) { 555 session = guestSession( engine ); 556 c_guestSession.set( session ); 557 } 558 559 return session; 560 } 561 562 /** 563 * Returns the total number of active wiki sessions for a particular wiki. This method delegates to the wiki's 564 * {@link SessionMonitor#sessions()} method. 565 * 566 * @param engine the wiki session 567 * @return the number of sessions 568 * @deprecated use {@link SessionMonitor#sessions()} instead 569 * @see SessionMonitor#sessions() 570 */ 571 @Deprecated 572 public static int sessions( final Engine engine ) { 573 final SessionMonitor monitor = SessionMonitor.getInstance( engine ); 574 return monitor.sessions(); 575 } 576 577 /** 578 * Returns Principals representing the current users known to a particular wiki. Each Principal will correspond to the 579 * value returned by each WikiSession's {@link #getUserPrincipal()} method. This method delegates to 580 * {@link SessionMonitor#userPrincipals()}. 581 * 582 * @param engine the wiki engine 583 * @return an array of Principal objects, sorted by name 584 * @deprecated use {@link SessionMonitor#userPrincipals()} instead 585 * @see SessionMonitor#userPrincipals() 586 */ 587 @Deprecated 588 public static Principal[] userPrincipals( final Engine engine ) { 589 final SessionMonitor monitor = SessionMonitor.getInstance( engine ); 590 return monitor.userPrincipals(); 591 } 592 593 @Override 594 public String getRemoteAddress() { 595 return remoteAddress; 596 } 597 598}