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;
}
}
}