Page Menu
Home
ClusterLabs Projects
Search
Configure Global Search
Log In
Files
F4639452
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Size
111 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/include/crm/common/xml_internal.h b/include/crm/common/xml_internal.h
index 923828a277..2c7b3d482a 100644
--- a/include/crm/common/xml_internal.h
+++ b/include/crm/common/xml_internal.h
@@ -1,402 +1,402 @@
/*
* Copyright 2017-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__CRM_COMMON_XML_INTERNAL__H
#define PCMK__CRM_COMMON_XML_INTERNAL__H
/*
* Internal-only wrappers for and extensions to libxml2 (libxslt)
*/
#include <stdlib.h>
#include <stdint.h> // uint32_t
#include <stdio.h>
#include <crm/crm.h> /* transitively imports qblog.h */
#include <crm/common/output_internal.h>
#include <crm/common/xml_names.h> // PCMK_XA_ID, PCMK_XE_CLONE
// This file is a wrapper for other {xml_*,xpath}_internal.h headers
#include <crm/common/xml_comment_internal.h>
#include <crm/common/xml_element_internal.h>
#include <crm/common/xml_idref_internal.h>
#include <crm/common/xml_io_internal.h>
#include <crm/common/xml_names_internal.h>
#include <crm/common/xpath_internal.h>
#include <libxml/relaxng.h>
#ifdef __cplusplus
extern "C" {
#endif
/*!
* \brief Base for directing lib{xml2,xslt} log into standard libqb backend
*
* This macro implements the core of what can be needed for directing
* libxml2 or libxslt error messaging into standard, preconfigured
* libqb-backed log stream.
*
* It's a bit unfortunate that libxml2 (and more sparsely, also libxslt)
* emits a single message by chunks (location is emitted separatedly from
* the message itself), so we have to take the effort to combine these
* chunks back to single message. Whether to do this or not is driven
* with \p dechunk toggle.
*
* The form of a macro was chosen for implicit deriving of __FILE__, etc.
* and also because static dechunking buffer should be differentiated per
* library (here we assume different functions referring to this macro
* will not ever be using both at once), preferably also per-library
* context of use to avoid clashes altogether.
*
* Note that we cannot use qb_logt, because callsite data have to be known
* at the moment of compilation, which it is not always the case -- xml_log
* (and unfortunately there's no clear explanation of the fail to compile).
*
* Also note that there's no explicit guard against said libraries producing
* never-newline-terminated chunks (which would just keep consuming memory),
* as it's quite improbable. Termination of the program in between the
* same-message chunks will raise a flag with valgrind and the likes, though.
*
* And lastly, regarding how dechunking combines with other non-message
* parameters -- for \p priority, most important running specification
* wins (possibly elevated to LOG_ERR in case of nonconformance with the
* newline-termination "protocol"), \p dechunk is expected to always be
* on once it was at the start, and the rest (\p postemit and \p prefix)
* are picked directly from the last chunk entry finalizing the message
* (also reasonable to always have it the same with all related entries).
*
* \param[in] priority Syslog priority for the message to be logged
* \param[in] dechunk Whether to dechunk new-line terminated message
* \param[in] postemit Code to be executed once message is sent out
* \param[in] prefix How to prefix the message or NULL for raw passing
* \param[in] fmt Format string as with printf-like functions
* \param[in] ap Variable argument list to supplement \p fmt format string
*/
#define PCMK__XML_LOG_BASE(priority, dechunk, postemit, prefix, fmt, ap) \
do { \
if (!(dechunk) && (prefix) == NULL) { /* quick pass */ \
qb_log_from_external_source_va(__func__, __FILE__, (fmt), \
(priority), __LINE__, 0, (ap)); \
(void) (postemit); \
} else { \
int CXLB_len = 0; \
char *CXLB_buf = NULL; \
static int CXLB_buffer_len = 0; \
static char *CXLB_buffer = NULL; \
static uint8_t CXLB_priority = 0; \
\
CXLB_len = vasprintf(&CXLB_buf, (fmt), (ap)); \
\
if (CXLB_len <= 0 || CXLB_buf[CXLB_len - 1] == '\n' || !(dechunk)) { \
if (CXLB_len < 0) { \
CXLB_buf = (char *) "LOG CORRUPTION HAZARD"; /*we don't modify*/\
CXLB_priority = QB_MIN(CXLB_priority, LOG_ERR); \
} else if (CXLB_len > 0 /* && (dechunk) */ \
&& CXLB_buf[CXLB_len - 1] == '\n') { \
CXLB_buf[CXLB_len - 1] = '\0'; \
} \
if (CXLB_buffer) { \
qb_log_from_external_source(__func__, __FILE__, "%s%s%s", \
CXLB_priority, __LINE__, 0, \
(prefix) != NULL ? (prefix) : "", \
CXLB_buffer, CXLB_buf); \
free(CXLB_buffer); \
} else { \
qb_log_from_external_source(__func__, __FILE__, "%s%s", \
(priority), __LINE__, 0, \
(prefix) != NULL ? (prefix) : "", \
CXLB_buf); \
} \
if (CXLB_len < 0) { \
CXLB_buf = NULL; /* restore temporary override */ \
} \
CXLB_buffer = NULL; \
CXLB_buffer_len = 0; \
(void) (postemit); \
\
} else if (CXLB_buffer == NULL) { \
CXLB_buffer_len = CXLB_len; \
CXLB_buffer = CXLB_buf; \
CXLB_buf = NULL; \
CXLB_priority = (priority); /* remember as a running severest */ \
\
} else { \
CXLB_buffer = realloc(CXLB_buffer, 1 + CXLB_buffer_len + CXLB_len); \
memcpy(CXLB_buffer + CXLB_buffer_len, CXLB_buf, CXLB_len); \
CXLB_buffer_len += CXLB_len; \
CXLB_buffer[CXLB_buffer_len] = '\0'; \
CXLB_priority = QB_MIN(CXLB_priority, (priority)); /* severest? */ \
} \
free(CXLB_buf); \
} \
} while (0)
/*!
* \internal
* \brief Bit flags to control format in XML logs and dumps
*/
enum pcmk__xml_fmt_options {
//! Exclude certain XML attributes (for calculating digests)
pcmk__xml_fmt_filtered = (1 << 0),
//! Include indentation and newlines
pcmk__xml_fmt_pretty = (1 << 1),
//! Include the opening tag of an XML element, and include XML comments
pcmk__xml_fmt_open = (1 << 3),
//! Include the children of an XML element
pcmk__xml_fmt_children = (1 << 4),
//! Include the closing tag of an XML element
pcmk__xml_fmt_close = (1 << 5),
// @COMPAT Can we start including text nodes unconditionally?
//! Include XML text nodes
pcmk__xml_fmt_text = (1 << 6),
};
void pcmk__xml_init(void);
void pcmk__xml_cleanup(void);
int pcmk__xml_show(pcmk__output_t *out, const char *prefix, const xmlNode *data,
int depth, uint32_t options);
int pcmk__xml_show_changes(pcmk__output_t *out, const xmlNode *xml);
enum pcmk__xml_artefact_ns {
pcmk__xml_artefact_ns_legacy_rng = 1,
pcmk__xml_artefact_ns_legacy_xslt,
pcmk__xml_artefact_ns_base_rng,
pcmk__xml_artefact_ns_base_xslt,
};
void pcmk__strip_xml_text(xmlNode *xml);
/*!
* \internal
* \brief Indicators of which XML characters to escape
*
* XML allows the escaping of special characters by replacing them with entity
* references (for example, <tt>"""</tt>) or character references (for
* example, <tt>" "</tt>).
*
* The special characters <tt>'&'</tt> (except as the beginning of an entity
* reference) and <tt>'<'</tt> are not allowed in their literal forms in XML
* character data. Character data is non-markup text (for example, the content
* of a text node). <tt>'>'</tt> is allowed under most circumstances; we escape
* it for safety and symmetry.
*
* For more details, see the "Character Data and Markup" section of the XML
* spec, currently section 2.4:
* https://www.w3.org/TR/xml/#dt-markup
*
* Attribute values are handled specially.
* * If an attribute value is delimited by single quotes, then single quotes
* must be escaped within the value.
* * Similarly, if an attribute value is delimited by double quotes, then double
* quotes must be escaped within the value.
* * A conformant XML processor replaces a literal whitespace character (tab,
* newline, carriage return, space) in an attribute value with a space
* (\c '#x20') character. However, a reference to a whitespace character (for
* example, \c "
" for \c '\n') does not get replaced.
* * For more details, see the "Attribute-Value Normalization" section of the
* XML spec, currently section 3.3.3. Note that the default attribute type
* is CDATA; we don't deal with NMTOKENS, etc.:
* https://www.w3.org/TR/xml/#AVNormalize
*
* Pacemaker always delimits attribute values with double quotes, so there's no
* need to escape single quotes.
*
* Newlines and tabs should be escaped in attribute values when XML is
* serialized to text, so that future parsing preserves them rather than
* normalizing them to spaces.
*
* We always escape carriage returns, so that they're not converted to spaces
* during attribute-value normalization and because displaying them as literals
* is messy.
*/
enum pcmk__xml_escape_type {
/*!
* For text nodes.
* * Escape \c '<', \c '>', and \c '&' using entity references.
* * Do not escape \c '\n' and \c '\t'.
* * Escape other non-printing characters using character references.
*/
pcmk__xml_escape_text,
/*!
* For attribute values.
* * Escape \c '<', \c '>', \c '&', and \c '"' using entity references.
* * Escape \c '\n', \c '\t', and other non-printing characters using
* character references.
*/
pcmk__xml_escape_attr,
/* @COMPAT Drop escaping of at least '\n' and '\t' for
* pcmk__xml_escape_attr_pretty when openstack-info, openstack-floating-ip,
* and openstack-virtual-ip resource agents no longer depend on it.
*
* At time of writing, openstack-info may set a multiline value for the
* openstack_ports node attribute. The other two agents query the value and
* require it to be on one line with no spaces.
*/
/*!
* For attribute values displayed in text output delimited by double quotes.
* * Escape \c '\n' as \c "\\n"
* * Escape \c '\r' as \c "\\r"
* * Escape \c '\t' as \c "\\t"
* * Escape \c '"' as \c "\\""
*/
pcmk__xml_escape_attr_pretty,
};
bool pcmk__xml_needs_escape(const char *text, enum pcmk__xml_escape_type type);
char *pcmk__xml_escape(const char *text, enum pcmk__xml_escape_type type);
/*!
* \internal
* \brief Get the root directory to scan XML artefacts of given kind for
*
* \param[in] ns governs the hierarchy nesting against the inherent root dir
*
* \return root directory to scan XML artefacts of given kind for
*/
char *
pcmk__xml_artefact_root(enum pcmk__xml_artefact_ns ns);
/*!
* \internal
* \brief Get the fully unwrapped path to particular XML artifact (RNG/XSLT)
*
* \param[in] ns denotes path forming details (parent dir, suffix)
* \param[in] filespec symbolic file specification to be combined with
* #artefact_ns to form the final path
* \return unwrapped path to particular XML artifact (RNG/XSLT)
*/
char *pcmk__xml_artefact_path(enum pcmk__xml_artefact_ns ns,
const char *filespec);
/*!
* \internal
* \brief Return first non-text child node of an XML node
*
* \param[in] parent XML node to check
*
* \return First non-text child node of \p parent (or NULL if none)
*/
static inline xmlNode *
pcmk__xml_first_child(const xmlNode *parent)
{
xmlNode *child = (parent? parent->children : NULL);
while (child && (child->type == XML_TEXT_NODE)) {
child = child->next;
}
return child;
}
/*!
* \internal
* \brief Return next non-text sibling node of an XML node
*
* \param[in] child XML node to check
*
* \return Next non-text sibling of \p child (or NULL if none)
*/
static inline xmlNode *
pcmk__xml_next(const xmlNode *child)
{
xmlNode *next = (child? child->next : NULL);
while (next && (next->type == XML_TEXT_NODE)) {
next = next->next;
}
return next;
}
void pcmk__xml_free(xmlNode *xml);
void pcmk__xml_free_doc(xmlDoc *doc);
xmlNode *pcmk__xml_copy(xmlNode *parent, xmlNode *src);
/*!
* \internal
* \brief Flags for operations affecting XML attributes
*/
enum pcmk__xa_flags {
//! Flag has no effect
pcmk__xaf_none = 0U,
//! Don't overwrite existing values
pcmk__xaf_no_overwrite = (1U << 0),
/*!
* Treat values as score updates where possible (see
* \c pcmk__xe_set_score())
*/
pcmk__xaf_score_update = (1U << 1),
};
void pcmk__xml_sanitize_id(char *id);
/* internal XML-related utilities */
enum xml_private_flags {
pcmk__xf_none = 0x0000,
pcmk__xf_dirty = 0x0001,
pcmk__xf_deleted = 0x0002,
pcmk__xf_created = 0x0004,
pcmk__xf_modified = 0x0008,
pcmk__xf_tracking = 0x0010,
pcmk__xf_processed = 0x0020,
pcmk__xf_skip = 0x0040,
pcmk__xf_moved = 0x0080,
pcmk__xf_acl_enabled = 0x0100,
pcmk__xf_acl_read = 0x0200,
pcmk__xf_acl_write = 0x0400,
pcmk__xf_acl_deny = 0x0800,
pcmk__xf_acl_create = 0x1000,
pcmk__xf_acl_denied = 0x2000,
pcmk__xf_lazy = 0x4000,
};
-void pcmk__set_xml_doc_flag(xmlNode *xml, enum xml_private_flags flag);
+void pcmk__xml_doc_set_flags(xmlDoc *doc, uint32_t flags);
bool pcmk__xml_tree_foreach(xmlNode *xml, bool (*fn)(xmlNode *, void *),
void *user_data);
static inline const char *
pcmk__xml_attr_value(const xmlAttr *attr)
{
return ((attr == NULL) || (attr->children == NULL))? NULL
: (const char *) attr->children->content;
}
/*!
* \internal
* \brief Check whether a given CIB element was modified in a CIB patchset
*
* \param[in] patchset CIB XML patchset
* \param[in] element XML tag of CIB element to check (\c NULL is equivalent
* to \c PCMK_XE_CIB). Supported values include any CIB
* element supported by \c pcmk__cib_abs_xpath_for().
*
* \return \c true if \p element was modified, or \c false otherwise
*/
bool pcmk__cib_element_in_patchset(const xmlNode *patchset,
const char *element);
#ifdef __cplusplus
}
#endif
#endif // PCMK__CRM_COMMON_XML_INTERNAL__H
diff --git a/lib/common/acl.c b/lib/common/acl.c
index cf42845aa5..5fb323c44c 100644
--- a/lib/common/acl.c
+++ b/lib/common/acl.c
@@ -1,919 +1,922 @@
/*
* 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);
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);
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__set_xml_doc_flag(target, pcmk__xf_acl_enabled);
+ 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__set_xml_doc_flag(tree, pcmk__xf_acl_denied); \
+ 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__tracking_xml_changes(xml, false) || !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);
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);
return false;
}
}
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;
}
diff --git a/lib/common/xml.c b/lib/common/xml.c
index 0443984f34..43f0ee8261 100644
--- a/lib/common/xml.c
+++ b/lib/common/xml.c
@@ -1,1638 +1,1654 @@
/*
* 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;
}
bool
pcmk__tracking_xml_changes(xmlNode *xml, bool lazy)
{
if(xml == NULL || xml->doc == NULL || xml->doc->_private == NULL) {
return FALSE;
} else if (!pcmk_is_set(((xml_doc_private_t *)xml->doc->_private)->flags,
pcmk__xf_tracking)) {
return FALSE;
} else if (lazy && !pcmk_is_set(((xml_doc_private_t *)xml->doc->_private)->flags,
pcmk__xf_lazy)) {
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__set_xml_doc_flag(xmlNode *xml, enum xml_private_flags flag)
+pcmk__xml_doc_set_flags(xmlDoc *doc, uint32_t flags)
{
- if (xml != NULL) {
- xml_doc_private_t *docpriv = xml->doc->_private;
+ if (doc != NULL) {
+ xml_doc_private_t *docpriv = doc->_private;
- pcmk__set_xml_flags(docpriv, flag);
+ pcmk__set_xml_flags(docpriv, flags);
}
}
// Mark document, element, and all element's parents as changed
void
pcmk__mark_xml_node_dirty(xmlNode *xml)
{
- pcmk__set_xml_doc_flag(xml, pcmk__xf_dirty);
+ 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__tracking_xml_changes(xml, false)) {
// 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;
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__tracking_xml_changes(node, false)) {
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__set_xml_doc_flag(xml, pcmk__xf_tracking);
+ pcmk__xml_doc_set_flags(xml->doc, pcmk__xf_tracking);
if(enforce_acls) {
if(acl_source == NULL) {
acl_source = xml;
}
- pcmk__set_xml_doc_flag(xml, pcmk__xf_acl_enabled);
+ pcmk__xml_doc_set_flags(xml->doc, pcmk__xf_acl_enabled);
pcmk__unpack_acl(acl_source, xml, user);
pcmk__apply_acl(xml);
}
}
bool xml_tracking_changes(xmlNode * xml)
{
return (xml != NULL) && (xml->doc != NULL) && (xml->doc->_private != NULL)
&& pcmk_is_set(((xml_doc_private_t *)(xml->doc->_private))->flags,
pcmk__xf_tracking);
}
bool xml_document_dirty(xmlNode *xml)
{
return (xml != NULL) && (xml->doc != NULL) && (xml->doc->_private != NULL)
&& pcmk_is_set(((xml_doc_private_t *)(xml->doc->_private))->flags,
pcmk__xf_dirty);
}
/*!
* \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 ((doc != NULL) && pcmk__tracking_xml_changes(node, false)
&& !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__set_xml_doc_flag(node, pcmk__xf_dirty);
+ 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__tracking_xml_changes(new_xml, TRUE)) {
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)
{
- pcmk__set_xml_doc_flag(new_xml, pcmk__xf_lazy);
+ 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(xml_tracking_changes(new_xml) == FALSE) {
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 = '.';
}
}
}
// LCOV_EXCL_STOP
// End deprecated API
diff --git a/lib/pacemaker/pcmk_acl.c b/lib/pacemaker/pcmk_acl.c
index 5f1df5e289..96f07969b3 100644
--- a/lib/pacemaker/pcmk_acl.c
+++ b/lib/pacemaker/pcmk_acl.c
@@ -1,398 +1,398 @@
/*
- * Copyright 2004-2024 the Pacemaker project contributors
+ * 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/parser.h>
#include <libxml/tree.h>
#include <libxml/xpath.h>
#include <libxslt/transform.h>
#include <libxslt/variables.h>
#include <libxslt/xsltutils.h>
#include <crm/crm.h>
#include <crm/common/xml.h>
#include <crm/common/xml_internal.h>
#include <crm/common/internal.h>
#include <pacemaker-internal.h>
#define ACL_NS_PREFIX "http://clusterlabs.org/ns/pacemaker/access/"
#define ACL_NS_Q_PREFIX "pcmk-access-"
#define ACL_NS_Q_WRITABLE (const xmlChar *) ACL_NS_Q_PREFIX "writable"
#define ACL_NS_Q_READABLE (const xmlChar *) ACL_NS_Q_PREFIX "readable"
#define ACL_NS_Q_DENIED (const xmlChar *) ACL_NS_Q_PREFIX "denied"
static const xmlChar *NS_WRITABLE = (const xmlChar *) ACL_NS_PREFIX "writable";
static const xmlChar *NS_READABLE = (const xmlChar *) ACL_NS_PREFIX "readable";
static const xmlChar *NS_DENIED = (const xmlChar *) ACL_NS_PREFIX "denied";
/*!
* \brief This function takes a node and marks it with the namespace
* given in the ns parameter.
*
* \param[in,out] i_node
* \param[in] ns
* \param[in,out] ret
* \param[in,out] ns_recycle_writable
* \param[in,out] ns_recycle_readable
* \param[in,out] ns_recycle_denied
*/
static void
pcmk__acl_mark_node_with_namespace(xmlNode *i_node, const xmlChar *ns, int *ret,
xmlNs **ns_recycle_writable,
xmlNs **ns_recycle_readable,
xmlNs **ns_recycle_denied)
{
if (ns == NS_WRITABLE)
{
if (*ns_recycle_writable == NULL)
{
*ns_recycle_writable = xmlNewNs(xmlDocGetRootElement(i_node->doc),
NS_WRITABLE, ACL_NS_Q_WRITABLE);
}
xmlSetNs(i_node, *ns_recycle_writable);
*ret = pcmk_rc_ok;
}
else if (ns == NS_READABLE)
{
if (*ns_recycle_readable == NULL)
{
*ns_recycle_readable = xmlNewNs(xmlDocGetRootElement(i_node->doc),
NS_READABLE, ACL_NS_Q_READABLE);
}
xmlSetNs(i_node, *ns_recycle_readable);
*ret = pcmk_rc_ok;
}
else if (ns == NS_DENIED)
{
if (*ns_recycle_denied == NULL)
{
*ns_recycle_denied = xmlNewNs(xmlDocGetRootElement(i_node->doc),
NS_DENIED, ACL_NS_Q_DENIED);
};
xmlSetNs(i_node, *ns_recycle_denied);
*ret = pcmk_rc_ok;
}
}
/*!
* \brief Annotate a given XML element or property and its siblings with
* XML namespaces to indicate ACL permissions
*
* \param[in,out] xml_modify XML to annotate
*
* \return A standard Pacemaker return code
* Namely:
* - pcmk_rc_ok upon success,
* - pcmk_rc_already if ACLs were not applicable,
* - pcmk_rc_schema_validation if the validation schema version
* is unsupported (see note), or
* - EINVAL or ENOMEM as appropriate;
*
* \note This function is recursive
*/
static int
annotate_with_siblings(xmlNode *xml_modify)
{
static xmlNs *ns_recycle_writable = NULL,
*ns_recycle_readable = NULL,
*ns_recycle_denied = NULL;
static const xmlDoc *prev_doc = NULL;
xmlNode *i_node = NULL;
const xmlChar *ns;
int ret = EINVAL; // nodes have not been processed yet
if (prev_doc == NULL || prev_doc != xml_modify->doc) {
prev_doc = xml_modify->doc;
ns_recycle_writable = ns_recycle_readable = ns_recycle_denied = NULL;
}
for (i_node = xml_modify; i_node != NULL; i_node = i_node->next) {
switch (i_node->type) {
case XML_ELEMENT_NODE:
- pcmk__set_xml_doc_flag(i_node, pcmk__xf_tracking);
+ pcmk__xml_doc_set_flags(i_node->doc, pcmk__xf_tracking);
if (!pcmk__check_acl(i_node, NULL, pcmk__xf_acl_read)) {
ns = NS_DENIED;
} else if (!pcmk__check_acl(i_node, NULL, pcmk__xf_acl_write)) {
ns = NS_READABLE;
} else {
ns = NS_WRITABLE;
}
pcmk__acl_mark_node_with_namespace(i_node, ns, &ret,
&ns_recycle_writable,
&ns_recycle_readable,
&ns_recycle_denied);
// @TODO Could replace recursion with iteration to save stack
if (i_node->properties != NULL) {
/* This is not entirely clear, but relies on the very same
* class-hierarchy emulation that libxml2 has firmly baked
* in its API/ABI
*/
ret |= annotate_with_siblings((xmlNodePtr)
i_node->properties);
}
if (i_node->children != NULL) {
ret |= annotate_with_siblings(i_node->children);
}
break;
case XML_ATTRIBUTE_NODE:
// We can utilize that parent has already been assigned the ns
if (!pcmk__check_acl(i_node->parent,
(const char *) i_node->name,
pcmk__xf_acl_read)) {
ns = NS_DENIED;
} else if (!pcmk__check_acl(i_node,
(const char *) i_node->name,
pcmk__xf_acl_write)) {
ns = NS_READABLE;
} else {
ns = NS_WRITABLE;
}
pcmk__acl_mark_node_with_namespace(i_node, ns, &ret,
&ns_recycle_writable,
&ns_recycle_readable,
&ns_recycle_denied);
break;
case XML_COMMENT_NODE:
// We can utilize that parent has already been assigned the ns
if (!pcmk__check_acl(i_node->parent,
(const char *) i_node->name,
pcmk__xf_acl_read)) {
ns = NS_DENIED;
} else if (!pcmk__check_acl(i_node->parent,
(const char *) i_node->name,
pcmk__xf_acl_write)) {
ns = NS_READABLE;
} else {
ns = NS_WRITABLE;
}
pcmk__acl_mark_node_with_namespace(i_node, ns, &ret,
&ns_recycle_writable,
&ns_recycle_readable,
&ns_recycle_denied);
break;
default:
break;
}
}
return ret;
}
int
pcmk__acl_annotate_permissions(const char *cred, const xmlDoc *cib_doc,
xmlDoc **acl_evaled_doc)
{
int ret;
xmlNode *target, *comment;
const char *validation;
CRM_CHECK(cred != NULL, return EINVAL);
CRM_CHECK(cib_doc != NULL, return EINVAL);
CRM_CHECK(acl_evaled_doc != NULL, return EINVAL);
/* avoid trivial accidental XML injection */
if (strpbrk(cred, "<>&") != NULL) {
return EINVAL;
}
if (!pcmk_acl_required(cred)) {
/* nothing to evaluate */
return pcmk_rc_already;
}
validation = crm_element_value(xmlDocGetRootElement(cib_doc),
PCMK_XA_VALIDATE_WITH);
if (pcmk__cmp_schemas_by_name(PCMK__COMPAT_ACL_2_MIN_INCL,
validation) > 0) {
return pcmk_rc_schema_validation;
}
target = pcmk__xml_copy(NULL, xmlDocGetRootElement((xmlDoc *) cib_doc));
if (target == NULL) {
return EINVAL;
}
pcmk__enable_acl(target, target, cred);
ret = annotate_with_siblings(target);
if (ret == pcmk_rc_ok) {
char *content = crm_strdup_printf("ACLs as evaluated for user %s",
cred);
comment = pcmk__xc_create(target->doc, content);
xmlAddPrevSibling(xmlDocGetRootElement(target->doc), comment);
*acl_evaled_doc = target->doc;
free(content);
} else {
pcmk__xml_free(target);
}
return ret;
}
int
pcmk__acl_evaled_render(xmlDoc *annotated_doc, enum pcmk__acl_render_how how,
xmlChar **doc_txt_ptr)
{
xmlDoc *xslt_doc;
xsltStylesheet *xslt;
xsltTransformContext *xslt_ctxt;
xmlDoc *res;
char *sfile;
static const char *params_namespace[] = {
"accessrendercfg:c-writable", ACL_NS_Q_PREFIX "writable:",
"accessrendercfg:c-readable", ACL_NS_Q_PREFIX "readable:",
"accessrendercfg:c-denied", ACL_NS_Q_PREFIX "denied:",
"accessrendercfg:c-reset", "",
"accessrender:extra-spacing", "no",
"accessrender:self-reproducing-prefix", ACL_NS_Q_PREFIX,
NULL
}, *params_useansi[] = {
/* start with hard-coded defaults, then adapt per the template ones */
"accessrendercfg:c-writable", "\x1b[32m",
"accessrendercfg:c-readable", "\x1b[34m",
"accessrendercfg:c-denied", "\x1b[31m",
"accessrendercfg:c-reset", "\x1b[0m",
"accessrender:extra-spacing", "no",
"accessrender:self-reproducing-prefix", ACL_NS_Q_PREFIX,
NULL
}, *params_noansi[] = {
"accessrendercfg:c-writable", "vvv---[ WRITABLE ]---vvv",
"accessrendercfg:c-readable", "vvv---[ READABLE ]---vvv",
"accessrendercfg:c-denied", "vvv---[ ~DENIED~ ]---vvv",
"accessrendercfg:c-reset", "",
"accessrender:extra-spacing", "yes",
"accessrender:self-reproducing-prefix", "",
NULL
};
const char **params;
int rc = pcmk_rc_ok;
xmlParserCtxtPtr parser_ctxt;
/* unfortunately, the input (coming from CIB originally) was parsed with
blanks ignored, and since the output is a conversion of XML to text
format (we would be covered otherwise thanks to implicit
pretty-printing), we need to dump the tree to string output first,
only to subsequently reparse it -- this time with blanks honoured */
xmlChar *annotated_dump;
int dump_size;
pcmk__assert(how != pcmk__acl_render_none);
// Color is the default render mode for terminals; text is default otherwise
if (how == pcmk__acl_render_default) {
if (isatty(STDOUT_FILENO)) {
how = pcmk__acl_render_color;
} else {
how = pcmk__acl_render_text;
}
}
xmlDocDumpFormatMemory(annotated_doc, &annotated_dump, &dump_size, 1);
/* res does not need private data: it's temporary and used only with libxslt
* functions
*/
res = xmlReadDoc(annotated_dump, "on-the-fly-access-render", NULL,
XML_PARSE_NONET);
pcmk__assert(res != NULL);
xmlFree(annotated_dump);
pcmk__xml_free_doc(annotated_doc);
annotated_doc = res;
sfile = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_base_xslt,
"access-render-2");
parser_ctxt = xmlNewParserCtxt();
pcmk__assert(sfile != NULL);
pcmk__mem_assert(parser_ctxt);
xslt_doc = xmlCtxtReadFile(parser_ctxt, sfile, NULL, XML_PARSE_NONET);
xslt = xsltParseStylesheetDoc(xslt_doc); /* acquires xslt_doc! */
if (xslt == NULL) {
crm_crit("Problem in parsing %s", sfile);
rc = EINVAL;
goto done;
}
xmlFreeParserCtxt(parser_ctxt);
xslt_ctxt = xsltNewTransformContext(xslt, annotated_doc);
pcmk__mem_assert(xslt_ctxt);
switch (how) {
case pcmk__acl_render_namespace:
params = params_namespace;
break;
case pcmk__acl_render_text:
params = params_noansi;
break;
default:
/* pcmk__acl_render_color is the only remaining option.
* The compiler complains about params possibly uninitialized if we
* don't use default here.
*/
params = params_useansi;
break;
}
xsltQuoteUserParams(xslt_ctxt, params);
res = xsltApplyStylesheetUser(xslt, annotated_doc, NULL,
NULL, NULL, xslt_ctxt);
pcmk__xml_free_doc(annotated_doc);
annotated_doc = NULL;
xsltFreeTransformContext(xslt_ctxt);
xslt_ctxt = NULL;
if (how == pcmk__acl_render_color && params != params_useansi) {
char **param_i = (char **) params;
do {
free(*param_i);
} while (*param_i++ != NULL);
free(params);
}
if (res == NULL) {
rc = EINVAL;
} else {
int doc_txt_len;
int temp = xsltSaveResultToString(doc_txt_ptr, &doc_txt_len, res, xslt);
pcmk__xml_free_doc(res);
if (temp != 0) {
rc = EINVAL;
}
}
done:
if (xslt != NULL) {
xsltFreeStylesheet(xslt);
}
free(sfile);
return rc;
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Thu, Jul 10, 3:00 AM (1 d, 5 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2009829
Default Alt Text
(111 KB)
Attached To
Mode
rP Pacemaker
Attached
Detach File
Event Timeline
Log In to Comment