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