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.auth.authorize; 020 021import org.apache.commons.lang3.ArrayUtils; 022import org.apache.logging.log4j.LogManager; 023import org.apache.logging.log4j.Logger; 024import org.apache.wiki.api.core.Context; 025import org.apache.wiki.api.core.Engine; 026import org.apache.wiki.api.core.Session; 027import org.apache.wiki.api.exceptions.NoRequiredPropertyException; 028import org.apache.wiki.api.exceptions.WikiException; 029import org.apache.wiki.auth.AuthenticationManager; 030import org.apache.wiki.auth.Authorizer; 031import org.apache.wiki.auth.GroupPrincipal; 032import org.apache.wiki.auth.NoSuchPrincipalException; 033import org.apache.wiki.auth.UserManager; 034import org.apache.wiki.auth.WikiPrincipal; 035import org.apache.wiki.auth.WikiSecurityException; 036import org.apache.wiki.auth.user.UserProfile; 037import org.apache.wiki.event.WikiEvent; 038import org.apache.wiki.event.WikiEventListener; 039import org.apache.wiki.event.WikiEventManager; 040import org.apache.wiki.event.WikiSecurityEvent; 041import org.apache.wiki.ui.InputValidator; 042import org.apache.wiki.util.ClassUtil; 043 044import java.security.Principal; 045import java.util.Arrays; 046import java.util.HashMap; 047import java.util.HashSet; 048import java.util.Map; 049import java.util.Properties; 050import java.util.Set; 051import java.util.StringTokenizer; 052 053 054/** 055 * <p> 056 * Facade class for storing, retrieving and managing wiki groups on behalf of AuthorizationManager, JSPs and other presentation-layer 057 * classes. GroupManager works in collaboration with a back-end {@link GroupDatabase}, which persists groups to permanent storage. 058 * </p> 059 * <p> 060 * <em>Note: prior to JSPWiki 2.4.19, GroupManager was an interface; it is now a concrete, final class. The aspects of GroupManager 061 * which previously extracted group information from storage (e.g., wiki pages) have been refactored into the GroupDatabase interface.</em> 062 * </p> 063 * @since 2.4.19 064 */ 065public class DefaultGroupManager implements GroupManager, Authorizer, WikiEventListener { 066 067 private static final Logger LOG = LogManager.getLogger( DefaultGroupManager.class ); 068 069 protected Engine m_engine; 070 071 protected WikiEventListener m_groupListener; 072 073 private GroupDatabase m_groupDatabase; 074 075 /** Map with GroupPrincipals as keys, and Groups as values */ 076 private final Map< Principal, Group > m_groups = new HashMap<>(); 077 078 /** {@inheritDoc} */ 079 @Override 080 public Principal findRole( final String name ) { 081 try { 082 final Group group = getGroup( name ); 083 return group.getPrincipal(); 084 } catch( final NoSuchPrincipalException e ) { 085 return null; 086 } 087 } 088 089 /** {@inheritDoc} */ 090 @Override 091 public Group getGroup( final String name ) throws NoSuchPrincipalException { 092 final Group group = m_groups.get( new GroupPrincipal( name ) ); 093 if( group != null ) { 094 return group; 095 } 096 throw new NoSuchPrincipalException( "Group " + name + " not found." ); 097 } 098 099 /** {@inheritDoc} */ 100 @Override 101 public GroupDatabase getGroupDatabase() throws WikiSecurityException { 102 if( m_groupDatabase != null ) { 103 return m_groupDatabase; 104 } 105 106 String dbClassName = "<unknown>"; 107 String dbInstantiationError = null; 108 Throwable cause = null; 109 try { 110 final Properties props = m_engine.getWikiProperties(); 111 dbClassName = props.getProperty( PROP_GROUPDATABASE ); 112 if( dbClassName == null ) { 113 dbClassName = XMLGroupDatabase.class.getName(); 114 } 115 LOG.info( "Attempting to load group database class {}", dbClassName ); 116 m_groupDatabase = ClassUtil.buildInstance( "org.apache.wiki.auth.authorize", dbClassName ); 117 m_groupDatabase.initialize( m_engine, m_engine.getWikiProperties() ); 118 LOG.info( "Group database initialized." ); 119 } catch( final ReflectiveOperationException e ) { 120 LOG.error( "UserDatabase {} cannot be instantiated", dbClassName, e ); 121 dbInstantiationError = "Access GroupDatabase class " + dbClassName + " denied"; 122 cause = e; 123 } catch( final NoRequiredPropertyException e ) { 124 LOG.error( "Missing property: " + e.getMessage() + "." ); 125 dbInstantiationError = "Missing property: " + e.getMessage(); 126 cause = e; 127 } 128 129 if( dbInstantiationError != null ) { 130 throw new WikiSecurityException( dbInstantiationError + " Cause: " + cause.getMessage(), cause ); 131 } 132 133 return m_groupDatabase; 134 } 135 136 /** {@inheritDoc} */ 137 @Override 138 public Principal[] getRoles() { 139 return m_groups.keySet().toArray( new Principal[0] ); 140 } 141 142 /** {@inheritDoc} */ 143 @Override 144 public void initialize( final Engine engine, final Properties props ) throws WikiSecurityException { 145 m_engine = engine; 146 147 try { 148 m_groupDatabase = getGroupDatabase(); 149 } catch( final WikiException e ) { 150 throw new WikiSecurityException( e.getMessage(), e ); 151 } 152 153 // Load all groups from the database into the cache 154 final Group[] groups = m_groupDatabase.groups(); 155 synchronized( m_groups ) { 156 for( final Group group : groups ) { 157 // Add new group to cache; fire GROUP_ADD event 158 m_groups.put( group.getPrincipal(), group ); 159 fireEvent( WikiSecurityEvent.GROUP_ADD, group ); 160 } 161 } 162 163 // Make the GroupManager listen for WikiEvents (WikiSecurityEvents for changed user profiles) 164 engine.getManager( UserManager.class ).addWikiEventListener( this ); 165 166 // Success! 167 LOG.info( "Authorizer GroupManager initialized successfully; loaded " + groups.length + " group(s)." ); 168 } 169 170 /** {@inheritDoc} */ 171 @Override 172 public boolean isUserInRole( final Session session, final Principal role ) { 173 // Always return false if session/role is null, or if role isn't a GroupPrincipal 174 if ( session == null || !( role instanceof GroupPrincipal ) || !session.isAuthenticated() ) { 175 return false; 176 } 177 178 // Get the group we're examining 179 final Group group = m_groups.get( role ); 180 if( group == null ) { 181 return false; 182 } 183 184 // Check each user principal to see if it belongs to the group 185 return Arrays.stream(session.getPrincipals()).anyMatch(principal -> AuthenticationManager.isUserPrincipal(principal) && group.isMember(principal)); 186 } 187 188 /** {@inheritDoc} */ 189 @Override 190 public Group parseGroup( String name, String memberLine, final boolean create ) throws WikiSecurityException { 191 // If null name parameter, it's because someone's creating a new group 192 if( name == null ) { 193 if( create ) { 194 name = "MyGroup"; 195 } else { 196 throw new WikiSecurityException( "Group name cannot be blank." ); 197 } 198 } else if( ArrayUtils.contains( Group.RESTRICTED_GROUPNAMES, name ) ) { 199 // Certain names are forbidden 200 throw new WikiSecurityException( "Illegal group name: " + name ); 201 } 202 name = name.trim(); 203 204 // Normalize the member line 205 if( InputValidator.isBlank( memberLine ) ) { 206 memberLine = ""; 207 } 208 memberLine = memberLine.trim(); 209 210 // Create or retrieve the group (may have been previously cached) 211 final Group group = new Group( name, m_engine.getApplicationName() ); 212 try { 213 final Group existingGroup = getGroup( name ); 214 215 // If existing, clone it 216 group.setCreator( existingGroup.getCreator() ); 217 group.setCreated( existingGroup.getCreated() ); 218 group.setModifier( existingGroup.getModifier() ); 219 group.setLastModified( existingGroup.getLastModified() ); 220 for( final Principal existingMember : existingGroup.members() ) { 221 group.add( existingMember ); 222 } 223 } catch( final NoSuchPrincipalException e ) { 224 // It's a new group.... throw error if we don't create new ones 225 if( !create ) { 226 throw new NoSuchPrincipalException( "Group '" + name + "' does not exist." ); 227 } 228 } 229 230 // If passed members not empty, overwrite 231 final String[] members = extractMembers( memberLine ); 232 if( members.length > 0 ) { 233 group.clear(); 234 for( final String member : members ) { 235 group.add( new WikiPrincipal( member ) ); 236 } 237 } 238 239 return group; 240 } 241 242 /** {@inheritDoc} */ 243 @Override 244 public void removeGroup( final String index ) throws WikiSecurityException { 245 if( index == null ) { 246 throw new IllegalArgumentException( "Group cannot be null." ); 247 } 248 249 final Group group = m_groups.get( new GroupPrincipal( index ) ); 250 if( group == null ) { 251 throw new NoSuchPrincipalException( "Group " + index + " not found" ); 252 } 253 254 // Delete the group 255 // TODO: need rollback procedure 256 synchronized( m_groups ) { 257 m_groups.remove( group.getPrincipal() ); 258 } 259 m_groupDatabase.delete( group ); 260 fireEvent( WikiSecurityEvent.GROUP_REMOVE, group ); 261 } 262 263 /** {@inheritDoc} */ 264 @Override 265 public void setGroup( final Session session, final Group group ) throws WikiSecurityException { 266 // TODO: check for appropriate permissions 267 268 // If group already exists, delete it; fire GROUP_REMOVE event 269 final Group oldGroup = m_groups.get( group.getPrincipal() ); 270 if( oldGroup != null ) { 271 fireEvent( WikiSecurityEvent.GROUP_REMOVE, oldGroup ); 272 synchronized( m_groups ) { 273 m_groups.remove( oldGroup.getPrincipal() ); 274 } 275 } 276 277 // Copy existing modifier info & timestamps 278 if( oldGroup != null ) { 279 group.setCreator( oldGroup.getCreator() ); 280 group.setCreated( oldGroup.getCreated() ); 281 group.setModifier( oldGroup.getModifier() ); 282 group.setLastModified( oldGroup.getLastModified() ); 283 } 284 285 // Add new group to cache; announce GROUP_ADD event 286 synchronized( m_groups ) { 287 m_groups.put( group.getPrincipal(), group ); 288 } 289 fireEvent( WikiSecurityEvent.GROUP_ADD, group ); 290 291 // Save the group to back-end database; if it fails, roll back to previous state. Note that the back-end 292 // MUST timestammp the create/modify fields in the Group. 293 try { 294 m_groupDatabase.save( group, session.getUserPrincipal() ); 295 } 296 297 // We got an exception! Roll back... 298 catch( final WikiSecurityException e ) { 299 if( oldGroup != null ) { 300 // Restore previous version, re-throw... 301 fireEvent( WikiSecurityEvent.GROUP_REMOVE, group ); 302 fireEvent( WikiSecurityEvent.GROUP_ADD, oldGroup ); 303 synchronized( m_groups ) { 304 m_groups.put( oldGroup.getPrincipal(), oldGroup ); 305 } 306 throw new WikiSecurityException( e.getMessage() + " (rolled back to previous version).", e ); 307 } 308 // Re-throw security exception 309 throw new WikiSecurityException( e.getMessage(), e ); 310 } 311 } 312 313 /** {@inheritDoc} */ 314 @Override 315 public void validateGroup( final Context context, final Group group ) { 316 final InputValidator validator = new InputValidator( MESSAGES_KEY, context ); 317 318 // Name cannot be null or one of the restricted names 319 try { 320 checkGroupName( context, group.getName() ); 321 } catch( final WikiSecurityException e ) { 322 } 323 324 // Member names must be "safe" strings 325 final Principal[] members = group.members(); 326 for( final Principal member : members ) { 327 validator.validateNotNull( member.getName(), "Full name", InputValidator.ID ); 328 } 329 } 330 331 /** {@inheritDoc} */ 332 @Override 333 public void checkGroupName( final Context context, final String name ) throws WikiSecurityException { 334 // TODO: groups cannot have the same name as a user 335 336 // Name cannot be null 337 final InputValidator validator = new InputValidator( MESSAGES_KEY, context ); 338 validator.validateNotNull( name, "Group name" ); 339 340 // Name cannot be one of the restricted names either 341 if( ArrayUtils.contains( Group.RESTRICTED_GROUPNAMES, name ) ) { 342 throw new WikiSecurityException( "The group name '" + name + "' is illegal. Choose another." ); 343 } 344 } 345 346 /** 347 * Extracts carriage-return separated members into a Set of String objects. 348 * 349 * @param memberLine the list of members 350 * @return the list of members 351 */ 352 protected String[] extractMembers( final String memberLine ) { 353 final Set< String > members = new HashSet<>(); 354 if( memberLine != null ) { 355 final StringTokenizer tok = new StringTokenizer( memberLine, "\n" ); 356 while( tok.hasMoreTokens() ) { 357 final String uid = tok.nextToken().trim(); 358 if( !uid.isEmpty() ) { 359 members.add( uid ); 360 } 361 } 362 } 363 return members.toArray( new String[0] ); 364 } 365 366 // events processing ....................................................... 367 368 /** {@inheritDoc} */ 369 @Override 370 public synchronized void addWikiEventListener( final WikiEventListener listener ) { 371 WikiEventManager.addWikiEventListener( this, listener ); 372 } 373 374 /** {@inheritDoc} */ 375 @Override 376 public synchronized void removeWikiEventListener( final WikiEventListener listener ) { 377 WikiEventManager.removeWikiEventListener( this, listener ); 378 } 379 380 /** {@inheritDoc} */ 381 @Override 382 public void actionPerformed( final WikiEvent event ) { 383 if( !( event instanceof WikiSecurityEvent ) ) { 384 return; 385 } 386 387 final WikiSecurityEvent se = ( WikiSecurityEvent )event; 388 if( se.getType() == WikiSecurityEvent.PROFILE_NAME_CHANGED ) { 389 final Session session = se.getSrc(); 390 final UserProfile[] profiles = ( UserProfile[] )se.getTarget(); 391 final Principal[] oldPrincipals = new Principal[] { new WikiPrincipal( profiles[ 0 ].getLoginName() ), 392 new WikiPrincipal( profiles[ 0 ].getFullname() ), new WikiPrincipal( profiles[ 0 ].getWikiName() ) }; 393 final Principal newPrincipal = new WikiPrincipal( profiles[ 1 ].getFullname() ); 394 395 // Examine each group 396 int groupsChanged = 0; 397 try { 398 for( final Group group : m_groupDatabase.groups() ) { 399 boolean groupChanged = false; 400 for( final Principal oldPrincipal : oldPrincipals ) { 401 if( group.isMember( oldPrincipal ) ) { 402 group.remove( oldPrincipal ); 403 group.add( newPrincipal ); 404 groupChanged = true; 405 } 406 } 407 if( groupChanged ) { 408 setGroup( session, group ); 409 groupsChanged++; 410 } 411 } 412 } catch( final WikiException e ) { 413 // Oooo! This is really bad... 414 LOG.error( "Could not change user name in Group lists because of GroupDatabase error:" + e.getMessage() ); 415 } 416 LOG.info( "Profile name change for '" + newPrincipal + "' caused " + groupsChanged + " groups to change also." ); 417 } 418 } 419 420}