DirectoryResource.java

/*
** Module   : DirectoryResource.java
** Abstract : Implements "directory" abstract resource.
**
** Copyright (c) 2004-2023, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- ----------------Description----------------------
** 001 NVS 20050420   @20800 Created the initial version. This class is 
**                           responsible for the "directory" abstract  
**                           resource which offers controllable access to  
**                           the P2J directory objects.
** 002 NVS 20050524   @21266 Some methods and members are moved to base
**                           abstract class.
** 003 NVS 20090701   @42998 Put the DirectoryRights class into a separate file
**                           due to a new requirement for all rights classes
*                            to be separate and serializable.
** 004 NVS 20090707   @43081 Added getRightsEditorName() method.
** 005 EVL 20160222          Javadoc fixes to make compatible with Oracle Java 8 for Solaris 10.
** 006 HC  20170612          Changes related to implementation of new GWT-based Admin client.
** 007 SVL 20180530          Made access mode constants public.
** 008 GBB 20230512          Logging methods replaced by CentralLogger/ConversionStatus.
*/ 
/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 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 Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
** 
** Additional terms under GNU Affero GPL version 3 section 7:
** 
**   Under Section 7 of the GNU Affero GPL version 3, the following additional
**   terms apply to the works covered under the License.  These additional terms
**   are non-permissive additional terms allowed under Section 7 of the GNU
**   Affero GPL version 3 and may not be removed by you.
** 
**   0. Attribution Requirement.
** 
**     You must preserve all legal notices or author attributions in the covered
**     work or Appropriate Legal Notices displayed by works containing the covered
**     work.  You may not remove from the covered work any author or developer
**     credit already included within the covered work.
** 
**   1. No License To Use Trademarks.
** 
**     This license does not grant any license or rights to use the trademarks
**     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
**     of Golden Code Development Corporation. You are not authorized to use the
**     name Golden Code, FWD, or the names of any author or contributor, for
**     publicity purposes without written authorization.
** 
**   2. No Misrepresentation of Affiliation.
** 
**     You may not represent yourself as Golden Code Development Corporation or FWD.
** 
**     You may not represent yourself for publicity purposes as associated with
**     Golden Code Development Corporation, FWD, or any author or contributor to
**     the covered work, without written authorization.
** 
**   3. No Misrepresentation of Source or Origin.
** 
**     You may not represent the covered work as solely your work.  All modified
**     versions of the covered work must be marked in a reasonable way to make it
**     clear that the modified work is not originating from Golden Code Development
**     Corporation or FWD.  All modified versions must contain the notices of
**     attribution required in this license.
*/

package com.goldencode.p2j.directory;

import com.goldencode.p2j.security.*;
import com.goldencode.p2j.util.logging.*;

/**
 * Implements the "directory" abstract resource. Instances of this resource 
 * control access to the P2J directory objects.
 * <p>The following is the defined access modes:
 * <ul>
 *   <li>enumerate - lists nodes and attributes; 
 *   <li>read - reads nodes and attribute values;
 *   <li>write - changes nodes and attribute values;
 *   <li>add - adds new attributes or values to attributes;
 *   <li>create - creates new nodes and attributes;
 *   <li>delete - deletes nodes and attributes
 * </ul>
 * <p>Rights objects for this resource are made of one mandatory bitfield 
 * having 8 bits of permissions and one optional boolean expression.
 * <p>
 * The permissions bits are defined as follows:
 * <ul> 
 *   <li>enumerate 
 *   <li>read
 *   <li>write
 *   <li>add
 *   <li>create
 *   <li>delete
 *   <li>denied
 *   <li>logic
 * </ul>
 * Bits from "enumerate" through "delete" control the corresponding access
 * modes. Bit "denied" explicitly denies any access, regardless of the
 * boolean expression and the "logic" bit (veto).
 * <p> 
 * Bit "logic" specifies the operation of the optional boolean expression and
 * is only taken into consideration when the expression is present:
 * <ul>
 *   <li>if set to 1, the <b>AND</b> operation is performed between the result
 *       of the permissions check and the boolean expression; the effect of 
 *       the expression in this case is restrictive; 
 *   <li>if set to 0, the <b>OR</b> operation is performed between the result
 *       of the permissions check and the boolean expression; the effect of 
 *       the expression in this case is permissive;
 * </ul>
 * <p>
 * The directory package uses package private methods defined in this class
 * for access rights checks. The interpretation of various access modes and
 * the meaning of particular operations against the directory nodes and
 * attributes is beyond control of this resource plugin.
 */
public class DirectoryResource
extends AbstractResource
{
   /** ENUMERATE access mode for directory resource */
   public static final int DIR_ENUMERATE_ACCESS = 1;

   /** READ access mode for directory resource */
   public static final int DIR_READ_ACCESS = 2;

   /** WRITE access mode for directory resource */
   public static final int DIR_WRITE_ACCESS = 4;

   /** ADD access mode for directory resource */
   public static final int DIR_ADD_ACCESS = 8;

   /** CREATE access mode for directory resource */
   public static final int DIR_CREATE_ACCESS = 16;

   /** DELETE access mode for directory resource */
   public static final int DIR_DELETE_ACCESS = 32;
   
   /** Logger. */
   private static final CentralLogger LOG = CentralLogger.get(DirectoryResource.class);
   
   /**
    * Constructor. 
    */
   public DirectoryResource()
   {
   }

   /**
    * Returns the plugin resource type name as a string.
    *
    * @return plugin resource type name
    */
   public String getTypeName()
   {
      return "directory";
   }

   /**
    * Returns an array of descriptions, one object per the plugin's access
    * rights item.
    * 
    * @return array of <code>Description</code>s, one per each plugin's access
    *         rights item. 
    */
   public Description[] describeRights()
   {
      Description[] items = new Description[2];
      items[0] = new Description(AttributeType.ATTR_BITFIELD,  // bitfield
                                 false,                        // mandatory
                                 false,                        // fix.size  
                                 8,                            // size
                                 "permissions",                // text label
                                 "bit set to ON enables access",
                                 new String[]                  // bit names
                                 {
                                    "enumerate",
                                    "read",
                                    "write", 
                                    "add",
                                    "create",
                                    "delete",
                                    "denied",
                                    "logic",
                                 },
                                 new BitSet(8)                 // no unused
                                 );
      items[1] = new Description(AttributeType.ATTR_STRING,    // string
                                 true,                         // optional
                                 true,                         // var.size  
                                 0,                            // size
                                 "condition",                  // text label
                                 "extra condition"
                                 );

      return items;
   }

   /**
    * Instantiates a plugin's class that implements the Rights interface,
    * using the array of objects representing a set of access rights fields.
    *
    * @param  rights
    *         an array of objects of proper types representing items in a set
    *         of access rights
    *
    * @return the reference to the interface.
    */
   public Rights getRightsInstance(Object[] rights)
   {
      return new DirectoryRights(BitSetHelper.fromBitField((BitField)rights[0]),
                                 (String)rights[1]);
   }

   /**
    * Checks whether a given string is a valid resource name
    * for this resource type.
    * <p>Valid names are:
    * <ul>
    *   <li>empty string for the root object;
    *   <li>multiple links in the path, separated with '/', where every link
    *       is made of alpha-numeric characters, dashes and underscores and is
    *       no longer than 256 characters.
    * </ul>
    * <p>
    * All these checks are performed by IdUtils.normalize method.
    *
    * @param  resource
    *         string naming a resource
    *
    * @return <code>true</code> if the name is syntactically correct 
    */
   public boolean isInstanceNameValid(String resource)
   {
      if (IdUtils.normalize(resource) == null)
         return false;

      return true;
   }

   /**
    * Checks whether a given array of objects representing a set of access
    * rights fields is acceptable.
    * <p>This method returns <code>true</code> only if:
    * <ul>
    *   <li>fields are of proper number and type;
    *   <li>DENIED bit is not set, or
    *   <li>DENIED bit is set and no other bit from ENUMERATE through DELETE
    *       is set.
    * </ul>
    *
    * @param  rights
    *         an array of objects of proper types representing items in a set
    *         of access rights
    *
    * @return <code>true</code> if the array is acceptable for an instance
    *         of access rights 
    */
   public boolean isRightsSetValid(Object[] rights)
   {
      if (rights.length != 2)
         return false;

      if (rights[0] == null)
         return false;

      if (!rights[0].getClass().isInstance(new BitField(1)))
         return false;

      if (rights[1] != null)
      {
         if (!rights[1].getClass().isInstance(""))
            return false;
      }

      BitSet bs = BitSetHelper.fromBitField((BitField)rights[0]);
      DirectoryRights newRights = new DirectoryRights(bs, (String)rights[1]);

      boolean denied = newRights.getPermission(DirectoryRights.
                                               BIT_DENIED_ACCESS);
      if (!denied)
         return true;

      boolean set = newRights.getPermission(DirectoryRights.
                                            BIT_ENUMERATE_ACCESS)  |
                    newRights.getPermission(DirectoryRights.
                                            BIT_READ_ACCESS)       |
                    newRights.getPermission(DirectoryRights.
                                            BIT_WRITE_ACCESS)      |
                    newRights.getPermission(DirectoryRights.
                                            BIT_ADD_ACCESS)        |
                    newRights.getPermission(DirectoryRights.
                                            BIT_CREATE_ACCESS)     |
                    newRights.getPermission(DirectoryRights.
                                            BIT_DELETE_ACCESS);
      if (set)
         return false;

      return true;
   }

   /**
    * Returns the plugin's exported variables and functions.
    * <p>
    * This implementation exports a library.
    *
    * @return plugin's instance of a library class for symbol resolution
    */
   public Object getLibrary()
   {
      return new DirectoryPool();
   }

   /**
    * Checks the given node for the specified access.
    *
    * @param  oId
    *         directory object ID - resource instance name
    *
    * @param  mode
    *         requested access mode
    *
    * @return <code>true</code> if access is allowed.
    */
    boolean checkAccess(String oId, int mode)
   {
      return dirAccessWorker(oId, mode); 
   }

   /**
    * Implements generalized access rights check worker.
    * As this resource is a hierarchy of names, the full name is
    * checked first. In case there was no rights objects found for the full
    * name, the partial names are checked.
    *
    * @param  oId
    *         directory object ID - resource instance name
    *
    * @param  mode
    *         requested access mode  
    *
    * @return <code>true</code> if access is allowed.
    */
   private boolean dirAccessWorker(String oId, int mode)
   {
      Boolean cached = null;
      boolean decision = false;
      int check = 0;

      String instanceName = oId;

      // check to see if there is a cached decision
      cached = sm.getCachedDecision(resourceIndex, instanceName, mode);
      if (cached != null)
      {
         return cached.booleanValue();
      }

      // initiate the ACL search
      int handle = sm.openRightsSearch(resourceIndex, instanceName, mode);
      LOG.finer("Search open: resId " + resourceIndex + ", instance " + 
                instanceName + ", mode " + mode + ", handle " + handle);   

      // rights check loop
      DirectoryRights rights = (DirectoryRights)sm.getNextRights(handle);

      if (rights == null)
      {
         // no rights defined for this name; climb the hierarchy up
         if (instanceName.length() == 0)     // reached the root
            return false;

         int last = instanceName.lastIndexOf('/');
         if (last == -1)                     // safety net; never happens
            return false;

         instanceName = instanceName.substring(0, last);
         decision = dirAccessWorker(instanceName, mode);
      }
      else
      {
      loop:
         while (rights != null)
         {
            LOG.finer("Search next: handle " + handle + ", rights " + rights);   
            check = checkSingle(instanceName, mode, rights);
            switch (check)
            {
               case -1:
                  break loop;
   
               case 0:
                  break;
    
               case 1:
                  decision = true;
                  break loop;
            }
   
            rights = (DirectoryRights)sm.getNextRights(handle);
         }
      }

      // close the ACL search
      LOG.finer("Search done: handle " + handle + ", decision " + decision + 
                ", cache true");   
      sm.closeRightsSearch(handle, decision, true);

      return decision;
   }

   /**
    * Implements a worker that performs access rights check on a single
    * instance of the DirectoryRights object.
    *
    * @param  oId
    *         directory object ID - resource instance name
    *
    * @param  mode
    *         requested access mode  
    *
    * @param rights
    *        instance of DirectoryRights to check 
    *
    * @return one of the following values:
    *         <ul>
    *            <li>-1 if access is explicitly denied or access mode is
    *                invalid
    *            <li>0 if decision for this access mode is FALSE
    *            <li>1 if decision for this access mode is TRUE
    *         </ul>
    */
   private int checkSingle(String oId, int mode, DirectoryRights rights)
   {
      // look at the DENIED permission first
      if (rights.getPermission(DirectoryRights.BIT_DENIED_ACCESS))
         return -1;

      // second, query the appropriate permission bit
      int bit = 0;
      switch (mode)
      {
         case DIR_ENUMERATE_ACCESS:
            bit = DirectoryRights.BIT_ENUMERATE_ACCESS;
            break;

         case DIR_READ_ACCESS:
            bit = DirectoryRights.BIT_READ_ACCESS;
            break;

         case DIR_WRITE_ACCESS:
            bit = DirectoryRights.BIT_WRITE_ACCESS;
            break;

         case DIR_ADD_ACCESS:
            bit = DirectoryRights.BIT_ADD_ACCESS;
            break;

         case DIR_CREATE_ACCESS:
            bit = DirectoryRights.BIT_CREATE_ACCESS;
            break;

         case DIR_DELETE_ACCESS:
            bit = DirectoryRights.BIT_DELETE_ACCESS;
            break;

         default: 
            return -1; 
      }

      boolean decision = rights.getPermission(bit);
      boolean logic = rights.getPermission(DirectoryRights.BIT_LOGIC);

      // third, figure out if the decision can be made now:
      // - no condition ==> return decision (0 or 1);
      // - logic is OR  and decision is TRUE  ==> return 1;
      // - logic is AND and decision is FALSE ==> return 0

      String condition = rights.getCondition();
      if (condition == null)
      {
         if (decision)
            return 1;
         else
            return 0;
      }

      if (logic == false && decision == true)
         return 1;

      if (logic == true && decision == false)
         return 0;

      // fourth, evaluate the condition and make the final decision
      SharedData share = new SharedData(oId, mode); 
      Boolean result = null;
      Object  o = sm.evaluate(resourceIndex, share, condition);
      if (o == null || !Boolean.class.isInstance(o))
         result = null;
      else
         result = (Boolean)o;

      boolean cond = result == null ? false : result.booleanValue();
      if (logic == false)
         decision = decision | cond;
      else
         decision = decision & cond;

      if (decision)
         return 1;
      else
         return 0;
   }
   
   /**
    * Implements the directory variable pool. 
    * <p>
    * Variables defined in this private pool are: 
    * <ul>
    *   <li>node - directory node name currently being accessed;
    *   <li>mode - access mode for the current operation.
    * </ul>
    * <p>
    * Methods defined in this private pool are: 
    * <ul>
    *   <li>boolean under(String path, String branch)
    *       <br>checks to see whether the directory node 'path' is under the
    *       specified tree branch.
    * </ul>
    */
   public class DirectoryPool
   {
      /**
       * Constructor. 
       */
      public DirectoryPool()
      {
      }

      /**
       * Gets the directory node under consideration.
       *
       * @return directory node name
       */
      public String getNode()
      {
         return ((SharedData)getLink()).getNode();
      }

      /**
       * Gets the access mode of the current operation.
       *
       * @return the string representation of the access mode as follows:
       * <ul>
       *   <li>"E" for "enumerate";
       *   <li>"R" for "read";
       *   <li>"W" for "write";
       *   <li>"A" for "add";
       *   <li>"C" for "create";
       *   <li>"D" for "delete";
       * </ul>
       */
      public String getMode()
      {
         int mode = ((SharedData)getLink()).getMode();

         switch (mode)
         {
            case DIR_ENUMERATE_ACCESS:
               return "E";

            case DIR_READ_ACCESS:
               return "R";

            case DIR_WRITE_ACCESS:
               return "W";

            case DIR_ADD_ACCESS:
               return "A";

            case DIR_CREATE_ACCESS:
               return "C";

            case DIR_DELETE_ACCESS:
               return "D";
         }

         return "U";     // never happens as mode is verified by now
      }

      /**
       * Relates the current directory node to a directory branch.
       * <p>
       * Since the implementation simply compares the strings using the
       * String.startsWith() method, the branch parameter should be made of
       * one or more complete links to produce a meaningful result.
       *
       * @param  branch
       *         directory tree branch to compare with
       *
       * @return <code>true</code> if the current path is under the branch,
       *         <code>false</code> otherwise.<br>
       *         <code>false</code> is returned if path is the directory root
       *         "".<br>
       *         <code>true</code> is returned if branch is the directory root
       *         "" (unless the path is the root, too).
       */
      public boolean isUnder(String branch)
      {
         return ((SharedData)getLink()).getNode().startsWith(branch);
      }
   }

   /**
    * Implements the directory variable pool shared data.
    * These are pieces of information taken from the access check being
    * performed. As these are maintained on per thread basis, the convenient
    * way to pass them between the access check method and the variable pool
    * methods is the link provided by AbstractResource class.
    * <p>
    * Variables defined in this private pool are: 
    * <ul>
    *   <li>node - directory node name currently being accessed;
    *   <li>mode - access mode for the current operation.
    * </ul>
    */
   private class SharedData
   {
      /** node name */
      private String node = null;

      /** access mode */
      private int mode = 0;

      /**
       * Constructor. 
       *
       * @param node
       *        node being checked
       *
       * @param mode
       *        requested access mode
       */
      SharedData(String node, int mode)
      {
         this.node = node;
         this.mode = mode;
      }

      /**
       * Gets the directory node name. 
       *
       * @return node name saved in this shared data
       */
      String getNode()
      {
         return node;
      }

      /**
       * Gets the access mode. 
       *
       * @return access mode saved in this shared data
       */
      int getMode()
      {
         return mode;
      }
   }
}