/*
 *  eXist Open Source Native XML Database
 *  Copyright (C) 2001 Wolfgang M. Meier
 *  meier@ifs.tu-darmstadt.de
 *  http://exist.sourceforge.net
 *
 *  This program 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
 *  of the License, or (at your option) any later version.
 *
 *  This program 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 program; if not, write to the Free Software
 *  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 * 
 *  $Id$
 */
package org.exist.security;

import java.util.ArrayList;
import java.util.Iterator;

import org.apache.log4j.Logger;
import org.exist.EXistException;
import org.exist.collections.Collection;
import org.exist.collections.IndexInfo;
import org.exist.collections.triggers.TriggerException;
import org.exist.dom.DocumentImpl;
import org.exist.security.xacml.ExistPDP;
import org.exist.storage.BrokerPool;
import org.exist.storage.DBBroker;
import org.exist.storage.sync.Sync;
import org.exist.storage.txn.TransactionManager;
import org.exist.storage.txn.Txn;
import org.exist.util.LockException;
import org.exist.util.MimeType;
import org.exist.util.hashtable.Int2ObjectHashMap;
import org.exist.xmldb.XmldbURI;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

/**
 * SecurityManager is responsible for managing users and groups.
 * 
 * There's only one SecurityManager for each database instance, which
 * may be obtained by {@link BrokerPool#getSecurityManager()}.
 * 
 * Users and groups are stored in the system collection, in document
 * users.xml. While it is possible to edit this file by hand, it
 * may lead to unexpected results, since SecurityManager reads 
 * users.xml only during database startup and shutdown.
 */
public class XMLSecurityManager implements SecurityManager {

	public final static String DBA_GROUP = "dba";
	public final static String DBA_USER = "admin";
	public final static String GUEST_GROUP = "guest";
	public final static String GUEST_USER = "guest";
	
	public final static User SYSTEM_USER = new User(DBA_USER, null, DBA_GROUP);
	
	private final static Logger LOG =
		Logger.getLogger(SecurityManager.class);

	private BrokerPool pool;
	private Int2ObjectHashMap groups = new Int2ObjectHashMap(65);
	private Int2ObjectHashMap users = new Int2ObjectHashMap(65);
	private int nextUserId = 0;
	private int nextGroupId = 0;

	private int defCollectionPermissions = Permission.DEFAULT_PERM;
    private int defResourcePermissions = Permission.DEFAULT_PERM;
    
    private ExistPDP pdp;

    public XMLSecurityManager() {
       
    }
	/**
	 * Initialize the security manager.
	 * 
	 * Checks if the file users.xml exists in the system collection of the database.
	 * If not, it is created with two default users: admin and guest.
	 *  
	 * @param pool
	 * @param sysBroker
	 */
    public void attach(BrokerPool pool, DBBroker sysBroker) {
       this.pool = pool;
       
       TransactionManager transact = pool.getTransactionManager();
       Txn txn = null;
       DBBroker broker = sysBroker;
       try {
          Collection sysCollection = broker.getCollection(XmldbURI.SYSTEM_COLLECTION_URI);
          if (sysCollection == null) {
             txn = transact.beginTransaction();
             sysCollection = broker.getOrCreateCollection(txn, XmldbURI.SYSTEM_COLLECTION_URI);
             sysCollection.setPermissions(0770);
             broker.saveCollection(txn, sysCollection);
             transact.commit(txn);
          }
          Document acl = sysCollection.getDocument(broker, ACL_FILE_URI);
          Element docElement = null;
          if (acl != null)
             docElement = acl.getDocumentElement();
          if (docElement == null) {
             LOG.debug("creating system users");
             User user = new User(DBA_USER, null);
             user.addGroup(DBA_GROUP);
             user.setUID(++nextUserId);
             users.put(user.getUID(), user);
             user = new User(GUEST_USER, GUEST_USER, GUEST_GROUP);
             user.setUID(++nextUserId);
             users.put(user.getUID(), user);
             newGroup(DBA_GROUP);
             newGroup(GUEST_GROUP);
             txn = transact.beginTransaction();
             save(broker, txn);
             transact.commit(txn);
          } else {
             LOG.debug("loading acl");
             Element root = acl.getDocumentElement();
             Attr version = root.getAttributeNode("version");
             int major = 0;
             int minor = 0;
             if (version!=null) {
                String [] numbers = version.getValue().split("\\.");
                major = Integer.parseInt(numbers[0]);
                minor = Integer.parseInt(numbers[1]);
             }
             NodeList nl = root.getChildNodes();
             Node node;
             Element next;
             User user;
             NodeList ul;
             String lastId;
             Group group;
             for (int i = 0; i < nl.getLength(); i++) {
                if(nl.item(i).getNodeType() != Node.ELEMENT_NODE)
                   continue;
                next = (Element) nl.item(i);
                if (next.getTagName().equals("users")) {
                   lastId = next.getAttribute("last-id");
                   try {
                      nextUserId = Integer.parseInt(lastId);
                   } catch (NumberFormatException e) {
                   }
                   ul = next.getChildNodes();
                   for (int j = 0; j < ul.getLength(); j++) {
                      node = ul.item(j);
                      if(node.getNodeType() == Node.ELEMENT_NODE &&
                              node.getLocalName().equals("user")) {
                         user = new User(major,minor,(Element)node);
                         users.put(user.getUID(), user);
                      }
                   }
                } else if (next.getTagName().equals("groups")) {
                   lastId = next.getAttribute("last-id");
                   try {
                      nextGroupId = Integer.parseInt(lastId);
                   } catch (NumberFormatException e) {
                   }
                   ul = next.getChildNodes();
                   for (int j = 0; j < ul.getLength(); j++) {
                      node = ul.item(j);
                      if(node.getNodeType() == Node.ELEMENT_NODE &&
                              node.getLocalName().equals("group")) {
                         group = new Group((Element)node);
                         groups.put(group.getId(), group);
                      }
                   }
                }
             }
          }
       } catch (Exception e) {
          transact.abort(txn);
          e.printStackTrace();
          LOG.debug("loading acl failed: " + e.getMessage());
       }
       // read default collection and resource permissions
       Integer defOpt = (Integer)
       broker.getConfiguration().getProperty("indexer.permissions.collection");
       if (defOpt != null)
          defCollectionPermissions = defOpt.intValue();
       defOpt = (Integer)
       broker.getConfiguration().getProperty("indexer.permissions.resource");
       if (defOpt != null)
          defResourcePermissions = defOpt.intValue();
       
       Boolean enableXACML = (Boolean)broker.getConfiguration().getProperty("xacml.enable");
       if(enableXACML != null && enableXACML.booleanValue()) {
          pdp = new ExistPDP(pool);
          LOG.debug("XACML enabled");
       }
    }
	public boolean isXACMLEnabled() {
		return pdp != null;
	}
	public ExistPDP getPDP() {
		return pdp;
	}
	public synchronized void deleteUser(String name) throws PermissionDeniedException {
		deleteUser(getUser(name));
	}
	
	public synchronized void deleteUser(User user) throws PermissionDeniedException {
		if(user == null)
			return;
		user = (User)users.remove(user.getUID());
		if(user != null)
			LOG.debug("user " + user.getName() + " removed");
		else
			LOG.debug("user not found");
		DBBroker broker = null;
        TransactionManager transact = pool.getTransactionManager();
        Txn txn = transact.beginTransaction();
		try {
			broker = pool.get();
			save(broker, txn);
            transact.commit(txn);
		} catch (EXistException e) {
            transact.abort(txn);
			e.printStackTrace();
		} finally {
			pool.release(broker);
		}
	}

	public synchronized User getUser(String name) {
		User user;
		for (Iterator i = users.valueIterator(); i.hasNext();) {
			user = (User) i.next();
			if (user.getName().equals(name))
				return user;
		}
		LOG.debug("user " + name + " not found");
		return null;
	}

	public synchronized User getUser(int uid) {
		final User user = (User)users.get(uid);
		if(user == null) {
			LOG.debug("user with uid " + uid + " not found");
        }
		return user;
	}
	
	public synchronized User[] getUsers() {
		User u[] = new User[users.size()];
		int j = 0;
		for (Iterator i = users.valueIterator(); i.hasNext(); j++)
			u[j] = (User) i.next();
		return u;
	}

	protected void newGroup(String name) {
		Group group = new Group(name, ++nextGroupId);
		groups.put(group.getId(), group);
	}

    public synchronized void addGroup(String name) {
        newGroup(name);
        DBBroker broker = null;
        TransactionManager transact = pool.getTransactionManager();
        Txn txn = transact.beginTransaction();
        try {
            broker = pool.get();
            save(broker, txn);
            transact.commit(txn);
        } catch (EXistException e) {
            transact.abort(txn);
            e.printStackTrace();
        } finally {
            pool.release(broker);
        }
    }
    
	public synchronized boolean hasGroup(String name) {
		Group group;
		for (Iterator i = groups.valueIterator(); i.hasNext();) {
			group = (Group) i.next();
			if (group.getName().equals(name))
				return true;
		}
		return false;
	}

	public synchronized Group getGroup(String name) {
		Group group;
		for (Iterator i = groups.valueIterator(); i.hasNext();) {
			group = (Group) i.next();
			if (group.getName().equals(name))
				return group;
		}
		return null;
	}

	public synchronized Group getGroup(int gid) {
		return (Group)groups.get(gid);
	}
	
	public synchronized String[] getGroups() {
		ArrayList list = new ArrayList(groups.size());
		Group group;
		for(Iterator i = groups.valueIterator(); i.hasNext(); ) {
			group = (Group) i.next();
			list.add(group.getName());
		}
		String[] gl = new String[list.size()];
		list.toArray(gl);
		return gl;
	}
	
	public synchronized boolean hasAdminPrivileges(User user) {
		return user.hasDbaRole();
	}

	public synchronized boolean hasUser(String name) {
		User user;
		for (Iterator i = users.valueIterator(); i.hasNext();) {
			user = (User) i.next();
			if (user.getName().equals(name))
				return true;
		}
		return false;
	}

	private synchronized void save(DBBroker broker, Txn transaction) throws EXistException {
		LOG.debug("storing acl file");
		StringBuffer buf = new StringBuffer();
        buf.append("<!-- Central user configuration. Editing this document will cause the security " +
                "to reload and update its internal database. Please handle with care! -->");
		buf.append("<auth version='1.0'>");
		// save groups
        buf.append("<!-- Please do not remove the guest and admin groups -->");
		buf.append("<groups last-id=\"");
		buf.append(Integer.toString(nextGroupId));
		buf.append("\">");
		for (Iterator i = groups.valueIterator(); i.hasNext();)
			buf.append(((Group) i.next()).toString());
		buf.append("</groups>");
		//save users
        buf.append("<!-- Please do not remove the admin user. -->");
		buf.append("<users last-id=\"");
		buf.append(Integer.toString(nextUserId));
		buf.append("\">");
		for (Iterator i = users.valueIterator(); i.hasNext();)
			buf.append(((User) i.next()).toString());
		buf.append("</users>");
		buf.append("</auth>");
        
		// store users.xml
		broker.flush();
		broker.sync(Sync.MAJOR_SYNC);
		try {
			broker.setUser(getUser(DBA_USER));
			Collection sysCollection = broker.getCollection(XmldbURI.SYSTEM_COLLECTION_URI);
            String data = buf.toString();
            IndexInfo info = sysCollection.validateXMLResource(transaction, broker, ACL_FILE_URI, data);
            DocumentImpl doc = info.getDocument();
            doc.getMetadata().setMimeType(MimeType.XML_TYPE.getName());
            sysCollection.store(transaction, broker, info, data, false);
			doc.setPermissions(0770);
			broker.saveCollection(transaction, doc.getCollection());
		} catch (SAXException e) {
			throw new EXistException(e.getMessage());
		} catch (PermissionDeniedException e) {
			throw new EXistException(e.getMessage());
		} catch (TriggerException e) {
			throw new EXistException(e.getMessage());
		} catch (LockException e) {
			throw new EXistException(e.getMessage());
		}
		broker.flush();
		broker.sync(Sync.MAJOR_SYNC);
	}

	public synchronized void setUser(User user) {
		if (user.getUID() < 0)
			user.setUID(++nextUserId);
		users.put(user.getUID(), user);
		String[] groups = user.getGroups();
        // if no group is specified, we automatically fall back to the guest group
        if (groups.length == 0)
            user.addGroup(GUEST_GROUP);
		for (int i = 0; i < groups.length; i++) {
			if (!hasGroup(groups[i]))
				newGroup(groups[i]);
		}
        TransactionManager transact = pool.getTransactionManager();
        Txn txn = transact.beginTransaction();
		DBBroker broker = null;
		try {
			broker = pool.get();
			save(broker, txn);
			createUserHome(broker, txn, user);
            transact.commit(txn);
		} catch (EXistException e) {
            transact.abort(txn);
			LOG.debug("error while creating user", e);
		} catch (PermissionDeniedException e) {
            transact.abort(txn);
			LOG.debug("error while creating home collection", e);
		} finally {
			pool.release(broker);
		}
	}
	
	public int getResourceDefaultPerms() {
		return defResourcePermissions;
	}
	
	public int getCollectionDefaultPerms() {
		return defCollectionPermissions;
	}
	
	private void createUserHome(DBBroker broker, Txn transaction, User user) 
	throws EXistException, PermissionDeniedException {
		if(user.getHome() == null)
			return;
		broker.setUser(getUser(DBA_USER));
		Collection home = broker.getOrCreateCollection(transaction, user.getHome());
		home.getPermissions().setOwner(user.getName());
		home.getPermissions().setGroup(user.getPrimaryGroup());
		broker.saveCollection(transaction, home);
	}
}
