AdminResource.java

/*
** Module   : AdminResource.java
** Abstract : Implements "admin" abstract resource.
**
** Copyright (c) 2009-2023, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- ----------------Description------------------------
** 001 NVS 20090921   @44096 Created the initial version. This class is 
**                           responsible for the "admin" abstract resource which
**                           offers controllable access to all admin functions.
** 002 NVS 20091007   @44106 Reworked access rights checks in a more systemic
**                           way.
** 003 SIY 20091216   @44511 Added "refresh" node.
** 004 HC  20170612          Changes related to implementation of new GWT-based Admin client.
** 005 TJD 20220504          Java 11 compatibility minor changes
** 006 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.security;

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

import java.util.logging.*;

/**
 * Implements the "admin" abstract resource. Instances of this resource 
 * control access to the named admin functions.
 * The admin resource implements a hierarchical name space, where the
 * nodes have the well-known names. Instance names are coded in a way the P2J
 * directory nodes or the Unix file system objects are coded:
 * <ul>
 *   <li>"/" designates the root admin permissions
 *   <li>"/console", "/accounts" etc designate the first level function groups
 *   <li>"/accounts/users" etc narrow down the scope of the function; it may
 *       or may not take some additional levels
 *   <li>a regular expression is allowed at the level 1 and below, provided it
 *       does not contain the forward slash and yields at least one match
 * </ul>
 * This hierarchy is expandable. The initial implementation is outlined
 * below. No matter how large the tree gets expanded, the following requirements
 * are always met:
 * <ul>
 *   <li>for every node in the tree, the child nodes form an enumeration of 
 *       the well-known names
 *   <li>non-leaf nodes give the following permissions:
 *     <ul>
 *       <li>explicit unlimited access to all the functions that may be coded
 *           in all the child nodes
 *       <li>explicit denial of access to all the functions that may be coded
 *           in all the child nodes
 *       <li>if neither of the explicit permissions is given, the access is
 *           controlled by the permissions as they are defined in the child
 *           nodes
 *     </ul> 
 *   <li>leaf nodes give permissions that are specific to the node
 * </ul>
 * The initial tree has the following nodes:
 * <ul>
 *   <li>"/" the root
 *   <li>"/accounts"
 *   <li>"/accounts/users" - the leaf node
 * </ul>
 * <p>The following access modes are defined for the /accounts/users:
 * <ul>
 *   <li>enumerate - lists defined user accounts; 
 *   <li>password - changes passwords only
 *   <li>group - allows assigning users to groups;
 *   <li>read - reads details of user accounts definitions;
 *   <li>write - changes user accounts definitions;
 *   <li>create - creates new user accounts;
 *   <li>delete - deletes user accounts
 * </ul>
 * <p>Rights objects for this resource are made of one mandatory bitfield 
 * having 2 bits for the non-leaf nodes and various number of bits for the leaf
 * nodes. 
 * <p>
 * For the non-leaf nodes, the permissions bits are defined as follows:
 * <ul> 
 *   <li>unlimited access 
 *   <li>denied access
 * </ul>
 * For the /accounts/users leaf nodes, the permissions bits are defined as
 * follows: 
 * <ul> 
 *   <li>enumerate 
 *   <li>password
 *   <li>group
 *   <li>read
 *   <li>write
 *   <li>create
 *   <li>delete
 *   <li>denied
 * </ul>
 * Bits from "enumerate" through "delete" control the corresponding access
 * modes. Bit "denied" explicitly denies any access.
 * <p>
 * The security package uses the package private methods defined in this class
 * for access rights checks. The interpretation of various access modes and
 * the meaning of particular operations is beyond control of this resource
 * plugin.
 * <p>
 * 
 */
public class AdminResource
extends AbstractResource
{
   /** Logger. */
   private static final CentralLogger LOG = CentralLogger.get(AdminResource.class);
   
   /**
    * Constructor. 
    */
   public AdminResource()
   {
   }

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

   /**
    * Returns an array of descriptions, one object per the plugin's access
    * rights item.
    * <p>This method was not designed to give descriptions of all possible
    * variations of rights, so it returns the most used one.
    * 
    * @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_INTEGER,   // integer
                                 false,                        // mandatory
                                 false,                        // fix.size  
                                 0,                            // size
                                 "type",                       // text label
                                 "defines the type of the bitfield"
                                 );
      items[1] = 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",
                                    "password",
                                    "group",
                                    "read",
                                    "write", 
                                    "create",
                                    "delete",
                                    "denied",
                                 },
                                 new BitSet(8)                 // no unused
                                 );

      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)
   {
      BitField bf = (BitField) rights[1];
      BitSet bs = BitSetHelper.fromBitField(bf);
      return new AdminRights((Integer)rights[0], bs);
   }

   /**
    * Checks whether a given string is a valid resource name
    * for this resource type.
    * <p>Valid names are those that belong to the namespace tree.
    *
    * @param  resource
    *         string naming a resource
    *
    * @return <code>true</code> if the name is valid 
    */
   public boolean isInstanceNameValid(String resource)
   {
      Node[] path = AdminResourceNamespace.findPath(resource);

      return path != null;
   }

   /**
    * 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
    *         <p>
    *         This implementation expects an Integer encoding the bitfield type
    *         and a bitfield of the corresponding size.
    *
    * @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 || rights[1] == null)
      {
         return false;
      }

      if (!rights[0].getClass().isInstance(Integer.valueOf(0)))
      {
         return false;
      }

      if (!rights[1].getClass().isInstance(new BitField(1)))
      {
         return false;
      }
      
      int type = ((Integer)rights[0]).intValue();
      
      if (type != AdminRights.ADMT_PATH && 
          type != AdminRights.ADMT_LEAF_USER &&
          type != AdminRights.ADMT_LEAF_REFRESH)
      {
         return false;
      }
      
      AdminRights newRights = new AdminRights((Integer)rights[0], 
                                              BitSetHelper.fromBitField((BitField)rights[1]));

      boolean denied = false;
      boolean set = false;
                                            
      switch (type)
      {
         case AdminRights.ADMT_PATH:
            denied = newRights.getPermission(AdminRights.ADTP_DENIED_ACCESS);
            if (!denied)
            {
               return true;
            }
            set = newRights.getPermission(AdminRights.ADTP_UNLIMITED_ACCESS);
            if (set)
            {
               return false;
            }
            break;
            
         case AdminRights.ADMT_LEAF_REFRESH:
            denied = newRights.getPermission(AdminRights.ADLR_DENIED_ACCESS);
            if (!denied)
            {
               return true;
            }
            set = newRights.getPermission(AdminRights.ADLR_REFRESH_ACCESS);
            if (set)
            {
               return false;
            }
            break;
            
         case AdminRights.ADMT_LEAF_USER:
            denied = newRights.getPermission(AdminRights.ADLU_DENIED_ACCESS);
            if (!denied)
            {
               return true;
            }
            set = newRights.getPermission(AdminRights.ADLU_ENUMERATE_ACCESS) |
                  newRights.getPermission(AdminRights.ADLU_PASSWORD_ACCESS)  |
                  newRights.getPermission(AdminRights.ADLU_GROUP_ACCESS)     |
                  newRights.getPermission(AdminRights.ADLU_READ_ACCESS)      |
                  newRights.getPermission(AdminRights.ADLU_WRITE_ACCESS)     |
                  newRights.getPermission(AdminRights.ADLU_CREATE_ACCESS)    |
                  newRights.getPermission(AdminRights.ADLU_DELETE_ACCESS);
            if (set)
            {
               return false;
            }
            break;
      }
      
      return true;
   }

   /**
    * Checks whether the current subject has any admin rights.
    * Currently, the decision is made based on the DENIED _ACCESS permission.
    * If the DENIED_ACCESS is not explicitly set for the subject for the root
    * admin resource, some admin rights are assumed.
    *
    * @return <code>true</code> if some admin rights are granted
    */
   boolean checkAnyAdminAccess()
   {
      return checkAnyAdminAccess("/"); 
   }
   
   /**
    * Checks whether the current subject has any admin rights on the specified
    * non-leaf resource instance.
    * Currently, the decision is made based on the DENIED _ACCESS permission.
    * If the DENIED_ACCESS is not explicitly set for the subject for this 
    * non-leaf resource instance, some admin rights are assumed.
    *
    * @param name
    *        resource instance name
    *
    * @return <code>true</code> if some admin rights are granted
    */
   boolean checkAnyAdminAccess(String name)
   {
      Node[] path = AdminResourceNamespace.findPath(name);

      if (path == null)
      {
         return false;
      }
      
      if (path[path.length - 1].getType() != AdminRights.ADMT_PATH)
      {
         return adminAccessWorker(name, -1) > 0;
      }
      
      return adminAccessWorker(name, -1) >= 0;
   }
   
   /**
    * Checks whether the current subject has the unlimited admin rights.
    *
    * @return <code>true</code> if the unlimited rights are granted
    */
   boolean checkUnlimitedAdminAccess()
   {
      return checkUnlimitedAdminAccess("/"); 
   }
   
   /**
    * Checks whether the current subject has the unlimited admin rights on
    * the specified non-leaf resource instance.
    *
    * @param name
    *        resource instance name
    *
    * @return <code>true</code> if the unlimited rights are granted
    */
   boolean checkUnlimitedAdminAccess(String name)
   {
      Node[] path = AdminResourceNamespace.findPath(name);

      if (path == null)
      {
         return false;
      }
      
      if (path[path.length - 1].getType() != AdminRights.ADMT_PATH)
      {
         return false;
      }
      
      return adminAccessWorker(name, -1) == 2;
   }
   
   /**
    * Checks the access rights of the current subject with regards to the
    * PASSWORD permission in the "/accounts/users" instance of the "admin"
    * abstract resource.
    *
    * @return <code>true</code> if access is allowed.
    */
   boolean checkPasswordAdminAccess()
   {
      return adminAccessWorker("/accounts/users", 
                               AdminRights.ADLU_PASSWORD_ACCESS) > 0;
   }
   
   /**
    * Generalized Access rights check.
    * Checks the access rights moving down the tree until a definitive answer
    * is found. For all the non-leaf nodes the denied and unlimited access is
    * checked first.
    *
    * @param name
    *        resource instance name
    *
    * @param mode
    *        access mode (requested permission)
    *
    * @return one of the following values:
    *         <ul>
    *            <li>-2 if the name or permission is invalid
    *            <li>-1 if access is explicitly denied
    *            <li>0 if access is not granted
    *            <li>1 if access is explicitly granted
    *            <li>2 if access is unlimited
    *         </ul>
    */
   int checkAdminAccess(String name, int mode)
   {
      return adminAccessWorker(name, mode);
   }
      
   /**
    * Access rights check worker.
    * Checks the access rights moving down the tree until a definitive answer
    * is found. For all the non-leaf nodes the denied and unlimited access is
    * checked first.
    *
    * @param name
    *        resource instance name
    *
    * @param permission
    *        requested permission; used for the leaf nodes only
    *
    * @return one of the following values:
    *         <ul>
    *            <li>-2 if the name or permission is invalid
    *            <li>-1 if access is explicitly denied
    *            <li>0 if access is not granted
    *            <li>1 if access is explicitly granted
    *            <li>2 if access is unlimited
    *         </ul>
    */
   private int adminAccessWorker(String name, int permission)
   {
      int rc = 0;
      
      Node[] path = AdminResourceNamespace.findPath(name);
      if (path == null)
      {
         return -2;
      }
      
      StringBuilder sb = new StringBuilder();
      sb.append("/");
      
      for (int i = 0; i < path.length; i++)
      {
         String part = sb.toString();
         int type = path[i].getType();
         
         if (type == AdminRights.ADMT_PATH)
         {
            // this is not a leaf; check unlimited and denied access first
            rc = nodeWorker(part, AdminRights.ADMT_PATH, -1);
            
            if (rc != 0)
            {
               return rc;
            }
            
            // no definite answer at this level; go deeper
            if (i > 0)
            {
               sb.append("/");
            }
            if (i < path.length - 1)
            {
               sb.append(path[i + 1].getName());
            }
         }
         else
         {
            rc = nodeWorker(part, type, permission); 
         }
      }
      
      return rc;
   }
   
   /**
    * Node access rights check worker. Checks whether the resource instance is
    * accessible for the requested access.
    *
    * @param instanceName
    *        resource instance name
    *
    * @param type
    *        node type
    *
    * @param mode
    *        requested permission; used for the leaf nodes only
    *
    * @return one of the following values:
    *         <ul>
    *            <li>-2 if the name or permission is invalid
    *            <li>-1 if access is explicitly denied
    *            <li>0 if access is not granted
    *            <li>1 if access is explicitly granted
    *            <li>2 if access is unlimited
    *         </ul>
    */
   int nodeWorker(String instanceName, int type, int mode)
   {
      int decision = 0;

      // initiate the ACL search
      int handle = sm.openRightsSearch(resourceIndex, instanceName, mode);

      boolean isFinerLoggable = LOG.isLoggable(Level.FINER);
      if (isFinerLoggable)
      {
         LOG.finer("Search open: resId " + resourceIndex + ", instance " +
                      instanceName + ", mode " + mode + ", handle " + handle);
      }

      // rights check loop
      AdminRights rights = (AdminRights) sm.getNextRights(handle);
      
      while (rights != null)
      {
         if (isFinerLoggable)
         {
            LOG.finer("Search next: handle " + handle + ", rights " + rights);
         }
         
         int rc = checkSingle(rights, type, mode);
         
         if (rc != 0)
         {
            // explicitly denied or granted
            decision = rc;
            break;
         }
         
         // otherwise continue search
         rights = (AdminRights) sm.getNextRights(handle);
      }
      
      if (isFinerLoggable)
      {
         LOG.finer("Search done: handle " + handle + ", decision " +
                      (decision > 0) + ", cache false");
      }
      
      // close the ACL search
      sm.closeRightsSearch(handle, decision > 0, false);

      return decision;
   }
   
   /**
    * Performs access rights check on a single
    * instance of the Rights object.
    *
    * @param rights
    *        The instance of AdminRights to check, must not be
    *        <code>null</code>. 
    *
    * @param type
    *        node type to compare the rights with
    *
    * @param mode
    *        used for the leaf nodes only; permission to check or -1;
    *        the latter means any permission and not denied access
    *
    * @return if the node type doesn't match. -2 is returned;
    *         for the path rights, the result is one of the following:
    *         <ul>
    *            <li>-1 if access is explicitly denied
    *            <li>0 if access is limited
    *            <li>2 if access is unlimited
    *         </ul>
    *         for the leaf rights, the result is one of the following:
    *         <ul>
    *            <li>-1 if access is explicitly denied
    *            <li>0 if access is not granted
    *            <li>1 if access is granted
    *         </ul>
    */
   private int checkSingle(AdminRights rights, int type, int mode)
   {
      if (rights.getType() != type)
      {
         return -2;
      }
      
      int result = 0;
      boolean bit = false;
      
      switch (type)
      {
         case AdminRights.ADMT_PATH:
            if (rights.getPermission(AdminRights.ADTP_DENIED_ACCESS))
            {
               return -1;
            }
            
            if (rights.getPermission(AdminRights.ADTP_UNLIMITED_ACCESS))
            {
               return 2;
            }
            
            return 0;
            
         case AdminRights.ADMT_LEAF_REFRESH:
            if (rights.getPermission(AdminRights.ADLR_DENIED_ACCESS))
            {
               return -1;
            }
            
            if (rights.getPermission(AdminRights.ADLR_REFRESH_ACCESS))
            {
               return 2;
            }
            
            return 0;
            
         case AdminRights.ADMT_LEAF_USER:
            if (mode < -1 || mode >= AdminRights.ADLU_SIZE)
            {
               return -2;
            }
            
            if (rights.getPermission(AdminRights.ADLU_DENIED_ACCESS))
            {
               return -1;
            }
            
            if (mode != -1)
            {
               result = rights.getPermission(mode) ? 1 : 0;
            }
            else
            {
               bit = rights.getPermission(AdminRights.ADLU_ENUMERATE_ACCESS) | 
                     rights.getPermission(AdminRights.ADLU_PASSWORD_ACCESS)  |
                     rights.getPermission(AdminRights.ADLU_GROUP_ACCESS)     |
                     rights.getPermission(AdminRights.ADLU_READ_ACCESS)      |
                     rights.getPermission(AdminRights.ADLU_WRITE_ACCESS)     |
                     rights.getPermission(AdminRights.ADLU_CREATE_ACCESS)    |
                     rights.getPermission(AdminRights.ADLU_DELETE_ACCESS)    |
                     rights.getPermission(AdminRights.ADLU_DENIED_ACCESS);
                     return bit ? 1 : 0;
            }
            return result;
            
         default:
            return -2;
      }
   }
}