Page Menu
Home
ClusterLabs Projects
Search
Configure Global Search
Log In
Files
F4639586
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Size
95 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/lib/common/acl.c b/lib/common/acl.c
index 66db534c1d..9cdafa7016 100644
--- a/lib/common/acl.c
+++ b/lib/common/acl.c
@@ -1,923 +1,923 @@
/*
* 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 <crm_internal.h>
#include <stdio.h>
#include <sys/types.h>
#include <pwd.h>
#include <string.h>
#include <stdlib.h>
#include <stdarg.h>
#include <libxml/tree.h> // xmlNode, etc.
#include <libxml/xmlstring.h> // xmlChar
#include <libxml/xpath.h> // xmlXPathObject, etc.
#include <crm/crm.h>
#include <crm/common/xml.h>
#include <crm/common/xml_internal.h>
#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;
}
/*
<acls>
<acl_target id="l33t-haxor"><role id="auto-l33t-haxor"/></acl_target>
<acl_role id="auto-l33t-haxor">
<acl_permission id="crook-nothing" kind="deny" xpath="/cib"/>
</acl_role>
<acl_target id="niceguy">
<role id="observer"/>
</acl_target>
<acl_role id="observer">
<acl_permission id="observer-read-1" kind="read" xpath="/cib"/>
<acl_permission id="observer-write-1" kind="write" xpath="//nvpair[@name='stonith-enabled']"/>
<acl_permission id="observer-write-2" kind="write" xpath="//nvpair[@name='target-role']"/>
</acl_role>
<acl_target id="badidea"><role id="auto-badidea"/></acl_target>
<acl_role id="auto-badidea">
<acl_permission id="badidea-resources" kind="read" xpath="//meta_attributes"/>
<acl_permission id="badidea-resources-2" kind="deny" reference="dummy-meta_attributes"/>
</acl_role>
</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);
+ docpriv->acl_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 = pcmk__xpath_find_one(source->doc, "//" PCMK_XE_ACLS,
LOG_NEVER);
- pcmk__str_update(&docpriv->user, user);
+ pcmk__str_update(&(docpriv->acl_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)
{
if (target == NULL) {
return;
}
pcmk__unpack_acl(acl_source, target, user);
pcmk__xml_doc_set_flags(target->doc, 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), "<unset>")
/*!
* \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__xml_doc_set_flags(tree->doc, 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__xml_doc_all_flags_set(xml->doc, pcmk__xf_tracking)
|| !xml_acl_enabled(xml)) {
return true;
}
docpriv = xml->doc->_private;
if (docpriv->acls == NULL) {
- check_acl_deny(xml, attr_name, "Lack of ", docpriv->user, mode);
+ check_acl_deny(xml, attr_name, "Lack of ", docpriv->acl_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, (const xmlChar *) 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)) {
const char *pfx = (parent != xml)? "Parent " : "";
- check_acl_deny(xml, attr_name, pfx, docpriv->user, mode);
+ check_acl_deny(xml, attr_name, pfx, docpriv->acl_user, mode);
return false;
}
}
- check_acl_deny(xml, attr_name, "Default ", docpriv->user, mode);
+ check_acl_deny(xml, attr_name, "Default ", docpriv->acl_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;
}
diff --git a/lib/common/crmcommon_private.h b/lib/common/crmcommon_private.h
index 47a17e7e59..8e1b453eb6 100644
--- a/lib/common/crmcommon_private.h
+++ b/lib/common/crmcommon_private.h
@@ -1,464 +1,488 @@
/*
* Copyright 2018-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.
*/
#ifndef PCMK__COMMON_CRMCOMMON_PRIVATE__H
#define PCMK__COMMON_CRMCOMMON_PRIVATE__H
/* This header is for the sole use of libcrmcommon, so that functions can be
* declared with G_GNUC_INTERNAL for efficiency.
*/
#include <stdint.h> // uint8_t, uint32_t
#include <stdbool.h> // bool
#include <sys/types.h> // size_t
#include <glib.h> // G_GNUC_INTERNAL, G_GNUC_PRINTF, gchar, etc.
#include <libxml/tree.h> // xmlNode, xmlAttr
#include <libxml/xmlstring.h> // xmlChar
#include <qb/qbipcc.h> // struct qb_ipc_response_header
#include <crm/common/ipc.h> // pcmk_ipc_api_t, crm_ipc_t, etc.
#include <crm/common/iso8601.h> // crm_time_t
#include <crm/common/logging.h> // LOG_NEVER
#include <crm/common/mainloop.h> // mainloop_io_t
#include <crm/common/output_internal.h> // pcmk__output_t
#include <crm/common/results.h> // crm_exit_t
#include <crm/common/rules.h> // pcmk_rule_input_t
#include <crm/common/xml_internal.h> // enum xml_private_flags
#ifdef __cplusplus
extern "C" {
#endif
// Decent chunk size for processing large amounts of data
#define PCMK__BUFFER_SIZE 4096
#if defined(PCMK__UNIT_TESTING)
#undef G_GNUC_INTERNAL
#define G_GNUC_INTERNAL
#endif
-/* When deleting portions of an XML tree, we keep a record so we can know later
- * (e.g. when checking differences) that something was deleted.
+/*!
+ * \internal
+ * \brief Information about an XML node that was deleted
+ *
+ * When change tracking is enabled and we delete an XML node using
+ * \c pcmk__xml_free(), we free it and add its path and position to a list in
+ * its document's private data. This allows us to display changes, generate
+ * patchsets, etc.
+ *
+ * Note that this does not happen when deleting an XML attribute using
+ * \c pcmk__xa_remove(). In that case:
+ * * If \c force is \c true, we remove the attribute without any tracking.
+ * * If \c force is \c false, we mark the attribute as deleted but leave it in
+ * place until we commit changes.
*/
typedef struct pcmk__deleted_xml_s {
- gchar *path;
- int position;
+ gchar *path; //!< XPath expression identifying the deleted node
+ int position; //!< Position of the deleted node among its siblings
} pcmk__deleted_xml_t;
+/*!
+ * \internal
+ * \brief Private data for an XML node
+ */
typedef struct xml_node_private_s {
- uint32_t check;
- uint32_t flags;
+ uint32_t check; //!< Magic number for checking integrity
+ uint32_t flags; //!< Group of <tt>enum xml_private_flags</tt>
} xml_node_private_t;
+/*!
+ * \internal
+ * \brief Private data for an XML document
+ */
typedef struct xml_doc_private_s {
- uint32_t check;
- uint32_t flags;
- char *user;
- GList *acls;
- GList *deleted_objs; // List of pcmk__deleted_xml_t
+ uint32_t check; //!< Magic number for checking integrity
+ uint32_t flags; //!< Group of <tt>enum xml_private_flags</tt>
+ char *acl_user; //!< User affected by \c acls (for logging)
+
+ //! ACLs to check requested changes against (list of \c xml_acl_t)
+ GList *acls;
+
+ //! XML nodes marked as deleted (list of \c pcmk__deleted_xml_t)
+ GList *deleted_objs;
} xml_doc_private_t;
// XML private data magic numbers
#define PCMK__XML_DOC_PRIVATE_MAGIC 0x81726354UL
#define PCMK__XML_NODE_PRIVATE_MAGIC 0x54637281UL
// XML entity references
#define PCMK__XML_ENTITY_AMP "&"
#define PCMK__XML_ENTITY_GT ">"
#define PCMK__XML_ENTITY_LT "<"
#define PCMK__XML_ENTITY_QUOT """
#define pcmk__set_xml_flags(xml_priv, flags_to_set) do { \
(xml_priv)->flags = pcmk__set_flags_as(__func__, __LINE__, \
LOG_NEVER, "XML", "XML node", (xml_priv)->flags, \
(flags_to_set), #flags_to_set); \
} while (0)
#define pcmk__clear_xml_flags(xml_priv, flags_to_clear) do { \
(xml_priv)->flags = pcmk__clear_flags_as(__func__, __LINE__, \
LOG_NEVER, "XML", "XML node", (xml_priv)->flags, \
(flags_to_clear), #flags_to_clear); \
} while (0)
G_GNUC_INTERNAL
const char *pcmk__xml_element_type_text(xmlElementType type);
G_GNUC_INTERNAL
bool pcmk__xml_reset_node_flags(xmlNode *xml, void *user_data);
G_GNUC_INTERNAL
void pcmk__xml_set_parent_flags(xmlNode *xml, uint64_t flags);
G_GNUC_INTERNAL
void pcmk__xml_new_private_data(xmlNode *xml);
G_GNUC_INTERNAL
void pcmk__xml_free_private_data(xmlNode *xml);
G_GNUC_INTERNAL
void pcmk__xml_free_node(xmlNode *xml);
G_GNUC_INTERNAL
xmlDoc *pcmk__xml_new_doc(void);
G_GNUC_INTERNAL
int pcmk__xml_position(const xmlNode *xml,
enum xml_private_flags ignore_if_set);
G_GNUC_INTERNAL
xmlNode *pcmk__xml_match(const xmlNode *haystack, const xmlNode *needle,
bool exact);
G_GNUC_INTERNAL
xmlNode *pcmk__xc_match(const xmlNode *root, const xmlNode *search_comment,
bool exact);
G_GNUC_INTERNAL
void pcmk__xc_update(xmlNode *parent, xmlNode *target, xmlNode *update);
G_GNUC_INTERNAL
void pcmk__free_acls(GList *acls);
G_GNUC_INTERNAL
void pcmk__unpack_acl(xmlNode *source, xmlNode *target, const char *user);
G_GNUC_INTERNAL
bool pcmk__is_user_in_group(const char *user, const char *group);
G_GNUC_INTERNAL
void pcmk__apply_acl(xmlNode *xml);
G_GNUC_INTERNAL
void pcmk__apply_creation_acl(xmlNode *xml, bool check_top);
G_GNUC_INTERNAL
int pcmk__xa_remove(xmlAttr *attr, bool force);
G_GNUC_INTERNAL
void pcmk__mark_xml_attr_dirty(xmlAttr *a);
G_GNUC_INTERNAL
bool pcmk__xa_filterable(const char *name);
G_GNUC_INTERNAL
void pcmk__log_xmllib_err(void *ctx, const char *fmt, ...)
G_GNUC_PRINTF(2, 3);
G_GNUC_INTERNAL
void pcmk__mark_xml_node_dirty(xmlNode *xml);
G_GNUC_INTERNAL
bool pcmk__marked_as_deleted(xmlAttrPtr a, void *user_data);
G_GNUC_INTERNAL
void pcmk__dump_xml_attr(const xmlAttr *attr, GString *buffer);
G_GNUC_INTERNAL
int pcmk__xe_set_score(xmlNode *target, const char *name, const char *value);
G_GNUC_INTERNAL
bool pcmk__xml_is_name_start_char(const char *utf8, int *len);
G_GNUC_INTERNAL
bool pcmk__xml_is_name_char(const char *utf8, int *len);
/*
* Date/times
*/
// For use with pcmk__add_time_from_xml()
enum pcmk__time_component {
pcmk__time_unknown,
pcmk__time_years,
pcmk__time_months,
pcmk__time_weeks,
pcmk__time_days,
pcmk__time_hours,
pcmk__time_minutes,
pcmk__time_seconds,
};
G_GNUC_INTERNAL
const char *pcmk__time_component_attr(enum pcmk__time_component component);
G_GNUC_INTERNAL
int pcmk__add_time_from_xml(crm_time_t *t, enum pcmk__time_component component,
const xmlNode *xml);
G_GNUC_INTERNAL
void pcmk__set_time_if_earlier(crm_time_t *target, const crm_time_t *source);
/*
* IPC
*/
#define PCMK__IPC_VERSION 1
#define PCMK__CONTROLD_API_MAJOR "1"
#define PCMK__CONTROLD_API_MINOR "0"
// IPC behavior that varies by daemon
typedef struct pcmk__ipc_methods_s {
/*!
* \internal
* \brief Allocate any private data needed by daemon IPC
*
* \param[in,out] api IPC API connection
*
* \return Standard Pacemaker return code
*/
int (*new_data)(pcmk_ipc_api_t *api);
/*!
* \internal
* \brief Free any private data used by daemon IPC
*
* \param[in,out] api_data Data allocated by new_data() method
*/
void (*free_data)(void *api_data);
/*!
* \internal
* \brief Perform daemon-specific handling after successful connection
*
* Some daemons require clients to register before sending any other
* commands. The controller requires a CRM_OP_HELLO (with no reply), and
* the CIB manager, executor, and fencer require a CRM_OP_REGISTER (with a
* reply). Ideally this would be consistent across all daemons, but for now
* this allows each to do its own authorization.
*
* \param[in,out] api IPC API connection
*
* \return Standard Pacemaker return code
*/
int (*post_connect)(pcmk_ipc_api_t *api);
/*!
* \internal
* \brief Check whether an IPC request results in a reply
*
* \param[in,out] api IPC API connection
* \param[in] request IPC request XML
*
* \return true if request would result in an IPC reply, false otherwise
*/
bool (*reply_expected)(pcmk_ipc_api_t *api, const xmlNode *request);
/*!
* \internal
* \brief Perform daemon-specific handling of an IPC message
*
* \param[in,out] api IPC API connection
* \param[in,out] msg Message read from IPC connection
*
* \return true if more IPC reply messages should be expected
*/
bool (*dispatch)(pcmk_ipc_api_t *api, xmlNode *msg);
/*!
* \internal
* \brief Perform daemon-specific handling of an IPC disconnect
*
* \param[in,out] api IPC API connection
*/
void (*post_disconnect)(pcmk_ipc_api_t *api);
} pcmk__ipc_methods_t;
// Implementation of pcmk_ipc_api_t
struct pcmk_ipc_api_s {
enum pcmk_ipc_server server; // Daemon this IPC API instance is for
enum pcmk_ipc_dispatch dispatch_type; // How replies should be dispatched
size_t ipc_size_max; // maximum IPC buffer size
crm_ipc_t *ipc; // IPC connection
mainloop_io_t *mainloop_io; // If using mainloop, I/O source for IPC
bool free_on_disconnect; // Whether disconnect should free object
pcmk_ipc_callback_t cb; // Caller-registered callback (if any)
void *user_data; // Caller-registered data (if any)
void *api_data; // For daemon-specific use
pcmk__ipc_methods_t *cmds; // Behavior that varies by daemon
};
typedef struct pcmk__ipc_header_s {
struct qb_ipc_response_header qb;
uint32_t size_uncompressed;
uint32_t size_compressed;
uint32_t flags;
uint8_t version;
} pcmk__ipc_header_t;
G_GNUC_INTERNAL
int pcmk__send_ipc_request(pcmk_ipc_api_t *api, const xmlNode *request);
G_GNUC_INTERNAL
void pcmk__call_ipc_callback(pcmk_ipc_api_t *api,
enum pcmk_ipc_event event_type,
crm_exit_t status, void *event_data);
G_GNUC_INTERNAL
unsigned int pcmk__ipc_buffer_size(unsigned int max);
G_GNUC_INTERNAL
bool pcmk__valid_ipc_header(const pcmk__ipc_header_t *header);
G_GNUC_INTERNAL
pcmk__ipc_methods_t *pcmk__attrd_api_methods(void);
G_GNUC_INTERNAL
pcmk__ipc_methods_t *pcmk__controld_api_methods(void);
G_GNUC_INTERNAL
pcmk__ipc_methods_t *pcmk__pacemakerd_api_methods(void);
G_GNUC_INTERNAL
pcmk__ipc_methods_t *pcmk__schedulerd_api_methods(void);
/*
* Logging
*/
//! XML is newly created
#define PCMK__XML_PREFIX_CREATED "++"
//! XML has been deleted
#define PCMK__XML_PREFIX_DELETED "--"
//! XML has been modified
#define PCMK__XML_PREFIX_MODIFIED "+ "
//! XML has been moved
#define PCMK__XML_PREFIX_MOVED "+~"
/*
* Output
*/
G_GNUC_INTERNAL
int pcmk__bare_output_new(pcmk__output_t **out, const char *fmt_name,
const char *filename, char **argv);
G_GNUC_INTERNAL
void pcmk__register_option_messages(pcmk__output_t *out);
G_GNUC_INTERNAL
void pcmk__register_patchset_messages(pcmk__output_t *out);
G_GNUC_INTERNAL
bool pcmk__output_text_get_fancy(pcmk__output_t *out);
/*
* Rules
*/
// How node attribute values may be compared in rules
enum pcmk__comparison {
pcmk__comparison_unknown,
pcmk__comparison_defined,
pcmk__comparison_undefined,
pcmk__comparison_eq,
pcmk__comparison_ne,
pcmk__comparison_lt,
pcmk__comparison_lte,
pcmk__comparison_gt,
pcmk__comparison_gte,
};
// How node attribute values may be parsed in rules
enum pcmk__type {
pcmk__type_unknown,
pcmk__type_string,
pcmk__type_integer,
pcmk__type_number,
pcmk__type_version,
};
// Where to obtain reference value for a node attribute comparison
enum pcmk__reference_source {
pcmk__source_unknown,
pcmk__source_literal,
pcmk__source_instance_attrs,
pcmk__source_meta_attrs,
};
G_GNUC_INTERNAL
enum pcmk__comparison pcmk__parse_comparison(const char *op);
G_GNUC_INTERNAL
enum pcmk__type pcmk__parse_type(const char *type, enum pcmk__comparison op,
const char *value1, const char *value2);
G_GNUC_INTERNAL
enum pcmk__reference_source pcmk__parse_source(const char *source);
G_GNUC_INTERNAL
int pcmk__cmp_by_type(const char *value1, const char *value2,
enum pcmk__type type);
G_GNUC_INTERNAL
int pcmk__unpack_duration(const xmlNode *duration, const crm_time_t *start,
crm_time_t **end);
G_GNUC_INTERNAL
int pcmk__evaluate_date_spec(const xmlNode *date_spec, const crm_time_t *now);
G_GNUC_INTERNAL
int pcmk__evaluate_attr_expression(const xmlNode *expression,
const pcmk_rule_input_t *rule_input);
G_GNUC_INTERNAL
int pcmk__evaluate_rsc_expression(const xmlNode *expr,
const pcmk_rule_input_t *rule_input);
G_GNUC_INTERNAL
int pcmk__evaluate_op_expression(const xmlNode *expr,
const pcmk_rule_input_t *rule_input);
/*
* Utils
*/
#define PCMK__PW_BUFFER_LEN 500
/*
* Schemas
*/
typedef struct {
unsigned char v[2];
} pcmk__schema_version_t;
enum pcmk__schema_validator {
pcmk__schema_validator_none,
pcmk__schema_validator_rng
};
typedef struct {
int schema_index;
char *name;
/*!
* List of XSLT stylesheets for upgrading from this schema version to the
* next one. Sorted by the order in which they should be applied to the CIB.
*/
GList *transforms;
void *cache;
enum pcmk__schema_validator validator;
pcmk__schema_version_t version;
} pcmk__schema_t;
G_GNUC_INTERNAL
GList *pcmk__find_x_0_schema(void);
#ifdef __cplusplus
}
#endif
#endif // PCMK__COMMON_CRMCOMMON_PRIVATE__H
diff --git a/lib/common/xml.c b/lib/common/xml.c
index 68bd6a592a..fe3eae7269 100644
--- a/lib/common/xml.c
+++ b/lib/common/xml.c
@@ -1,1665 +1,1664 @@
/*
* 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 <crm_internal.h>
#include <stdarg.h>
#include <stdint.h> // uint32_t
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h> // stat(), S_ISREG, etc.
#include <sys/types.h>
#include <glib.h> // gboolean, GString
#include <libxml/parser.h> // xmlCleanupParser()
#include <libxml/tree.h> // xmlNode, etc.
#include <libxml/xmlstring.h> // xmlChar, xmlGetUTF8Char()
#include <crm/crm.h>
#include <crm/common/xml.h>
#include <crm/common/xml_internal.h> // PCMK__XML_LOG_BASE, etc.
#include "crmcommon_private.h"
//! libxml2 supports only XML version 1.0, at least as of libxml2-2.12.5
#define XML_VERSION ((const xmlChar *) "1.0")
/*!
* \internal
* \brief Get a string representation of an XML element type for logging
*
* \param[in] type XML element type
*
* \return String representation of \p type
*/
const char *
pcmk__xml_element_type_text(xmlElementType type)
{
static const char *const element_type_names[] = {
[XML_ELEMENT_NODE] = "element",
[XML_ATTRIBUTE_NODE] = "attribute",
[XML_TEXT_NODE] = "text",
[XML_CDATA_SECTION_NODE] = "CDATA section",
[XML_ENTITY_REF_NODE] = "entity reference",
[XML_ENTITY_NODE] = "entity",
[XML_PI_NODE] = "PI",
[XML_COMMENT_NODE] = "comment",
[XML_DOCUMENT_NODE] = "document",
[XML_DOCUMENT_TYPE_NODE] = "document type",
[XML_DOCUMENT_FRAG_NODE] = "document fragment",
[XML_NOTATION_NODE] = "notation",
[XML_HTML_DOCUMENT_NODE] = "HTML document",
[XML_DTD_NODE] = "DTD",
[XML_ELEMENT_DECL] = "element declaration",
[XML_ATTRIBUTE_DECL] = "attribute declaration",
[XML_ENTITY_DECL] = "entity declaration",
[XML_NAMESPACE_DECL] = "namespace declaration",
[XML_XINCLUDE_START] = "XInclude start",
[XML_XINCLUDE_END] = "XInclude end",
};
// Assumes the numeric values of the indices are in ascending order
if ((type < XML_ELEMENT_NODE) || (type > XML_XINCLUDE_END)) {
return "unrecognized type";
}
return element_type_names[type];
}
/*!
* \internal
* \brief Apply a function to each XML node in a tree (pre-order, depth-first)
*
* \param[in,out] xml XML tree to traverse
* \param[in,out] fn Function to call for each node (returns \c true to
* continue traversing the tree or \c false to stop)
* \param[in,out] user_data Argument to \p fn
*
* \return \c false if any \p fn call returned \c false, or \c true otherwise
*
* \note This function is recursive.
*/
bool
pcmk__xml_tree_foreach(xmlNode *xml, bool (*fn)(xmlNode *, void *),
void *user_data)
{
if (xml == NULL) {
return true;
}
if (!fn(xml, user_data)) {
return false;
}
for (xml = pcmk__xml_first_child(xml); xml != NULL;
xml = pcmk__xml_next(xml)) {
if (!pcmk__xml_tree_foreach(xml, fn, user_data)) {
return false;
}
}
return true;
}
void
pcmk__xml_set_parent_flags(xmlNode *xml, uint64_t flags)
{
for (; xml != NULL; xml = xml->parent) {
xml_node_private_t *nodepriv = xml->_private;
if (nodepriv != NULL) {
pcmk__set_xml_flags(nodepriv, flags);
}
}
}
/*!
* \internal
* \brief Set flags for an XML document
*
* \param[in,out] doc XML document
* \param[in] flags Group of <tt>enum xml_private_flags</tt>
*/
void
pcmk__xml_doc_set_flags(xmlDoc *doc, uint32_t flags)
{
if (doc != NULL) {
xml_doc_private_t *docpriv = doc->_private;
pcmk__set_xml_flags(docpriv, flags);
}
}
/*!
* \internal
* \brief Check whether the given flags are set for an XML document
*
* \param[in] doc XML document to check
* \param[in] flags Group of <tt>enum xml_private_flags</tt>
*
* \return \c true if all of \p flags are set for \p doc, or \c false otherwise
*/
bool
pcmk__xml_doc_all_flags_set(const xmlDoc *doc, uint32_t flags)
{
if (doc != NULL) {
xml_doc_private_t *docpriv = doc->_private;
return (docpriv != NULL) && pcmk_all_flags_set(docpriv->flags, flags);
}
return false;
}
// Mark document, element, and all element's parents as changed
void
pcmk__mark_xml_node_dirty(xmlNode *xml)
{
if (xml == NULL) {
return;
}
pcmk__xml_doc_set_flags(xml->doc, pcmk__xf_dirty);
pcmk__xml_set_parent_flags(xml, pcmk__xf_dirty);
}
/*!
* \internal
* \brief Clear flags on an XML node
*
* \param[in,out] xml XML node whose flags to reset
* \param[in,out] user_data Ignored
*
* \return \c true (to continue traversing the tree)
*
* \note This is compatible with \c pcmk__xml_tree_foreach().
*/
bool
pcmk__xml_reset_node_flags(xmlNode *xml, void *user_data)
{
xml_node_private_t *nodepriv = xml->_private;
if (nodepriv != NULL) {
nodepriv->flags = pcmk__xf_none;
}
return true;
}
/*!
* \internal
* \brief Set the \c pcmk__xf_dirty and \c pcmk__xf_created flags on an XML node
*
* \param[in,out] xml Node whose flags to set
* \param[in] user_data Ignored
*
* \return \c true (to continue traversing the tree)
*
* \note This is compatible with \c pcmk__xml_tree_foreach().
*/
static bool
mark_xml_dirty_created(xmlNode *xml, void *user_data)
{
xml_node_private_t *nodepriv = xml->_private;
if (nodepriv != NULL) {
pcmk__set_xml_flags(nodepriv, pcmk__xf_dirty|pcmk__xf_created);
}
return true;
}
/*!
* \internal
* \brief Mark an XML tree as dirty and created, and mark its parents dirty
*
* Also mark the document dirty.
*
* \param[in,out] xml Tree to mark as dirty and created
*/
static void
mark_xml_tree_dirty_created(xmlNode *xml)
{
pcmk__assert(xml != NULL);
if (!pcmk__xml_doc_all_flags_set(xml->doc, pcmk__xf_tracking)) {
// Tracking is disabled for entire document
return;
}
// Mark all parents and document dirty
pcmk__mark_xml_node_dirty(xml);
pcmk__xml_tree_foreach(xml, mark_xml_dirty_created, NULL);
}
// Free an XML object previously marked as deleted
static void
free_deleted_object(void *data)
{
if(data) {
pcmk__deleted_xml_t *deleted_obj = data;
g_free(deleted_obj->path);
free(deleted_obj);
}
}
// Free and NULL user, ACLs, and deleted objects in an XML node's private data
static void
reset_xml_private_data(xml_doc_private_t *docpriv)
{
if (docpriv != NULL) {
pcmk__assert(docpriv->check == PCMK__XML_DOC_PRIVATE_MAGIC);
- free(docpriv->user);
- docpriv->user = NULL;
+ pcmk__str_update(&(docpriv->acl_user), NULL);
if (docpriv->acls != NULL) {
pcmk__free_acls(docpriv->acls);
docpriv->acls = NULL;
}
if(docpriv->deleted_objs) {
g_list_free_full(docpriv->deleted_objs, free_deleted_object);
docpriv->deleted_objs = NULL;
}
}
}
/*!
* \internal
* \brief Allocate and initialize private data for an XML node
*
* \param[in,out] node XML node whose private data to initialize
* \param[in] user_data Ignored
*
* \return \c true (to continue traversing the tree)
*
* \note This is compatible with \c pcmk__xml_tree_foreach().
*/
static bool
new_private_data(xmlNode *node, void *user_data)
{
CRM_CHECK(node != NULL, return true);
if (node->_private != NULL) {
return true;
}
switch (node->type) {
case XML_DOCUMENT_NODE:
{
xml_doc_private_t *docpriv =
pcmk__assert_alloc(1, sizeof(xml_doc_private_t));
docpriv->check = PCMK__XML_DOC_PRIVATE_MAGIC;
node->_private = docpriv;
pcmk__set_xml_flags(docpriv, pcmk__xf_dirty|pcmk__xf_created);
}
break;
case XML_ELEMENT_NODE:
case XML_ATTRIBUTE_NODE:
case XML_COMMENT_NODE:
{
xml_node_private_t *nodepriv =
pcmk__assert_alloc(1, sizeof(xml_node_private_t));
nodepriv->check = PCMK__XML_NODE_PRIVATE_MAGIC;
node->_private = nodepriv;
pcmk__set_xml_flags(nodepriv, pcmk__xf_dirty|pcmk__xf_created);
for (xmlAttr *iter = pcmk__xe_first_attr(node); iter != NULL;
iter = iter->next) {
new_private_data((xmlNode *) iter, user_data);
}
}
break;
case XML_TEXT_NODE:
case XML_DTD_NODE:
case XML_CDATA_SECTION_NODE:
return true;
default:
CRM_LOG_ASSERT(node->type == XML_ELEMENT_NODE);
return true;
}
if (pcmk__xml_doc_all_flags_set(node->doc, pcmk__xf_tracking)) {
pcmk__mark_xml_node_dirty(node);
}
return true;
}
/*!
* \internal
* \brief Free private data for an XML node
*
* \param[in,out] node XML node whose private data to free
* \param[in] user_data Ignored
*
* \return \c true (to continue traversing the tree)
*
* \note This is compatible with \c pcmk__xml_tree_foreach().
*/
static bool
free_private_data(xmlNode *node, void *user_data)
{
CRM_CHECK(node != NULL, return true);
if (node->_private == NULL) {
return true;
}
if (node->type == XML_DOCUMENT_NODE) {
reset_xml_private_data((xml_doc_private_t *) node->_private);
} else {
xml_node_private_t *nodepriv = node->_private;
pcmk__assert(nodepriv->check == PCMK__XML_NODE_PRIVATE_MAGIC);
for (xmlAttr *iter = pcmk__xe_first_attr(node); iter != NULL;
iter = iter->next) {
free_private_data((xmlNode *) iter, user_data);
}
}
free(node->_private);
node->_private = NULL;
return true;
}
/*!
* \internal
* \brief Allocate and initialize private data recursively for an XML tree
*
* \param[in,out] node XML node whose private data to initialize
*/
void
pcmk__xml_new_private_data(xmlNode *xml)
{
pcmk__xml_tree_foreach(xml, new_private_data, NULL);
}
/*!
* \internal
* \brief Free private data recursively for an XML tree
*
* \param[in,out] node XML node whose private data to free
*/
void
pcmk__xml_free_private_data(xmlNode *xml)
{
pcmk__xml_tree_foreach(xml, free_private_data, NULL);
}
void
xml_track_changes(xmlNode * xml, const char *user, xmlNode *acl_source, bool enforce_acls)
{
if (xml == NULL) {
return;
}
xml_accept_changes(xml);
crm_trace("Tracking changes%s to %p", enforce_acls?" with ACLs":"", xml);
pcmk__xml_doc_set_flags(xml->doc, pcmk__xf_tracking);
if(enforce_acls) {
if(acl_source == NULL) {
acl_source = xml;
}
pcmk__xml_doc_set_flags(xml->doc, pcmk__xf_acl_enabled);
pcmk__unpack_acl(acl_source, xml, user);
pcmk__apply_acl(xml);
}
}
/*!
* \internal
* \brief Return ordinal position of an XML node among its siblings
*
* \param[in] xml XML node to check
* \param[in] ignore_if_set Don't count siblings with this flag set
*
* \return Ordinal position of \p xml (starting with 0)
*/
int
pcmk__xml_position(const xmlNode *xml, enum xml_private_flags ignore_if_set)
{
int position = 0;
for (const xmlNode *cIter = xml; cIter->prev; cIter = cIter->prev) {
xml_node_private_t *nodepriv = ((xmlNode*)cIter->prev)->_private;
if (!pcmk_is_set(nodepriv->flags, ignore_if_set)) {
position++;
}
}
return position;
}
/*!
* \internal
* \brief Remove all attributes marked as deleted from an XML node
*
* \param[in,out] xml XML node whose deleted attributes to remove
* \param[in,out] user_data Ignored
*
* \return \c true (to continue traversing the tree)
*
* \note This is compatible with \c pcmk__xml_tree_foreach().
*/
static bool
accept_attr_deletions(xmlNode *xml, void *user_data)
{
pcmk__xml_reset_node_flags(xml, NULL);
pcmk__xe_remove_matching_attrs(xml, pcmk__marked_as_deleted, NULL);
return true;
}
/*!
* \internal
* \brief Find first child XML node matching another given XML node
*
* \param[in] haystack XML whose children should be checked
* \param[in] needle XML to match (comment content or element name and ID)
* \param[in] exact If true and needle is a comment, position must match
*/
xmlNode *
pcmk__xml_match(const xmlNode *haystack, const xmlNode *needle, bool exact)
{
CRM_CHECK(needle != NULL, return NULL);
if (needle->type == XML_COMMENT_NODE) {
return pcmk__xc_match(haystack, needle, exact);
} else {
const char *id = pcmk__xe_id(needle);
const char *attr = (id == NULL)? NULL : PCMK_XA_ID;
return pcmk__xe_first_child(haystack, (const char *) needle->name, attr,
id);
}
}
void
xml_accept_changes(xmlNode * xml)
{
xmlNode *top = NULL;
xml_doc_private_t *docpriv = NULL;
if(xml == NULL) {
return;
}
crm_trace("Accepting changes to %p", xml);
docpriv = xml->doc->_private;
top = xmlDocGetRootElement(xml->doc);
reset_xml_private_data(xml->doc->_private);
if (!pcmk_is_set(docpriv->flags, pcmk__xf_dirty)) {
docpriv->flags = pcmk__xf_none;
return;
}
docpriv->flags = pcmk__xf_none;
pcmk__xml_tree_foreach(top, accept_attr_deletions, NULL);
}
/*!
* \internal
* \brief Create a new XML document
*
* \return Newly allocated XML document (guaranteed not to be \c NULL)
*
* \note The caller is responsible for freeing the return value using
* \c pcmk__xml_free_doc().
*/
xmlDoc *
pcmk__xml_new_doc(void)
{
xmlDoc *doc = xmlNewDoc(XML_VERSION);
pcmk__mem_assert(doc);
pcmk__xml_new_private_data((xmlNode *) doc);
return doc;
}
/*!
* \internal
* \brief Free a new XML document
*
* \param[in,out] doc XML document to free
*/
void
pcmk__xml_free_doc(xmlDoc *doc)
{
if (doc != NULL) {
pcmk__xml_free_private_data((xmlNode *) doc);
xmlFreeDoc(doc);
}
}
/*!
* \internal
* \brief Check whether the first character of a string is an XML NameStartChar
*
* See https://www.w3.org/TR/xml/#NT-NameStartChar.
*
* This is almost identical to libxml2's \c xmlIsDocNameStartChar(), but they
* don't expose it as part of the public API.
*
* \param[in] utf8 UTF-8 encoded string
* \param[out] len If not \c NULL, where to store size in bytes of first
* character in \p utf8
*
* \return \c true if \p utf8 begins with a valid XML NameStartChar, or \c false
* otherwise
*/
bool
pcmk__xml_is_name_start_char(const char *utf8, int *len)
{
int c = 0;
int local_len = 0;
if (len == NULL) {
len = &local_len;
}
/* xmlGetUTF8Char() abuses the len argument. At call time, it must be set to
* "the minimum number of bytes present in the sequence... to assure the
* next character is completely contained within the sequence." It's similar
* to the "n" in the strn*() functions. However, this doesn't make any sense
* for null-terminated strings, and there's no value that indicates "keep
* going until '\0'." So we set it to 4, the max number of bytes in a UTF-8
* character.
*
* At return, it's set to the actual number of bytes in the char, or 0 on
* error.
*/
*len = 4;
// Note: xmlGetUTF8Char() assumes a 32-bit int
c = xmlGetUTF8Char((const xmlChar *) utf8, len);
if (c < 0) {
GString *buf = g_string_sized_new(32);
for (int i = 0; (i < 4) && (utf8[i] != '\0'); i++) {
g_string_append_printf(buf, " 0x%.2X", utf8[i]);
}
crm_info("Invalid UTF-8 character (bytes:%s)",
(pcmk__str_empty(buf->str)? " <none>" : buf->str));
g_string_free(buf, TRUE);
return false;
}
return (c == '_')
|| (c == ':')
|| ((c >= 'a') && (c <= 'z'))
|| ((c >= 'A') && (c <= 'Z'))
|| ((c >= 0xC0) && (c <= 0xD6))
|| ((c >= 0xD8) && (c <= 0xF6))
|| ((c >= 0xF8) && (c <= 0x2FF))
|| ((c >= 0x370) && (c <= 0x37D))
|| ((c >= 0x37F) && (c <= 0x1FFF))
|| ((c >= 0x200C) && (c <= 0x200D))
|| ((c >= 0x2070) && (c <= 0x218F))
|| ((c >= 0x2C00) && (c <= 0x2FEF))
|| ((c >= 0x3001) && (c <= 0xD7FF))
|| ((c >= 0xF900) && (c <= 0xFDCF))
|| ((c >= 0xFDF0) && (c <= 0xFFFD))
|| ((c >= 0x10000) && (c <= 0xEFFFF));
}
/*!
* \internal
* \brief Check whether the first character of a string is an XML NameChar
*
* See https://www.w3.org/TR/xml/#NT-NameChar.
*
* This is almost identical to libxml2's \c xmlIsDocNameChar(), but they don't
* expose it as part of the public API.
*
* \param[in] utf8 UTF-8 encoded string
* \param[out] len If not \c NULL, where to store size in bytes of first
* character in \p utf8
*
* \return \c true if \p utf8 begins with a valid XML NameChar, or \c false
* otherwise
*/
bool
pcmk__xml_is_name_char(const char *utf8, int *len)
{
int c = 0;
int local_len = 0;
if (len == NULL) {
len = &local_len;
}
// See comment regarding len in pcmk__xml_is_name_start_char()
*len = 4;
// Note: xmlGetUTF8Char() assumes a 32-bit int
c = xmlGetUTF8Char((const xmlChar *) utf8, len);
if (c < 0) {
GString *buf = g_string_sized_new(32);
for (int i = 0; (i < 4) && (utf8[i] != '\0'); i++) {
g_string_append_printf(buf, " 0x%.2X", utf8[i]);
}
crm_info("Invalid UTF-8 character (bytes:%s)",
(pcmk__str_empty(buf->str)? " <none>" : buf->str));
g_string_free(buf, TRUE);
return false;
}
return ((c >= 'a') && (c <= 'z'))
|| ((c >= 'A') && (c <= 'Z'))
|| ((c >= '0') && (c <= '9'))
|| (c == '_')
|| (c == ':')
|| (c == '-')
|| (c == '.')
|| (c == 0xB7)
|| ((c >= 0xC0) && (c <= 0xD6))
|| ((c >= 0xD8) && (c <= 0xF6))
|| ((c >= 0xF8) && (c <= 0x2FF))
|| ((c >= 0x300) && (c <= 0x36F))
|| ((c >= 0x370) && (c <= 0x37D))
|| ((c >= 0x37F) && (c <= 0x1FFF))
|| ((c >= 0x200C) && (c <= 0x200D))
|| ((c >= 0x203F) && (c <= 0x2040))
|| ((c >= 0x2070) && (c <= 0x218F))
|| ((c >= 0x2C00) && (c <= 0x2FEF))
|| ((c >= 0x3001) && (c <= 0xD7FF))
|| ((c >= 0xF900) && (c <= 0xFDCF))
|| ((c >= 0xFDF0) && (c <= 0xFFFD))
|| ((c >= 0x10000) && (c <= 0xEFFFF));
}
/*!
* \internal
* \brief Sanitize a string so it is usable as an XML ID
*
* An ID must match the Name production as defined here:
* https://www.w3.org/TR/xml/#NT-Name.
*
* Convert an invalid start character to \c '_'. Convert an invalid character
* after the start character to \c '.'.
*
* \param[in,out] id String to sanitize
*/
void
pcmk__xml_sanitize_id(char *id)
{
bool valid = true;
int len = 0;
// If id is empty or NULL, there's no way to make it a valid XML ID
pcmk__assert(!pcmk__str_empty(id));
/* @TODO Suppose there are two strings and each has an invalid ID character
* in the same position. The strings are otherwise identical. Both strings
* will be sanitized to the same valid ID, which is incorrect.
*
* The caller is responsible for ensuring the sanitized ID does not already
* exist in a given XML document before using it, if uniqueness is desired.
*/
valid = pcmk__xml_is_name_start_char(id, &len);
CRM_CHECK(len > 0, return); // UTF-8 encoding error
if (!valid) {
*id = '_';
for (int i = 1; i < len; i++) {
id[i] = '.';
}
}
for (id += len; *id != '\0'; id += len) {
valid = pcmk__xml_is_name_char(id, &len);
CRM_CHECK(len > 0, return); // UTF-8 encoding error
if (!valid) {
for (int i = 0; i < len; i++) {
id[i] = '.';
}
}
}
}
/*!
* \internal
* \brief Free an XML tree without ACL checks or change tracking
*
* \param[in,out] xml XML node to free
*/
void
pcmk__xml_free_node(xmlNode *xml)
{
pcmk__xml_free_private_data(xml);
xmlUnlinkNode(xml);
xmlFreeNode(xml);
}
/*!
* \internal
* \brief Free an XML tree if ACLs allow; track deletion if tracking is enabled
*
* If \p node is the root of its document, free the entire document.
*
* \param[in,out] node XML node to free
* \param[in] position Position of \p node among its siblings for change
* tracking (negative to calculate automatically if
* needed)
*/
static void
free_xml_with_position(xmlNode *node, int position)
{
xmlDoc *doc = NULL;
xml_node_private_t *nodepriv = NULL;
if (node == NULL) {
return;
}
doc = node->doc;
nodepriv = node->_private;
if ((doc != NULL) && (xmlDocGetRootElement(doc) == node)) {
/* @TODO Should we check ACLs first? Otherwise it seems like we could
* free the root element without write permission.
*/
pcmk__xml_free_doc(doc);
return;
}
if (!pcmk__check_acl(node, NULL, pcmk__xf_acl_write)) {
GString *xpath = NULL;
pcmk__if_tracing({}, return);
xpath = pcmk__element_xpath(node);
qb_log_from_external_source(__func__, __FILE__,
"Cannot remove %s %x", LOG_TRACE,
__LINE__, 0, xpath->str, nodepriv->flags);
g_string_free(xpath, TRUE);
return;
}
if (pcmk__xml_doc_all_flags_set(node->doc, pcmk__xf_tracking)
&& !pcmk_is_set(nodepriv->flags, pcmk__xf_created)) {
xml_doc_private_t *docpriv = doc->_private;
GString *xpath = pcmk__element_xpath(node);
if (xpath != NULL) {
pcmk__deleted_xml_t *deleted_obj = NULL;
crm_trace("Deleting %s %p from %p", xpath->str, node, doc);
deleted_obj = pcmk__assert_alloc(1, sizeof(pcmk__deleted_xml_t));
deleted_obj->path = g_string_free(xpath, FALSE);
deleted_obj->position = -1;
// Record the position only for XML comments for now
if (node->type == XML_COMMENT_NODE) {
if (position >= 0) {
deleted_obj->position = position;
} else {
deleted_obj->position = pcmk__xml_position(node,
pcmk__xf_skip);
}
}
docpriv->deleted_objs = g_list_append(docpriv->deleted_objs,
deleted_obj);
pcmk__xml_doc_set_flags(node->doc, pcmk__xf_dirty);
}
}
pcmk__xml_free_node(node);
}
/*!
* \internal
* \brief Free an XML tree if ACLs allow; track deletion if tracking is enabled
*
* If \p xml is the root of its document, free the entire document.
*
* \param[in,out] xml XML node to free
*/
void
pcmk__xml_free(xmlNode *xml)
{
free_xml_with_position(xml, -1);
}
/*!
* \internal
* \brief Make a deep copy of an XML node under a given parent
*
* \param[in,out] parent XML element that will be the copy's parent (\c NULL
* to create a new XML document with the copy as root)
* \param[in] src XML node to copy
*
* \return Deep copy of \p src, or \c NULL if \p src is \c NULL
*/
xmlNode *
pcmk__xml_copy(xmlNode *parent, xmlNode *src)
{
xmlNode *copy = NULL;
if (src == NULL) {
return NULL;
}
if (parent == NULL) {
xmlDoc *doc = NULL;
// The copy will be the root element of a new document
pcmk__assert(src->type == XML_ELEMENT_NODE);
doc = pcmk__xml_new_doc();
copy = xmlDocCopyNode(src, doc, 1);
pcmk__mem_assert(copy);
xmlDocSetRootElement(doc, copy);
} else {
copy = xmlDocCopyNode(src, parent->doc, 1);
pcmk__mem_assert(copy);
xmlAddChild(parent, copy);
}
pcmk__xml_new_private_data(copy);
return copy;
}
/*!
* \internal
* \brief Remove XML text nodes from specified XML and all its children
*
* \param[in,out] xml XML to strip text from
*/
void
pcmk__strip_xml_text(xmlNode *xml)
{
xmlNode *iter = xml->children;
while (iter) {
xmlNode *next = iter->next;
switch (iter->type) {
case XML_TEXT_NODE:
pcmk__xml_free_node(iter);
break;
case XML_ELEMENT_NODE:
/* Search it */
pcmk__strip_xml_text(iter);
break;
default:
/* Leave it */
break;
}
iter = next;
}
}
/*!
* \internal
* \brief Check whether a string has XML special characters that must be escaped
*
* See \c pcmk__xml_escape() and \c pcmk__xml_escape_type for more details.
*
* \param[in] text String to check
* \param[in] type Type of escaping
*
* \return \c true if \p text has special characters that need to be escaped, or
* \c false otherwise
*/
bool
pcmk__xml_needs_escape(const char *text, enum pcmk__xml_escape_type type)
{
if (text == NULL) {
return false;
}
while (*text != '\0') {
switch (type) {
case pcmk__xml_escape_text:
switch (*text) {
case '<':
case '>':
case '&':
return true;
case '\n':
case '\t':
break;
default:
if (g_ascii_iscntrl(*text)) {
return true;
}
break;
}
break;
case pcmk__xml_escape_attr:
switch (*text) {
case '<':
case '>':
case '&':
case '"':
return true;
default:
if (g_ascii_iscntrl(*text)) {
return true;
}
break;
}
break;
case pcmk__xml_escape_attr_pretty:
switch (*text) {
case '\n':
case '\r':
case '\t':
case '"':
return true;
default:
break;
}
break;
default: // Invalid enum value
pcmk__assert(false);
break;
}
text = g_utf8_next_char(text);
}
return false;
}
/*!
* \internal
* \brief Replace special characters with their XML escape sequences
*
* \param[in] text Text to escape
* \param[in] type Type of escaping
*
* \return Newly allocated string equivalent to \p text but with special
* characters replaced with XML escape sequences (or \c NULL if \p text
* is \c NULL). If \p text is not \c NULL, the return value is
* guaranteed not to be \c NULL.
*
* \note There are libxml functions that purport to do this:
* \c xmlEncodeEntitiesReentrant() and \c xmlEncodeSpecialChars().
* However, their escaping is incomplete. See:
* https://discourse.gnome.org/t/intended-use-of-xmlencodeentitiesreentrant-vs-xmlencodespecialchars/19252
* \note The caller is responsible for freeing the return value using
* \c g_free().
*/
gchar *
pcmk__xml_escape(const char *text, enum pcmk__xml_escape_type type)
{
GString *copy = NULL;
if (text == NULL) {
return NULL;
}
copy = g_string_sized_new(strlen(text));
while (*text != '\0') {
// Don't escape any non-ASCII characters
if ((*text & 0x80) != 0) {
size_t bytes = g_utf8_next_char(text) - text;
g_string_append_len(copy, text, bytes);
text += bytes;
continue;
}
switch (type) {
case pcmk__xml_escape_text:
switch (*text) {
case '<':
g_string_append(copy, PCMK__XML_ENTITY_LT);
break;
case '>':
g_string_append(copy, PCMK__XML_ENTITY_GT);
break;
case '&':
g_string_append(copy, PCMK__XML_ENTITY_AMP);
break;
case '\n':
case '\t':
g_string_append_c(copy, *text);
break;
default:
if (g_ascii_iscntrl(*text)) {
g_string_append_printf(copy, "&#x%.2X;", *text);
} else {
g_string_append_c(copy, *text);
}
break;
}
break;
case pcmk__xml_escape_attr:
switch (*text) {
case '<':
g_string_append(copy, PCMK__XML_ENTITY_LT);
break;
case '>':
g_string_append(copy, PCMK__XML_ENTITY_GT);
break;
case '&':
g_string_append(copy, PCMK__XML_ENTITY_AMP);
break;
case '"':
g_string_append(copy, PCMK__XML_ENTITY_QUOT);
break;
default:
if (g_ascii_iscntrl(*text)) {
g_string_append_printf(copy, "&#x%.2X;", *text);
} else {
g_string_append_c(copy, *text);
}
break;
}
break;
case pcmk__xml_escape_attr_pretty:
switch (*text) {
case '"':
g_string_append(copy, "\\\"");
break;
case '\n':
g_string_append(copy, "\\n");
break;
case '\r':
g_string_append(copy, "\\r");
break;
case '\t':
g_string_append(copy, "\\t");
break;
default:
g_string_append_c(copy, *text);
break;
}
break;
default: // Invalid enum value
pcmk__assert(false);
break;
}
text = g_utf8_next_char(text);
}
return g_string_free(copy, FALSE);
}
/*!
* \internal
* \brief Set a flag on all attributes of an XML element
*
* \param[in,out] xml XML node to set flags on
* \param[in] flag XML private flag to set
*/
static void
set_attrs_flag(xmlNode *xml, enum xml_private_flags flag)
{
for (xmlAttr *attr = pcmk__xe_first_attr(xml); attr; attr = attr->next) {
pcmk__set_xml_flags((xml_node_private_t *) (attr->_private), flag);
}
}
/*!
* \internal
* \brief Add an XML attribute to a node, marked as deleted
*
* When calculating XML changes, we need to know when an attribute has been
* deleted. Add the attribute back to the new XML, so that we can check the
* removal against ACLs, and mark it as deleted for later removal after
* differences have been calculated.
*
* \param[in,out] new_xml XML to modify
* \param[in] element Name of XML element that changed (for logging)
* \param[in] attr_name Name of attribute that was deleted
* \param[in] old_value Value of attribute that was deleted
*/
static void
mark_attr_deleted(xmlNode *new_xml, const char *element, const char *attr_name,
const char *old_value)
{
xml_doc_private_t *docpriv = new_xml->doc->_private;
xmlAttr *attr = NULL;
xml_node_private_t *nodepriv;
/* Restore the old value (without setting dirty flag recursively upwards or
* checking ACLs)
*/
pcmk__clear_xml_flags(docpriv, pcmk__xf_tracking);
crm_xml_add(new_xml, attr_name, old_value);
pcmk__set_xml_flags(docpriv, pcmk__xf_tracking);
// Reset flags (so the attribute doesn't appear as newly created)
attr = xmlHasProp(new_xml, (const xmlChar *) attr_name);
nodepriv = attr->_private;
nodepriv->flags = 0;
// Check ACLs and mark restored value for later removal
pcmk__xa_remove(attr, false);
crm_trace("XML attribute %s=%s was removed from %s",
attr_name, old_value, element);
}
/*
* \internal
* \brief Check ACLs for a changed XML attribute
*/
static void
mark_attr_changed(xmlNode *new_xml, const char *element, const char *attr_name,
const char *old_value)
{
xml_doc_private_t *docpriv = new_xml->doc->_private;
char *vcopy = crm_element_value_copy(new_xml, attr_name);
crm_trace("XML attribute %s was changed from '%s' to '%s' in %s",
attr_name, old_value, vcopy, element);
// Restore the original value (without checking ACLs)
pcmk__clear_xml_flags(docpriv, pcmk__xf_tracking);
crm_xml_add(new_xml, attr_name, old_value);
pcmk__set_xml_flags(docpriv, pcmk__xf_tracking);
// Change it back to the new value, to check ACLs
crm_xml_add(new_xml, attr_name, vcopy);
free(vcopy);
}
/*!
* \internal
* \brief Mark an XML attribute as having changed position
*
* \param[in,out] new_xml XML to modify
* \param[in] element Name of XML element that changed (for logging)
* \param[in,out] old_attr Attribute that moved, in original XML
* \param[in,out] new_attr Attribute that moved, in \p new_xml
* \param[in] p_old Ordinal position of \p old_attr in original XML
* \param[in] p_new Ordinal position of \p new_attr in \p new_xml
*/
static void
mark_attr_moved(xmlNode *new_xml, const char *element, xmlAttr *old_attr,
xmlAttr *new_attr, int p_old, int p_new)
{
xml_node_private_t *nodepriv = new_attr->_private;
crm_trace("XML attribute %s moved from position %d to %d in %s",
old_attr->name, p_old, p_new, element);
// Mark document, element, and all element's parents as changed
pcmk__mark_xml_node_dirty(new_xml);
// Mark attribute as changed
pcmk__set_xml_flags(nodepriv, pcmk__xf_dirty|pcmk__xf_moved);
nodepriv = (p_old > p_new)? old_attr->_private : new_attr->_private;
pcmk__set_xml_flags(nodepriv, pcmk__xf_skip);
}
/*!
* \internal
* \brief Calculate differences in all previously existing XML attributes
*
* \param[in,out] old_xml Original XML to compare
* \param[in,out] new_xml New XML to compare
*/
static void
xml_diff_old_attrs(xmlNode *old_xml, xmlNode *new_xml)
{
xmlAttr *attr_iter = pcmk__xe_first_attr(old_xml);
while (attr_iter != NULL) {
const char *name = (const char *) attr_iter->name;
xmlAttr *old_attr = attr_iter;
xmlAttr *new_attr = xmlHasProp(new_xml, attr_iter->name);
const char *old_value = pcmk__xml_attr_value(attr_iter);
attr_iter = attr_iter->next;
if (new_attr == NULL) {
mark_attr_deleted(new_xml, (const char *) old_xml->name, name,
old_value);
} else {
xml_node_private_t *nodepriv = new_attr->_private;
int new_pos = pcmk__xml_position((xmlNode*) new_attr,
pcmk__xf_skip);
int old_pos = pcmk__xml_position((xmlNode*) old_attr,
pcmk__xf_skip);
const char *new_value = crm_element_value(new_xml, name);
// This attribute isn't new
pcmk__clear_xml_flags(nodepriv, pcmk__xf_created);
if (strcmp(new_value, old_value) != 0) {
mark_attr_changed(new_xml, (const char *) old_xml->name, name,
old_value);
} else if ((old_pos != new_pos)
&& !pcmk__xml_doc_all_flags_set(new_xml->doc,
pcmk__xf_lazy
|pcmk__xf_tracking)) {
/* pcmk__xf_tracking is always set by xml_calculate_changes()
* before this function is called, so only the pcmk__xf_lazy
* check is truly relevant.
*/
mark_attr_moved(new_xml, (const char *) old_xml->name,
old_attr, new_attr, old_pos, new_pos);
}
}
}
}
/*!
* \internal
* \brief Check all attributes in new XML for creation
*
* For each of a given XML element's attributes marked as newly created, accept
* (and mark as dirty) or reject the creation according to ACLs.
*
* \param[in,out] new_xml XML to check
*/
static void
mark_created_attrs(xmlNode *new_xml)
{
xmlAttr *attr_iter = pcmk__xe_first_attr(new_xml);
while (attr_iter != NULL) {
xmlAttr *new_attr = attr_iter;
xml_node_private_t *nodepriv = attr_iter->_private;
attr_iter = attr_iter->next;
if (pcmk_is_set(nodepriv->flags, pcmk__xf_created)) {
const char *attr_name = (const char *) new_attr->name;
crm_trace("Created new attribute %s=%s in %s",
attr_name, pcmk__xml_attr_value(new_attr),
new_xml->name);
/* Check ACLs (we can't use the remove-then-create trick because it
* would modify the attribute position).
*/
if (pcmk__check_acl(new_xml, attr_name, pcmk__xf_acl_write)) {
pcmk__mark_xml_attr_dirty(new_attr);
} else {
// Creation was not allowed, so remove the attribute
pcmk__xa_remove(new_attr, true);
}
}
}
}
/*!
* \internal
* \brief Calculate differences in attributes between two XML nodes
*
* \param[in,out] old_xml Original XML to compare
* \param[in,out] new_xml New XML to compare
*/
static void
xml_diff_attrs(xmlNode *old_xml, xmlNode *new_xml)
{
set_attrs_flag(new_xml, pcmk__xf_created); // cleared later if not really new
xml_diff_old_attrs(old_xml, new_xml);
mark_created_attrs(new_xml);
}
/*!
* \internal
* \brief Add an XML child element to a node, marked as deleted
*
* When calculating XML changes, we need to know when a child element has been
* deleted. Add the child back to the new XML, so that we can check the removal
* against ACLs, and mark it as deleted for later removal after differences have
* been calculated.
*
* \param[in,out] old_child Child element from original XML
* \param[in,out] new_parent New XML to add marked copy to
*/
static void
mark_child_deleted(xmlNode *old_child, xmlNode *new_parent)
{
// Re-create the child element so we can check ACLs
xmlNode *candidate = pcmk__xml_copy(new_parent, old_child);
// Clear flags on new child and its children
pcmk__xml_tree_foreach(candidate, pcmk__xml_reset_node_flags, NULL);
// Check whether ACLs allow the deletion
pcmk__apply_acl(xmlDocGetRootElement(candidate->doc));
// Remove the child again (which will track it in document's deleted_objs)
free_xml_with_position(candidate,
pcmk__xml_position(old_child, pcmk__xf_skip));
if (pcmk__xml_match(new_parent, old_child, true) == NULL) {
pcmk__set_xml_flags((xml_node_private_t *) (old_child->_private),
pcmk__xf_skip);
}
}
static void
mark_child_moved(xmlNode *old_child, xmlNode *new_parent, xmlNode *new_child,
int p_old, int p_new)
{
xml_node_private_t *nodepriv = new_child->_private;
crm_trace("Child element %s with "
PCMK_XA_ID "='%s' moved from position %d to %d under %s",
new_child->name, pcmk__s(pcmk__xe_id(new_child), "<no id>"),
p_old, p_new, new_parent->name);
pcmk__mark_xml_node_dirty(new_parent);
pcmk__set_xml_flags(nodepriv, pcmk__xf_moved);
if (p_old > p_new) {
nodepriv = old_child->_private;
} else {
nodepriv = new_child->_private;
}
pcmk__set_xml_flags(nodepriv, pcmk__xf_skip);
}
// Given original and new XML, mark new XML portions that have changed
static void
mark_xml_changes(xmlNode *old_xml, xmlNode *new_xml, bool check_top)
{
xmlNode *old_child = NULL;
xmlNode *new_child = NULL;
xml_node_private_t *nodepriv = NULL;
CRM_CHECK(new_xml != NULL, return);
if (old_xml == NULL) {
mark_xml_tree_dirty_created(new_xml);
pcmk__apply_creation_acl(new_xml, check_top);
return;
}
nodepriv = new_xml->_private;
CRM_CHECK(nodepriv != NULL, return);
if(nodepriv->flags & pcmk__xf_processed) {
/* Avoid re-comparing nodes */
return;
}
pcmk__set_xml_flags(nodepriv, pcmk__xf_processed);
xml_diff_attrs(old_xml, new_xml);
// Check for differences in the original children
for (old_child = pcmk__xml_first_child(old_xml); old_child != NULL;
old_child = pcmk__xml_next(old_child)) {
new_child = pcmk__xml_match(new_xml, old_child, true);
if (new_child != NULL) {
mark_xml_changes(old_child, new_child, true);
} else {
mark_child_deleted(old_child, new_xml);
}
}
// Check for moved or created children
new_child = pcmk__xml_first_child(new_xml);
while (new_child != NULL) {
xmlNode *next = pcmk__xml_next(new_child);
old_child = pcmk__xml_match(old_xml, new_child, true);
if (old_child == NULL) {
// This is a newly created child
nodepriv = new_child->_private;
pcmk__set_xml_flags(nodepriv, pcmk__xf_skip);
// May free new_child
mark_xml_changes(old_child, new_child, true);
} else {
/* Check for movement, we already checked for differences */
int p_new = pcmk__xml_position(new_child, pcmk__xf_skip);
int p_old = pcmk__xml_position(old_child, pcmk__xf_skip);
if(p_old != p_new) {
mark_child_moved(old_child, new_xml, new_child, p_old, p_new);
}
}
new_child = next;
}
}
void
xml_calculate_significant_changes(xmlNode *old_xml, xmlNode *new_xml)
{
if (new_xml != NULL) {
pcmk__xml_doc_set_flags(new_xml->doc, pcmk__xf_lazy);
}
xml_calculate_changes(old_xml, new_xml);
}
// Called functions may set the \p pcmk__xf_skip flag on parts of \p old_xml
void
xml_calculate_changes(xmlNode *old_xml, xmlNode *new_xml)
{
CRM_CHECK((old_xml != NULL) && (new_xml != NULL)
&& pcmk__xe_is(old_xml, (const char *) new_xml->name)
&& pcmk__str_eq(pcmk__xe_id(old_xml), pcmk__xe_id(new_xml),
pcmk__str_none),
return);
if (!pcmk__xml_doc_all_flags_set(new_xml->doc, pcmk__xf_tracking)) {
xml_track_changes(new_xml, NULL, NULL, FALSE);
}
mark_xml_changes(old_xml, new_xml, FALSE);
}
/*!
* \internal
* \brief Initialize the Pacemaker XML environment
*
* Set an XML buffer allocation scheme, set XML node create and destroy
* callbacks, and load schemas into the cache.
*/
void
pcmk__xml_init(void)
{
// @TODO Try to find a better caller than crm_log_preinit()
static bool initialized = false;
if (!initialized) {
initialized = true;
/* Double the buffer size when the buffer needs to grow. The default
* allocator XML_BUFFER_ALLOC_EXACT was found to cause poor performance
* due to the number of reallocs.
*/
xmlSetBufferAllocationScheme(XML_BUFFER_ALLOC_DOUBLEIT);
// Load schemas into the cache
pcmk__schema_init();
}
}
/*!
* \internal
* \brief Tear down the Pacemaker XML environment
*
* Destroy schema cache and clean up memory allocated by libxml2.
*/
void
pcmk__xml_cleanup(void)
{
pcmk__schema_cleanup();
xmlCleanupParser();
}
char *
pcmk__xml_artefact_root(enum pcmk__xml_artefact_ns ns)
{
static const char *base = NULL;
char *ret = NULL;
if (base == NULL) {
base = pcmk__env_option(PCMK__ENV_SCHEMA_DIRECTORY);
}
if (pcmk__str_empty(base)) {
base = PCMK_SCHEMA_DIR;
}
switch (ns) {
case pcmk__xml_artefact_ns_legacy_rng:
case pcmk__xml_artefact_ns_legacy_xslt:
ret = strdup(base);
break;
case pcmk__xml_artefact_ns_base_rng:
case pcmk__xml_artefact_ns_base_xslt:
ret = crm_strdup_printf("%s/base", base);
break;
default:
crm_err("XML artefact family specified as %u not recognized", ns);
}
return ret;
}
static char *
find_artefact(enum pcmk__xml_artefact_ns ns, const char *path, const char *filespec)
{
char *ret = NULL;
switch (ns) {
case pcmk__xml_artefact_ns_legacy_rng:
case pcmk__xml_artefact_ns_base_rng:
if (pcmk__ends_with(filespec, ".rng")) {
ret = crm_strdup_printf("%s/%s", path, filespec);
} else {
ret = crm_strdup_printf("%s/%s.rng", path, filespec);
}
break;
case pcmk__xml_artefact_ns_legacy_xslt:
case pcmk__xml_artefact_ns_base_xslt:
if (pcmk__ends_with(filespec, ".xsl")) {
ret = crm_strdup_printf("%s/%s", path, filespec);
} else {
ret = crm_strdup_printf("%s/%s.xsl", path, filespec);
}
break;
default:
crm_err("XML artefact family specified as %u not recognized", ns);
}
return ret;
}
char *
pcmk__xml_artefact_path(enum pcmk__xml_artefact_ns ns, const char *filespec)
{
struct stat sb;
char *base = pcmk__xml_artefact_root(ns);
char *ret = NULL;
ret = find_artefact(ns, base, filespec);
free(base);
if (stat(ret, &sb) != 0 || !S_ISREG(sb.st_mode)) {
const char *remote_schema_dir = pcmk__remote_schema_dir();
free(ret);
ret = find_artefact(ns, remote_schema_dir, filespec);
}
return ret;
}
// Deprecated functions kept only for backward API compatibility
// LCOV_EXCL_START
#include <crm/common/xml_compat.h>
xmlNode *
copy_xml(xmlNode *src)
{
xmlDoc *doc = pcmk__xml_new_doc();
xmlNode *copy = NULL;
copy = xmlDocCopyNode(src, doc, 1);
pcmk__mem_assert(copy);
xmlDocSetRootElement(doc, copy);
pcmk__xml_new_private_data(copy);
return copy;
}
void
crm_xml_init(void)
{
pcmk__xml_init();
}
void
crm_xml_cleanup(void)
{
pcmk__xml_cleanup();
}
void
pcmk_free_xml_subtree(xmlNode *xml)
{
pcmk__xml_free_node(xml);
}
void
free_xml(xmlNode *child)
{
pcmk__xml_free(child);
}
void
crm_xml_sanitize_id(char *id)
{
char *c;
for (c = id; *c; ++c) {
switch (*c) {
case ':':
case '#':
*c = '.';
}
}
}
bool
xml_tracking_changes(xmlNode *xml)
{
return (xml != NULL)
&& pcmk__xml_doc_all_flags_set(xml->doc, pcmk__xf_tracking);
}
bool
xml_document_dirty(xmlNode *xml)
{
return (xml != NULL)
&& pcmk__xml_doc_all_flags_set(xml->doc, pcmk__xf_dirty);
}
// LCOV_EXCL_STOP
// End deprecated API
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Thu, Jul 10, 3:12 AM (12 h, 12 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2009873
Default Alt Text
(95 KB)
Attached To
Mode
rP Pacemaker
Attached
Detach File
Event Timeline
Log In to Comment