diff --git a/cts/cli/regression.acls.exp b/cts/cli/regression.acls.exp
index be91b93455..8c263309e4 100644
--- a/cts/cli/regression.acls.exp
+++ b/cts/cli/regression.acls.exp
@@ -1,2875 +1,2875 @@
=#=#=#= Begin test: Configure some ACLs =#=#=#=
=#=#=#= Current cib after: Configure some ACLs =#=#=#=
=#=#=#= End test: Configure some ACLs - OK (0) =#=#=#=
* Passed: cibadmin - Configure some ACLs
=#=#=#= Begin test: Enable ACLs =#=#=#=
=#=#=#= Current cib after: Enable ACLs =#=#=#=
=#=#=#= End test: Enable ACLs - OK (0) =#=#=#=
* Passed: crm_attribute - Enable ACLs
=#=#=#= Begin test: Set cluster option =#=#=#=
=#=#=#= Current cib after: Set cluster option =#=#=#=
=#=#=#= End test: Set cluster option - OK (0) =#=#=#=
* Passed: crm_attribute - Set cluster option
=#=#=#= Begin test: New ACL role =#=#=#=
=#=#=#= Current cib after: New ACL role =#=#=#=
=#=#=#= End test: New ACL role - OK (0) =#=#=#=
* Passed: cibadmin - New ACL role
=#=#=#= Begin test: New ACL target =#=#=#=
=#=#=#= Current cib after: New ACL target =#=#=#=
=#=#=#= End test: New ACL target - OK (0) =#=#=#=
* Passed: cibadmin - New ACL target
=#=#=#= Begin test: Another ACL role =#=#=#=
=#=#=#= Current cib after: Another ACL role =#=#=#=
=#=#=#= End test: Another ACL role - OK (0) =#=#=#=
* Passed: cibadmin - Another ACL role
=#=#=#= Begin test: Another ACL target =#=#=#=
=#=#=#= Current cib after: Another ACL target =#=#=#=
=#=#=#= End test: Another ACL target - OK (0) =#=#=#=
* Passed: cibadmin - Another ACL target
=#=#=#= Begin test: Updated ACL =#=#=#=
=#=#=#= Current cib after: Updated ACL =#=#=#=
=#=#=#= End test: Updated ACL - OK (0) =#=#=#=
* Passed: cibadmin - Updated ACL
=#=#=#= Begin test: unknownguy: Query configuration =#=#=#=
Call failed: Permission denied
=#=#=#= End test: unknownguy: Query configuration - Insufficient privileges (4) =#=#=#=
* Passed: cibadmin - unknownguy: Query configuration
=#=#=#= Begin test: unknownguy: Set enable-acl =#=#=#=
crm_attribute: Error performing operation: Permission denied
=#=#=#= End test: unknownguy: Set enable-acl - Insufficient privileges (4) =#=#=#=
* Passed: crm_attribute - unknownguy: Set enable-acl
=#=#=#= Begin test: unknownguy: Set stonith-enabled =#=#=#=
crm_attribute: Error performing operation: Permission denied
=#=#=#= End test: unknownguy: Set stonith-enabled - Insufficient privileges (4) =#=#=#=
* Passed: crm_attribute - unknownguy: Set stonith-enabled
=#=#=#= Begin test: unknownguy: Create a resource =#=#=#=
-pcmk__check_acl trace: User 'unknownguy' without ACLs denied read/write access to /cib/configuration/resources/primitive[@id='dummy']
+pcmk__check_acl trace: Lack of ACL denies user 'unknownguy' read/write access to /cib/configuration/resources/primitive[@id='dummy']
pcmk__apply_creation_acl trace: ACLs disallow creation of with id="dummy"
Call failed: Permission denied
=#=#=#= End test: unknownguy: Create a resource - Insufficient privileges (4) =#=#=#=
* Passed: cibadmin - unknownguy: Create a resource
=#=#=#= Begin test: l33t-haxor: Query configuration =#=#=#=
Call failed: Permission denied
=#=#=#= End test: l33t-haxor: Query configuration - Insufficient privileges (4) =#=#=#=
* Passed: cibadmin - l33t-haxor: Query configuration
=#=#=#= Begin test: l33t-haxor: Set enable-acl =#=#=#=
crm_attribute: Error performing operation: Permission denied
=#=#=#= End test: l33t-haxor: Set enable-acl - Insufficient privileges (4) =#=#=#=
* Passed: crm_attribute - l33t-haxor: Set enable-acl
=#=#=#= Begin test: l33t-haxor: Set stonith-enabled =#=#=#=
crm_attribute: Error performing operation: Permission denied
=#=#=#= End test: l33t-haxor: Set stonith-enabled - Insufficient privileges (4) =#=#=#=
* Passed: crm_attribute - l33t-haxor: Set stonith-enabled
=#=#=#= Begin test: l33t-haxor: Create a resource =#=#=#=
pcmk__check_acl trace: Parent ACL denies user 'l33t-haxor' read/write access to /cib/configuration/resources/primitive[@id='dummy']
pcmk__apply_creation_acl trace: ACLs disallow creation of with id="dummy"
Call failed: Permission denied
=#=#=#= End test: l33t-haxor: Create a resource - Insufficient privileges (4) =#=#=#=
* Passed: cibadmin - l33t-haxor: Create a resource
=#=#=#= Begin test: niceguy: Query configuration =#=#=#=
=#=#=#= End test: niceguy: Query configuration - OK (0) =#=#=#=
* Passed: cibadmin - niceguy: Query configuration
=#=#=#= Begin test: niceguy: Set enable-acl =#=#=#=
pcmk__check_acl trace: Default ACL denies user 'niceguy' read/write access to /cib/configuration/crm_config/cluster_property_set[@id='cib-bootstrap-options']/nvpair[@id='cib-bootstrap-options-enable-acl'][@value]
Error setting enable-acl=false (section=crm_config, set=): Permission denied
crm_attribute: Error performing operation: Permission denied
=#=#=#= End test: niceguy: Set enable-acl - Insufficient privileges (4) =#=#=#=
* Passed: crm_attribute - niceguy: Set enable-acl
=#=#=#= Begin test: niceguy: Set stonith-enabled =#=#=#=
pcmk__apply_creation_acl trace: ACLs allow creation of with id="cib-bootstrap-options-stonith-enabled"
=#=#=#= Current cib after: niceguy: Set stonith-enabled =#=#=#=
=#=#=#= End test: niceguy: Set stonith-enabled - OK (0) =#=#=#=
* Passed: crm_attribute - niceguy: Set stonith-enabled
=#=#=#= Begin test: niceguy: Create a resource =#=#=#=
pcmk__check_acl trace: Default ACL denies user 'niceguy' read/write access to /cib/configuration/resources/primitive[@id='dummy']
pcmk__apply_creation_acl trace: ACLs disallow creation of with id="dummy"
Call failed: Permission denied
=#=#=#= End test: niceguy: Create a resource - Insufficient privileges (4) =#=#=#=
* Passed: cibadmin - niceguy: Create a resource
=#=#=#= Begin test: root: Query configuration =#=#=#=
=#=#=#= End test: root: Query configuration - OK (0) =#=#=#=
* Passed: cibadmin - root: Query configuration
=#=#=#= Begin test: root: Set stonith-enabled =#=#=#=
=#=#=#= Current cib after: root: Set stonith-enabled =#=#=#=
=#=#=#= End test: root: Set stonith-enabled - OK (0) =#=#=#=
* Passed: crm_attribute - root: Set stonith-enabled
=#=#=#= Begin test: root: Create a resource =#=#=#=
=#=#=#= Current cib after: root: Create a resource =#=#=#=
=#=#=#= End test: root: Create a resource - OK (0) =#=#=#=
* Passed: cibadmin - root: Create a resource
=#=#=#= Begin test: root: Create another resource (with description) =#=#=#=
=#=#=#= Current cib after: root: Create another resource (with description) =#=#=#=
=#=#=#= End test: root: Create another resource (with description) - OK (0) =#=#=#=
* Passed: cibadmin - root: Create another resource (with description)
=#=#=#= Begin test: l33t-haxor: Create a resource meta attribute =#=#=#=
Could not obtain the current CIB: Permission denied
crm_resource: Error performing operation: Insufficient privileges
=#=#=#= End test: l33t-haxor: Create a resource meta attribute - Insufficient privileges (4) =#=#=#=
* Passed: crm_resource - l33t-haxor: Create a resource meta attribute
=#=#=#= Begin test: l33t-haxor: Query a resource meta attribute =#=#=#=
Could not obtain the current CIB: Permission denied
crm_resource: Error performing operation: Insufficient privileges
=#=#=#= End test: l33t-haxor: Query a resource meta attribute - Insufficient privileges (4) =#=#=#=
* Passed: crm_resource - l33t-haxor: Query a resource meta attribute
=#=#=#= Begin test: l33t-haxor: Remove a resource meta attribute =#=#=#=
Could not obtain the current CIB: Permission denied
crm_resource: Error performing operation: Insufficient privileges
=#=#=#= End test: l33t-haxor: Remove a resource meta attribute - Insufficient privileges (4) =#=#=#=
* Passed: crm_resource - l33t-haxor: Remove a resource meta attribute
=#=#=#= Begin test: niceguy: Create a resource meta attribute =#=#=#=
unpack_resources error: Resource start-up disabled since no STONITH resources have been defined
unpack_resources error: Either configure some or disable STONITH with the stonith-enabled option
unpack_resources error: NOTE: Clusters with shared data need STONITH to ensure data integrity
pcmk__apply_creation_acl trace: Creation of scaffolding with id="dummy-meta_attributes" is implicitly allowed
pcmk__apply_creation_acl trace: ACLs allow creation of with id="dummy-meta_attributes-target-role"
Set 'dummy' option: id=dummy-meta_attributes-target-role set=dummy-meta_attributes name=target-role value=Stopped
=#=#=#= Current cib after: niceguy: Create a resource meta attribute =#=#=#=
=#=#=#= End test: niceguy: Create a resource meta attribute - OK (0) =#=#=#=
* Passed: crm_resource - niceguy: Create a resource meta attribute
=#=#=#= Begin test: niceguy: Query a resource meta attribute =#=#=#=
unpack_resources error: Resource start-up disabled since no STONITH resources have been defined
unpack_resources error: Either configure some or disable STONITH with the stonith-enabled option
unpack_resources error: NOTE: Clusters with shared data need STONITH to ensure data integrity
Stopped
=#=#=#= Current cib after: niceguy: Query a resource meta attribute =#=#=#=
=#=#=#= End test: niceguy: Query a resource meta attribute - OK (0) =#=#=#=
* Passed: crm_resource - niceguy: Query a resource meta attribute
=#=#=#= Begin test: niceguy: Remove a resource meta attribute =#=#=#=
unpack_resources error: Resource start-up disabled since no STONITH resources have been defined
unpack_resources error: Either configure some or disable STONITH with the stonith-enabled option
unpack_resources error: NOTE: Clusters with shared data need STONITH to ensure data integrity
Deleted 'dummy' option: id=dummy-meta_attributes-target-role name=target-role
=#=#=#= Current cib after: niceguy: Remove a resource meta attribute =#=#=#=
=#=#=#= End test: niceguy: Remove a resource meta attribute - OK (0) =#=#=#=
* Passed: crm_resource - niceguy: Remove a resource meta attribute
=#=#=#= Begin test: niceguy: Create a resource meta attribute =#=#=#=
unpack_resources error: Resource start-up disabled since no STONITH resources have been defined
unpack_resources error: Either configure some or disable STONITH with the stonith-enabled option
unpack_resources error: NOTE: Clusters with shared data need STONITH to ensure data integrity
pcmk__apply_creation_acl trace: ACLs allow creation of with id="dummy-meta_attributes-target-role"
Set 'dummy' option: id=dummy-meta_attributes-target-role set=dummy-meta_attributes name=target-role value=Started
=#=#=#= Current cib after: niceguy: Create a resource meta attribute =#=#=#=
=#=#=#= End test: niceguy: Create a resource meta attribute - OK (0) =#=#=#=
* Passed: crm_resource - niceguy: Create a resource meta attribute
=#=#=#= Begin test: badidea: Query configuration - implied deny =#=#=#=
=#=#=#= End test: badidea: Query configuration - implied deny - OK (0) =#=#=#=
* Passed: cibadmin - badidea: Query configuration - implied deny
=#=#=#= Begin test: betteridea: Query configuration - explicit deny =#=#=#=
=#=#=#= End test: betteridea: Query configuration - explicit deny - OK (0) =#=#=#=
* Passed: cibadmin - betteridea: Query configuration - explicit deny
=#=#=#= Begin test: niceguy: Replace - remove acls =#=#=#=
pcmk__check_acl trace: Default ACL denies user 'niceguy' read/write access to /cib[@epoch]
pcmk__check_acl trace: Default ACL denies user 'niceguy' read/write access to /cib/configuration/acls
Call failed: Permission denied
=#=#=#= End test: niceguy: Replace - remove acls - Insufficient privileges (4) =#=#=#=
* Passed: cibadmin - niceguy: Replace - remove acls
=#=#=#= Begin test: niceguy: Replace - create resource =#=#=#=
pcmk__check_acl trace: Default ACL denies user 'niceguy' read/write access to /cib[@epoch]
pcmk__check_acl trace: Default ACL denies user 'niceguy' read/write access to /cib/configuration/resources/primitive[@id='dummy2']
pcmk__apply_creation_acl trace: ACLs disallow creation of with id="dummy2"
Call failed: Permission denied
=#=#=#= End test: niceguy: Replace - create resource - Insufficient privileges (4) =#=#=#=
* Passed: cibadmin - niceguy: Replace - create resource
=#=#=#= Begin test: niceguy: Replace - modify attribute (deny) =#=#=#=
pcmk__check_acl trace: Default ACL denies user 'niceguy' read/write access to /cib[@epoch]
pcmk__check_acl trace: Default ACL denies user 'niceguy' read/write access to /cib/configuration/crm_config/cluster_property_set[@id='cib-bootstrap-options']/nvpair[@id='cib-bootstrap-options-enable-acl'][@value]
Call failed: Permission denied
=#=#=#= End test: niceguy: Replace - modify attribute (deny) - Insufficient privileges (4) =#=#=#=
* Passed: cibadmin - niceguy: Replace - modify attribute (deny)
=#=#=#= Begin test: niceguy: Replace - delete attribute (deny) =#=#=#=
pcmk__check_acl trace: Default ACL denies user 'niceguy' read/write access to /cib[@epoch]
pcmk__check_acl trace: Default ACL denies user 'niceguy' read/write access to /cib/configuration/resources/primitive[@id='dummy_desc']
Call failed: Permission denied
=#=#=#= End test: niceguy: Replace - delete attribute (deny) - Insufficient privileges (4) =#=#=#=
* Passed: cibadmin - niceguy: Replace - delete attribute (deny)
=#=#=#= Begin test: niceguy: Replace - create attribute (deny) =#=#=#=
pcmk__check_acl trace: Default ACL denies user 'niceguy' read/write access to /cib[@epoch]
pcmk__check_acl trace: Default ACL denies user 'niceguy' read/write access to /cib/configuration/resources/primitive[@id='dummy'][@description]
Call failed: Permission denied
=#=#=#= End test: niceguy: Replace - create attribute (deny) - Insufficient privileges (4) =#=#=#=
* Passed: cibadmin - niceguy: Replace - create attribute (deny)
=#=#=#= Begin test: bob: Replace - create attribute (direct allow) =#=#=#=
=#=#=#= End test: bob: Replace - create attribute (direct allow) - OK (0) =#=#=#=
* Passed: cibadmin - bob: Replace - create attribute (direct allow)
=#=#=#= Begin test: bob: Replace - modify attribute (direct allow) =#=#=#=
=#=#=#= End test: bob: Replace - modify attribute (direct allow) - OK (0) =#=#=#=
* Passed: cibadmin - bob: Replace - modify attribute (direct allow)
=#=#=#= Begin test: bob: Replace - delete attribute (direct allow) =#=#=#=
=#=#=#= End test: bob: Replace - delete attribute (direct allow) - OK (0) =#=#=#=
* Passed: cibadmin - bob: Replace - delete attribute (direct allow)
=#=#=#= Begin test: joe: Replace - create attribute (inherited allow) =#=#=#=
=#=#=#= End test: joe: Replace - create attribute (inherited allow) - OK (0) =#=#=#=
* Passed: cibadmin - joe: Replace - create attribute (inherited allow)
=#=#=#= Begin test: joe: Replace - modify attribute (inherited allow) =#=#=#=
=#=#=#= End test: joe: Replace - modify attribute (inherited allow) - OK (0) =#=#=#=
* Passed: cibadmin - joe: Replace - modify attribute (inherited allow)
=#=#=#= Begin test: joe: Replace - delete attribute (inherited allow) =#=#=#=
=#=#=#= End test: joe: Replace - delete attribute (inherited allow) - OK (0) =#=#=#=
* Passed: cibadmin - joe: Replace - delete attribute (inherited allow)
=#=#=#= Begin test: mike: Replace - create attribute (allow overrides deny) =#=#=#=
=#=#=#= End test: mike: Replace - create attribute (allow overrides deny) - OK (0) =#=#=#=
* Passed: cibadmin - mike: Replace - create attribute (allow overrides deny)
=#=#=#= Begin test: mike: Replace - modify attribute (allow overrides deny) =#=#=#=
=#=#=#= End test: mike: Replace - modify attribute (allow overrides deny) - OK (0) =#=#=#=
* Passed: cibadmin - mike: Replace - modify attribute (allow overrides deny)
=#=#=#= Begin test: mike: Replace - delete attribute (allow overrides deny) =#=#=#=
=#=#=#= End test: mike: Replace - delete attribute (allow overrides deny) - OK (0) =#=#=#=
* Passed: cibadmin - mike: Replace - delete attribute (allow overrides deny)
=#=#=#= Begin test: mike: Create another resource =#=#=#=
pcmk__apply_creation_acl trace: ACLs allow creation of with id="dummy2"
=#=#=#= Current cib after: mike: Create another resource =#=#=#=
=#=#=#= End test: mike: Create another resource - OK (0) =#=#=#=
* Passed: cibadmin - mike: Create another resource
=#=#=#= Begin test: chris: Replace - create attribute (deny overrides allow) =#=#=#=
pcmk__check_acl trace: Parent ACL denies user 'chris' read/write access to /cib/configuration/resources/primitive[@id='dummy'][@description]
Call failed: Permission denied
=#=#=#= End test: chris: Replace - create attribute (deny overrides allow) - Insufficient privileges (4) =#=#=#=
* Passed: cibadmin - chris: Replace - create attribute (deny overrides allow)
=#=#=#= Begin test: chris: Replace - modify attribute (deny overrides allow) =#=#=#=
pcmk__check_acl trace: Parent ACL denies user 'chris' read/write access to /cib/configuration/resources/primitive[@id='dummy'][@description]
Call failed: Permission denied
=#=#=#= End test: chris: Replace - modify attribute (deny overrides allow) - Insufficient privileges (4) =#=#=#=
* Passed: cibadmin - chris: Replace - modify attribute (deny overrides allow)
=#=#=#= Begin test: chris: Replace - delete attribute (deny overrides allow) =#=#=#=
pcmk__check_acl trace: Parent ACL denies user 'chris' read/write access to /cib/configuration/resources/primitive[@id='dummy2']
Call failed: Permission denied
=#=#=#= End test: chris: Replace - delete attribute (deny overrides allow) - Insufficient privileges (4) =#=#=#=
* Passed: cibadmin - chris: Replace - delete attribute (deny overrides allow)
diff --git a/lib/common/acl.c b/lib/common/acl.c
index 1360e9d22f..0c39395571 100644
--- a/lib/common/acl.c
+++ b/lib/common/acl.c
@@ -1,939 +1,917 @@
/*
* Copyright 2004-2025 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU Lesser General Public License
* version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include // xmlXPathObject, etc.
#include
#include
#include
#include "crmcommon_private.h"
typedef struct xml_acl_s {
enum xml_private_flags mode;
gchar *xpath;
} xml_acl_t;
static void
free_acl(void *data)
{
if (data) {
xml_acl_t *acl = data;
g_free(acl->xpath);
free(acl);
}
}
void
pcmk__free_acls(GList *acls)
{
g_list_free_full(acls, free_acl);
}
static GList *
create_acl(const xmlNode *xml, GList *acls, enum xml_private_flags mode)
{
xml_acl_t *acl = NULL;
const char *tag = crm_element_value(xml, PCMK_XA_OBJECT_TYPE);
const char *ref = crm_element_value(xml, PCMK_XA_REFERENCE);
const char *xpath = crm_element_value(xml, PCMK_XA_XPATH);
const char *attr = crm_element_value(xml, PCMK_XA_ATTRIBUTE);
if ((tag == NULL) && (ref == NULL) && (xpath == NULL)) {
// Schema should prevent this, but to be safe ...
crm_trace("Ignoring ACL <%s> element without selection criteria",
xml->name);
return NULL;
}
acl = pcmk__assert_alloc(1, sizeof (xml_acl_t));
acl->mode = mode;
if (xpath) {
acl->xpath = g_strdup(xpath);
crm_trace("Unpacked ACL <%s> element using xpath: %s",
xml->name, acl->xpath);
} else {
GString *buf = g_string_sized_new(128);
if ((ref != NULL) && (attr != NULL)) {
// NOTE: schema currently does not allow this
pcmk__g_strcat(buf, "//", pcmk__s(tag, "*"), "[@" PCMK_XA_ID "='",
ref, "' and @", attr, "]", NULL);
} else if (ref != NULL) {
pcmk__g_strcat(buf, "//", pcmk__s(tag, "*"), "[@" PCMK_XA_ID "='",
ref, "']", NULL);
} else if (attr != NULL) {
pcmk__g_strcat(buf, "//", pcmk__s(tag, "*"), "[@", attr, "]", NULL);
} else {
pcmk__g_strcat(buf, "//", pcmk__s(tag, "*"), NULL);
}
acl->xpath = buf->str;
g_string_free(buf, FALSE);
crm_trace("Unpacked ACL <%s> element as xpath: %s",
xml->name, acl->xpath);
}
return g_list_append(acls, acl);
}
/*!
* \internal
* \brief Unpack a user, group, or role subtree of the ACLs section
*
* \param[in] acl_top XML of entire ACLs section
* \param[in] acl_entry XML of ACL element being unpacked
* \param[in,out] acls List of ACLs unpacked so far
*
* \return New head of (possibly modified) acls
*
* \note This function is recursive
*/
static GList *
parse_acl_entry(const xmlNode *acl_top, const xmlNode *acl_entry, GList *acls)
{
for (const xmlNode *child = pcmk__xe_first_child(acl_entry, NULL, NULL,
NULL);
child != NULL; child = pcmk__xe_next(child, NULL)) {
if (pcmk__xe_is(child, PCMK_XE_ACL_PERMISSION)) {
const char *kind = crm_element_value(child, PCMK_XA_KIND);
pcmk__assert(kind != NULL);
crm_trace("Unpacking <" PCMK_XE_ACL_PERMISSION "> element of "
"kind '%s'",
kind);
if (pcmk__str_eq(kind, PCMK_VALUE_READ, pcmk__str_none)) {
acls = create_acl(child, acls, pcmk__xf_acl_read);
} else if (pcmk__str_eq(kind, PCMK_VALUE_WRITE, pcmk__str_none)) {
acls = create_acl(child, acls, pcmk__xf_acl_write);
} else if (pcmk__str_eq(kind, PCMK_VALUE_DENY, pcmk__str_none)) {
acls = create_acl(child, acls, pcmk__xf_acl_deny);
} else {
crm_warn("Ignoring unknown ACL kind '%s'", kind);
}
} else if (pcmk__xe_is(child, PCMK_XE_ROLE)) {
const char *ref_role = crm_element_value(child, PCMK_XA_ID);
crm_trace("Unpacking <" PCMK_XE_ROLE "> element");
if (ref_role == NULL) {
continue;
}
for (xmlNode *role = pcmk__xe_first_child(acl_top, NULL, NULL,
NULL);
role != NULL; role = pcmk__xe_next(role, NULL)) {
const char *role_id = NULL;
if (!pcmk__xe_is(role, PCMK_XE_ACL_ROLE)) {
continue;
}
role_id = crm_element_value(role, PCMK_XA_ID);
if (pcmk__str_eq(ref_role, role_id, pcmk__str_none)) {
crm_trace("Unpacking referenced role '%s' in <%s> element",
role_id, acl_entry->name);
acls = parse_acl_entry(acl_top, role, acls);
break;
}
}
}
}
return acls;
}
/*
*/
static const char *
acl_to_text(enum xml_private_flags flags)
{
if (pcmk_is_set(flags, pcmk__xf_acl_deny)) {
return "deny";
} else if (pcmk_any_flags_set(flags, pcmk__xf_acl_write|pcmk__xf_acl_create)) {
return "read/write";
} else if (pcmk_is_set(flags, pcmk__xf_acl_read)) {
return "read";
}
return "none";
}
void
pcmk__apply_acl(xmlNode *xml)
{
GList *aIter = NULL;
xml_doc_private_t *docpriv = xml->doc->_private;
xml_node_private_t *nodepriv;
xmlXPathObject *xpathObj = NULL;
if (!xml_acl_enabled(xml)) {
crm_trace("Skipping ACLs for user '%s' because not enabled for this XML",
docpriv->user);
return;
}
for (aIter = docpriv->acls; aIter != NULL; aIter = aIter->next) {
int max = 0, lpc = 0;
xml_acl_t *acl = aIter->data;
xpathObj = pcmk__xpath_search(xml->doc, acl->xpath);
max = pcmk__xpath_num_results(xpathObj);
for (lpc = 0; lpc < max; lpc++) {
xmlNode *match = pcmk__xpath_result(xpathObj, lpc);
if (match == NULL) {
continue;
}
/* @COMPAT If the ACL's XPath matches a node that is neither an
* element nor a document, we apply the ACL to the parent element
* rather than to the matched node. For example, if the XPath
* matches a "score" attribute, then it applies to every element
* that contains a "score" attribute. That is, the XPath expression
* "//@score" matches all attributes named "score", but we apply the
* ACL to all elements containing such an attribute.
*
* This behavior is incorrect from an XPath standpoint and is thus
* confusing and counterintuitive. The correct way to match all
* elements containing a "score" attribute is to use an XPath
* predicate: "// *[@score]". (Space inserted after slashes so that
* GCC doesn't throw an error about nested comments.)
*
* Additionally, if an XPath expression matches the entire document
* (for example, "/"), then the ACL applies to the document's root
* element if it exists.
*
* These behaviors should be changed so that the ACL applies to the
* nodes matched by the XPath expression, or so that it doesn't
* apply at all if applying an ACL to an attribute doesn't make
* sense.
*
* Unfortunately, we document in Pacemaker Explained that matching
* attributes is a valid way to match elements: "Attributes may be
* specified in the XPath to select particular elements, but the
* permissions apply to the entire element."
*
* So we have to keep this behavior at least until a compatibility
* break. Even then, it's not feasible in the general case to
* transform such XPath expressions using XSLT.
*/
match = pcmk__xpath_match_element(match);
if (match == NULL) {
continue;
}
nodepriv = match->_private;
pcmk__set_xml_flags(nodepriv, acl->mode);
// Build a GString only if tracing is enabled
pcmk__if_tracing(
{
GString *path = pcmk__element_xpath(match);
crm_trace("Applying %s ACL to %s matched by %s",
acl_to_text(acl->mode), path->str, acl->xpath);
g_string_free(path, TRUE);
},
{}
);
}
crm_trace("Applied %s ACL %s (%d match%s)",
acl_to_text(acl->mode), acl->xpath, max,
((max == 1)? "" : "es"));
xmlXPathFreeObject(xpathObj);
}
}
/*!
* \internal
* \brief Unpack ACLs for a given user into the
* metadata of the target XML tree
*
* Taking the description of ACLs from the source XML tree and
* marking up the target XML tree with access information for the
* given user by tacking it onto the relevant nodes
*
* \param[in] source XML with ACL definitions
* \param[in,out] target XML that ACLs will be applied to
* \param[in] user Username whose ACLs need to be unpacked
*/
void
pcmk__unpack_acl(xmlNode *source, xmlNode *target, const char *user)
{
xml_doc_private_t *docpriv = NULL;
if ((target == NULL) || (target->doc == NULL)
|| (target->doc->_private == NULL)) {
return;
}
docpriv = target->doc->_private;
if (!pcmk_acl_required(user)) {
crm_trace("Not unpacking ACLs because not required for user '%s'",
user);
} else if (docpriv->acls == NULL) {
xmlNode *acls = get_xpath_object("//" PCMK_XE_ACLS, source, LOG_NEVER);
pcmk__str_update(&docpriv->user, user);
if (acls) {
xmlNode *child = NULL;
for (child = pcmk__xe_first_child(acls, NULL, NULL, NULL);
child != NULL; child = pcmk__xe_next(child, NULL)) {
if (pcmk__xe_is(child, PCMK_XE_ACL_TARGET)) {
const char *id = crm_element_value(child, PCMK_XA_NAME);
if (id == NULL) {
id = crm_element_value(child, PCMK_XA_ID);
}
if (id && strcmp(id, user) == 0) {
crm_debug("Unpacking ACLs for user '%s'", id);
docpriv->acls = parse_acl_entry(acls, child, docpriv->acls);
}
} else if (pcmk__xe_is(child, PCMK_XE_ACL_GROUP)) {
const char *id = crm_element_value(child, PCMK_XA_NAME);
if (id == NULL) {
id = crm_element_value(child, PCMK_XA_ID);
}
if (id && pcmk__is_user_in_group(user,id)) {
crm_debug("Unpacking ACLs for group '%s'", id);
docpriv->acls = parse_acl_entry(acls, child, docpriv->acls);
}
}
}
}
}
}
/*!
* \internal
* \brief Copy source to target and set xf_acl_enabled flag in target
*
* \param[in] acl_source XML with ACL definitions
* \param[in,out] target XML that ACLs will be applied to
* \param[in] user Username whose ACLs need to be set
*/
void
pcmk__enable_acl(xmlNode *acl_source, xmlNode *target, const char *user)
{
pcmk__unpack_acl(acl_source, target, user);
pcmk__set_xml_doc_flag(target, pcmk__xf_acl_enabled);
pcmk__apply_acl(target);
}
static inline bool
test_acl_mode(enum xml_private_flags allowed, enum xml_private_flags requested)
{
if (pcmk_is_set(allowed, pcmk__xf_acl_deny)) {
return false;
} else if (pcmk_all_flags_set(allowed, requested)) {
return true;
} else if (pcmk_is_set(requested, pcmk__xf_acl_read)
&& pcmk_is_set(allowed, pcmk__xf_acl_write)) {
return true;
} else if (pcmk_is_set(requested, pcmk__xf_acl_create)
&& pcmk_any_flags_set(allowed, pcmk__xf_acl_write|pcmk__xf_created)) {
return true;
}
return false;
}
/*!
* \internal
* \brief Rid XML tree of all unreadable nodes and node properties
*
* \param[in,out] xml Root XML node to be purged of attributes
*
* \return true if this node or any of its children are readable
* if false is returned, xml will be freed
*
* \note This function is recursive
*/
static bool
purge_xml_attributes(xmlNode *xml)
{
xmlNode *child = NULL;
xmlAttr *xIter = NULL;
bool readable_children = false;
xml_node_private_t *nodepriv = xml->_private;
if (test_acl_mode(nodepriv->flags, pcmk__xf_acl_read)) {
crm_trace("%s[@" PCMK_XA_ID "=%s] is readable",
xml->name, pcmk__xe_id(xml));
return true;
}
xIter = xml->properties;
while (xIter != NULL) {
xmlAttr *tmp = xIter;
const char *prop_name = (const char *)xIter->name;
xIter = xIter->next;
if (strcmp(prop_name, PCMK_XA_ID) == 0) {
continue;
}
pcmk__xa_remove(tmp, true);
}
child = pcmk__xml_first_child(xml);
while ( child != NULL ) {
xmlNode *tmp = child;
child = pcmk__xml_next(child);
readable_children |= purge_xml_attributes(tmp);
}
if (!readable_children) {
// Nothing readable under here, so purge completely
pcmk__xml_free(xml);
}
return readable_children;
}
/*!
* \brief Copy ACL-allowed portions of specified XML
*
* \param[in] user Username whose ACLs should be used
* \param[in] acl_source XML containing ACLs
* \param[in] xml XML to be copied
* \param[out] result Copy of XML portions readable via ACLs
*
* \return true if xml exists and ACLs are required for user, false otherwise
* \note If this returns true, caller should use \p result rather than \p xml
*/
bool
xml_acl_filtered_copy(const char *user, xmlNode *acl_source, xmlNode *xml,
xmlNode **result)
{
GList *aIter = NULL;
xmlNode *target = NULL;
xml_doc_private_t *docpriv = NULL;
*result = NULL;
if ((xml == NULL) || !pcmk_acl_required(user)) {
crm_trace("Not filtering XML because ACLs not required for user '%s'",
user);
return false;
}
crm_trace("Filtering XML copy using user '%s' ACLs", user);
target = pcmk__xml_copy(NULL, xml);
if (target == NULL) {
return true;
}
pcmk__enable_acl(acl_source, target, user);
docpriv = target->doc->_private;
for(aIter = docpriv->acls; aIter != NULL && target; aIter = aIter->next) {
int max = 0;
xml_acl_t *acl = aIter->data;
if (acl->mode != pcmk__xf_acl_deny) {
/* Nothing to do */
} else if (acl->xpath) {
int lpc = 0;
xmlXPathObject *xpathObj = pcmk__xpath_search(target->doc,
acl->xpath);
max = pcmk__xpath_num_results(xpathObj);
for(lpc = 0; lpc < max; lpc++) {
xmlNode *match = pcmk__xpath_result(xpathObj, lpc);
if (match == NULL) {
continue;
}
// @COMPAT See COMPAT comment in pcmk__apply_acl()
match = pcmk__xpath_match_element(match);
if (match == NULL) {
continue;
}
if (!purge_xml_attributes(match) && (match == target)) {
crm_trace("ACLs deny user '%s' access to entire XML document",
user);
xmlXPathFreeObject(xpathObj);
return true;
}
}
crm_trace("ACLs deny user '%s' access to %s (%d %s)",
user, acl->xpath, max,
pcmk__plural_alt(max, "match", "matches"));
xmlXPathFreeObject(xpathObj);
}
}
if (!purge_xml_attributes(target)) {
crm_trace("ACLs deny user '%s' access to entire XML document", user);
return true;
}
if (docpriv->acls) {
g_list_free_full(docpriv->acls, free_acl);
docpriv->acls = NULL;
} else {
crm_trace("User '%s' without ACLs denied access to entire XML document",
user);
pcmk__xml_free(target);
target = NULL;
}
if (target) {
*result = target;
}
return true;
}
/*!
* \internal
* \brief Check whether creation of an XML element is implicitly allowed
*
* Check whether XML is a "scaffolding" element whose creation is implicitly
* allowed regardless of ACLs (that is, it is not in the ACL section and has
* no attributes other than \c PCMK_XA_ID).
*
* \param[in] xml XML element to check
*
* \return true if XML element is implicitly allowed, false otherwise
*/
static bool
implicitly_allowed(const xmlNode *xml)
{
GString *path = NULL;
for (xmlAttr *prop = xml->properties; prop != NULL; prop = prop->next) {
if (strcmp((const char *) prop->name, PCMK_XA_ID) != 0) {
return false;
}
}
path = pcmk__element_xpath(xml);
pcmk__assert(path != NULL);
if (strstr((const char *) path->str, "/" PCMK_XE_ACLS "/") != NULL) {
g_string_free(path, TRUE);
return false;
}
g_string_free(path, TRUE);
return true;
}
#define display_id(xml) pcmk__s(pcmk__xe_id(xml), "")
/*!
* \internal
* \brief Drop XML nodes created in violation of ACLs
*
* Given an XML element, free all of its descendant nodes created in violation
* of ACLs, with the exception of allowing "scaffolding" elements (i.e. those
* that aren't in the ACL section and don't have any attributes other than
* \c PCMK_XA_ID).
*
* \param[in,out] xml XML to check
* \param[in] check_top Whether to apply checks to argument itself
* (if true, xml might get freed)
*
* \note This function is recursive
*/
void
pcmk__apply_creation_acl(xmlNode *xml, bool check_top)
{
xml_node_private_t *nodepriv = xml->_private;
if (pcmk_is_set(nodepriv->flags, pcmk__xf_created)) {
if (implicitly_allowed(xml)) {
crm_trace("Creation of <%s> scaffolding with " PCMK_XA_ID "=\"%s\""
" is implicitly allowed",
xml->name, display_id(xml));
} else if (pcmk__check_acl(xml, NULL, pcmk__xf_acl_write)) {
crm_trace("ACLs allow creation of <%s> with " PCMK_XA_ID "=\"%s\"",
xml->name, display_id(xml));
} else if (check_top) {
/* is_root=true should be impossible with check_top=true, but check
* for sanity
*/
bool is_root = (xmlDocGetRootElement(xml->doc) == xml);
xml_doc_private_t *docpriv = xml->doc->_private;
crm_trace("ACLs disallow creation of %s<%s> with "
PCMK_XA_ID "=\"%s\"",
(is_root? "root element " : ""), xml->name,
display_id(xml));
// pcmk__xml_free() checks ACLs if enabled, which would fail
pcmk__clear_xml_flags(docpriv, pcmk__xf_acl_enabled);
pcmk__xml_free(xml);
if (!is_root) {
// If root, the document was freed. Otherwise re-enable ACLs.
pcmk__set_xml_flags(docpriv, pcmk__xf_acl_enabled);
}
return;
} else {
crm_notice("ACLs would disallow creation of %s<%s> with "
PCMK_XA_ID "=\"%s\"",
((xml == xmlDocGetRootElement(xml->doc))? "root element " : ""),
xml->name, display_id(xml));
}
}
for (xmlNode *cIter = pcmk__xml_first_child(xml); cIter != NULL; ) {
xmlNode *child = cIter;
cIter = pcmk__xml_next(cIter); /* In case it is free'd */
pcmk__apply_creation_acl(child, true);
}
}
/*!
* \brief Check whether or not an XML node is ACL-denied
*
* \param[in] xml node to check
*
* \return true if XML node exists and is ACL-denied, false otherwise
*/
bool
xml_acl_denied(const xmlNode *xml)
{
if (xml && xml->doc && xml->doc->_private){
xml_doc_private_t *docpriv = xml->doc->_private;
return pcmk_is_set(docpriv->flags, pcmk__xf_acl_denied);
}
return false;
}
void
xml_acl_disable(xmlNode *xml)
{
if (xml_acl_enabled(xml)) {
xml_doc_private_t *docpriv = xml->doc->_private;
/* Catch anything that was created but shouldn't have been */
pcmk__apply_acl(xml);
pcmk__apply_creation_acl(xml, false);
pcmk__clear_xml_flags(docpriv, pcmk__xf_acl_enabled);
}
}
/*!
* \brief Check whether or not an XML node is ACL-enabled
*
* \param[in] xml node to check
*
* \return true if XML node exists and is ACL-enabled, false otherwise
*/
bool
xml_acl_enabled(const xmlNode *xml)
{
if (xml && xml->doc && xml->doc->_private){
xml_doc_private_t *docpriv = xml->doc->_private;
return pcmk_is_set(docpriv->flags, pcmk__xf_acl_enabled);
}
return false;
}
+/*!
+ * \internal
+ * \brief Deny access to an XML tree's document based on ACLs
+ *
+ * \param[in,out] xml XML tree
+ * \param[in] attr_name Name of attribute being accessed in \p xml (for
+ * logging only)
+ * \param[in] prefix Prefix describing ACL that denied access (for
+ * logging only)
+ * \param[in] user User accessing \p xml (for logging only)
+ * \param[in] mode Access mode
+ */
+#define check_acl_deny(xml, attr_name, prefix, user, mode) do { \
+ xmlNode *tree = xml; \
+ \
+ pcmk__set_xml_doc_flag(tree, pcmk__xf_acl_denied); \
+ pcmk__if_tracing( \
+ { \
+ GString *xpath = pcmk__element_xpath(tree); \
+ \
+ if ((attr_name) != NULL) { \
+ pcmk__g_strcat(xpath, "[@", attr_name, "]", NULL); \
+ } \
+ qb_log_from_external_source(__func__, __FILE__, \
+ "%sACL denies user '%s' %s " \
+ "access to %s", \
+ LOG_TRACE, __LINE__, 0 , \
+ prefix, user, \
+ acl_to_text(mode), xpath->str); \
+ g_string_free(xpath, TRUE); \
+ }, \
+ {} \
+ ); \
+ } while (false);
+
bool
pcmk__check_acl(xmlNode *xml, const char *attr_name,
enum xml_private_flags mode)
{
xml_doc_private_t *docpriv = NULL;
pcmk__assert((xml != NULL) && (xml->doc->_private != NULL));
if (!pcmk__tracking_xml_changes(xml, false) || !xml_acl_enabled(xml)) {
return true;
}
docpriv = xml->doc->_private;
-
if (docpriv->acls == NULL) {
- pcmk__set_xml_doc_flag(xml, pcmk__xf_acl_denied);
-
- pcmk__if_tracing(
- {
- GString *xpath = pcmk__element_xpath(xml);
-
- if (attr_name != NULL) {
- pcmk__g_strcat(xpath, "[@", attr_name, "]", NULL);
- }
-
- qb_log_from_external_source(__func__, __FILE__,
- "User '%s' without ACLs denied %s "
- "access to %s",
- LOG_TRACE, __LINE__, 0,
- docpriv->user, acl_to_text(mode),
- xpath->str);
- g_string_free(xpath, TRUE);
- },
- {}
- );
+ check_acl_deny(xml, attr_name, "Lack of ", docpriv->user, mode);
return false;
}
/* Walk the tree upwards looking for xml_acl_* flags
* - Creating an attribute requires write permissions for the node
* - Creating a child requires write permissions for the parent
*/
if (attr_name != NULL) {
xmlAttr *attr = xmlHasProp(xml, (pcmkXmlStr) attr_name);
if ((attr != NULL) && (mode == pcmk__xf_acl_create)) {
mode = pcmk__xf_acl_write;
}
}
for (const xmlNode *parent = xml;
(parent != NULL) && (parent->_private != NULL);
parent = parent->parent) {
const xml_node_private_t *nodepriv = parent->_private;
if (test_acl_mode(nodepriv->flags, mode)) {
return true;
}
if (pcmk_is_set(nodepriv->flags, pcmk__xf_acl_deny)) {
- pcmk__set_xml_doc_flag(xml, pcmk__xf_acl_denied);
-
- pcmk__if_tracing(
- {
- GString *xpath = pcmk__element_xpath(xml);
-
- if (attr_name != NULL) {
- pcmk__g_strcat(xpath, "[@", attr_name, "]", NULL);
- }
+ const char *pfx = (parent != xml)? "Parent " : "";
- qb_log_from_external_source(__func__, __FILE__,
- "%sACL denies user '%s' %s "
- "access to %s",
- LOG_TRACE, __LINE__, 0,
- (parent != xml)? "Parent ": "",
- docpriv->user,
- acl_to_text(mode), xpath->str);
- g_string_free(xpath, TRUE);
- },
- {}
- );
+ check_acl_deny(xml, attr_name, pfx, docpriv->user, mode);
return false;
}
}
- pcmk__set_xml_doc_flag(xml, pcmk__xf_acl_denied);
-
- pcmk__if_tracing(
- {
- GString *xpath = pcmk__element_xpath(xml);
-
- if (attr_name != NULL) {
- pcmk__g_strcat(xpath, "[@", attr_name, "]", NULL);
- }
-
- qb_log_from_external_source(__func__, __FILE__,
- "Default ACL denies user '%s' %s "
- "access to %s",
- LOG_TRACE, __LINE__, 0,
- docpriv->user, acl_to_text(mode),
- xpath->str);
- g_string_free(xpath, TRUE);
- },
- {}
- );
+ check_acl_deny(xml, attr_name, "Default ", docpriv->user, mode);
return false;
}
/*!
* \brief Check whether ACLs are required for a given user
*
* \param[in] User name to check
*
* \return true if the user requires ACLs, false otherwise
*/
bool
pcmk_acl_required(const char *user)
{
if (pcmk__str_empty(user)) {
crm_trace("ACLs not required because no user set");
return false;
} else if (!strcmp(user, CRM_DAEMON_USER) || !strcmp(user, "root")) {
crm_trace("ACLs not required for privileged user %s", user);
return false;
}
crm_trace("ACLs required for %s", user);
return true;
}
char *
pcmk__uid2username(uid_t uid)
{
struct passwd *pwent = getpwuid(uid);
if (pwent == NULL) {
crm_perror(LOG_INFO, "Cannot get user details for user ID %d", uid);
return NULL;
}
return pcmk__str_copy(pwent->pw_name);
}
/*!
* \internal
* \brief Set the ACL user field properly on an XML request
*
* Multiple user names are potentially involved in an XML request: the effective
* user of the current process; the user name known from an IPC client
* connection; and the user name obtained from the request itself, whether by
* the current standard XML attribute name or an older legacy attribute name.
* This function chooses the appropriate one that should be used for ACLs, sets
* it in the request (using the standard attribute name, and the legacy name if
* given), and returns it.
*
* \param[in,out] request XML request to update
* \param[in] field Alternate name for ACL user name XML attribute
* \param[in] peer_user User name as known from IPC connection
*
* \return ACL user name actually used
*/
const char *
pcmk__update_acl_user(xmlNode *request, const char *field,
const char *peer_user)
{
static const char *effective_user = NULL;
const char *requested_user = NULL;
const char *user = NULL;
if (effective_user == NULL) {
effective_user = pcmk__uid2username(geteuid());
if (effective_user == NULL) {
effective_user = pcmk__str_copy("#unprivileged");
crm_err("Unable to determine effective user, assuming unprivileged for ACLs");
}
}
requested_user = crm_element_value(request, PCMK__XA_ACL_TARGET);
if (requested_user == NULL) {
/* Currently, different XML attribute names are used for the ACL user in
* different contexts (PCMK__XA_ATTR_USER, PCMK__XA_CIB_USER, etc.).
* The caller may specify that name as the field argument.
*
* @TODO Standardize on PCMK__XA_ACL_TARGET and eventually drop the
* others once rolling upgrades from versions older than that are no
* longer supported.
*/
requested_user = crm_element_value(request, field);
}
if (!pcmk__is_privileged(effective_user)) {
/* We're not running as a privileged user, set or overwrite any existing
* value for PCMK__XA_ACL_TARGET
*/
user = effective_user;
} else if (peer_user == NULL && requested_user == NULL) {
/* No user known or requested, use 'effective_user' and make sure one is
* set for the request
*/
user = effective_user;
} else if (peer_user == NULL) {
/* No user known, trusting 'requested_user' */
user = requested_user;
} else if (!pcmk__is_privileged(peer_user)) {
/* The peer is not a privileged user, set or overwrite any existing
* value for PCMK__XA_ACL_TARGET
*/
user = peer_user;
} else if (requested_user == NULL) {
/* Even if we're privileged, make sure there is always a value set */
user = peer_user;
} else {
/* Legal delegation to 'requested_user' */
user = requested_user;
}
// This requires pointer comparison, not string comparison
if (user != crm_element_value(request, PCMK__XA_ACL_TARGET)) {
crm_xml_add(request, PCMK__XA_ACL_TARGET, user);
}
if (field != NULL && user != crm_element_value(request, field)) {
crm_xml_add(request, field, user);
}
return requested_user;
}