package org.dbxml.core.security;

/*
 * dbXML License, Version 1.0
 *
 *
 * Copyright (c) 1999-2001 The dbXML Group, L.L.C.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. The end-user documentation included with the redistribution,
 *    if any, must include the following acknowledgment:
 *       "This product includes software developed by
 *        The dbXML Group (http://www.dbxml.com/)."
 *    Alternately, this acknowledgment may appear in the software
 *    itself, if and wherever such third-party acknowledgments normally
 *    appear.
 *
 * 4. The names "dbXML" and "The dbXML Group" must not be used to
 *    endorse or promote products derived from this software without
 *    prior written permission. For written permission, please contact
 *    info@dbxml.com.
 *
 * 5. Products derived from this software may not be called "dbXML",
 *    nor may "dbXML" appear in their name, without prior written
 *    permission of The dbXML Group.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL THE DBXML GROUP OR ITS CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
 * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
 * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
 * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * $Id: LocalSecurityManager.java,v 1.10 2001/06/13 19:54:39 tom Exp $
 */

 import org.dbxml.server.*;
 import org.dbxml.core.*;
 import org.dbxml.xml.dom.*;

 import java.util.*;
 import java.io.*;
 import org.w3c.dom.*;

/**
 * Implements security based off of configurations stored in the local database
 * configuration.
 */
public class LocalSecurityManager implements DBSecurityManager {
   private static final String ACL_COLLECTION = "system/SysAccess";
   private static final String ACL_FILE = "config/acl.xml";
   private static final String PERMISSIONS = "permissions";
   private static final String COLLECTIONS = "collections";
   private static final String COLLECTION = "collection";
   private static final String DOCUMENTS = "documents";
   private static final String XMLOBJECTS = "xmlobjects";
   private static final String XMLOBJECT = "xmlobject";
   private static final String RESOURCES = "resources";
   private static final String RESOURCE = "resource";
   private static final String NAME = "name";
   private static final String CORE = "core";
   private static final String ADMIN = "admin";
   private static final String ALL_ACCESS_GROUP = "dbxml-sys-all";

   protected Configuration config = null;
   protected Database db = null;
   protected HashMap permissions = null;

   private static ThreadLocal credentials = new ThreadLocal();

   // When the security manager is first started it is set into startup mode
   // which disables access control so system collections can be loaded.
   private boolean startup = true;

   // Can be set in the server configuration to disable security. This is here
   // mainly for development should probably be removed in production.
   private boolean disabled = false;

   /**
    * Initialize the security manager. The security manager can not actually be
    * used until readConfig() is called.
    *
    * @param db the Database instance associated with this manager
    * @param disabled whether security should be enabled or not. true to
    *        disable.
    */
   public LocalSecurityManager(Database db, boolean disabled) {
      this.db = db;
      this.disabled = disabled;

      permissions = new HashMap();
   }

   /**
    * getName retrieves the contextually important name of the object
    *
    * @return The object's name
    */
   public String getName() {
      return "LocalSecurityManager";
   }

   /**
    * Determines if the provided credentials are valid for accessing the
    * specified resource.
    *
    * @param resource The resource to check access for.
    * @param requestedAccess The access level being requested.
    * @param cred The credentials to use to authenticate
    * @exception AccessDeniedException Thrown when access to the resource is
    *            denied.
    * @exception InvalidCredentialsException Thrown if the provided credentials
    *            are not valid.
    */
   public void checkAccess(String resource, int requestedAccess, Credentials cred)
         throws AccessDeniedException, InvalidCredentialsException {
      // If we're in startup mode or security is disabled no access checking
      // can be done.
      if (startup || disabled) {
         return;
      }

      if (cred == null) {
         throw new InvalidCredentialsException();
      }

      ResourcePermissions perms = (ResourcePermissions)permissions.get(resource);
      // get list of groups
      ArrayList groups = cred.getGroups();

      // If the admin group exists unrestricted access is allowed.
      if (groups.contains(ADMIN)) {
          return;
      }

      if (perms != null) {
         // for each group check if they have access
         for (int i = 0; i < groups.size(); i++) {
            String group = (String)groups.get(i);
            // Check if the resource has been granted default full access.
            if (perms.checkPermission(Permission.ALLOW, requestedAccess,
                  ALL_ACCESS_GROUP)) {
               return;
            }

            // Check first for explicit deny. If a deny is found for any group
            // then access is automatically denied.
            if (perms.checkPermission(Permission.DENY, requestedAccess, group)) {
               throw new AccessDeniedException();
            }
            else {
               if (perms.checkPermission(Permission.ALLOW, requestedAccess, group)) {
                  return;
               }
            }
         }
      }

      throw new AccessDeniedException();
   }

   /**
    * Determines if the credentials retrieved from thread local storage are
    * valid for accessing the specified resource.
    *
    * @param resource The resource to check access for.
    * @param requestedAccess The access level being requested.
    * @param cred The credentials to use to authenticate
    * @exception AccessDeniedException Thrown when access to the resource is
    *            denied.
    * @exception InvalidCredentialsException Thrown if the credentials retrieved
    *            from thread local storage are not valid.
    */
   public void checkAccess(String resource, int requestedAccess)
         throws AccessDeniedException, InvalidCredentialsException {
      checkAccess(resource, requestedAccess, (Credentials)credentials.get());
   }

   /**
    * Authenticates the user and retrieves a credentials object for
    * the provided username and password. If
    * an existing object can be found in the cache it will be returned otherwise
    * a new Credentials instance will be created and returned. Cached objects
    * are verified before being returned.
    *
    * @param username The username to authenticate
    * @param password The password used to authenticate the user.
    * @exception InvalidPasswordException Thrown when the password provided does
    *            not match for the username.
    * @exception UnknownUserException Thrown when the username can not be found.
    */
   public Credentials authenticate(String username, String password)
         throws InvalidPasswordException, UnknownUserException,
            InvalidCredentialsException {
      Credentials cred =
         new PasswordCredentials(username, password, db);

      // Add the credentials to thread local storage for the requesting thread.
      credentials.set(cred);

      return cred;
   }

   /**
    * Removes any credentials associated with the current thread. Any further
    * access by the thread will result in a InvalidCredentialsException being
    * thrown.
    */
   public void logout() {
      credentials.set(null);
   }

   public void setActive() {
      this.startup = false;
   }

   /**
    * Reads the ACL security configuration from the database.
    */
   public void readConfig() {
      String base = "/";
      try {
         // Read in the configuration. For now we read it from the file system.
         // TODO: Eventually it should be stored in the database
         Document doc = null;

         // TODO: aclFile should be configurable via either the command line or
         // via system.xml. Probably system.xml.
         String aclFile = ACL_FILE;
         try {
            FileInputStream fis = new FileInputStream(aclFile);
            doc = DOMParser.toDocument(fis);
            fis.close();
         }
         catch ( Exception e ) {
            org.dbxml.Debug.println("\u0007FATAL ERROR: Reading security configuration file '" + aclFile + "'");
            org.dbxml.Debug.printStackTrace(e);
            System.exit(1);
         }

//         Document doc =
//            db.getCollection(ACL_COLLECTION).getDocument(ACL_FILE);
         if (doc != null) {
            Configuration config = new Configuration(doc.getDocumentElement(), false);
            Configuration core = config.getChild(CORE);
            final String resourceName = base + core.getAttribute(NAME) + "/";
            handleConfig(resourceName, core.getChild(PERMISSIONS, true));

            Configuration collections = core.getChild(COLLECTIONS);
            if (collections != null) {
               collections.processChildren(COLLECTION, new ConfigurationCallback() {
                     public void process(Configuration cfg) {
                        try {
                           org.dbxml.Debug.println(cfg.getName());
                           String name = resourceName + cfg.getAttribute(NAME) + "/";
                           handleCollection(resourceName, cfg);
                        }
                        catch (ReadOnlyException e) {
                           org.dbxml.Debug.printStackTrace(e);
                        }
                     }
                  }
               );
            }
         }
         else {
            org.dbxml.Debug.println("Access Control List could not be loaded");
         }
      }
      catch (Exception e) {
         org.dbxml.Debug.printStackTrace(e);
      }
   }

   /**
    * Handles the configuration of ACLs for a collection instance.
    */
   protected void handleCollection(String base, Configuration config)
         throws ReadOnlyException {
      if (config != null) {
         final String resourceName = base + config.getAttribute(NAME) + "/";

         Configuration child = config.getChild(PERMISSIONS, true);
         if (child != null) {
            handleConfig(resourceName, child);
         }

         processChildren(resourceName, XMLOBJECTS, XMLOBJECT, config);

         child = config.getChild(DOCUMENTS, true);
         if (child != null) {
            processChildren(resourceName, RESOURCES, RESOURCE, child);
            processChildren(resourceName, XMLOBJECTS, XMLOBJECT, child);
         }

         Configuration collections = config.getChild(COLLECTIONS);
         if (collections != null) {
            collections.processChildren(COLLECTION, new ConfigurationCallback() {
                  public void process(Configuration cfg) {
                     try {
                        String name = resourceName + cfg.getAttribute(NAME) + "/";
                        handleCollection(resourceName, cfg);
                     }
                     catch (ReadOnlyException e) {
                        org.dbxml.Debug.printStackTrace(e);
                     }
                  }
               }
            );
         }
      }
   }

   protected void processChildren(final String resourceName, String childContainer,
         final String child, Configuration config) throws ReadOnlyException {

      Configuration container = config.getChild(childContainer, true);
      if (container != null) {
         container.processChildren(child, new ConfigurationCallback() {
               public void process(Configuration cfg) {
                  try {
                     String name = resourceName + cfg.getAttribute(NAME) + "/";
                     handleConfig(name, cfg.getChild(PERMISSIONS, true));
                  }
                  catch (ReadOnlyException e) {
                     org.dbxml.Debug.printStackTrace(e);
                  }
               }
            }
         );
      }
   }

   protected void handleConfig(String resourceName, Configuration config) {
      if (config != null) {
         try {
            ResourcePermissions perm = new ResourcePermissions(resourceName);
            perm.setConfig(config);
            permissions.put(resourceName, perm);
         }
         catch (NullPointerException e) {
            org.dbxml.Debug.println("Name attribute is required for all ACL objects");
         }
      }
   }

   /**
    * setConfig sets the configuration information for the Configurable
    * object instance.
    *
    * @param config The configuration Node
    */
   public void setConfig(Configuration config) {
      this.config = config;
   }

   /**
    * getConfig retrieves the configuration information for the
    * Configurable object instance.
    *
    * @return The configuration Node
    */
   public Configuration getConfig() {
      return config;
   }
}
