/***********************************************************************
 *
 *  Copyright (C) 2006 Novell, Inc. All Rights Reserved.
 *
 *  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; version 2.1
 *  of the License.
 *
 *  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
 *  Library 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, Novell, Inc.
 *
 *  To contact Novell about this file by physical or electronic mail,
 *  you may find current contact information at www.novell.com.
 *
 *  Author: Juan Carlos Luciani <jluciani@novell.com>
 *
 ***********************************************************************/

package com.novell.casa.authtoksvc;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.apache.xerces.parsers.DOMParser;
import org.apache.xml.serialize.OutputFormat;
import org.apache.xml.serialize.XMLSerializer;

import java.io.*;

/**
 *
 * Class for the creation and editing of auth.policy files.
 *
 */
public final class AuthPolicyEditor
{
   private static final String usage =
      "usage: AuthPolicyEditor -op [-entry realm:mechanismName[:mechanismInfo]] [-refentry realm:mechanismName] -file policyFilePath\n\n" +
      "   where:\n" +
      "      -op - Corresponds to one of the following operations:\n" +
      "         -create  - Create new auth policy file\n" +
      "         -list    - List auth source entries\n" +
      "         -prepend - Insert auth source entry at the head\n" +
      "         -append  - Insert auth source entry at the tail\n" +
      "         -insert  - Insert auth source entry after specified reference entry\n" +
      "         -remove  - Remove auth source entry\n" +
      "      -file - Path the the auth policy file\n" +
      "      -entry - Auth source entry to be inserted or removed. Must be followed by\n" +
      "               a string formated as follows:\n" +
      "                  insert operations format: realm:mechanismName or realm:mechanismName:mechanismInfo\n" +
      "                  remove operations format: realm:mechanismName\n" +
      "      -refentry - Reference auth source entry. Must be followed by a string formated\n" +
      "                  as follows: realm:mechanismName\n";

   private static final String initialPolicy =
      "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
      "<auth_policy>\n" +
      "</auth_policy>\n";

   private final static String AuthSourceElementName = "auth_source";
   private final static String RealmElementName = "realm";
   private final static String MechanismElementName = "mechanism";
   private final static String MechanismInfoElementName = "mechanism_info";
   private final static String Krb5Mechanism = "Krb5Authenticate";
   private final static String PwdMechanism = "PwdAuthenticate";


   /**
    * Returns the formal mechanism name if well known.
    *
    * @param mechName   Name of mechanism.
    * @return Mechanism formal name.
    */
   private static String mechFormalName(String mechName)
   {
      String formalName;

      if (mechName.equalsIgnoreCase(Krb5Mechanism))
         formalName = Krb5Mechanism;
      else if (mechName.equalsIgnoreCase(PwdMechanism))
         formalName = PwdMechanism;
      else
         formalName = mechName;

      return formalName;
   }

   /**
    * Update the contents of the specified file with the provided
    * policy document.
    *
    * @param filePath   Path to policy file to be updated.
    * @param doc        Policy document.
    * @return           True if successful.
    */
   private static boolean updateFile(String filePath, Document doc)
   {
      boolean result = false;
      FileOutputStream out = null;

      // Update the file with the specified document
      // after removing the text nodes.
      try
      {
         // Remove text nodes
         Element root = doc.getDocumentElement();
         Node child;
         Node next = root.getFirstChild();
         while ((child = next) != null)
         {
            next = child.getNextSibling();
            if (child.getNodeType() == Node.TEXT_NODE)
            {
               // Remove the node
               root.removeChild(child);
            }
         }

         // Update file
         File f = new File(filePath);
         out = new FileOutputStream(f);
         OutputFormat format = new OutputFormat(doc);
         XMLSerializer serializer = new XMLSerializer(out, format);
         serializer.serialize(doc.getDocumentElement());

         result = true;
      }
      catch (IOException e)
      {
         System.err.println("Error writing to file " + filePath + ", exception: " + e.toString());
      }
      catch (SecurityException e)
      {
         System.err.println("SecurityException writting to file " + filePath);
      }
      finally
      {
         if (out != null)
         {
            try
            {
               out.flush();
               out.close();
            }
            catch (IOException e)
            {
               // Do nothing
            }
         }
      }

      return result;
   }

   /**
    * Gets document for the specified policy file.
    *
    * @param filePath   Path to the policy file.
    * @return           Document representation of the policy file.
    */
   private static Document getPolicyFileDoc(String filePath)
   {
      Document doc = null;
      FileInputStream inStream = null;

      try
      {
         // Get an input stream to read from policy file
         File f = new File(filePath);
         inStream = new FileInputStream(f);
         InputSource source = new InputSource(inStream);

         DOMParser parser = new DOMParser();
         parser.parse(source);
         doc = parser.getDocument();
      }
      catch (FileNotFoundException e)
      {
         System.err.println("Policy file " + filePath + " not found");
      }
      catch (SecurityException e)
      {
         System.err.println("SecurityException accessing " + filePath);
      }
      catch (IOException e)
      {
         System.err.println("IOException accessing " + filePath + " Exception=" + e.toString());
      }
      catch (SAXException e)
      {
        System.err.println("Policy file " + filePath + " format error");
      }
      finally
      {
         if (inStream != null)
         {
            try
            {
               inStream.close();
            }
            catch (IOException e)
            {
               // Do nothing
            }
         }
      }

      return doc;
   }

   /**
    * Gets a starting policy document.
    *
    * @return  Starting policy document.
    */
   private static Document getPolicyDoc()
   {
      Document doc = null;
      StringReader reader = null;

      try
      {
         reader = new StringReader(initialPolicy);
         InputSource source = new InputSource(reader);

         DOMParser parser = new DOMParser();
         parser.parse(source);
         doc = parser.getDocument();
      }
      catch (Exception e)
      {
        System.err.println("Program error, exception: " + e.toString());
      }
      finally
      {
         if (reader != null)
         {
            try
            {
               reader.close();
            }
            catch (Exception e)
            {
               // Do nothing
            }
         }
      }

      return doc;
   }

   /**
    * List the auth_source entries in the specified policy file.
    *
    * @param filePath   Path to the policy file.
    * @return           True if the operation is successfully performed.
    */
   private static boolean performListOperation(String filePath)
   {
      boolean opPerformed = false;

      // List the auth sources present in the policy file
      Document doc = getPolicyFileDoc(filePath);
      if (doc != null)
      {
         // Go through the elements of the document
         Element root = doc.getDocumentElement();
         Node auth_source_node;
         Node next_auth_source_node = root.getFirstChild();
         while ((auth_source_node = next_auth_source_node) != null)
         {
            next_auth_source_node = auth_source_node.getNextSibling();
            if (auth_source_node.getNodeType() == Node.ELEMENT_NODE
                && auth_source_node.getLocalName().equalsIgnoreCase("auth_source"))
            {
               // IMPORTANT WARNING - Do not change the output format without changing utilities
               // that depend on it such as the yast2-ats module.
               System.out.println("Auth_Source:");

               // We are dealing with an auth_source, display its children.
               Node child;
               Node next = auth_source_node.getFirstChild();
               while ((child = next) != null)
               {
                  next = child.getNextSibling();
                  if (child.getNodeType() == Node.ELEMENT_NODE)
                  {
                     if (child.getLocalName().equalsIgnoreCase("realm"))
                     {
                        System.out.println("\tIdentity Source:" + child.getTextContent());
                     }
                     else if (child.getLocalName().equalsIgnoreCase("mechanism"))
                     {
                        System.out.println("\tAuthentication Mechanism:" + child.getTextContent());
                     }
                     else if (child.getLocalName().equalsIgnoreCase("mechanism_info"))
                     {
                        System.out.println("\tAuthentication Mechanism Info:" + child.getTextContent());
                     }
                  }
               }
            }
         }

         opPerformed = true;
      }

      return opPerformed;
   }

   /**
    * Create policy file.
    *
    * @param filePath   Path to the settings file.
    * @return           True if the operation is successfully performed.
    */
   private static boolean performCreateOperation(String filePath)
   {
      boolean opPerformed = false;

      // create a policy file
      Document doc = getPolicyDoc();
      if (doc != null)
      {
         FileOutputStream out = null;

         try
         {
            File f = new File(filePath);
            boolean createStatus = f.createNewFile();
            if (createStatus)
            {
               out = new FileOutputStream(f);
               OutputFormat format = new OutputFormat(doc);
               XMLSerializer serializer = new XMLSerializer(out, format);
               serializer.serialize(doc.getDocumentElement());

               opPerformed = true;
            }
            else
            {
               System.err.println("File " + filePath + " already exists");
            }
         }
         catch (IOException e)
         {
            System.err.println("Error creating file " + filePath + ", exception: " + e.toString());
         }
         catch (SecurityException e)
         {
            System.err.println("SecurityException creating " + filePath);
         }
         finally
         {
            if (out != null)
            {
               try
               {
                  out.flush();
                  out.close();
               }
               catch (IOException e)
               {
                  // Do nothing
               }
            }
         }
      }

      return opPerformed;
   }

   /**
    * Prepend the auth_source entry to the specified policy file.
    *
    * @param filePath   Path to the policy file.
    * @param entry      Auth_source entry to be prepended. Entry is formated as
    *                   follows: realm:mech:mechinfo (Note that the mechinfo
    *                   is optional).
    * @return           True if operation is successfully performed.
    */
   private static boolean performPrependOperation(String filePath, String entry)
   {
      boolean opPerformed = false;

      // Prepend auth source entry to the policy file
      Document doc = getPolicyFileDoc(filePath);
      if (doc != null)
      {
         // Parse the entry into its components. Entry is formated as
         // follows: realm:mech:mechinfo (Note that the mechinfo
         // is optional).
         String[] entryComponents = entry.split(":");
         if (entryComponents.length >= 2
             && entryComponents.length <= 3)
         {
            // Create and prepend the entry elements
            Element root = doc.getDocumentElement();
            Element auth_source_element = doc.createElement(AuthSourceElementName);
            Element realm_element = doc.createElement(RealmElementName);
            realm_element.setTextContent(entryComponents[0]);
            auth_source_element.appendChild(realm_element);
            Element mechanism_element = doc.createElement(MechanismElementName);
            mechanism_element.setTextContent(mechFormalName(entryComponents[1]));
            auth_source_element.appendChild(mechanism_element);
            if (entryComponents.length == 3)
            {
               Element mechanism_info_element = doc.createElement(MechanismInfoElementName);
               mechanism_info_element.setTextContent(mechFormalName(entryComponents[2]));
               auth_source_element.appendChild(mechanism_info_element);
            }

            Element firstEntry = null;
            Node child;
            Node next = root.getFirstChild();
            while ((child = next) != null)
            {
               next = child.getNextSibling();
               if (child.getNodeType() == Node.ELEMENT_NODE)
               {
                  // This is the first entry
                  firstEntry = (Element) child;
                  break;
               }
            }
            if (firstEntry != null)
               root.insertBefore(auth_source_element, firstEntry);
            else
               root.appendChild(auth_source_element);

            // Update the file
            opPerformed = updateFile(filePath, doc);
         }
         else
         {
            System.err.println("Invalid entry format");
         }
      }

      return opPerformed;
   }

   /**
    * Append the auth_source entry to the specified policy file.
    *
    * @param filePath   Path to the policy file.
    * @param entry      Auth_source entry to be appended. Entry is formated as
    *                   follows: realm:mech:mechinfo (Note that the mechinfo
    *                   is optional).
    * @return           True if operation is successfully performed.
    */
   private static boolean performAppendOperation(String filePath, String entry)
   {
      boolean opPerformed = false;

      // Append auth source entry to the policy file
      Document doc = getPolicyFileDoc(filePath);
      if (doc != null)
      {
         // Parse the entry into its components. Entry is formated as
         // follows: realm:mech:mechinfo (Note that the mechinfo
         // is optional).
         String[] entryComponents = entry.split(":");
         if (entryComponents.length >= 2
             && entryComponents.length <= 3)
         {
            // Create and append the entry elements
            Element root = doc.getDocumentElement();
            Element auth_source_element = doc.createElement(AuthSourceElementName);
            Element realm_element = doc.createElement(RealmElementName);
            realm_element.setTextContent(entryComponents[0]);
            auth_source_element.appendChild(realm_element);
            Element mechanism_element = doc.createElement(MechanismElementName);
            mechanism_element.setTextContent(mechFormalName(entryComponents[1]));
            auth_source_element.appendChild(mechanism_element);
            if (entryComponents.length == 3)
            {
               Element mechanism_info_element = doc.createElement(MechanismInfoElementName);
               mechanism_info_element.setTextContent(mechFormalName(entryComponents[2]));
               auth_source_element.appendChild(mechanism_info_element);
            }
            root.appendChild(auth_source_element);

            // Update the file
            opPerformed = updateFile(filePath, doc);
         }
         else
         {
            System.err.println("Invalid entry format");
         }
      }

      return opPerformed;
   }

   /**
    * Insert the auth_source entry to the specified policy file.
    *
    * @param filePath   Path to the policy file.
    * @param entry      Auth_source entry to be inserted. Entry is formated as
    *                   follows: realm:mech:mechinfo (Note that the mechinfo
    *                   is optional).
    * @param refEntry   Reference auth_source entry (New entry is inserted after
    *                   it). Entry is formated as follows: realm:mech.
    * @return           True if operation is successfully performed.
    */
   private static boolean performInsertOperation(String filePath, String entry, String refEntry)
   {
      boolean opPerformed = false;

      // Remove auth sources present in the policy file
      Document doc = getPolicyFileDoc(filePath);
      if (doc != null)
      {
         // Parse the entries into their components. Entry is formated as
         // follows: realm:mech:mechinfo (Note that the mechinfo
         // is optional).
         String[] entryComponents = entry.split(":");
         String[] refEntryComponents = refEntry.split(":");
         if (refEntryComponents.length == 2
             && entryComponents.length >= 2
             && entryComponents.length <= 3)
         {
            // Go through the elements of the document
            Element root = doc.getDocumentElement();
            Node curr_auth_source_node;
            Node next_auth_source_node = root.getFirstChild();
            while ((curr_auth_source_node = next_auth_source_node) != null)
            {
               next_auth_source_node = curr_auth_source_node.getNextSibling();
               if (curr_auth_source_node.getNodeType() == Node.ELEMENT_NODE
                   && curr_auth_source_node.getLocalName().equalsIgnoreCase("auth_source"))
               {
                  // We are dealing with an auth_source, check if this is the
                  // reference entry.
                  boolean realmMatch = false;
                  boolean mechanismMatch = false;

                  Node child;
                  Node next = curr_auth_source_node.getFirstChild();
                  while ((child = next) != null)
                  {
                     next = child.getNextSibling();
                     if (child.getNodeType() == Node.ELEMENT_NODE)
                     {
                        if (child.getLocalName().equalsIgnoreCase(RealmElementName))
                        {
                           // Compare the realm name
                           if (child.getTextContent().equalsIgnoreCase(refEntryComponents[0]))
                              realmMatch = true;
                        }
                        else if (child.getLocalName().equalsIgnoreCase(MechanismElementName))
                        {
                           // Compare the realm name
                           if (child.getTextContent().equalsIgnoreCase(mechFormalName(refEntryComponents[1])))
                              mechanismMatch = true;
                        }
                     }
                  }

                  // Insert entry after current entry if we have a match for the reference entry
                  if (realmMatch && mechanismMatch)
                  {
                     Element auth_source_element = doc.createElement(AuthSourceElementName);
                     Element realm_element = doc.createElement(RealmElementName);
                     realm_element.setTextContent(entryComponents[0]);
                     auth_source_element.appendChild(realm_element);
                     Element mechanism_element = doc.createElement(MechanismElementName);
                     mechanism_element.setTextContent(mechFormalName(entryComponents[1]));
                     auth_source_element.appendChild(mechanism_element);
                     if (entryComponents.length == 3)
                     {
                        Element mechanism_info_element = doc.createElement(MechanismInfoElementName);
                        mechanism_info_element.setTextContent(mechFormalName(entryComponents[2]));
                        auth_source_element.appendChild(mechanism_info_element);
                     }
                     curr_auth_source_node.getNextSibling();
                     Element nextEntry = null;
                     next = curr_auth_source_node.getNextSibling();
                     while ((child = next) != null)
                     {
                        next = child.getNextSibling();
                        if (child.getNodeType() == Node.ELEMENT_NODE)
                        {
                           // This is the next entry
                           nextEntry = (Element) child;
                           break;
                        }
                     }
                     if (nextEntry != null)
                        root.insertBefore(auth_source_element, nextEntry);
                     else
                        root.appendChild(auth_source_element);

                     // Update the file
                     opPerformed = updateFile(filePath, doc);
                     break;
                  }
               }
            }
         }
         else
         {
            System.err.println("Invalid entry format");
         }
      }

      return opPerformed;
   }

   /**
    * Remove the auth_source entry from the specified policy file.
    *
    * @param filePath   Path to the policy file.
    * @param entry      Auth_source entry to be removed. Entry is formated as
    *                   follows: realm:mech.
    * @return           True if operation is successfully performed.
    */
   private static boolean performRemoveOperation(String filePath, String entry)
   {
      boolean opPerformed = false;

      // Remove auth sources present in the policy file
      Document doc = getPolicyFileDoc(filePath);
      if (doc != null)
      {
         // Parse the entry into its components. Entry is formated as
         // follows: realm:mech:mechinfo (Note that the mechinfo
         // is optional).
         String[] entryComponents = entry.split(":");
         if (entryComponents.length == 2)
         {
            // Go through the elements of the document
            Element root = doc.getDocumentElement();
            Node auth_source_node;
            Node next_auth_source_node = root.getFirstChild();
            while ((auth_source_node = next_auth_source_node) != null)
            {
               next_auth_source_node = auth_source_node.getNextSibling();
               if (auth_source_node.getNodeType() == Node.ELEMENT_NODE
                   && auth_source_node.getLocalName().equalsIgnoreCase("auth_source"))
               {
                  // We are dealing with an auth_source, check if this is the entry
                  // that must be removed.
                  boolean realmMatch = false;
                  boolean mechanismMatch = false;

                  Node child;
                  Node next = auth_source_node.getFirstChild();
                  while ((child = next) != null)
                  {
                     next = child.getNextSibling();
                     if (child.getNodeType() == Node.ELEMENT_NODE)
                     {
                        if (child.getLocalName().equalsIgnoreCase(RealmElementName))
                        {
                           // Compare the realm name
                           if (child.getTextContent().equalsIgnoreCase(entryComponents[0]))
                              realmMatch = true;
                        }
                        else if (child.getLocalName().equalsIgnoreCase(MechanismElementName))
                        {
                           // Compare the realm name
                           if (child.getTextContent().equalsIgnoreCase(mechFormalName(entryComponents[1])))
                              mechanismMatch = true;
                        }
                     }
                  }

                  // Remove current entry if it matches
                  if (realmMatch && mechanismMatch)
                  {
                     System.err.println("RemovingChild");
                     root.removeChild(auth_source_node);

                     // Update the file
                     opPerformed = updateFile(filePath, doc);
                     break;
                  }
               }
            }
         }
         else
         {
            System.err.println("Invalid entry format");
         }
      }

      return opPerformed;
   }

   /**
   *  Applications Entry Point.
   *
   * @param args  Arguments.
   */
   public static void main(String[] args)
   {
      String op = null;
      boolean opPerformed = false;
      boolean argumentsError = false;
      String filePath = null;
      String entry = null;
      String refEntry = null;

      // Process the command line arguments
      for (int i = 0; i < args.length; i++)
      {
         // Proceed based on the command
         if (args[i].equalsIgnoreCase("-list"))
         {
            // List operation requested
            if (op == null)
            {
               op = "list";
            }
            else
            {
               argumentsError = true;
               break;
            }
         }
         else if (args[i].equalsIgnoreCase("-create"))
         {
            // Create operation requested
            if (op == null)
            {
               op = "create";
            }
            else
            {
               argumentsError = true;
               break;
            }
         }
         else if (args[i].equalsIgnoreCase("-prepend"))
         {
            // Prepend operation requested
            if (op == null)
            {
               op = "prepend";
            }
            else
            {
               argumentsError = true;
               break;
            }
         }
         else if (args[i].equalsIgnoreCase("-append"))
         {
            // Append operation requested
            if (op == null)
            {
               op = "append";
            }
            else
            {
               argumentsError = true;
               break;
            }
         }
         else if (args[i].equalsIgnoreCase("-insert"))
         {
            // Insert operation requested
            if (op == null)
            {
               op = "insert";
            }
            else
            {
               argumentsError = true;
               break;
            }
         }
         else if (args[i].equalsIgnoreCase("-remove"))
         {
            // Remove operation requested
            if (op == null)
            {
               op = "remove";
            }
            else
            {
               argumentsError = true;
               break;
            }
         }
         else if (args[i].equalsIgnoreCase("-entry"))
         {
            // The next argument should contain the entry information
            if (args.length > (i + 1))
            {
               entry = args[i + 1];
               i++;
            }
            else
            {
               argumentsError = true;
               break;
            }
         }
         else if (args[i].equalsIgnoreCase("-refentry"))
         {
            // The next argument should contain the reference entry information
            if (args.length > (i + 1))
            {
               refEntry = args[i + 1];
               i++;
            }
            else
            {
               argumentsError = true;
               break;
            }
         }
         else if (args[i].equalsIgnoreCase("-file"))
         {
            // The next argument should contain the filepath
            if (args.length > (i + 1))
            {
               filePath = args[i + 1];
               i++;
            }
            else
            {
               argumentsError = true;
               break;
            }
         }
         else
         {
            argumentsError = true;
         }
      }

      // Proceed based on the specified parameters
      if (argumentsError == false)
      {
         if (filePath != null && op != null)
         {
            System.err.println("Dealing with policy file: " + filePath);

            // Proceed based on the operation requested
            if (op.compareTo("list") == 0)
            {
               opPerformed = performListOperation(filePath);
            }
            else if (op.compareTo("create") == 0)
            {
               opPerformed = performCreateOperation(filePath);
            }
            else if (op.compareTo("prepend") == 0)
            {
               // Verify that the required parameters were specified
               if (entry != null)
               {
                  opPerformed = performPrependOperation(filePath, entry);
               }
               else
               {
                  argumentsError = true;
               }
            }
            else if (op.compareTo("append") == 0)
            {
               // Verify that the required parameters were specified
               if (entry != null)
               {
                  opPerformed = performAppendOperation(filePath, entry);
               }
               else
               {
                  argumentsError = true;
               }
            }
            else if (op.compareTo("insert") == 0)
            {
               // Verify that the required parameters were specified
               if (entry != null && refEntry != null)
               {
                  opPerformed = performInsertOperation(filePath, entry, refEntry);
               }
               else
               {
                  argumentsError = true;
               }
            }
            else if (op.compareTo("remove") == 0)
            {
               // Verify that the required parameters were specified
               if (entry != null)
               {
                  opPerformed = performRemoveOperation(filePath, entry);
               }
               else
               {
                  argumentsError = true;
               }
            }
            else
            {
               System.err.println("Tool error");
            }
         }
         else
         {
            argumentsError = true;
         }
      }

      // Display the usage string if we encountered an error with the
      // command line arguments.
      if (argumentsError)
         System.out.print(usage);

      // Set the exit code appropriatedly
      if (opPerformed)
         System.exit(0);
      else
         System.exit(1);
   }
}
