Merge branch 'shibIntegration' into develop

This commit is contained in:
Robin Eklund 2011-07-07 16:13:26 +02:00
commit ba89f7f02d
6 changed files with 443 additions and 114 deletions

@ -1,5 +1,9 @@
package se.su.dsv.scipro;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.apache.wicket.Page;
@ -9,6 +13,7 @@ import org.apache.wicket.Response;
import org.apache.wicket.Session;
import org.apache.wicket.authorization.strategies.CompoundAuthorizationStrategy;
import org.apache.wicket.protocol.http.WebApplication;
import org.apache.wicket.protocol.http.WebRequest;
import org.odlabs.wiquery.ui.themes.IThemableApplication;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;
@ -44,18 +49,6 @@ import se.su.dsv.scipro.knol.resource.page.ResourcePage;
import se.su.dsv.scipro.loginlogout.pages.LoginPage;
import se.su.dsv.scipro.loginlogout.pages.LogoutPage;
import se.su.dsv.scipro.message.pages.PrivateMessagesPage;
import se.su.dsv.scipro.peer.pages.PeerRequestSubmissionPage;
import se.su.dsv.scipro.peer.pages.PeerReviewTemplateCreationPage;
import se.su.dsv.scipro.peer.pages.PeerReviewTemplatePage;
import se.su.dsv.scipro.peer.pages.PeerReviewTemplatePreviewPage;
import se.su.dsv.scipro.peer.pages.ProjectPeerPortalPage;
import se.su.dsv.scipro.peer.pages.ProjectPeerReviewGuidePage;
import se.su.dsv.scipro.peer.pages.ProjectPeerReviewPage;
import se.su.dsv.scipro.peer.pages.ProjectPeerStatsPage;
import se.su.dsv.scipro.peer.pages.SupervisorPeerPortalPage;
import se.su.dsv.scipro.peer.pages.SupervisorPeerReviewGuidePage;
import se.su.dsv.scipro.peer.pages.SupervisorPeerReviewPage;
import se.su.dsv.scipro.peer.pages.SupervisorPeerStatsPage;
import se.su.dsv.scipro.project.pages.FinalSeminarProjectListPage;
import se.su.dsv.scipro.project.pages.NoActiveProjectPage;
import se.su.dsv.scipro.project.pages.ProjectEventPage;
@ -69,6 +62,7 @@ import se.su.dsv.scipro.repository.pages.RepositoryDownloadPage;
import se.su.dsv.scipro.repository.pages.SysAdminFilePage;
import se.su.dsv.scipro.schedule.templates.pages.ScheduleTemplateDetailsPage;
import se.su.dsv.scipro.security.auth.ComponentSecurityLogger;
import se.su.dsv.scipro.security.auth.ExternalAuthenticationRequestHelper;
import se.su.dsv.scipro.security.auth.MetaDataActionStrategy;
import se.su.dsv.scipro.security.auth.RoleBasedAuthorizationStrategy;
import se.su.dsv.scipro.supervisor.pages.SupervisorAntiPlagiarismLinkPage;
@ -108,6 +102,11 @@ public class SciProApplication extends RepositoryApplication implements IThemabl
*/
private String systemNotice = null;
/**
* Logger instance.
* @TODO Inject
*/
private Logger logger = Logger.getLogger(this.getClass());
/**
* Constructor
*/
@ -274,8 +273,7 @@ public class SciProApplication extends RepositoryApplication implements IThemabl
@SuppressWarnings("unchecked")
protected void setKerberosConfigs(){
javax.servlet.ServletContext context = getServletContext();
Logger logger = Logger.getRootLogger();
java.util.Set<String> resources = context.getResourcePaths("/WEB-INF/classes/");
Set<String> resources = context.getResourcePaths("/WEB-INF/classes/");
if(!resources.contains(jaasPath) || !resources.contains(krb5Path)){
logger.log(Level.FATAL, "Path to authentication config files not correct. " +
"Users will not be able to log in!");
@ -307,6 +305,49 @@ public class SciProApplication extends RepositoryApplication implements IThemabl
public Session newSession(Request request, Response response) {
return new SciProSession(request);
}
@Override
public WebRequest newWebRequest(final HttpServletRequest request){
final WebRequest webRequest = super.newWebRequest(request);
if(attemptExternalAuthentication(webRequest)){
logger.debug("External authentication used");
}
return webRequest;
}
/**
* Private utility, just to keep stuff out of the calling method.
* In short: scan incoming request for REMOTE_USER, check for a session, create one if none exists, attempt authentication and return status.
* @param usingRequest
* @return true if external auth was completed, false if not.
*/
private boolean attemptExternalAuthentication(final WebRequest usingRequest){
SciProSession session = (SciProSession)getSessionStore().lookup(usingRequest);
final ExternalAuthenticationRequestHelper helper = new ExternalAuthenticationRequestHelper(usingRequest.getHttpServletRequest());
if(helper.isExternalAuthSupported()){//Attempt external auth
if(session == null){//Can't do this without a usable session, attempt manual bind
getSessionStore().bind(usingRequest,new SciProSession(usingRequest));
session = (SciProSession)getSessionStore().lookup(usingRequest);
}
if(session != null){
if(session.isLoggedIn()){
if(!helper.isRemoteUserValid(session.getUser())){//This check may not be needed and may hinder performance, but better safe than sorry for now.
logger.warn("User is logged in, but conflicting info is supplied via external authentication protocols.");
}
}else{
//logger.info("Attempting sign in with external auth data");
if(!helper.signIn(session)){
logger.error("User passes external authentication but cannot be signed in.");
}else{
logger.debug("Signed in user '"+helper.getExternalAuthRemoteUser()+"' via external authentication");
return true;
}
}
}else{
throw new IllegalStateException("External authentication was attempted, but no session was available.");
}
}
return false;
}
@Override
public ResourceReference getTheme(Session session) {

@ -57,6 +57,9 @@ public class SciProSession extends WebSession {
private String loggedInIdentity = null;
//Logger instance
private static Logger logger = Logger.getLogger(SciProSession.class);
/*
* It's important not to hold the actual object, if we do, no changes to it
* or it's attributes will be visible for the user unless they logout or re-select their active project.
@ -105,16 +108,115 @@ public class SciProSession extends WebSession {
return loggedInIdentity;
}
public synchronized boolean login(final String username, final String password) throws FailedLoginException, NullPointerException{
final boolean loginSuccess = authenticate(username, password);
if(loginSuccess){
loggedIn = true;
/**
* Local login procedures, delegates authentication to authenticate() and sign in procedures to signInAuthenticatedUser() if authentication succeeds.
* An administrator can log in as a different user in the system by using 'adminusername::otherusername' notation,
* this is implemented as a regular login followed by a su-like switch (via switchAuthenticatedUser).
* @param username is expected to be in the format "localdsvusername" or "localdsvusername::optionalotherusername".
*/
public boolean login(final String username, final String password) throws FailedLoginException, NullPointerException{
String[] tmp = username.split("::");
String authenticatedUserName = username;
String switchToUserName = null;
if(tmp.length > 0){
authenticatedUserName = tmp[0];
if(tmp.length > 1)
switchToUserName = tmp[1];
}
return loginSuccess;
//logger.info("Attempting authentication as "+authenticatedUserName);
final boolean authenticationSuccess = authenticate(authenticatedUserName, password);
if(!authenticationSuccess)
return false;
//logger.info("Successfully authenticated user "+authenticatedUserName);
final boolean signInSuccess = signInAuthenticatedUser(authenticatedUserName,"DSV.SU.SE");
if(switchToUserName != null){
if(!switchAuthenticatedUser(switchToUserName,"DSV.SU.SE"))
logger.error("Can't switch from '"+authenticatedUserName+"' to '"+switchToUserName+"'");
}
return signInSuccess;
}
/**
* Sign in a user and bypass normal authentication routines completely.
* This user has to already be present in database tables or login will fail.
* @param username is expected to be one of the users registered username
* @param realm is expected to be the matching realm from the supplied username
* @return status of the sign-in request
*/
public boolean signInAuthenticatedUser(final String username, final String realm){
//Query for the user
user = userDao.getUserByUsername(username,realm);
if(user == null){
try{
user = doUserLookup(username);
if(user == null){
throw new NullPointerException("No user with username "+username+" found in the database or in daisy.");
}
} catch (NullPointerException e) {
throw e;
}
}
//Set mail-address session attributes
setLoggedInIdentity(username+"@"+realm);
if(user.getEmailAddress() == null || user.getEmailAddress().trim().equals("")){
user.setEmailAddress(getLoggedInIdentity());
user = userDao.save(user);
}
//Assign roles
if(roleDao.isStudent(user)){
iRoles.add(new Student());
}
if(roleDao.isExternal(user)){
iRoles.add(new External());
}
if(roleDao.isEmployee(user)){
iRoles.add(new Employee());
}
if(roleDao.isAdmin(user)){
iRoles.add(new Admin());
}
if(roleDao.isSysadmin(user)){
iRoles.add(new SysAdmin());
}
if(iRoles.isEmpty()){
iRoles.add(new DefaultRole());
}
//Set active project from users settings
UserSettings userSettings = userSettingsDao.getUserSettings(user);
if(userSettings != null){
Project activeProject = userSettings.getActiveProject();
if(activeProject != null)
activeProjectId = activeProject.getId();
}
else {
userSettings = new UserSettings(user);
}
// With this userSettings.created() will return a users first login date and userSettings.modified() will return last login
userSettings.setLastModified(new Date());
userSettings = userSettingsDao.save(userSettings);
logger.info("User: "+getLoggedInIdentity()+ " logged in to "+this.getApplication().getClass().getSimpleName()+
" as: "+user.getFirstName()+" "+user.getLastName()+" "+user.getUserNames()+ " at: "+new Date());
loggedIn = true;
return loggedIn;
}
/**
* Switch currently logged in user (basically a poor mans 'su') to someone else, the currently logged in user has to be registered as an admin for this to succeed.
* Also, the new user has to exist in local database tables or it will fail.
* @param suUser
* @param suRealm
* @return true if the switch was successful, else false.
*/
public boolean switchAuthenticatedUser(final String suUser, final String suRealm){
logger.info("Currently logged in user: '"+user.getEmailAddress()+"' attempting switch to '"+suUser+"'");
if(suUser != null && roleDao.isSysadmin(user)){
iRoles.clear();
return signInAuthenticatedUser(suUser, suRealm);
}else{
logger.error("User does not have the privilege to switch user");
}
return false;
}
public synchronized void logout()
{
loggedIn = false;
@ -123,96 +225,18 @@ public class SciProSession extends WebSession {
public synchronized final boolean isLoggedIn() {
return loggedIn;
}
/**
* Uses normal Wicket/JAAS stuff to authenticate a user/password combination.
* @param username
* @param password
* @return true if authentication succeeds, else false.
* @throws NullPointerException
* @throws FailedLoginException
*/
private synchronized boolean authenticate(String username, final String password) throws NullPointerException, FailedLoginException {
Authenticator auth = new Authenticator();
/*
* An administrator can log in as a different user in the system by using 'adminusername::otherusername' for login
*/
String[] tmp = username.split("::");
String loggedInAsUsername = null;
if(tmp.length > 0){
username = tmp[0];
if(tmp.length > 1)
loggedInAsUsername = tmp[1];
}
try {
try{
auth.authenticate(username, password);
this.user = userDao.getUserByUsername(username);
if( user == null){
try{
user = doUserLookup(username);
if(user == null){
throw new NullPointerException("No user with username "+username+" found in the database, or in daisy, despite successful authentication ");
}
} catch (NullPointerException e) {
throw e;
}
}
setLoggedInIdentity(username+"@"+"dsv.su.se");
if(user.getEmailAddress() == null || user.getEmailAddress().trim().equals("")){
user.setEmailAddress(getLoggedInIdentity());
user = userDao.save(user);
}
/*
* Here we switch the logged in user to be that of the person chosen be the logged-in-as by a sysadmin
*/
if(loggedInAsUsername != null && roleDao.isSysadmin(user)){
user = userDao.getUserByUsername(loggedInAsUsername);
if( user == null){
try{
user = doUserLookup(loggedInAsUsername);
if(user == null){
throw new NullPointerException("No user with username "+loggedInAsUsername+" found in the database, or in daisy, despite successful authentication ");
}
} catch (NullPointerException e) {
throw e;
}
}
}
if(roleDao.isStudent(user)){
iRoles.add(new Student());
}
if(roleDao.isExternal(user)){
iRoles.add(new External());
}
if(roleDao.isEmployee(user)){
iRoles.add(new Employee());
}
if(roleDao.isAdmin(user)){
iRoles.add(new Admin());
}
if(roleDao.isSysadmin(user)){
iRoles.add(new SysAdmin());
}
if(iRoles.isEmpty()){
iRoles.add(new DefaultRole());
}
/*
* Set active project from users settings
*/
UserSettings userSettings = userSettingsDao.getUserSettings(user);
if(userSettings != null){
Project activeProject = userSettings.getActiveProject();
if(activeProject != null)
activeProjectId = activeProject.getId();
}
else {
userSettings = new UserSettings(user);
}
/*
* With this userSettings.created() will return a users first login date and userSettings.modified() will return last login
*/
userSettings.setLastModified(new Date());
userSettings = userSettingsDao.save(userSettings);
Logger logger = Logger.getLogger("Application");
logger.log(Level.INFO, "User: "+getLoggedInIdentity()+ " logged in to "+this.getApplication().getClass().getSimpleName()+
" as: "+user.getFirstName()+" "+user.getLastName()+" "+user.getUserNames()+ " at: "+new Date());
return true;
} catch (FailedLoginException e) {
//e.printStackTrace();

@ -0,0 +1,179 @@
package se.su.dsv.scipro.security.auth;
import java.security.Policy.Parameters;
import java.util.Enumeration;
import java.util.Map;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import org.apache.log4j.Logger;
import se.su.dsv.scipro.SciProSession;
import se.su.dsv.scipro.data.dataobjects.User;
import se.su.dsv.scipro.data.dataobjects.Username;
/**
* Helper class for performing authentication via request-headers.
* Typical usage:
* <code>
* helper = new ExternalAuthentificationRequestHelper(request);
* if(helper.isExternalAuthSupported() && helper.signIn(session))
* success();
* else
* fail();
* </code>
* @author robi-ekl
*
*/
public final class ExternalAuthenticationRequestHelper{
//Wrapped request
private final HttpServletRequest req;
//remote user attribute
private String remoteUser;
//if remote user is on the username@realm form, this attribute holds the username
private String remoteUserId;
//if remote user is on the username@realm form, this attribute holds the realm
private String remoteUserRealm;
//logger instance
private Logger logger = Logger.getLogger(this.getClass());
/**
* Construct a utility wrapper from a servlet request.
* @param request
*/
public ExternalAuthenticationRequestHelper(final HttpServletRequest request){
req = request;
formatUserString();
}
/**
* Exposed query method.
* @return The remote user as retrieved from the request, null if none exists.
*/
public String getExternalAuthRemoteUser(){
return remoteUser;
}
/**
* Exposed query method.
* @return If remote user is on the username@realm form, this attribute holds the userid, else getExternalAuthRemoteUser().
*/
public String getExternalAuthRemoteUserId(){
return remoteUserId;
}
/**
* Exposed query method.
* @return If remote user is on the username@realm form, this attribute holds the realm, else null.
*/
public String getExternalAuthRemoteUserRealm(){
return remoteUserRealm;
}
/**
* Internal query method
*/
private boolean isExternalAuthInfoOnRequest(){
return (req.getRemoteUser()!=null);
}
/**
* Internal query method
*/
private String getExternalAuthType(){
return req.getAuthType();
}
/**
* Query method.
* @return true if the application is configured to accept external authentication and the needed information is available on the request, else false.
*/
public boolean isExternalAuthSupported(){
return (true && isExternalAuthInfoOnRequest());
}
/**
* Private utility method for dumping headers.
*/
public void dumpAuthInfo(){
logger.debug("---Standard methods---");
logger.debug("Request implementation:" + req.getClass().getName());
logger.debug("getAuthType = '" + getExternalAuthType() +"'");
logger.debug("getRemoteUser = '"+getExternalAuthRemoteUser()+"'");
logger.debug("---Scanning explicitly named attributes---");
final String[] params = {"Shib-Application-ID","Shib-Session-ID","Shib-Identity-Provider",
"Shib-Authentication-Instant","Shib-Authentication-Method","Shib-AuthnContext-Class","Shib-AuthnContext-Decl","REMOTE_USER","HTTP_REMOTE_USER"};
for(final String param:params){
logger.debug("getParameter'"+param+"' = '"+req.getParameter(param)+"'");
logger.debug("getHeader'"+param+"' = '"+req.getHeader(param)+"'");
logger.debug("getAttribute'"+param+"' = '"+req.getAttribute(param)+"'");
}
{
logger.debug("---Iterating over all attributes, parameters, headers and ENV-variables---");
logger.debug("All available attributes: ");
@SuppressWarnings("rawtypes") Enumeration attributes = req.getAttributeNames();
while(attributes.hasMoreElements()){
String name = (String)attributes.nextElement();
logger.debug("\t'"+name+"' = '"+req.getAttribute(name)+"'");
}
}
{
logger.debug("All available parameters: ");
@SuppressWarnings("rawtypes") Enumeration parameters = req.getParameterNames();
while(parameters.hasMoreElements()){
String name = (String)parameters.nextElement();
logger.debug("\t'"+name+"' = '"+req.getParameter(name)+"'");
}
}
{
logger.debug("All available headers: ");
@SuppressWarnings("rawtypes") Enumeration headers = req.getHeaderNames();
while(headers.hasMoreElements()){
String name = (String)headers.nextElement();
logger.debug("\t'"+name+"' = '"+req.getHeader(name)+"'");
}
}
{
logger.debug("All available ENV-variables:");
Map<String,String> envs = System.getenv();
for(String key : envs.keySet()){
logger.debug("'"+key+"' = '"+envs.get(key)+"'");
}
}
}
/**
* Query method for checking the validity of a User against this wrapped request.
* @param user
* @return true if the user matches that on the request, false if not.
*/
public boolean isRemoteUserValid(final User user){
if(user != null && isExternalAuthSupported()){
final Set<Username> usernames = user.getUserNames();
for(final Username username:usernames){
if(username.getUserName().equals(getExternalAuthRemoteUserId()) && username.getRealm().equals(getExternalAuthRemoteUserRealm())){
return true;
}
}
}
return false;
}
/**
* Private utility method.
*/
private void formatUserString(){
remoteUser = req.getRemoteUser();
if(remoteUser != null && remoteUser.contains("@")){
String[] split = remoteUser.split("@");
remoteUserId = split[0];
remoteUserRealm = split[1].toUpperCase();
}else{
remoteUserId = remoteUser;
remoteUserRealm = "DSV.SU.SE";
}
}
/**
* Signs the stored remote user in on the given SciProSession.
* @param session
* @return true on success, else false.
*/
public boolean signIn(final SciProSession session){
if(session != null){
//dumpAuthInfo();
return session.signInAuthenticatedUser(getExternalAuthRemoteUserId(), getExternalAuthRemoteUserRealm());
}
return false;
}
}

@ -0,0 +1,71 @@
package se.su.dsv.scipro.security.auth;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import org.apache.log4j.Logger;
/**
* Throw-away implementation of a servlet filter, main task is to fake the getRemoteUser() call for the request chain.
*/
public final class MockRemoteUserFilter implements Filter {
private Logger logger = Logger.getLogger(this.getClass());
//Default value unless supplied via init parameter
private String fakedUser = "SOME_GUY";
private FilterConfig cfg = null;
/**
* Default constructor.
*/
public MockRemoteUserFilter() {
}
/**
* @see Filter#destroy()
*/
public void destroy() {
cfg = null;
}
/**
* Wraps the passed request and alters the behavior of getRemoteUser() for later links of the chain.
* @see Filter#doFilter(ServletRequest, ServletResponse, FilterChain)
*/
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
logger.info("Faking external authentication user: "+fakedUser);
if(cfg != null){
HttpServletRequestWrapper wrapper = new ModifiedRemoteUserRequestWrapper((HttpServletRequest)request,fakedUser);
// pass the request along the filter chain
chain.doFilter(wrapper, response);
return;
}
chain.doFilter(request, response);
}
/**
* @see Filter#init(FilterConfig)
*/
public void init(FilterConfig fConfig) throws ServletException {
cfg = fConfig;
if(cfg!=null){
fakedUser = cfg.getInitParameter("fakedUser");
}
}
/**
* Private RequestWrapper, of no interest to anyone outside of this class.
*/
class ModifiedRemoteUserRequestWrapper extends HttpServletRequestWrapper{
private final String fakedUser;
ModifiedRemoteUserRequestWrapper(final HttpServletRequest request,final String fakedUser){
super(request);
this.fakedUser = fakedUser;
}
@Override
public String getRemoteUser(){
return fakedUser;
}
}
}

@ -1,6 +1,6 @@
log4j.appender.Stdout=org.apache.log4j.ConsoleAppender
log4j.appender.Stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.Stdout.layout.conversionPattern=%-5p - %-26.26c{1} - %m\n
log4j.appender.Stdout.layout.conversionPattern=%d{HH:mm:ss,SSS}[%p] - %c{1} - %m\n
log4j.rootLogger=INFO,Stdout
@ -8,5 +8,3 @@ log4j.logger.wicket=INFO
log4j.logger.wicket.protocol.http.HttpSessionStore=INFO
log4j.logger.wicket.version=INFO
log4j.logger.wicket.RequestCycle=INFO

@ -10,11 +10,29 @@
<filter-name>osiv</filter-name>
<filter-class>org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>osiv</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- Filter for faking an authenticated REMOTE_USER, should never be used in a production environment-->
<!-- filter>
<description>
</description>
<display-name>MockRemoteUserFilter</display-name>
<filter-name>MockRemoteUserFilter</filter-name>
<filter-class>se.su.dsv.scipro.security.auth.MockRemoteUserFilter</filter-class>
<init-param>
<description>
</description>
<param-name>fakedUser</param-name>
<param-value>robi-ekl@dsv.su.se</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>MockRemoteUserFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping-->
<filter>
<filter-name>wicket.WicketWarp</filter-name>
@ -24,11 +42,9 @@
<param-value>se.su.dsv.scipro.SciProApplication</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>wicket.WicketWarp</filter-name>
<url-pattern>/*</url-pattern>
<!-- These to be able to enable a non-standard not found page -->
<dispatcher>REQUEST</dispatcher>
<dispatcher>ERROR</dispatcher>