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