001/**
002 * Copyright 2005-2016 The Kuali Foundation
003 *
004 * Licensed under the Educational Community License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.opensource.org/licenses/ecl2.php
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.kuali.rice.kim.sesn;
017
018import java.util.Date;
019import java.util.HashMap;
020import java.util.Iterator;
021import java.util.List;
022import java.util.Map;
023
024import org.apache.commons.logging.Log;
025import org.apache.commons.logging.LogFactory;
026import org.kuali.rice.kim.sesn.timeouthandlers.TimeoutHandler;
027import org.springframework.dao.IncorrectResultSizeDataAccessException;
028import org.springframework.jdbc.core.JdbcTemplate;
029
030/**
031 * This class is used to interface with the distributed session database.
032 * 
033 * TODO: add clear principals to clearSesn
034 * @author Kuali Rice Team (rice.collab@kuali.org)
035 *
036 */
037public class DistributedSession {
038    public static final String DEFAULT_PREFIX="DST";
039    private static String prefix = DEFAULT_PREFIX;
040    private JdbcTemplate jdbcTemplate;
041    private TimeoutHandler timeoutHandler;
042    private boolean allowInsertOnTouch = false;
043    
044    private static final Log logger = LogFactory.getLog(DistributedSession.class);
045
046    /**
047     * @param timeoutHandler the timeoutHandler to set
048     */
049    public void setTimeoutHandler(TimeoutHandler timeoutHandler) {
050        this.timeoutHandler = timeoutHandler;
051    }
052
053    /**
054     * @param jdbcTemplate the jdbcTemplate to set
055     */
056    public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
057        this.jdbcTemplate = jdbcTemplate;
058    }
059    
060    /**
061     * This method determines if the Session Ticket is valid.
062     * 
063     * @param DST - the Distributed Session Ticket
064     * @return true if the session is valid
065     */
066    public boolean isSesnValid(String DST) {
067        logger.debug("isSesnValid(DST)");
068        return isSesnValid (DST, new HashMap<String,Object>());
069    }
070    
071    /**
072     * This method determines if the Session Ticket is valid.
073     * 
074     * @param DST - the Distributed Session Ticket
075     * @param timoutArgs - Additional information on which to base timeouts
076     * @return true if the session is valid
077     */
078    public boolean isSesnValid(String DST, Map<String,Object> timeoutArgs) {
079        logger.debug("isSesnValid(DST, timeoutArgs)");
080        boolean bRet = false;
081        String sql = "select sesnID, lastAccessDt, maxIdleTime from authnsesn where sesnID=?";
082        
083        if (DST != null) {
084            Object[] args = { DST };
085            
086            try {
087                Map<String,Object> fields = jdbcTemplate.queryForMap(sql, args);
088                fields.put("maxIdleTime", this.getMaxIdleTime((Long)fields.get("maxIdleTime"), (Date)fields.get("lastAccessDt")));
089                fields.putAll(timeoutArgs);
090                
091                if (logger.isDebugEnabled()) {
092                    logger.debug("ARGUMENTS number:" + fields.size());
093                    for (Iterator<Map.Entry<String,Object>> i = fields.entrySet().iterator(); i.hasNext(); ) {
094                        Map.Entry<String,Object> entry = (Map.Entry<String,Object>)i.next();
095                        logger.debug("ARGUMENT " + entry.getKey() + ":" + entry.getValue());
096                    }
097                }
098                
099                if(!timeoutHandler.hasTimedOut(fields)) {
100                    logger.debug("Session not timed out");
101                    bRet = true;
102                } else {
103                    logger.debug("Session timed out");
104                }
105            } catch (Exception e) {
106                logger.debug(e);
107            }
108        } 
109        else {
110            logger.debug("Session ID is null");           
111        }
112                
113        return bRet;
114    }
115    
116    
117    /**
118     * This method returns the list of principals currently authenticated
119     * that are associated with the provided Distributed Session Ticket
120     * 
121     * @param DST - the Distributed Session Ticket
122     * @return the List of authenticated principals
123     */
124    public List<String> getAuthenticatedPricipals(String DST) {
125        String sql = "select principalID from authnsesn where sesnID=?";
126        Object args[] = { DST };
127        
128        return jdbcTemplate.queryForList(sql, args, String.class);
129    }
130    
131    /**
132     * This method removes the session
133     * 
134     * @param DST - the Distributed Session Ticket
135     */
136    public void clearSesn(String DST) {
137        String sql = "delete from authnsesn where sesnID='" + DST + "'";
138        
139        jdbcTemplate.execute(sql);
140    }
141    
142    
143    /**
144     * This method takes an authenticated principal and generates a
145     * Distributed Session Ticket and updates the session database
146     * 
147     * @param principalID - the id of the authenticated entity
148     * @return DST - the Distributed Session Ticket
149     */
150    public String createSesn(String principalID) {
151        String DST = this.generateDST();
152        
153        this.touchSesn(DST);
154        this.addPrincipalToSesn(DST, principalID);
155
156        return DST;
157    }
158    
159    /**
160     * This method generates a unique Distributed Session Ticket
161     * 
162     * @return DST - the Distributed Session Ticket
163     */
164    public String generateDST() {
165        return prefix + "-" + SessionIdGenerator.getNewString();
166    }
167    
168    /**
169     * This method updates the session
170     * 
171     * @param DST - the Distributed Session Ticket
172     */
173    public void touchSesn(String DST) {
174        String sql = "select lastAccessDt, maxIdleTime from authnsesn where sesnID=?";
175        String updateSql = "";
176        Object[] args = { DST },
177               updateArgs;
178        Long maxIdleTime;
179        
180        try {
181            if (logger.isDebugEnabled()) {
182                logger.debug("ARGUMENTS number:" + args.length);
183                logger.debug("ARGUMENTS 0:" + args[0]);
184            }
185            Map<String,Object> fields = jdbcTemplate.queryForMap(sql, args);
186            Date lastAccessDt = (Date)fields.get("lastAccessDt");
187            if (logger.isDebugEnabled()) {
188                logger.debug("Last Access:" + lastAccessDt);
189            }
190            maxIdleTime = getMaxIdleTime((Long)fields.get("maxIdleTime"), lastAccessDt);
191            
192            
193            updateSql = "update authnsesn set lastAccessDt=NOW(), maxIdleTime = ? where sesnID=?";
194            updateArgs = new Object[] { maxIdleTime, DST };
195            jdbcTemplate.update(updateSql, updateArgs);
196        } 
197        // catch if no or more than 1 results are returned
198        catch (IncorrectResultSizeDataAccessException ex) {
199            if (this.allowInsertOnTouch) {
200                maxIdleTime = new Long(0);
201                
202                updateSql = "insert into authnsesn (sesnID, insertDt, lastAccessDt, maxIdleTime) values (?, NOW(), NOW(), ?)";
203                updateArgs = new Object[] { DST, maxIdleTime };
204                jdbcTemplate.update(updateSql, updateArgs);
205            }
206        }
207    }
208    
209    
210    /**
211     * This method returns the greater of the stored max idle time and 
212     * time since last access
213     * 
214     * @param oldMaxIdleTime - the previous max idle time
215     * @param lastAccessDt - the timestamp of last access
216     * @return the max idle time
217     */
218    public Long getMaxIdleTime(Long oldMaxIdleTime, Date lastAccessDt) {
219        Long maxIdleTime = oldMaxIdleTime;
220        
221        if (logger.isDebugEnabled()) {
222            logger.debug("Max Idle:" + maxIdleTime);
223        }
224        long curIdleTime = System.currentTimeMillis()-lastAccessDt.getTime();
225        if (logger.isDebugEnabled()) {
226            logger.debug("Curr Idle:" + curIdleTime);
227        }
228        if (curIdleTime > maxIdleTime) {
229            maxIdleTime = new Long(curIdleTime);
230        }
231        
232        return maxIdleTime;
233    }
234    
235    /**
236     * This method appends a principal to an active session
237     * 
238     * @param DST - the Distributed Session Ticket
239     * @param principalID - the id of the authenticated entity
240     */
241    public void addPrincipalToSesn(String DST, String principalID) {
242        // this will fail if the record already exists 
243        try {
244            String updateSql = "insert into authnsesnprincipal (sesnID, principalID) values (?, ?)";
245            
246            jdbcTemplate.update(updateSql, new Object[] { DST, principalID });
247            if (logger.isDebugEnabled()) {
248                logger.debug("Added Principal to Sesn:" + principalID + " " + DST);
249            }
250        }
251        catch (Exception e) {
252            if (logger.isDebugEnabled()) {
253                logger.debug("Principal Probably already exists:" + principalID + " " + DST);
254            }
255        }
256    }
257
258    /**
259     * @return the prefix
260     */
261    public static String getPrefix() {
262        return DistributedSession.prefix;
263    }
264
265    /**
266     * @param prefix the prefix to set
267     */
268    public static void setPrefix(String prefix) {
269        DistributedSession.prefix = prefix;
270    }
271
272    /**
273     * @param allowInsertOnTouch the allowInsertOnTouch to set
274     */
275    public void setAllowInsertOnTouch(boolean allowInsertOnTouch) {
276        this.allowInsertOnTouch = allowInsertOnTouch;
277    }
278}