/*
* 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);
}
}
}