/* * Copyright (c) 2016 OBiBa. All rights reserved. * * This program and the accompanying materials * are made available under the terms of the GNU Public License v3.0. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.apache.shiro.realm.crowd; import java.rmi.RemoteException; import java.util.EnumSet; import java.util.Map; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.DisabledAccountException; import org.apache.shiro.authc.ExpiredCredentialsException; import org.apache.shiro.authc.IncorrectCredentialsException; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.authc.pam.UnsupportedTokenException; import org.apache.shiro.authz.AuthorizationException; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.atlassian.crowd.exception.ApplicationAccessDeniedException; import com.atlassian.crowd.exception.ExpiredCredentialException; import com.atlassian.crowd.exception.InactiveAccountException; import com.atlassian.crowd.exception.InvalidAuthenticationException; import com.atlassian.crowd.exception.InvalidAuthorizationTokenException; import com.atlassian.crowd.exception.UserNotFoundException; import com.atlassian.crowd.service.soap.client.SecurityServerClient; /** * A realm that authenticates and obtains its roles from a Atlassian Crowd server. * <p> * The Crowd server as the concept of role and group memberships. Both of which can be can be mapped to Shiro roles. * This realm implementation allows the deployer to select either or both memberships to map to Shiro roles. * </p> * <h5>Crowd client configuration</h5> * <p> * In your applicationContext.xml, add the following: * </p> * <pre> * {@code<!-- This will load Crowd SecurityServerClient stuff --> * <import resource="classpath:org/obiba/org.obiba.security/crowd-context.xml" /> * * <bean id="crowdRealm" class="CrowdRealm" autowire="byType"> * <property name="roleSources"> * <bean class="java.util.EnumSet" factory-method="of"> * <constructor-arg> * <bean class="RoleSource" factory-method="valueOf"> * <constructor-arg value="ROLES_FROM_CROWD_GROUPS" /> * </bean> * </constructor-arg> * </bean> * </property> * <!-- Specify mapping between Crowd groups/roles and your application roles --> * <property name="groupRolesMap"> * <map> * <entry key="group1" value="SYSTEM_ADMINISTRATOR" /> * <entry key="group2" value="PARTICIPANT_MANAGER" /> * </map> * </property> * </bean> * } * </pre> * You also need to tell where is the Crowd instance to SecurityServerClient by defining a crowd.properties file. You * can copy this file from your Crowd installation folder CROWD_INSTALL/client or from folder * obiba-commons/obiba-core/src/main/test/resources. * <p> * Copy also the <b>crowd-ehcache.xml</b> file to configure caching. * </p> * <p> * Add these 2 properties to your config file: * </p> * <pre> * crowd.properties.path = file:/config-path/crowd.properties * crowd-ehcache.xml.path = file:/config-path/crowd-ehcache.xml * </pre> * Here is a template of crowd.properties: * <pre> * application.name = crowd_client * application.password = password * application.login.url = http://localhost:8095/crowd/console/ * * crowd.server.url = http://localhost:8095/crowd/services/ * crowd.base.url = http://localhost:8095/crowd/ * * session.isauthenticated = session.isauthenticated * session.tokenkey = session.tokenkey * session.validationinterval = 2 * session.lastvalidation = session.lastvalidation * </pre> * Add crowd-integration-client dependency to your pom.xml: * <pre>{@code * <dependency> * <groupId>com.atlassian.crowd</groupId> * <artifactId>crowd-integration-client</artifactId> * <version>2.5.1</version> * </dependency> * <!-- Need to define this manually because of Maven bug --> * <dependency> * <groupId>org.codehaus.xfire</groupId> * <artifactId>xfire-aegis</artifactId> * <version>1.2.6</version> * </dependency> * }</pre> * * @version $Rev: 1026849 $ $Date: 2010-10-24 11:08:56 -0700 (Sun, 24 Oct 2010) $ * @see <a href="https://confluence.atlassian.com/display/CROWD/The+crowd.properties+File">https://confluence.atlassian.com/display/CROWD/The+crowd.properties+File</a> * @see <a href="https://confluence.atlassian.com/display/CROWD024/Passing+the+crowd.properties+File+as+an+Environment+Variable">https://confluence.atlassian.com/display/CROWD024/Passing+the+crowd.properties+File+as+an+Environment+Variable</a> * @see <a href="https://code.google.com/a/apache-extras.org/p/atlassian-crowd-realm">https://code.google.com/a/apache-extras.org/p/atlassian-crowd-realm</a> */ @SuppressWarnings("UnusedDeclaration") public class CrowdRealm extends AuthorizingRealm { private static final Logger log = LoggerFactory.getLogger(CrowdRealm.class); private SecurityServerClient securityServerClient; private EnumSet<RoleSource> roleSources = EnumSet.of(RoleSource.ROLES_FROM_CROWD_ROLES); private Map<String, String> groupRolesMap; /** * A simple constructor for a Shiro Crowd realm. * <p> * It is expected that an initialized Crowd client will be subsequently * set using {@link #setSecurityServerClient(SecurityServerClient)}. * </p> */ public CrowdRealm() { } /** * Initialize the Shiro Crowd realm with an instance of * {@link SecurityServerClient}. The method {@link SecurityServerClient#authenticate} * is assumed to be called by the creator of this realm. * * @param securityServerClient an instance of {@link SecurityServerClient} to be used when communicating with the Crowd server */ public CrowdRealm(SecurityServerClient securityServerClient) { if(securityServerClient == null) throw new IllegalArgumentException("Crowd client cannot be null"); this.securityServerClient = securityServerClient; } /** * Set the client to use when communicating with the Crowd server. * <p> * It is assumed that the Crowd client has already authenticated with the * Crowd server. * </p> * @param securityServerClient the client to use when communicating with the Crowd server */ public void setSecurityServerClient(SecurityServerClient securityServerClient) { this.securityServerClient = securityServerClient; } /** * Obtain the kinds of Crowd memberships that will serve as sources for * Shiro roles. * * @return an enum set of role source directives. */ public EnumSet<RoleSource> getRoleSources() { return roleSources; } /** * Set the kinds of Crowd memberships that will serve as sources for * Shiro roles. * * @param roleSources an enum set of role source directives. */ public void setRoleSources(EnumSet<RoleSource> roleSources) { this.roleSources = roleSources; } /** * Set mapping between Crowd groups/roles and application roles * * @param groupRolesMap */ public void setGroupRolesMap(Map<String, String> groupRolesMap) { this.groupRolesMap = groupRolesMap; } /** * {@inheritDoc} */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { log.trace("Collecting authorization info from realm {}", getName()); SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); for(Object principal : principalCollection.fromRealm(getName())) { log.trace("Collecting roles from {}", principal); try { collectCrowdRoles(authorizationInfo, principal); collectCrowdGroups(authorizationInfo, principal); } catch(RemoteException re) { throw new AuthorizationException("Unable to obtain Crowd group memberships for principal " + principal + ".", re); } catch(InvalidAuthenticationException e) { throw new AuthorizationException("Unable to obtain Crowd group memberships for principal " + principal + ".", e); } catch(InvalidAuthorizationTokenException e) { throw new AuthorizationException("Unable to obtain Crowd group memberships for principal " + principal + ".", e); } catch(UserNotFoundException e) { throw new AuthorizationException("Unable to obtain Crowd group memberships for principal " + principal + ".", e); } } return authorizationInfo; } private void collectCrowdGroups(SimpleAuthorizationInfo authorizationInfo, Object principal) throws RemoteException, InvalidAuthorizationTokenException, UserNotFoundException, InvalidAuthenticationException { if(roleSources.contains(RoleSource.ROLES_FROM_CROWD_GROUPS)) { log.trace("Collecting Shiro roles from Crowd group memberships"); for(String group : securityServerClient.findGroupMemberships(principal.toString())) { addRole(authorizationInfo, group); } } } private void collectCrowdRoles(SimpleAuthorizationInfo authorizationInfo, Object principal) throws RemoteException, InvalidAuthorizationTokenException, UserNotFoundException, InvalidAuthenticationException { if(roleSources.contains(RoleSource.ROLES_FROM_CROWD_ROLES)) { log.trace("Collecting Shiro roles from Crowd role memberships"); for(String role : securityServerClient.findRoleMemberships(principal.toString())) { addRole(authorizationInfo, role); } } } private void addRole(SimpleAuthorizationInfo authorizationInfo, String role) { if(groupRolesMap == null) { log.trace("Adding role {}", role); authorizationInfo.addRole(role); } else { String mappedRole = groupRolesMap.get(role); if(mappedRole == null) { log.warn("Role {} is not mapped", role); } else { log.trace("Adding role {} (mapped from {})", mappedRole, role); authorizationInfo.addRole(mappedRole); } } } /** * {@inheritDoc} */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { log.trace("Collecting authentication info from realm {}", getName()); log.trace("securityServerClient: {}", securityServerClient); if(!(authenticationToken instanceof UsernamePasswordToken)) { throw new UnsupportedTokenException( "Unsupported token of type " + authenticationToken.getClass().getName() + ". " + UsernamePasswordToken.class.getName() + " is required." ); } UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken; try { securityServerClient.authenticatePrincipalSimple(token.getUsername(), new String(token.getPassword())); return new SimpleAuthenticationInfo(token.getPrincipal(), token.getCredentials(), getName()); } catch(RemoteException e) { throw new AuthenticationException("Unable to obtain authenticate principal " + token.getUsername() + " in Crowd.", e); } catch(InvalidAuthenticationException e) { throw new IncorrectCredentialsException("Incorrect credentials for principal " + token.getUsername() + " in " + "Crowd.", e); } catch(ApplicationAccessDeniedException e) { throw new AuthenticationException("Access denied for principal " + token.getUsername() + " in Crowd.", e); } catch(InvalidAuthorizationTokenException e) { throw new IncorrectCredentialsException("Incorrect credentials for principal " + token.getUsername() + " in " + "Crowd.", e); } catch(InactiveAccountException e) { throw new DisabledAccountException("Inactive principal " + token.getUsername() + " in Crowd.", e); } catch(ExpiredCredentialException e) { throw new ExpiredCredentialsException("Expired principal " + token.getUsername() + " in Crowd.", e); } } }