package examples.security.delegatingrealm;


import examples.security.util.RealmProperties;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.security.Principal;
import java.security.acl.Acl;
import java.security.acl.Group;
import java.security.acl.NotOwnerException;
import java.security.acl.Permission;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;
import weblogic.logging.LogOutputStream;
import weblogic.security.acl.BasicRealm;
import weblogic.security.acl.DebuggableRealm;
import weblogic.security.acl.DelegatedRealm;
import weblogic.security.acl.ManageableRealm;
import weblogic.security.acl.User;
import weblogic.security.acl.UserInfo;
import weblogic.utils.UnsupportedOperationException;


/**
 * This realm delegates calls to methods of other objects.  This can
 * be useful if, for example, you want to obtain user and group
 * information from the LDAP realm, but maintain ACLs in the RDBMS
 * realm. <p>
 *
 * To use this class, set its name as the value of your
 * <tt>weblogic.security.realmClass</tt> property, and create a
 * <tt>delegating.properties</tt> file.  The file uses standard Java
 * properties file syntax, and should contain entries such as the
 * following:
 *
 *<pre>
   getUser = weblogic.security.ldaprealm.LDAPRealm.getUser
   getGroup = weblogic.security.ldaprealm.LDAPRealm.getGroup
   authenticate.1 = weblogic.security.ldaprealm.LDAPRealm.authenticate
   authenticate.2 = examples.security.rdbmsrealm.RDBMSRealm.authenticate
   getAcl = examples.security.rdbmsrealm.RDBMSRealm.getAcl
 </pre>
 *
 * The above properties delegate user and group calls to the LDAP
 * realm.  The LDAP realm is the primary authentication delegate and
 * the RDBMS realm is the secondary authentication delegate, which is
 * consulted if the primary returns null.  ACL-related
 * methods are delegated to the RDBMS realm. <p>
 *
 * You should examine the source of this class before using it, to
 * make sure it behaves as you expect.  It should be easy to tailor to
 * your exact needs.
 *
 * @see #deleteUser
 * @author Copyright (c) 2000 by BEA Systems, Inc. All Rights Reserved.
 */

public class DelegatingRealm
  implements ManageableRealm, DebuggableRealm, DelegatedRealm
{
  // A few implementation notes and caveats:

  // You may not be able to fully "mix and match" objects from
  // different realms without some care.  For example, the default
  // RDBMS realm implementation looks for groups and users in the
  // database.  If you delegate ACLs to the RDBMS realm and delegate
  // users to, say, the LDAP realm, you won't be able to add an LDAP
  // user to the RDBMS realm without making some modifications to both
  // the RDBMS realm (to look back in the Delegating realm for users
  // it can't find) and the Delegating realm (so it won't recursively
  // call back into the RDBMS realm if that's also specified as a
  // source of users - this would lead to a recursive loop and a stack
  // overflow).

  private static final String PROPS_FILE = "delegating.properties";

  private static final String PROPS_PREFIX = "";
  
  private static RealmProperties props;

  private static Hashtable proxyInstances = new Hashtable();
  
  static
  {
    try
    {
      props = new RealmProperties(DelegatingRealm.class, PROPS_FILE,
				  PROPS_PREFIX);
    }
    catch (IOException e)
    {
      throw new DelegatingException(PROPS_FILE + " not found", e);
    }
  }

  private static Vector proxies = new Vector();

  private static Vector realms = new Vector();
  
  private String realmName = "delegating";
  
  private Proxy[] getUserProxies =
    getProxies("getUser", new Class[]{ String.class });

  private Proxy[] authenticateProxies =
    getProxies("authenticate", new Class[]{ UserInfo.class });

  private Proxy[] getGroupProxies =
    getProxies("getGroup", new Class[]{ String.class });

  private Proxy[] getAclOwnerProxies =
    getProxies("getAclOwner", new Class[]{ Object.class });


  private Proxy[] getAclProxies =
    getProxies("getAcl", new Class[]{ String.class });


  private Proxy[] getAclSepProxies =
    getProxies("getAclSep", new Class[]{ String.class, Character.class });


  private Proxy[] getPermissionProxies =
    getProxies("getPermission", new Class[]{ String.class });

  private Proxy[] getUsersProxies =
    getProxies("getUsers", new Class[0]);

  private Proxy[] getGroupsProxies =
    getProxies("getGroups", new Class[0]);

  private Proxy[] getAclsProxies =
    getProxies("getAcls", new Class[0]);

  private Proxy[] getPermissionsProxies =
    getProxies("getPermissions", new Class[0]);

  private Proxy[] newUserProxies =
    getProxies("newUser", new Class[]{ String.class, Object.class });

  private Proxy[] newGroupProxies =
    getProxies("newGroup", new Class[]{ String.class });

  private Proxy[] newAclProxies =
    getProxies("newAcl", new Class[]{ Principal.class, String.class });

  private Proxy[] newPermissionProxies =
    getProxies("newPermission", new Class[]{ String.class });

  private Proxy[] deleteUserProxies =
    getProxies("deleteUser", new Class[]{ User.class });

  private Proxy[] deleteGroupProxies =
    getProxies("deleteGroup", new Class[]{ Group.class });

  private Proxy[] deleteAclProxies =
    getProxies("deleteAcl", new Class[]{ Principal.class, Acl.class });

  private Proxy[] deletePermissionProxies =
    getProxies("deletePermission", new Class[]{ Permission.class });

  private Proxy[] setPermissionProxies =
    getProxies("setPermission",
	       new Class[]{ Acl.class, Principal.class,
			    Permission.class, Boolean.class });

  private LogOutputStream log;


  public DelegatingRealm()
  {
    super();
  }



  private static Proxy[] getProxies(String propName, Class[] paramTypes)
  {
    Vector mv = new Vector();

    Proxy proxy = getProxy(propName, paramTypes);

    if (proxy == null)
    {
      return null;
    }

    mv.addElement(proxy);

    if ((proxy = getProxy(propName + ".0", paramTypes)) != null)
    {
      mv.addElement(proxy);
    }

    for (int i = 1; (proxy = getProxy(propName + "." + i,
				      paramTypes)) != null; i++)
    {
      mv.addElement(proxy);
    }

    Proxy[] prxs = new Proxy[mv.size()];

    mv.copyInto(prxs);

    return prxs;
  }


  private static Proxy getProxy(String propName, Class[] paramTypes)
  {
    String methodName = props.get(propName);

    if (methodName == null)
    {
      return null;
    }
    
    int lastDot = methodName.lastIndexOf('.');

    if (lastDot == -1)
    {
      throw new RuntimeException("internal property name error");
    }

    String className = methodName.substring(0, lastDot);

    methodName = methodName.substring(lastDot + 1);

    Class clazz = null;
    
    // If we delegate several calls to methods on a single class, make
    // sure we use just one instance of that class, so we don't create
    // too many unnecessary objects.
    
    Object obj = proxyInstances.get(className);
    
    if (obj == null)
    {
      try
      {
	clazz = Class.forName(className);
      }
      catch (ClassNotFoundException e)
      {
	throw new DelegatingException(e);
      }
    } else {
      clazz = obj.getClass();
    }

    try
    {
      boolean newlyCreated = false;
      
      if (obj == null)
      {
	obj = clazz.newInstance();

	proxyInstances.put(className, obj);
	newlyCreated = true;
      }
      
      Proxy p = new Proxy(obj, clazz.getMethod(methodName, paramTypes));

      if (newlyCreated && obj instanceof BasicRealm)
      {
	realms.addElement(obj);
      }

      proxies.addElement(p);

      return p;
    }
    catch (NoSuchMethodException e)
    {
      throw new DelegatingException(e);
    }
    catch (InstantiationException e)
    {
      throw new DelegatingException(e);
    }
    catch (IllegalAccessException e)
    {
      throw new DelegatingException(e);
    }
  }


  public void init(String name, Object ownerCredential)
    throws NotOwnerException
  {
    Enumeration enum = realms.elements();

    while (enum.hasMoreElements())
    {
      BasicRealm realm = (BasicRealm) enum.nextElement();

      realm.init(name, ownerCredential);
    }
  }

  
  public void load(String name, Object credential)
    throws ClassNotFoundException, IOException, NotOwnerException
  {
    Enumeration enum = realms.elements();

    while (enum.hasMoreElements())
    {
      BasicRealm realm = (BasicRealm) enum.nextElement();

      realm.load(name, credential);
    }
  }

  
  public void save(String name)
    throws IOException
  {
    Enumeration enum = realms.elements();

    while (enum.hasMoreElements())
    {
      BasicRealm realm = (BasicRealm) enum.nextElement();

      realm.save(name);
    }
  }

  
  public String getName()
  {
    return realmName;
  }

  
  public void setDebug(boolean enable)
  {
    if (enable && log == null)
    {
      log = new LogOutputStream("DelegatingRealm");
    }
    if (!enable)
    {
      log = null;
    }

    Enumeration enum = realms.elements();

    while (enum.hasMoreElements())
    {
      Object realm = enum.nextElement();

      if (realm instanceof DebuggableRealm)
      {
	((DebuggableRealm) realm).setDebug(enable);
      }
    }
  }


  public User getUser(String name)
  {
    return (User) callProxy(getUserProxies, new Object[]{ name });
  }

  
  public User getUser(UserInfo userInfo)
  {
    return authenticate(userInfo);
  }

  
  public User authenticate(UserInfo userInfo)
  {
    return (User) callProxy(authenticateProxies, new Object[]{ userInfo });
  }

  
  public Group getGroup(String name)
  {
    return (Group) callProxy(getGroupProxies, new Object[]{ name });
  }

  
  public Principal getAclOwner(Object credential)
  {
    return (Principal) callProxy(getAclOwnerProxies,
				 new Object[]{ credential });
  }

  
  public Acl getAcl(String name)
  {
    return (Acl) callProxy(getAclProxies, new Object[]{ name });
  }

  
  public Acl getAcl(String name, char separator)
  {
    return (Acl) callProxy(getAclSepProxies,
			   new Object[]{ name, new Character(separator) });
  }


  public Permission getPermission(String name)
  {
    return (Permission) callProxy(getPermissionProxies, new Object[]{ name });
  }

  
  public Enumeration getUsers()
  {
    return (Enumeration) callProxy(getUsersProxies, new Object[0]);
  }
  
  
  public Enumeration getGroups()
  {
    return (Enumeration) callProxy(getGroupsProxies, new Object[0]);
  }
  
  
  public Enumeration getAcls()
  {
    return (Enumeration) callProxy(getAclsProxies, new Object[0]);
  }
  
  
  public Enumeration getPermissions()
  {
    return (Enumeration) callProxy(getPermissionsProxies, new Object[0]);
  }


  public User newUser(String name, Object credential, Object constraints)
    throws SecurityException
  {
    return (User) callProxy(newUserProxies,
			    new Object[]{ name, credential, constraints });
  }


  public Group newGroup(String name)
    throws SecurityException
  {
    return (Group) callProxy(newGroupProxies, new Object[]{ name });
  }


  public Acl newAcl(Principal owner, String name)
    throws SecurityException
  {
    return (Acl) callProxy(newAclProxies, new Object[]{ owner, name });
  }


  public Permission newPermission(String name)
    throws SecurityException
  {
    return (Permission) callProxy(newPermissionProxies, new Object[]{ name });
  }


  /**
   * This method has an implementation note that you should read. <p>
   *
   * For this and other methods that have return type <tt>void</tt>,
   * if you specify multiple delegates in the properties file, only
   * the first will be called.  To change this behavior, edit the
   * methods you are interested in so that they call
   * <tt>callProxies</tt> instead.
   */

  public void deleteUser(User user)
    throws SecurityException
  {
    callProxy(deleteGroupProxies, new Object[]{ user });
  }


  public void deleteGroup(Group group)
    throws SecurityException
  {
    callProxy(deleteGroupProxies, new Object[]{ group });
  }


  public void deleteAcl(Principal owner, Acl acl)
    throws SecurityException
  {
    callProxy(deleteAclProxies, new Object[]{ owner, acl });
  }


  public void deletePermission(Permission perm)
    throws SecurityException
  {
    callProxy(deletePermissionProxies, new Object[]{ perm });
  }


  public void setPermission(Acl acl, Principal principal,
			    Permission permission, boolean allow)
  {
    callProxy(setPermissionProxies, new Object[]{ acl, principal, permission,
						  new Boolean(allow) });
  }


  /**
   * Call a method on every one of a set of proxies.
   *
   * @see callProxyEx
   */
  private void callProxies(Proxy[] proxies, Object[] args)
  {
    callProxy(proxies, args, true);
  }
  
  /**
   * Call a method on a set of proxies.
   *
   * @see callProxyEx
   */
  private Object callProxy(Proxy[] proxies, Object[] args)
  {
    return callProxy(proxies, args, false);
  }
  
  /**
   * Call a method on a set of proxies.
   *
   * @see callProxyEx
   */
  private Object callProxy(Proxy[] proxies, Object[] args, boolean callAll)
  {
    try
    {
      return callProxyEx(proxies, args, callAll);
    }
    catch (RuntimeException e)
    {
      throw e;
    }
    catch (Exception e)
    {
      throw new DelegatingException("invocation failed", e);
    }
  }
  

  /**
   * Call a method on a set of proxies.  Attempt to obtain in a
   * sensible way any exception that may be thrown.  If the callAll
   * parameter is set to true, all proxies are called, and the return
   * value of the last is returned.  Otherwise, calling stops at the
   * first method to return a non-null value, and its return value is
   * returned.
   */
  private Object callProxyEx(Proxy[] proxies, Object[] args, boolean callAll)
    throws Exception
  {
    if (proxies == null)
    {
      throw new UnsupportedOperationException("operation not supported by delegates");
    }
    
    Object result = null;
    
    try
    {
      for (int i = 0; i < proxies.length; i++)
      {
	if (callAll == false && (result = proxies[i].invoke(args)) != null)
	{
	  break;
	}
      }
    }
    catch (InvocationTargetException e)
    {
      Throwable t = e.getTargetException();

      if (t instanceof Exception)
      {
	throw (Exception) t;
      } else {
	throw new DelegatingException("invocation failed", t);
      }
    }

    return result;
  }

  
  public LogOutputStream getDebugLog()
  {
    return log;
  }


  private static class Proxy
  {
    Object object;

    Method method;

    Proxy(Object obj, Method meth)
    {
      object = obj;
      method = meth;
    }

    Object invoke(Object[] args)
      throws InvocationTargetException
    {
      try
      {
	return method.invoke(object, args);
      }
      catch (IllegalAccessException e)
      {
	throw new DelegatingException("delegation failed", e);
      }
    }
  }


  private Object delegator;

  
  /**
   * Set the delegator of all of our delegated objects.  We explicitly
   * avoid setting ourselves up as the delegator, because we want to
   * be as transparent as possible.
   */
  public void setDelegator(Object obj)
  {
    delegator = obj;

    Enumeration enum = proxyInstances.elements();

    while (enum.hasMoreElements())
    {
      Object o = enum.nextElement();

      if (o instanceof DelegatedRealm)
      {
	((DelegatedRealm) o).setDelegator(obj);
      }
    }
  }


  public Object getDelegator()
  {
    return delegator;
  }
}





