/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2006, Red Hat Middleware LLC, and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This 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 software 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 software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.jboss.jmx.adaptor.html;

import java.io.IOException;
import java.lang.reflect.Method;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.StringTokenizer;

import javax.security.auth.Subject;
import javax.security.jacc.PolicyContext;
import javax.security.jacc.PolicyContextException;
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.HttpServletResponse;

import org.jboss.logging.Logger;
import org.jboss.security.SimpleGroup; 

//$Id: JMXOpsAccessControlFilter.java 57210 2006-09-26 12:29:11Z dimitris@jboss.org $

/**
 *  JBAS-3311: Access Control on JMX Operations in the JMX Console.
 *  Filter that allows Role Based Authorization of the various 
 *  JMX Operations. The actions that come as part of the request are:
 *  displayMBeans
 *  inspectMBean
 *  updateAttributes - Operations that involve updation of jmx attributes
 *  invokeOp - Operations that involve "invoke"
 *  invokeOpByName
 *  @author <a href="mailto:Anil.Saldhana@jboss.org">Anil Saldhana</a>
 *  @since  Jun 12, 2006
 *  @version $Revision: 57210 $
 */
public class JMXOpsAccessControlFilter implements Filter
{ 
   private static Logger log = Logger.getLogger(JMXOpsAccessControlFilter.class);
   private boolean trace = log.isTraceEnabled();
   private static final String ACTION_PARAM = "action"; 
   private static final String DISPLAY_MBEANS_ACTION = "displayMBeans";
   private static final String INSPECT_MBEAN_ACTION = "inspectMBean";
   private static final String UPDATE_ATTRIBUTES_ACTION = "updateAttributes";
   private static final String INVOKE_OP_ACTION = "invokeOp";
   private static final String INVOKE_OP_BY_NAME_ACTION = "invokeOpByName";
   
   private List updateAttributesRoles = null;
   private List invokeOpRoles = null; 
   //Rare usecase
   private List invokeMBeanRoles = null;
   
   private ArrayList subjectRoles = null;
   
   //An authorization delegate that the user can plug in which can do the
   //authorization decisions - when deeper access control usecases arise
   //The Authorization Delegate should have a method
   //public Boolean authorize(ServletRequest,ServletResponse,List)
   private Object authorizationDelegate = null;
   
   /**
    * @see Filter#init(javax.servlet.FilterConfig)
    */
   public void init(FilterConfig filterConfig) throws ServletException
   { 
      String updateAttributesStr = filterConfig.getInitParameter("updateAttributes");
      if(updateAttributesStr != null && updateAttributesStr.length() > 0)
         updateAttributesRoles = this.getRoles(updateAttributesStr);
      
      String invokeOpStr = filterConfig.getInitParameter("invokeOp");
      if(invokeOpStr != null && invokeOpStr.length() > 0)
         invokeOpRoles = this.getRoles(invokeOpStr); 
      
      String inspectMBeanStr = filterConfig.getInitParameter("inspectMBean");
      if(inspectMBeanStr != null && inspectMBeanStr.length() > 0)
         invokeMBeanRoles = this.getRoles(inspectMBeanStr);
      
      //Optional - Authorization Delegate
      String delegateStr = filterConfig.getInitParameter("authorizationDelegate");
      if(delegateStr != null && delegateStr.length() > 0)
         authorizationDelegate = this.instantiate(delegateStr);
   }

   /**
    * @see Filter#doFilter(javax.servlet.ServletRequest, 
    * javax.servlet.ServletResponse, javax.servlet.FilterChain)
    */
   public void doFilter(ServletRequest request, ServletResponse response, 
                   FilterChain chain) 
   throws IOException, ServletException
   {  
      boolean passThrough = true;
      subjectRoles = getSubjectRoles();
      
      String action = request.getParameter(ACTION_PARAM);

      if( action == null )
         action = DISPLAY_MBEANS_ACTION;
      
      if( action.equals(UPDATE_ATTRIBUTES_ACTION))
         passThrough = authorize(request, response, updateAttributesRoles);
      else if( action.equals(INVOKE_OP_ACTION) || action.equals(INVOKE_OP_BY_NAME_ACTION))
         passThrough = authorize(request, response,invokeOpRoles);
      else if( action.equals(INSPECT_MBEAN_ACTION))
         passThrough = authorize(request, response,invokeMBeanRoles);
      
      if(!passThrough)
        ((HttpServletResponse)response).setStatus(HttpServletResponse.SC_FORBIDDEN); 
      else 
         chain.doFilter(request, response);
   }

   /**
    * @see Filter#destroy()
    */
   public void destroy()
   { 
   }  

   /**
    * Authorize the JMX Operations
    * If there is an Authorization Delegate plugged in, it will
    * be consulted for access control
    * @param request
    * @param response
    * @param listToCheck
    * @return
    */
   private boolean authorize(ServletRequest request, 
         ServletResponse response, List listToCheck)
   {
      //Check if there is an authorization delegate
      if(authorizationDelegate != null)
         return checkWithDelegate(request,response,listToCheck);

      if(listToCheck == null || listToCheck.size() == 0)
         return true;
      
      boolean result = false;
      int len = this.subjectRoles.size();
      for(int i = 0; i < len; i++)
      {
         String subjectRole = (String)subjectRoles.get(i);
         result = listToCheck.contains(subjectRole);
         if(result)
            break;
      } 
      return result;  
   } 
   
   private boolean checkWithDelegate(ServletRequest request, 
         ServletResponse response, List listToCheck)
   {
      Boolean result = Boolean.FALSE;
      String name = "authorize";
      Class[] args = new Class[] {ServletRequest.class, ServletResponse.class,
            List.class}; 
      try
      {
         Method meth = authorizationDelegate.getClass().getMethod(name,args);
         result = (Boolean)meth.invoke(authorizationDelegate,
               new Object[]{request,response,listToCheck});
      }
      catch ( Exception e)
      {
         if(trace)
            log.error("Error invoking AuthorizationDelegate:",e);
      } 
      return result.booleanValue();
   }
   
   
   /**
    * Get a list of roles from the string that is comma-delimited
    * @param commaSeperatedRoles
    * @return
    */
   private List getRoles(String commaSeperatedRoles)
   {
      StringTokenizer st = new StringTokenizer(commaSeperatedRoles,",");
      int numTokens = st.countTokens();
      String[] strArr = new String[numTokens]; 
      for(int i=0; i < numTokens; i++)
      {
         strArr[i] = st.nextToken();
      }
      return Arrays.asList(strArr);
   }
   
   /**
    * Get a list of roles from the authenticated subject
    * @return
    */
   private ArrayList getSubjectRoles()
   { 
      ArrayList alist = new ArrayList();
      
      String SUBJECT_CONTEXT_KEY = "javax.security.auth.Subject.container";
      try
      {
         Subject caller = (Subject) PolicyContext.getContext(SUBJECT_CONTEXT_KEY);
         Iterator iter = caller.getPrincipals().iterator();
         while(iter != null && iter.hasNext())
         {
            Principal p = (Principal)iter.next();
            if(p instanceof SimpleGroup)
            {
               SimpleGroup sg = (SimpleGroup)p;
               String name = sg.getName();
               if("Roles".equals(name))
               {
                  Enumeration en = sg.members();
                  while(en.hasMoreElements()) 
                  {
                    String role = en.nextElement().toString();
                    if(role != null) 
                      alist.add(role);
                  }
               }
            }
         }
      }
      catch (PolicyContextException e)
      {
         if(trace)
            log.trace("Error obtaining authenticated subject:",e); 
      } 
      if(trace)
         log.trace("Subject Roles="+alist);
      return alist;
   } 
   

   /**
    * Instantiate The Authorization Delegate
    * @param delegateStr
    * @return
    */
   public Object instantiate(String delegateStr)
   {
      ClassLoader cl = Thread.currentThread().getContextClassLoader();
      Object obj = null;
      try
      {
         Class clazz = cl.loadClass(delegateStr);
         obj = clazz.newInstance();
      }
      catch (Exception e)
      { 
         if(trace)
            log.error("Error instantiating AuthorizationDelegate:",e);
      }
      return obj; 
   }
}
