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