/* * This program is free software; you can redistribute it and/or modify it under the * terms of the GNU General Public License, version 2 as published by the Free Software * Foundation. * * You should have received a copy of the GNU General Public License along with this * program; if not, you can obtain a copy at http://www.gnu.org/licenses/gpl-2.0.html * or from the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. * * * Copyright 2006 - 2016 Pentaho Corporation. All rights reserved. */ package org.pentaho.platform.repository2.unified.jcr.jackrabbit.security; import java.security.Principal; import java.security.acl.Group; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Properties; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import javax.jcr.Session; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.jackrabbit.api.security.principal.PrincipalIterator; import org.apache.jackrabbit.core.config.LoginModuleConfig; import org.apache.jackrabbit.core.security.AnonymousPrincipal; import org.apache.jackrabbit.core.security.SecurityConstants; import org.apache.jackrabbit.core.security.UserPrincipal; import org.apache.jackrabbit.core.security.principal.AdminPrincipal; import org.apache.jackrabbit.core.security.principal.EveryonePrincipal; import org.apache.jackrabbit.core.security.principal.PrincipalIteratorAdapter; import org.apache.jackrabbit.core.security.principal.PrincipalProvider; import org.pentaho.platform.api.engine.ICacheManager; import org.pentaho.platform.api.engine.IConfiguration; import org.pentaho.platform.api.engine.ISystemConfig; import org.pentaho.platform.api.engine.IUserRoleListService; import org.pentaho.platform.engine.core.system.PentahoSystem; import org.pentaho.platform.repository2.unified.jcr.JcrAclMetadataStrategy.AclMetadataPrincipal; import org.pentaho.platform.repository2.unified.jcr.JcrTenantUtils; import org.pentaho.platform.repository2.unified.jcr.jackrabbit.security.messages.Messages; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.util.Assert; /** * A Jackrabbit {@code PrincipalProvider} that delegates to a Pentaho {@link UserDetailsService}. * <p/> * <p> A {@code java.security.Principal} represents a user. A {@code java.security.acl.Group} represents a group. In * Spring Security, a group is called a role or authority or granted authority. Arguments to the method {@link * #getPrincipal(String)} can either be a Principal or Group. In other words, {@link #getPrincipal(String)} * might be called with an argument of a Spring Security granted authority. This happens when access control entries * (ACEs) grant access to roles and the system needs to verify the role is known. </p> * <p/> * <p> Jackrabbit assumes a unified space of all user and role names. The PrincipalProvider is responsible for * determining the type of a principal/group from its name. </p> * <p/> * <p> This implementation caches users and roles, but not passwords. Optionally, this implementation can take advantage * of a Spring Security UserCache. If available, it will use said cache for role membership lookups. Also note that the * removal of a role or user from the system will not be noticed by this implementation. (A restart of Jackrabbit is * required.) </p> * <p/> * <p> There are users and roles that are never expected to be in any backing store. By default, these are "everyone" (a * role), "anonymous" (a user), "administrators" (a role), and "admin" (a user). </p> * <p/> * <p> This implementation never returns null from {@link #getPrincipal(String)}. As a result, a {@code * NoSuchPrincipalException} is never thrown. See the method for details. </p> * * @author mlowery */ public class SpringSecurityPrincipalProvider implements PrincipalProvider { public static final String ROLE_CACHE_REGION = "principalProviderRoleCache"; public static final String USER_CACHE_REGION = "principalProviderUserCache"; private ICacheManager cacheManager; // ~ Static fields/initializers // ====================================================================================== private Log logger = LogFactory.getLog( SpringSecurityPrincipalProvider.class ); // ~ Instance fields // ================================================================================================= private UserDetailsService userDetailsService; private IUserRoleListService userRoleListService; private String adminId; private AdminPrincipal adminPrincipal; private String anonymousId; private final AnonymousPrincipal anonymousPrincipal = new AnonymousPrincipal(); final boolean ACCOUNT_NON_EXPIRED = true; final boolean CREDS_NON_EXPIRED = true; final boolean ACCOUNT_NON_LOCKED = true; /** * flag indicating whether or not UserDetailsService is called during creation of user's principal * @link http://jira.pentaho.com/browse/BACKLOG-6498 */ private boolean skipUserVerification; private final String SKIP_USER_VERIFICATION_PROP_KEY = "skipUserVerificationOnPrincipalCreation"; private final boolean SKIP_USER_VERIFICATION_DEFAULT_VALUE = true; private ISystemConfig systemConfig = PentahoSystem.get( ISystemConfig.class ); /** * flag indicating if the instance has not been {@link #close() closed} */ private final AtomicBoolean initialized = new AtomicBoolean( false ); void setCacheManager( ICacheManager cacheManager ) { this.cacheManager = cacheManager; } // ~ Constructors // ==================================================================================================== public SpringSecurityPrincipalProvider() { super(); } // ~ Methods // ========================================================================================================= /** * {@inheritDoc} */ public void init( final Properties options ) { synchronized ( initialized ) { if ( initialized.get() ) { throw new IllegalStateException( Messages.getInstance().getString( "SpringSecurityPrincipalProvider.ERROR_0001_ALREADY_INITIALIZED" ) ); //$NON-NLS-1$ } } adminId = options.getProperty( LoginModuleConfig.PARAM_ADMIN_ID, SecurityConstants.ADMIN_ID ); adminPrincipal = new AdminPrincipal( adminId ); if ( logger.isTraceEnabled() ) { logger.trace( String.format( "using adminId [%s]", adminId ) ); //$NON-NLS-1$ } anonymousId = options.getProperty( LoginModuleConfig.PARAM_ANONYMOUS_ID, SecurityConstants.ANONYMOUS_ID ); if ( logger.isTraceEnabled() ) { logger.trace( String.format( "using anonymousId [%s]", anonymousId ) ); //$NON-NLS-1$ } cacheManager = PentahoSystem.getCacheManager( null ); if ( cacheManager != null ) { if ( !cacheManager.cacheEnabled( USER_CACHE_REGION ) ) { cacheManager.addCacheRegion( USER_CACHE_REGION ); } if ( !cacheManager.cacheEnabled( ROLE_CACHE_REGION ) ) { cacheManager.addCacheRegion( ROLE_CACHE_REGION ); } } initSkipUserVerification( options ); initialized.set( true ); } public void close() { checkInitialized(); clearCaches(); cacheManager = null; initialized.set( false ); } public synchronized void clearCaches() { if ( cacheManager != null ) { cacheManager.clearRegionCache( ROLE_CACHE_REGION ); cacheManager.clearRegionCache( USER_CACHE_REGION ); } } /** * {@inheritDoc} */ public synchronized boolean canReadPrincipal( final Session session, final Principal principalToRead ) { checkInitialized(); return true; } /** * {@inheritDoc} * <p/> * <p> Attempts to load user using given {@code principalName} using a Pentaho {@code UserDetailsService}. If it fails * to find user, it returns a {@link Group} which will be caught by {@code SpringSecurityLoginModule}. </p> */ public synchronized Principal getPrincipal( final String principalName ) { if ( logger.isDebugEnabled() ) { logger.debug( "principalName: [" + principalName + "]" ); } checkInitialized(); Assert.notNull( principalName ); // first handle AclMetadataPrincipal, admin, anonymous, and everyone // specially if ( AclMetadataPrincipal.isAclMetadataPrincipal( principalName ) ) { return new AclMetadataPrincipal( principalName ); } else if ( adminId.equals( principalName ) ) { return adminPrincipal; } else if ( anonymousId.equals( principalName ) ) { return anonymousPrincipal; } else if ( EveryonePrincipal.getInstance().getName().equals( principalName ) ) { return EveryonePrincipal.getInstance(); } else { if ( JcrTenantUtils.isTenantedUser( principalName ) ) { // 1. then try the user cache if ( cacheManager != null ) { Principal userFromUserCache = (Principal) cacheManager .getFromRegionCache( USER_CACHE_REGION, JcrTenantUtils.getTenantedUser( principalName ) ); if ( userFromUserCache != null ) { if ( logger.isTraceEnabled() ) { logger.trace( "user " + principalName + " found in cache" ); //$NON-NLS-1$ //$NON-NLS-2$ } return userFromUserCache; } else { if ( logger.isTraceEnabled() ) { logger.trace( "user " + principalName + " not found in cache" ); //$NON-NLS-1$ //$NON-NLS-2$ } } } else { if ( logger.isTraceEnabled() ) { logger.trace( " Cache is not available. Will create a principal for user [" + principalName + ']' ); } } // 2. then try the springSecurityUserCache and, failing that, actual // back-end user lookup // it may not be necessary to get user's details to emit principal, if ( skipUserVerification || internalGetUserDetails( principalName ) != null ) { final Principal user = new UserPrincipal( principalName ); if ( cacheManager != null ) { cacheManager.putInRegionCache( USER_CACHE_REGION, principalName, user ); } return user; } } else if ( JcrTenantUtils.isTenatedRole( principalName ) ) { // 1. first try the role cache if ( cacheManager != null ) { Principal roleFromCache = (Principal) cacheManager .getFromRegionCache( ROLE_CACHE_REGION, JcrTenantUtils.getTenantedRole( principalName ) ); if ( roleFromCache != null ) { if ( logger.isTraceEnabled() ) { logger.trace( "role " + principalName + " found in cache" ); //$NON-NLS-1$ //$NON-NLS-2$ } return roleFromCache; } else { if ( logger.isTraceEnabled() ) { logger.trace( "role " + principalName + " not found in cache" ); //$NON-NLS-1$ //$NON-NLS-2$ } } } else { if ( logger.isTraceEnabled() ) { logger.trace( " Cache is not available. Will create a principal for role [" + principalName + ']' ); } } // 2. finally just assume role; this assumption serves two purposes: // (1) avoid any role search config by the user // and (2) performance (if we don't care that a role is not // present--why look it up); finally, a Group returned // by this class will be caught in // SpringSecurityLoginModule.getPrincipal and the login will fail final Principal roleToCache = createSpringSecurityRolePrincipal( principalName ); if ( cacheManager != null ) { cacheManager.putInRegionCache( ROLE_CACHE_REGION, principalName, roleToCache ); } if ( logger.isTraceEnabled() ) { logger.trace( "assuming " + principalName + " is a role" ); //$NON-NLS-1$ //$NON-NLS-2$ } return roleToCache; } return null; } } /** * {@inheritDoc} * <p/> * <p> Called from {@code AbstractLoginModule.getPrincipals()} </p> */ public PrincipalIterator getGroupMembership( final Principal principal ) { checkInitialized(); Assert.notNull( principal ); // first handle anonymous and everyone specially Set<Principal> groups = new HashSet<Principal>(); if ( principal instanceof AnonymousPrincipal ) { return PrincipalIteratorAdapter.EMPTY; } else if ( principal instanceof EveryonePrincipal ) { return PrincipalIteratorAdapter.EMPTY; } // make sure it's a user; also, repo admins are never in back-end--no // need to attempt to look them up; also acl // metadata principals never have group membership if ( !( principal instanceof Group ) && !( principal instanceof AdminPrincipal ) && !( principal instanceof AclMetadataPrincipal ) ) { UserDetails user = internalGetUserDetails( principal.getName() ); if ( user == null ) { return new PrincipalIteratorAdapter( groups ); } for ( final GrantedAuthority role : user.getAuthorities() ) { final String roleAuthority = role.getAuthority(); Principal fromCache; if ( cacheManager == null ) { fromCache = null; } else { fromCache = (Principal) cacheManager.getFromRegionCache( ROLE_CACHE_REGION, roleAuthority ); } if ( fromCache != null ) { groups.add( fromCache ); } else { groups.add( createSpringSecurityRolePrincipal( roleAuthority ) ); } } } groups.add( EveryonePrincipal.getInstance() ); if ( logger.isTraceEnabled() ) { logger.trace( "group membership for principal=" + principal + " is " + groups ); //$NON-NLS-1$ //$NON-NLS-2$ } return new PrincipalIteratorAdapter( groups ); } /** * Gets user details. Checks cache first. */ protected UserDetails internalGetUserDetails( final String username ) { if ( username != null && username.equals( "administrators" ) ) { return null; } // optimization for when running in pre-authenticated mode (i.e. Spring Security filters have setup holder with // current user meaning we don't have to hit the back-end again) Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if ( auth != null ) { Object ssPrincipal = auth.getPrincipal(); if ( ssPrincipal instanceof UserDetails ) { if ( username.equals( ( (UserDetails) ssPrincipal ).getUsername() ) ) { return (UserDetails) ssPrincipal; } } } UserDetails user = null; // user cache not available or user not in cache; do lookup List<GrantedAuthority> auths = null; List<GrantedAuthority> authorities = null; UserDetails newUser = null; if ( getUserDetailsService() != null ) { try { user = getUserDetailsService().loadUserByUsername( username ); // We will use the authorities from the Authentication object of SecurityContextHolder. //Authentication object is null then we will get it from IUserRoleListService if ( auth == null || auth.getAuthorities() == null || auth.getAuthorities().size() == 0 ) { if ( logger.isTraceEnabled() ) { logger.trace( "Authentication object from SecurityContextHolder is null," + " so getting the roles for [ " + user.getUsername() + " ] from IUserRoleListService " ); //$NON-NLS-1$ } List<String> roles = getUserRoleListService().getRolesForUser( JcrTenantUtils.getCurrentTenant(), username ); authorities = new ArrayList<GrantedAuthority>( roles.size() ); for ( int i = 0; i < roles.size(); i++ ) { authorities.add( new SimpleGrantedAuthority( roles.get( i ) ) ); } } else { authorities = new ArrayList<GrantedAuthority>( auth.getAuthorities().size() ); for ( GrantedAuthority authority : auth.getAuthorities() ) { authorities.add( authority ); } } auths = new ArrayList<GrantedAuthority>( authorities.size() ); // cache the roles while we're here for ( int i = 0; i < authorities.size(); i++ ) { String role = authorities.get( i ).getAuthority(); final String tenatedRoleString = JcrTenantUtils.getTenantedRole( role ); if ( cacheManager != null ) { Object rolePrincipal = cacheManager.getFromRegionCache( ROLE_CACHE_REGION, role ); if ( rolePrincipal == null ) { final SpringSecurityRolePrincipal ssRolePrincipal = new SpringSecurityRolePrincipal( tenatedRoleString ); cacheManager.putInRegionCache( ROLE_CACHE_REGION, role, ssRolePrincipal ); } } auths.add( new SimpleGrantedAuthority( tenatedRoleString ) ); } if ( logger.isTraceEnabled() ) { logger.trace( "found user in back-end " + user.getUsername() ); //$NON-NLS-1$ } } catch ( UsernameNotFoundException e ) { if ( logger.isTraceEnabled() ) { logger .trace( "username " + username + " not in cache or back-end; returning null" ); //$NON-NLS-1$ //$NON-NLS-2$ } } if ( user != null ) { if ( auths == null || auths.size() <= 0 ) { logger.trace( "Authorities are null, so creating an empty Auth array == " + user.getUsername() ); // auth is null so we are going to pass an empty auths collection auths = new ArrayList<GrantedAuthority>(); } String password = user.getPassword() != null ? user.getPassword() : ""; newUser = new User( user.getUsername(), password, user.isEnabled(), ACCOUNT_NON_EXPIRED, CREDS_NON_EXPIRED, ACCOUNT_NON_LOCKED, auths ); } } return newUser; } protected void checkInitialized() { synchronized ( initialized ) { if ( !initialized.get() ) { throw new IllegalStateException( Messages.getInstance().getString( "SpringSecurityPrincipalProvider.ERROR_0003_NOT_INITIALIZED" ) ); //$NON-NLS-1$ } } } /** * {@inheritDoc} * <p/> * <p> Not implemented. This method only ever called from method in {@code PrincipalManagerImpl} and that method is * never called. </p> */ public PrincipalIterator findPrincipals( final String simpleFilter ) { throw new UnsupportedOperationException(); } /** * {@inheritDoc} * <p/> * <p> Not implemented. This method only ever called from method in {@code PrincipalManagerImpl} and that method is * never called. </p> */ public PrincipalIterator findPrincipals( final String simpleFilter, final int searchType ) { throw new UnsupportedOperationException(); } /** * {@inheritDoc} * <p/> * <p> Not implemented. This method only ever called from method in {@code PrincipalManagerImpl} and that method is * never called. </p> */ public PrincipalIterator getPrincipals( final int searchType ) { throw new UnsupportedOperationException(); } protected UserDetailsService getUserDetailsService() { if ( null != userDetailsService ) { return userDetailsService; } else { if ( PentahoSystem.getInitializedOK() ) { userDetailsService = PentahoSystem.get( UserDetailsService.class ); return userDetailsService; } else { return null; } } } protected IUserRoleListService getUserRoleListService() { if ( null != userRoleListService ) { return userRoleListService; } else { if ( PentahoSystem.getInitializedOK() ) { userRoleListService = PentahoSystem.get( IUserRoleListService.class ); return userRoleListService; } else { return null; } } } private SpringSecurityRolePrincipal createSpringSecurityRolePrincipal( String principal ) { return new SpringSecurityRolePrincipal( JcrTenantUtils.getTenantedRole( principal ) ); } private void initSkipUserVerification( final Properties prop ) { skipUserVerification = SKIP_USER_VERIFICATION_DEFAULT_VALUE; // default behaviour if ( prop != null && prop.containsKey( SKIP_USER_VERIFICATION_PROP_KEY ) && !prop.getProperty( SKIP_USER_VERIFICATION_PROP_KEY ).isEmpty() ) { // reading property from the class initialization properties is useful for unit testing skipUserVerification = Boolean.valueOf( prop.getProperty( SKIP_USER_VERIFICATION_PROP_KEY, String.valueOf( SKIP_USER_VERIFICATION_DEFAULT_VALUE ) ) ); } else if ( systemConfig != null ) { try { // reading property from security.properties ( standard behaviour ) IConfiguration config = this.systemConfig.getConfiguration( "security" ); // security.properties if ( config != null && config.getProperties().containsKey( SKIP_USER_VERIFICATION_PROP_KEY ) && !config.getProperties().getProperty( SKIP_USER_VERIFICATION_PROP_KEY ).isEmpty() ) { skipUserVerification = Boolean.valueOf( config.getProperties().getProperty( SKIP_USER_VERIFICATION_PROP_KEY, String.valueOf( SKIP_USER_VERIFICATION_DEFAULT_VALUE ) ) ); } } catch ( Exception ex ) { logger.error( ex ); } } logger.info( "Property '" + SKIP_USER_VERIFICATION_PROP_KEY + "' is '" + skipUserVerification + "'" ); } }