/*
 * JBoss, Home of Professional Open Source.
 * See the COPYRIGHT.txt file distributed with this work for information
 * regarding copyright ownership.  Some portions may be licensed
 * to Red Hat, Inc. under one or more contributor license agreements.
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library 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
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301 USA.
 */

package org.teiid.services;


import java.security.Principal;
import java.security.acl.Group;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;

import javax.security.auth.Subject;
import javax.security.auth.login.LoginException;

import org.teiid.adminapi.VDB;
import org.teiid.adminapi.VDB.ConnectionType;
import org.teiid.adminapi.impl.SessionMetadata;
import org.teiid.adminapi.impl.VDBMetaData;
import org.teiid.client.security.InvalidSessionException;
import org.teiid.client.security.LogonException;
import org.teiid.client.security.SessionToken;
import org.teiid.core.CoreConstants;
import org.teiid.core.util.ArgCheck;
import org.teiid.core.util.PropertiesUtils;
import org.teiid.deployers.VDBRepository;
import org.teiid.dqp.internal.process.DQPCore;
import org.teiid.dqp.internal.process.DQPWorkContext;
import org.teiid.dqp.service.SessionService;
import org.teiid.dqp.service.SessionServiceException;
import org.teiid.logging.AuditMessage;
import org.teiid.logging.LogConstants;
import org.teiid.logging.LogManager;
import org.teiid.logging.MessageLevel;
import org.teiid.net.ServerConnection;
import org.teiid.net.TeiidURL;
import org.teiid.net.socket.AuthenticationType;
import org.teiid.runtime.RuntimePlugin;
import org.teiid.security.Credentials;
import org.teiid.security.GSSResult;
import org.teiid.security.SecurityHelper;
import org.teiid.vdb.runtime.VDBKey;


/**
 * This class serves as the primary implementation of the Session Service.
 */
public class SessionServiceImpl implements SessionService {
	public static final String GSS_PATTERN_PROPERTY = "gss-pattern"; //$NON-NLS-1$
	public static final String PASSWORD_PATTERN_PROPERTY = "password-pattern"; //$NON-NLS-1$
	public static final String SECURITY_DOMAIN_PROPERTY = "security-domain"; //$NON-NLS-1$
	public static final String AUTHENTICATION_TYPE_PROPERTY = "authentication-type"; //$NON-NLS-1$
	public static final String AT = "@"; //$NON-NLS-1$
	/*
	 * Configuration state
	 */
    private long sessionMaxLimit = DEFAULT_MAX_SESSIONS;
	private long sessionExpirationTimeLimit = DEFAULT_SESSION_EXPIRATION;
	private AuthenticationType defaultAuthenticationType = AuthenticationType.USERPASSWORD;
	
	private static boolean CHECK_PING = PropertiesUtils.getBooleanProperty(System.getProperties(), "org.teiid.checkPing", true); //$NON-NLS-1$
	/*
	 * Injected state
	 */
	private VDBRepository vdbRepository;
    protected SecurityHelper securityHelper;

    private DQPCore dqp;

    private Map<String, SessionMetadata> sessionCache = new ConcurrentHashMap<String, SessionMetadata>();
    private Timer sessionMonitor = null;    
    private String securityDomainName;
	private boolean trustAllLocal = true;
	private boolean allowSecurityDomainQualifier;
    
    public void setSecurityDomain(String domainName) {
        this.securityDomainName = domainName;
    }
    
    // -----------------------------------------------------------------------------------
    // S E R V I C E - R E L A T E D M E T H O D S
    // -----------------------------------------------------------------------------------

    private void monitorSessions() {
		long currentTime = System.currentTimeMillis();
		for (SessionMetadata info : sessionCache.values()) {
			try {
    			if (CHECK_PING && !info.isEmbedded() && currentTime - info.getLastPingTime() > ServerConnection.PING_INTERVAL * 5) {
    				LogManager.logInfo(LogConstants.CTX_SECURITY, RuntimePlugin.Util.gs(RuntimePlugin.Event.TEIID40007, info.getSessionId()));
    				closeSession(info.getSessionId());
    			} else if (sessionExpirationTimeLimit > 0 && currentTime - info.getCreatedTime() > sessionExpirationTimeLimit) {
    				LogManager.logInfo(LogConstants.CTX_SECURITY,RuntimePlugin.Util.gs(RuntimePlugin.Event.TEIID40008, info.getSessionId()));
    				closeSession(info.getSessionId());
    			}
			} catch (Exception e) {
				LogManager.logDetail(LogConstants.CTX_SECURITY, e, "error running session monitor, unable to monitor:", info.getSessionId()); //$NON-NLS-1$
			}
		}
	}

	@Override
	public void closeSession(String sessionID) throws InvalidSessionException {
		if (LogManager.isMessageToBeRecorded(LogConstants.CTX_SECURITY, MessageLevel.DETAIL)) {
			LogManager.logDetail(LogConstants.CTX_SECURITY, new Object[] {"closeSession", sessionID}); //$NON-NLS-1$
		}
		SessionMetadata info = getSessionInfo(sessionID, true);
		if (LogManager.isMessageToBeRecorded(LogConstants.CTX_AUDITLOGGING, MessageLevel.DETAIL)) {
			LogManager.logDetail(LogConstants.CTX_AUDITLOGGING, new AuditMessage("session", "logoff", info)); //$NON-NLS-1$ //$NON-NLS-2$
		}
		if (info.getVDBName() != null) {
            try {
    			dqp.terminateSession(info.getSessionId());
            } catch (Exception e) {
                LogManager.logWarning(LogConstants.CTX_SECURITY,e, RuntimePlugin.Util.gs(RuntimePlugin.Event.TEIID40018));
            }
		}
		info.setSecurityContext(null);
		info.setClosed();
		info.getSessionVariables().clear();
	}
	
	@Override
	public SessionMetadata createSession(String vdbName,
			String vdbVersion, AuthenticationType authType, String userName,
			Credentials credentials, String applicationName, Properties properties)
			throws LoginException, SessionServiceException {
		ArgCheck.isNotNull(applicationName);
        ArgCheck.isNotNull(properties);
        
        Object securityContext = null;
        Subject subject = null;

        String hostName = properties.getProperty(TeiidURL.CONNECTION.CLIENT_HOSTNAME);
        String ipAddress = properties.getProperty(TeiidURL.CONNECTION.CLIENT_IP_ADDRESS);
        String clientMac = properties.getProperty(TeiidURL.CONNECTION.CLIENT_MAC);
        boolean onlyAllowPassthrough = Boolean.valueOf(properties.getProperty(TeiidURL.CONNECTION.PASSTHROUGH_AUTHENTICATION,
                "false")); //$NON-NLS-1$

        AuditMessage.LogonInfo info = new AuditMessage.LogonInfo(vdbName, vdbVersion, authType.toString(), userName, applicationName, hostName, ipAddress, clientMac, onlyAllowPassthrough);
        
        if (LogManager.isMessageToBeRecorded(LogConstants.CTX_AUDITLOGGING, MessageLevel.DETAIL)) {
        	LogManager.logDetail(LogConstants.CTX_AUDITLOGGING, new AuditMessage("session", "logon-request", info, null)); //$NON-NLS-1$ //$NON-NLS-2$
        }
        try {
        
	        // Validate VDB and version if logging on to server product...
	        VDBMetaData vdb = null;
	        if (vdbName != null) {
	        	vdb = getActiveVDB(vdbName, vdbVersion);
	        	if (vdb == null) {
        	        throw new SessionServiceException(RuntimePlugin.Event.TEIID40046, RuntimePlugin.Util.gs(RuntimePlugin.Event.TEIID40046, vdbName, vdbVersion));
	        	}
	        }
	
	        if (sessionMaxLimit > 0 && getActiveSessionsCount() >= sessionMaxLimit) {
	             throw new SessionServiceException(RuntimePlugin.Event.TEIID40043, RuntimePlugin.Util.gs(RuntimePlugin.Event.TEIID40043, new Long(sessionMaxLimit)));
	        }
	        
	        String securityDomain = getSecurityDomain(userName, vdbName, vdbVersion, vdb);
	        
			if (securityDomain != null) {
		        // Authenticate user...
		        // if not authenticated, this method throws exception
	            LogManager.logDetail(LogConstants.CTX_SECURITY, new Object[] {"authenticateUser", userName, applicationName}); //$NON-NLS-1$
	
	        	String baseUserName = userName;
	        	if (allowSecurityDomainQualifier) {
	        	    baseUserName = getBaseUsername(userName);
	        	}
	    		if (onlyAllowPassthrough || authType.equals(AuthenticationType.GSS)) {
	        		subject = this.securityHelper.getSubjectInContext(securityDomain);
	    	        if (subject == null) {
	    	        	if ((!onlyAllowPassthrough || !(trustAllLocal && DQPWorkContext.getWorkContext().isLocal()))) {
	    	        		throw new LoginException(RuntimePlugin.Util.gs(RuntimePlugin.Event.TEIID40087));
	    	        	}
	    	        } else {
	    	        	userName = getUserName(subject, baseUserName);
	    	        }
	    	        securityContext = this.securityHelper.getSecurityContext();
	        	} else {
	        		userName = baseUserName;
	        		securityContext = this.securityHelper.authenticate(securityDomain, baseUserName, credentials, applicationName);
	        		subject = this.securityHelper.getSubjectInContext(securityContext);
	        	}
			}
			else {
	        	LogManager.logDetail(LogConstants.CTX_SECURITY, RuntimePlugin.Util.gs(RuntimePlugin.Event.TEIID40117)); 
			}
	        
	        long creationTime = System.currentTimeMillis();
	
	        // Return a new session info object
	        SessionMetadata newSession = new SessionMetadata();
	        newSession.setSessionToken(new SessionToken(userName));
	        newSession.setSessionId(newSession.getSessionToken().getSessionID());
	        newSession.setUserName(userName);
	        newSession.setCreatedTime(creationTime);
	        newSession.setApplicationName(applicationName);
	        newSession.setClientHostName(hostName);
			newSession.setIPAddress(ipAddress);
			newSession.setClientHardwareAddress(clientMac);
	        newSession.setSecurityDomain(securityDomain);
	        if (vdb != null) {
		        newSession.setVDBName(vdb.getName());
		        newSession.setVDBVersion(vdb.getVersion());
	        }
	        
	        // these are local no need for monitoring.
	        newSession.setSubject(subject);
	        newSession.setSecurityContext(securityContext);
	        newSession.setVdb(vdb);
	        if (LogManager.isMessageToBeRecorded(LogConstants.CTX_SECURITY, MessageLevel.DETAIL)) {
	        	LogManager.logDetail(LogConstants.CTX_SECURITY, new Object[] {"Logon successful, created", newSession }); //$NON-NLS-1$
	        }
	        this.sessionCache.put(newSession.getSessionId(), newSession);
	        if (LogManager.isMessageToBeRecorded(LogConstants.CTX_AUDITLOGGING, MessageLevel.DETAIL)) {
	        	LogManager.logDetail(LogConstants.CTX_AUDITLOGGING, new AuditMessage("session", "logon-success", newSession)); //$NON-NLS-1$ //$NON-NLS-2$
	        }
	        return newSession;
        } catch (LoginException e) {
	        if (LogManager.isMessageToBeRecorded(LogConstants.CTX_AUDITLOGGING, MessageLevel.DETAIL)) {
	        	LogManager.logDetail(LogConstants.CTX_AUDITLOGGING, new AuditMessage("session", "logon-fail", info, e)); //$NON-NLS-1$ //$NON-NLS-2$
	        }
        	throw e;
        } catch (SessionServiceException e) {
	        if (LogManager.isMessageToBeRecorded(LogConstants.CTX_AUDITLOGGING, MessageLevel.DETAIL)) {
	        	LogManager.logDetail(LogConstants.CTX_AUDITLOGGING, new AuditMessage("session", "logon-fail", info, e)); //$NON-NLS-1$ //$NON-NLS-2$
	        }
        	throw e;
        }
	}
	
	/**
	 * 
	 * @param vdbName
	 * @param vdbVersion
	 * @return the vdb or null if it doesn't exist
	 * @throws SessionServiceException if the version is not valid or the vdb doesn't accept connections
	 */
	protected VDBMetaData getActiveVDB(String vdbName, String vdbVersion) throws SessionServiceException {
		VDBMetaData vdb = null;
		
		try {
			if (vdbVersion == null) {
				vdb = this.vdbRepository.getLiveVDB(vdbName);
			}
			else {
				vdb = this.vdbRepository.getLiveVDB(vdbName, vdbVersion);
			}
		} catch (NumberFormatException e) {
			 throw new SessionServiceException(RuntimePlugin.Event.TEIID40045, e, RuntimePlugin.Util.gs(RuntimePlugin.Event.TEIID40045, vdbVersion));
		}
		if (vdb == null) {
		    return null;
		}
		if (vdb.getConnectionType() == ConnectionType.NONE) {
			 throw new SessionServiceException(RuntimePlugin.Event.TEIID40048, RuntimePlugin.Util.gs(RuntimePlugin.Event.TEIID40048, vdbName, vdbVersion));
		}
		return vdb;
	}

	@Override
	public Collection<SessionMetadata> getActiveSessions() {
		return new ArrayList<SessionMetadata>(this.sessionCache.values());
	}
	
	@Override
	public SessionMetadata getActiveSession(String sessionID) {
		return this.sessionCache.get(sessionID);
	}	

	@Override
	public int getActiveSessionsCount() throws SessionServiceException{
		return this.sessionCache.size();
	}

	@Override
	public Collection<SessionMetadata> getSessionsLoggedInToVDB(VDBKey key) {
		ArrayList<SessionMetadata> results = new ArrayList<SessionMetadata>();
		for (SessionMetadata info : this.sessionCache.values()) {
			if (info.getVdb() != null && key.equals(info.getVdb().getAttachment(VDBKey.class))) {
				results.add(info);
			}
		}
		return results;
	}

	@Override
	public void pingServer(String sessionID) throws InvalidSessionException {
		SessionMetadata info = getSessionInfo(sessionID, false);
		info.setLastPingTime(System.currentTimeMillis());
		this.sessionCache.put(sessionID, info);
		LogManager.logDetail(LogConstants.CTX_SECURITY, "Keep-alive ping received for session:", sessionID); //$NON-NLS-1$
	}

	@Override
	public boolean terminateSession(String terminatedSessionID, String adminSessionID) {
		Object[] params = {adminSessionID, terminatedSessionID};
		LogManager.logInfo(LogConstants.CTX_SECURITY, RuntimePlugin.Util.gs(RuntimePlugin.Event.TEIID40009, params));
		try {
			closeSession(terminatedSessionID);
			return true;
		} catch (InvalidSessionException e) {
			LogManager.logDetail(LogConstants.CTX_SECURITY,e,e.getMessage());
			return false;
		}
	}

	@Override
	public SessionMetadata validateSession(String sessionID) throws InvalidSessionException, SessionServiceException {
		SessionMetadata info = getSessionInfo(sessionID, false);
		return info;
	}

	private SessionMetadata getSessionInfo(String sessionID, boolean remove)
			throws InvalidSessionException {
		if (sessionID == null) {
			 throw new InvalidSessionException(RuntimePlugin.Event.TEIID40041, RuntimePlugin.Util.gs(RuntimePlugin.Event.TEIID40041));
		}
		SessionMetadata info = remove?this.sessionCache.remove(sessionID):this.sessionCache.get(sessionID);
		if (info == null) {
			 throw new InvalidSessionException(RuntimePlugin.Event.TEIID40042, RuntimePlugin.Util.gs(RuntimePlugin.Event.TEIID40042, sessionID));
		}
		return info;
	}
	
	public long getSessionMaxLimit() {
		return this.sessionMaxLimit;
	}
	
	public void setSessionMaxLimit(long limit) {
		this.sessionMaxLimit = limit;
	}
	
	public long getSessionExpirationTimeLimit() {
		return this.sessionExpirationTimeLimit;
	}
	
	public void setSessionExpirationTimeLimit(long limit) {
		this.sessionExpirationTimeLimit = limit;
	}
		
	public void setAuthenticationType(AuthenticationType flag) {
		this.defaultAuthenticationType = flag;		
	}	
	
	public void start() {
		LogManager.logDetail(LogConstants.CTX_SECURITY, new Object[] {"Default security domain configured=", this.securityDomainName}); //$NON-NLS-1$
		this.sessionMonitor = new Timer("SessionMonitor", true); //$NON-NLS-1$
        this.sessionMonitor.schedule(new TimerTask() {
        	@Override
        	public void run() {
        		monitorSessions();
        	}
        }, ServerConnection.PING_INTERVAL * 3, ServerConnection.PING_INTERVAL * 2);
	}

	public void stop(){
		if (sessionMonitor != null) {
			this.sessionMonitor.cancel();
		}
		this.sessionCache.clear();
	}

	public void setVDBRepository(VDBRepository repo) {
		this.vdbRepository = repo;
	}
	
	public void setSecurityHelper(SecurityHelper securityHelper) {
		this.securityHelper = securityHelper;
	}
	
	public void setDqp(DQPCore dqp) {
		this.dqp = dqp;
	}
	
	@Override
	public SecurityHelper getSecurityHelper() {
		return securityHelper;
	}

    static String getBaseUsername(String username) {
        if (username == null) {
            return username;
        }
        
        int index = getQualifierIndex(username);

        String result = username;
        
        if (index != -1) {
            result = username.substring(0, index);
        }
        
        //strip the escape character from the remaining ats
        return result.replaceAll("\\\\"+AT, AT); //$NON-NLS-1$
    }
    
    static String getDomainName(String username) {
        if (username == null) {
            return username;
        }
        
        int index = getQualifierIndex(username);
        
        if (index != -1) {
            return username.substring(index + 1);
        }
        
        return null;
    }
    
    static int getQualifierIndex(String username) {
        int index = username.length();
        while ((index = username.lastIndexOf(AT, --index)) != -1) {
            if (index > 0 && username.charAt(index - 1) != '\\') {
                return index;
            }
        }
        
        return -1;
    }
    
	@Override
	public AuthenticationType getAuthenticationType(String vdbName, String version, String userName) throws LogonException {
		if (userName == null) {
			userName = CoreConstants.DEFAULT_ANON_USERNAME;
		}
		if (vdbName != null) {
			VDB vdb = null;
			try {
				vdb = getActiveVDB(vdbName, version);
			} catch (SessionServiceException e) {
				throw new LogonException(e);
			}
			if (vdb != null) {
				String gssPattern = vdb.getPropertyValue(GSS_PATTERN_PROPERTY);
				
				//TODO: cache the patterns
				
				if (gssPattern != null && Pattern.matches(gssPattern, userName)) {
					return AuthenticationType.GSS;
				}
				
				String passwordPattern = vdb.getPropertyValue(PASSWORD_PATTERN_PROPERTY);
				
				if (passwordPattern != null && Pattern.matches(passwordPattern, userName)) {
					return AuthenticationType.USERPASSWORD;
				}
				
				String typeProperty = vdb.getPropertyValue(AUTHENTICATION_TYPE_PROPERTY);
				if (typeProperty != null) {
					return AuthenticationType.valueOf(typeProperty);
				}
			}
		}
		return this.defaultAuthenticationType;
	}

	public String getSecurityDomain(String userName, String vdbName, String version, VDB vdb) throws LoginException {
		String securityDomain = null;
		if (allowSecurityDomainQualifier) {
		    securityDomain = getDomainName(userName);
		}
		if (vdbName != null) {
	    	try {    		
	    		if (vdb == null) {
	    			vdb = getActiveVDB(vdbName, version);
	    		}
	    		if (vdb != null) {
					String typeProperty = vdb.getPropertyValue(SECURITY_DOMAIN_PROPERTY);				
					if (typeProperty != null) {
						if (securityDomain != null && !typeProperty.equals(securityDomain)) {
							//conflicting
			    			throw new LoginException(RuntimePlugin.Util.gs(RuntimePlugin.Event.TEIID40116));
						}
						return typeProperty;
					}
	    		}
			} catch (SessionServiceException e) {
				// ignore and return default, this only occur if the name and version are wrong 
			}			
		} 
		if (securityDomain != null) {
			if (this.securityDomainName != null && this.securityDomainName.equals(securityDomain)) {
				return securityDomain;
			}
			throw new LoginException(RuntimePlugin.Util.gs(RuntimePlugin.Event.TEIID40116));
		}
		
		return this.securityDomainName;
	}
	
	@Override
	public GSSResult neogitiateGssLogin(String user, String vdbName,
			String vdbVersion, byte[] serviceTicket) throws LoginException, LogonException {
		String securityDomain = getSecurityDomain(user, vdbName, vdbVersion, null);
		if (securityDomain == null ) {
			 throw new LogonException(RuntimePlugin.Event.TEIID40059, RuntimePlugin.Util.gs(RuntimePlugin.Event.TEIID40059));
		}
		return this.securityHelper.negotiateGssLogin(securityDomain, serviceTicket);
	}
	
	public AuthenticationType getDefaultAuthenticationType() {
		return defaultAuthenticationType;
	}
	
    private String getUserName(Subject subject, String userName) {
        Set<Principal> principals = subject.getPrincipals();
        for (Principal p:principals) {
            if (p instanceof Group) {
                continue;
            }
            return p.getName();
        }
        return userName;
    }
    
    public boolean isTrustAllLocal() {
		return trustAllLocal;
	}
    
    public void setTrustAllLocal(boolean trustAllLocal) {
		this.trustAllLocal = trustAllLocal;
	}
    
    public void setAllowSecurityDomainQualifier(boolean useSecurityDomainQualifier) {
        this.allowSecurityDomainQualifier = useSecurityDomainQualifier;
    }
    
    public boolean isAllowSecurityDomainQualifier() {
        return allowSecurityDomainQualifier;
    }
}
