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