diff --git a/cts/cli/regression.acls.exp b/cts/cli/regression.acls.exp
index df3f77131b..6083ed790c 100644
--- a/cts/cli/regression.acls.exp
+++ b/cts/cli/regression.acls.exp
@@ -1,4438 +1,4442 @@
Created new pacemaker configuration
Setting up shadow instance
A new shadow instance was created. To begin using it paste the following into your shell:
CIB_shadow=cts-cli ; export CIB_shadow
=#=#=#= Begin test: Configure some ACLs =#=#=#=
=#=#=#= Current cib after: Configure some ACLs =#=#=#=
=#=#=#= End test: Configure some ACLs - OK (0) =#=#=#=
* Passed: cibadmin - Configure some ACLs
=#=#=#= Begin test: Enable ACLs =#=#=#=
=#=#=#= Current cib after: Enable ACLs =#=#=#=
=#=#=#= End test: Enable ACLs - OK (0) =#=#=#=
* Passed: crm_attribute - Enable ACLs
=#=#=#= Begin test: Set cluster option =#=#=#=
=#=#=#= Current cib after: Set cluster option =#=#=#=
=#=#=#= End test: Set cluster option - OK (0) =#=#=#=
* Passed: crm_attribute - Set cluster option
=#=#=#= Begin test: New ACL =#=#=#=
=#=#=#= Current cib after: New ACL =#=#=#=
=#=#=#= End test: New ACL - OK (0) =#=#=#=
* Passed: cibadmin - New ACL
=#=#=#= Begin test: Another ACL =#=#=#=
=#=#=#= Current cib after: Another ACL =#=#=#=
=#=#=#= End test: Another ACL - OK (0) =#=#=#=
* Passed: cibadmin - Another ACL
=#=#=#= Begin test: Updated ACL =#=#=#=
=#=#=#= Current cib after: Updated ACL =#=#=#=
=#=#=#= End test: Updated ACL - OK (0) =#=#=#=
* Passed: cibadmin - Updated ACL
=#=#=#= Begin test: unknownguy: Query configuration =#=#=#=
Call failed: Permission denied
=#=#=#= End test: unknownguy: Query configuration - Insufficient privileges (4) =#=#=#=
* Passed: cibadmin - unknownguy: Query configuration
=#=#=#= Begin test: unknownguy: Set enable-acl =#=#=#=
crm_attribute: Error performing operation: Permission denied
=#=#=#= End test: unknownguy: Set enable-acl - Insufficient privileges (4) =#=#=#=
* Passed: crm_attribute - unknownguy: Set enable-acl
=#=#=#= Begin test: unknownguy: Set stonith-enabled =#=#=#=
crm_attribute: Error performing operation: Permission denied
=#=#=#= End test: unknownguy: Set stonith-enabled - Insufficient privileges (4) =#=#=#=
* Passed: crm_attribute - unknownguy: Set stonith-enabled
=#=#=#= Begin test: unknownguy: Create a resource =#=#=#=
pcmk__check_acl trace: User 'unknownguy' without ACLs denied read/write access to /cib/configuration/resources/primitive[@id]
-pcmk__check_acl trace: User 'unknownguy' without ACLs denied read/write access to /cib/configuration/resources/primitive[@class]
-pcmk__check_acl trace: User 'unknownguy' without ACLs denied read/write access to /cib/configuration/resources/primitive[@provider]
-pcmk__check_acl trace: User 'unknownguy' without ACLs denied read/write access to /cib/configuration/resources/primitive[@type]
pcmk__apply_creation_acl trace: Creation of scaffolding with id="" is implicitly allowed
Call failed: Permission denied
+
+
+
+
+
=#=#=#= End test: unknownguy: Create a resource - Insufficient privileges (4) =#=#=#=
* Passed: cibadmin - unknownguy: Create a resource
=#=#=#= Begin test: l33t-haxor: Query configuration =#=#=#=
Call failed: Permission denied
=#=#=#= End test: l33t-haxor: Query configuration - Insufficient privileges (4) =#=#=#=
* Passed: cibadmin - l33t-haxor: Query configuration
=#=#=#= Begin test: l33t-haxor: Set enable-acl =#=#=#=
crm_attribute: Error performing operation: Permission denied
=#=#=#= End test: l33t-haxor: Set enable-acl - Insufficient privileges (4) =#=#=#=
* Passed: crm_attribute - l33t-haxor: Set enable-acl
=#=#=#= Begin test: l33t-haxor: Set stonith-enabled =#=#=#=
crm_attribute: Error performing operation: Permission denied
=#=#=#= End test: l33t-haxor: Set stonith-enabled - Insufficient privileges (4) =#=#=#=
* Passed: crm_attribute - l33t-haxor: Set stonith-enabled
=#=#=#= Begin test: l33t-haxor: Create a resource =#=#=#=
pcmk__check_acl trace: Parent ACL denies user 'l33t-haxor' read/write access to /cib/configuration/resources/primitive[@id='dummy']
pcmk__apply_creation_acl trace: ACLs disallow creation of with id="dummy"
Call failed: Permission denied
=#=#=#= End test: l33t-haxor: Create a resource - Insufficient privileges (4) =#=#=#=
* Passed: cibadmin - l33t-haxor: Create a resource
=#=#=#= Begin test: niceguy: Query configuration =#=#=#=
=#=#=#= End test: niceguy: Query configuration - OK (0) =#=#=#=
* Passed: cibadmin - niceguy: Query configuration
=#=#=#= Begin test: niceguy: Set enable-acl =#=#=#=
pcmk__check_acl trace: Default ACL denies user 'niceguy' read/write access to /cib/configuration/crm_config/cluster_property_set[@id='cib-bootstrap-options']/nvpair[@id='cib-bootstrap-options-enable-acl'][@value]
Error setting enable-acl=false (section=crm_config, set=): Permission denied
crm_attribute: Error performing operation: Permission denied
=#=#=#= End test: niceguy: Set enable-acl - Insufficient privileges (4) =#=#=#=
* Passed: crm_attribute - niceguy: Set enable-acl
=#=#=#= Begin test: niceguy: Set stonith-enabled =#=#=#=
pcmk__apply_creation_acl trace: ACLs allow creation of with id="cib-bootstrap-options-stonith-enabled"
=#=#=#= Current cib after: niceguy: Set stonith-enabled =#=#=#=
=#=#=#= End test: niceguy: Set stonith-enabled - OK (0) =#=#=#=
* Passed: crm_attribute - niceguy: Set stonith-enabled
=#=#=#= Begin test: niceguy: Create a resource =#=#=#=
pcmk__check_acl trace: Default ACL denies user 'niceguy' read/write access to /cib/configuration/resources/primitive[@id='dummy']
pcmk__apply_creation_acl trace: ACLs disallow creation of with id="dummy"
Call failed: Permission denied
=#=#=#= End test: niceguy: Create a resource - Insufficient privileges (4) =#=#=#=
* Passed: cibadmin - niceguy: Create a resource
=#=#=#= Begin test: root: Query configuration =#=#=#=
=#=#=#= End test: root: Query configuration - OK (0) =#=#=#=
* Passed: cibadmin - root: Query configuration
=#=#=#= Begin test: root: Set stonith-enabled =#=#=#=
=#=#=#= Current cib after: root: Set stonith-enabled =#=#=#=
=#=#=#= End test: root: Set stonith-enabled - OK (0) =#=#=#=
* Passed: crm_attribute - root: Set stonith-enabled
=#=#=#= Begin test: root: Create a resource =#=#=#=
=#=#=#= Current cib after: root: Create a resource =#=#=#=
=#=#=#= End test: root: Create a resource - OK (0) =#=#=#=
* Passed: cibadmin - root: Create a resource
=#=#=#= Begin test: l33t-haxor: Create a resource meta attribute =#=#=#=
crm_resource: Error performing operation: Insufficient privileges
=#=#=#= End test: l33t-haxor: Create a resource meta attribute - Insufficient privileges (4) =#=#=#=
* Passed: crm_resource - l33t-haxor: Create a resource meta attribute
=#=#=#= Begin test: l33t-haxor: Query a resource meta attribute =#=#=#=
crm_resource: Error performing operation: Insufficient privileges
=#=#=#= End test: l33t-haxor: Query a resource meta attribute - Insufficient privileges (4) =#=#=#=
* Passed: crm_resource - l33t-haxor: Query a resource meta attribute
=#=#=#= Begin test: l33t-haxor: Remove a resource meta attribute =#=#=#=
crm_resource: Error performing operation: Insufficient privileges
=#=#=#= End test: l33t-haxor: Remove a resource meta attribute - Insufficient privileges (4) =#=#=#=
* Passed: crm_resource - l33t-haxor: Remove a resource meta attribute
=#=#=#= Begin test: niceguy: Create a resource meta attribute =#=#=#=
unpack_resources error: Resource start-up disabled since no STONITH resources have been defined
unpack_resources error: Either configure some or disable STONITH with the stonith-enabled option
unpack_resources error: NOTE: Clusters with shared data need STONITH to ensure data integrity
pcmk__apply_creation_acl trace: Creation of scaffolding with id="dummy-meta_attributes" is implicitly allowed
pcmk__apply_creation_acl trace: ACLs allow creation of with id="dummy-meta_attributes-target-role"
Set 'dummy' option: id=dummy-meta_attributes-target-role set=dummy-meta_attributes name=target-role value=Stopped
=#=#=#= Current cib after: niceguy: Create a resource meta attribute =#=#=#=
=#=#=#= End test: niceguy: Create a resource meta attribute - OK (0) =#=#=#=
* Passed: crm_resource - niceguy: Create a resource meta attribute
=#=#=#= Begin test: niceguy: Query a resource meta attribute =#=#=#=
unpack_resources error: Resource start-up disabled since no STONITH resources have been defined
unpack_resources error: Either configure some or disable STONITH with the stonith-enabled option
unpack_resources error: NOTE: Clusters with shared data need STONITH to ensure data integrity
Stopped
=#=#=#= Current cib after: niceguy: Query a resource meta attribute =#=#=#=
=#=#=#= End test: niceguy: Query a resource meta attribute - OK (0) =#=#=#=
* Passed: crm_resource - niceguy: Query a resource meta attribute
=#=#=#= Begin test: niceguy: Remove a resource meta attribute =#=#=#=
unpack_resources error: Resource start-up disabled since no STONITH resources have been defined
unpack_resources error: Either configure some or disable STONITH with the stonith-enabled option
unpack_resources error: NOTE: Clusters with shared data need STONITH to ensure data integrity
Deleted 'dummy' option: id=dummy-meta_attributes-target-role name=target-role
=#=#=#= Current cib after: niceguy: Remove a resource meta attribute =#=#=#=
=#=#=#= End test: niceguy: Remove a resource meta attribute - OK (0) =#=#=#=
* Passed: crm_resource - niceguy: Remove a resource meta attribute
=#=#=#= Begin test: niceguy: Create a resource meta attribute =#=#=#=
unpack_resources error: Resource start-up disabled since no STONITH resources have been defined
unpack_resources error: Either configure some or disable STONITH with the stonith-enabled option
unpack_resources error: NOTE: Clusters with shared data need STONITH to ensure data integrity
pcmk__apply_creation_acl trace: ACLs allow creation of with id="dummy-meta_attributes-target-role"
Set 'dummy' option: id=dummy-meta_attributes-target-role set=dummy-meta_attributes name=target-role value=Started
=#=#=#= Current cib after: niceguy: Create a resource meta attribute =#=#=#=
=#=#=#= End test: niceguy: Create a resource meta attribute - OK (0) =#=#=#=
* Passed: crm_resource - niceguy: Create a resource meta attribute
=#=#=#= Begin test: badidea: Query configuration - implied deny =#=#=#=
=#=#=#= End test: badidea: Query configuration - implied deny - OK (0) =#=#=#=
* Passed: cibadmin - badidea: Query configuration - implied deny
=#=#=#= Begin test: betteridea: Query configuration - explicit deny =#=#=#=
=#=#=#= End test: betteridea: Query configuration - explicit deny - OK (0) =#=#=#=
* Passed: cibadmin - betteridea: Query configuration - explicit deny
=#=#=#= Begin test: niceguy: Replace - remove acls =#=#=#=
pcmk__check_acl trace: Default ACL denies user 'niceguy' read/write access to /cib[@epoch]
pcmk__check_acl trace: Default ACL denies user 'niceguy' read/write access to /cib/configuration/acls
Call failed: Permission denied
=#=#=#= End test: niceguy: Replace - remove acls - Insufficient privileges (4) =#=#=#=
* Passed: cibadmin - niceguy: Replace - remove acls
=#=#=#= Begin test: niceguy: Replace - create resource =#=#=#=
pcmk__check_acl trace: Default ACL denies user 'niceguy' read/write access to /cib[@epoch]
pcmk__check_acl trace: Default ACL denies user 'niceguy' read/write access to /cib/configuration/resources/primitive[@id='dummy2']
pcmk__apply_creation_acl trace: ACLs disallow creation of with id="dummy2"
Call failed: Permission denied
=#=#=#= End test: niceguy: Replace - create resource - Insufficient privileges (4) =#=#=#=
* Passed: cibadmin - niceguy: Replace - create resource
=#=#=#= Begin test: niceguy: Replace - modify attribute (deny) =#=#=#=
pcmk__check_acl trace: Default ACL denies user 'niceguy' read/write access to /cib[@epoch]
pcmk__check_acl trace: Default ACL denies user 'niceguy' read/write access to /cib/configuration/crm_config/cluster_property_set[@id='cib-bootstrap-options']/nvpair[@id='cib-bootstrap-options-enable-acl'][@value]
Call failed: Permission denied
=#=#=#= End test: niceguy: Replace - modify attribute (deny) - Insufficient privileges (4) =#=#=#=
* Passed: cibadmin - niceguy: Replace - modify attribute (deny)
=#=#=#= Begin test: niceguy: Replace - delete attribute (deny) =#=#=#=
pcmk__check_acl trace: Default ACL denies user 'niceguy' read/write access to /cib[@epoch]
pcmk__check_acl trace: Default ACL denies user 'niceguy' read/write access to /cib/configuration/crm_config/cluster_property_set[@id='cib-bootstrap-options']/nvpair[@id='cib-bootstrap-options-enable-acl']
Call failed: Permission denied
=#=#=#= End test: niceguy: Replace - delete attribute (deny) - Insufficient privileges (4) =#=#=#=
* Passed: cibadmin - niceguy: Replace - delete attribute (deny)
=#=#=#= Begin test: niceguy: Replace - create attribute (deny) =#=#=#=
pcmk__check_acl trace: Default ACL denies user 'niceguy' read/write access to /cib[@epoch]
pcmk__check_acl trace: Default ACL denies user 'niceguy' read/write access to /cib/configuration/resources/primitive[@id='dummy'][@description]
Call failed: Permission denied
=#=#=#= End test: niceguy: Replace - create attribute (deny) - Insufficient privileges (4) =#=#=#=
* Passed: cibadmin - niceguy: Replace - create attribute (deny)
=#=#=#= Begin test: bob: Replace - create attribute (direct allow) =#=#=#=
=#=#=#= End test: bob: Replace - create attribute (direct allow) - OK (0) =#=#=#=
* Passed: cibadmin - bob: Replace - create attribute (direct allow)
=#=#=#= Begin test: bob: Replace - modify attribute (direct allow) =#=#=#=
=#=#=#= End test: bob: Replace - modify attribute (direct allow) - OK (0) =#=#=#=
* Passed: cibadmin - bob: Replace - modify attribute (direct allow)
=#=#=#= Begin test: bob: Replace - delete attribute (direct allow) =#=#=#=
=#=#=#= End test: bob: Replace - delete attribute (direct allow) - OK (0) =#=#=#=
* Passed: cibadmin - bob: Replace - delete attribute (direct allow)
=#=#=#= Begin test: joe: Replace - create attribute (inherited allow) =#=#=#=
=#=#=#= End test: joe: Replace - create attribute (inherited allow) - OK (0) =#=#=#=
* Passed: cibadmin - joe: Replace - create attribute (inherited allow)
=#=#=#= Begin test: joe: Replace - modify attribute (inherited allow) =#=#=#=
=#=#=#= End test: joe: Replace - modify attribute (inherited allow) - OK (0) =#=#=#=
* Passed: cibadmin - joe: Replace - modify attribute (inherited allow)
=#=#=#= Begin test: joe: Replace - delete attribute (inherited allow) =#=#=#=
=#=#=#= End test: joe: Replace - delete attribute (inherited allow) - OK (0) =#=#=#=
* Passed: cibadmin - joe: Replace - delete attribute (inherited allow)
=#=#=#= Begin test: mike: Replace - create attribute (allow overrides deny) =#=#=#=
=#=#=#= End test: mike: Replace - create attribute (allow overrides deny) - OK (0) =#=#=#=
* Passed: cibadmin - mike: Replace - create attribute (allow overrides deny)
=#=#=#= Begin test: mike: Replace - modify attribute (allow overrides deny) =#=#=#=
=#=#=#= End test: mike: Replace - modify attribute (allow overrides deny) - OK (0) =#=#=#=
* Passed: cibadmin - mike: Replace - modify attribute (allow overrides deny)
=#=#=#= Begin test: mike: Replace - delete attribute (allow overrides deny) =#=#=#=
=#=#=#= End test: mike: Replace - delete attribute (allow overrides deny) - OK (0) =#=#=#=
* Passed: cibadmin - mike: Replace - delete attribute (allow overrides deny)
=#=#=#= Begin test: chris: Replace - create attribute (deny overrides allow) =#=#=#=
pcmk__check_acl trace: Parent ACL denies user 'chris' read/write access to /cib/configuration/resources/primitive[@id='dummy'][@description]
Call failed: Permission denied
=#=#=#= End test: chris: Replace - create attribute (deny overrides allow) - Insufficient privileges (4) =#=#=#=
* Passed: cibadmin - chris: Replace - create attribute (deny overrides allow)
=#=#=#= Begin test: chris: Replace - modify attribute (deny overrides allow) =#=#=#=
pcmk__check_acl trace: Parent ACL denies user 'chris' read/write access to /cib/configuration/resources/primitive[@id='dummy'][@description]
Call failed: Permission denied
=#=#=#= End test: chris: Replace - modify attribute (deny overrides allow) - Insufficient privileges (4) =#=#=#=
* Passed: cibadmin - chris: Replace - modify attribute (deny overrides allow)
=#=#=#= Begin test: chris: Replace - delete attribute (deny overrides allow) =#=#=#=
pcmk__check_acl trace: Parent ACL denies user 'chris' read/write access to /cib/configuration/resources/primitive[@id='dummy']
Call failed: Permission denied
=#=#=#= End test: chris: Replace - delete attribute (deny overrides allow) - Insufficient privileges (4) =#=#=#=
* Passed: cibadmin - chris: Replace - delete attribute (deny overrides allow)
!#!#!#!#! Upgrading to latest CIB schema and re-testing !#!#!#!#!
=#=#=#= Begin test: root: Upgrade to latest CIB schema =#=#=#=
pcmk__apply_creation_acl trace: ACLs allow creation of with id="observer-read-1"
pcmk__apply_creation_acl trace: ACLs allow creation of with id="observer-write-1"
pcmk__apply_creation_acl trace: ACLs allow creation of with id="observer-write-2"
pcmk__apply_creation_acl trace: ACLs allow creation of with id="admin-read-1"
pcmk__apply_creation_acl trace: ACLs allow creation of with id="admin-write-1"
pcmk__apply_creation_acl trace: ACLs allow creation of with id="super_user-write-1"
pcmk__apply_creation_acl trace: ACLs allow creation of with id="rsc-writer-deny-1"
pcmk__apply_creation_acl trace: ACLs allow creation of with id="rsc-writer-write-1"
pcmk__apply_creation_acl trace: ACLs allow creation of with id="rsc-denied-write-1"
pcmk__apply_creation_acl trace: ACLs allow creation of with id="rsc-denied-deny-1"
pcmk__apply_creation_acl trace: ACLs allow creation of with id="l33t-haxor"
pcmk__apply_creation_acl trace: ACLs allow creation of with id="auto-l33t-haxor"
pcmk__apply_creation_acl trace: ACLs allow creation of with id="auto-l33t-haxor"
pcmk__apply_creation_acl trace: ACLs allow creation of with id="crook-nothing"
pcmk__apply_creation_acl trace: ACLs allow creation of with id="niceguy"
pcmk__apply_creation_acl trace: ACLs allow creation of with id="observer"
pcmk__apply_creation_acl trace: ACLs allow creation of with id="bob"
pcmk__apply_creation_acl trace: ACLs allow creation of with id="admin"
pcmk__apply_creation_acl trace: ACLs allow creation of with id="joe"
pcmk__apply_creation_acl trace: ACLs allow creation of with id="super_user"
pcmk__apply_creation_acl trace: ACLs allow creation of with id="mike"
pcmk__apply_creation_acl trace: ACLs allow creation of with id="rsc_writer"
pcmk__apply_creation_acl trace: ACLs allow creation of with id="chris"
pcmk__apply_creation_acl trace: ACLs allow creation of with id="rsc_denied"
pcmk__apply_creation_acl trace: ACLs allow creation of with id="badidea"
pcmk__apply_creation_acl trace: ACLs allow creation of with id="auto-badidea"
pcmk__apply_creation_acl trace: ACLs allow creation of with id="auto-badidea"
pcmk__apply_creation_acl trace: ACLs allow creation of with id="badidea-resources"
pcmk__apply_creation_acl trace: ACLs allow creation of with id="betteridea"
pcmk__apply_creation_acl trace: ACLs allow creation of with id="auto-betteridea"
pcmk__apply_creation_acl trace: ACLs allow creation of with id="auto-betteridea"
pcmk__apply_creation_acl trace: ACLs allow creation of with id="betteridea-nothing"
pcmk__apply_creation_acl trace: ACLs allow creation of with id="betteridea-resources"
=#=#=#= Current cib after: root: Upgrade to latest CIB schema =#=#=#=
=#=#=#= End test: root: Upgrade to latest CIB schema - OK (0) =#=#=#=
* Passed: cibadmin - root: Upgrade to latest CIB schema
=#=#=#= Begin test: unknownguy: Query configuration =#=#=#=
Call failed: Permission denied
=#=#=#= End test: unknownguy: Query configuration - Insufficient privileges (4) =#=#=#=
* Passed: cibadmin - unknownguy: Query configuration
=#=#=#= Begin test: unknownguy: Set enable-acl =#=#=#=
crm_attribute: Error performing operation: Permission denied
=#=#=#= End test: unknownguy: Set enable-acl - Insufficient privileges (4) =#=#=#=
* Passed: crm_attribute - unknownguy: Set enable-acl
=#=#=#= Begin test: unknownguy: Set stonith-enabled =#=#=#=
crm_attribute: Error performing operation: Permission denied
=#=#=#= End test: unknownguy: Set stonith-enabled - Insufficient privileges (4) =#=#=#=
* Passed: crm_attribute - unknownguy: Set stonith-enabled
=#=#=#= Begin test: unknownguy: Create a resource =#=#=#=
pcmk__check_acl trace: User 'unknownguy' without ACLs denied read/write access to /cib/configuration/resources/primitive[@id]
-pcmk__check_acl trace: User 'unknownguy' without ACLs denied read/write access to /cib/configuration/resources/primitive[@class]
-pcmk__check_acl trace: User 'unknownguy' without ACLs denied read/write access to /cib/configuration/resources/primitive[@provider]
-pcmk__check_acl trace: User 'unknownguy' without ACLs denied read/write access to /cib/configuration/resources/primitive[@type]
pcmk__apply_creation_acl trace: Creation of scaffolding with id="" is implicitly allowed
Call failed: Permission denied
+
+
+
+
+
=#=#=#= End test: unknownguy: Create a resource - Insufficient privileges (4) =#=#=#=
* Passed: cibadmin - unknownguy: Create a resource
=#=#=#= Begin test: l33t-haxor: Query configuration =#=#=#=
Call failed: Permission denied
=#=#=#= End test: l33t-haxor: Query configuration - Insufficient privileges (4) =#=#=#=
* Passed: cibadmin - l33t-haxor: Query configuration
=#=#=#= Begin test: l33t-haxor: Set enable-acl =#=#=#=
crm_attribute: Error performing operation: Permission denied
=#=#=#= End test: l33t-haxor: Set enable-acl - Insufficient privileges (4) =#=#=#=
* Passed: crm_attribute - l33t-haxor: Set enable-acl
=#=#=#= Begin test: l33t-haxor: Set stonith-enabled =#=#=#=
crm_attribute: Error performing operation: Permission denied
=#=#=#= End test: l33t-haxor: Set stonith-enabled - Insufficient privileges (4) =#=#=#=
* Passed: crm_attribute - l33t-haxor: Set stonith-enabled
=#=#=#= Begin test: l33t-haxor: Create a resource =#=#=#=
pcmk__check_acl trace: Parent ACL denies user 'l33t-haxor' read/write access to /cib/configuration/resources/primitive[@id='dummy']
pcmk__apply_creation_acl trace: ACLs disallow creation of with id="dummy"
Call failed: Permission denied
=#=#=#= End test: l33t-haxor: Create a resource - Insufficient privileges (4) =#=#=#=
* Passed: cibadmin - l33t-haxor: Create a resource
=#=#=#= Begin test: niceguy: Query configuration =#=#=#=
=#=#=#= End test: niceguy: Query configuration - OK (0) =#=#=#=
* Passed: cibadmin - niceguy: Query configuration
=#=#=#= Begin test: niceguy: Set enable-acl =#=#=#=
pcmk__check_acl trace: Default ACL denies user 'niceguy' read/write access to /cib/configuration/crm_config/cluster_property_set[@id='cib-bootstrap-options']/nvpair[@id='cib-bootstrap-options-enable-acl'][@value]
Error setting enable-acl=false (section=crm_config, set=): Permission denied
crm_attribute: Error performing operation: Permission denied
=#=#=#= End test: niceguy: Set enable-acl - Insufficient privileges (4) =#=#=#=
* Passed: crm_attribute - niceguy: Set enable-acl
=#=#=#= Begin test: niceguy: Set stonith-enabled =#=#=#=
=#=#=#= Current cib after: niceguy: Set stonith-enabled =#=#=#=
=#=#=#= End test: niceguy: Set stonith-enabled - OK (0) =#=#=#=
* Passed: crm_attribute - niceguy: Set stonith-enabled
=#=#=#= Begin test: niceguy: Create a resource =#=#=#=
pcmk__check_acl trace: Default ACL denies user 'niceguy' read/write access to /cib/configuration/resources/primitive[@id='dummy']
pcmk__apply_creation_acl trace: ACLs disallow creation of with id="dummy"
Call failed: Permission denied
=#=#=#= End test: niceguy: Create a resource - Insufficient privileges (4) =#=#=#=
* Passed: cibadmin - niceguy: Create a resource
=#=#=#= Begin test: root: Query configuration =#=#=#=
=#=#=#= End test: root: Query configuration - OK (0) =#=#=#=
* Passed: cibadmin - root: Query configuration
=#=#=#= Begin test: root: Set stonith-enabled =#=#=#=
=#=#=#= Current cib after: root: Set stonith-enabled =#=#=#=
=#=#=#= End test: root: Set stonith-enabled - OK (0) =#=#=#=
* Passed: crm_attribute - root: Set stonith-enabled
=#=#=#= Begin test: root: Create a resource =#=#=#=
=#=#=#= Current cib after: root: Create a resource =#=#=#=
=#=#=#= End test: root: Create a resource - OK (0) =#=#=#=
* Passed: cibadmin - root: Create a resource
=#=#=#= Begin test: l33t-haxor: Create a resource meta attribute =#=#=#=
crm_resource: Error performing operation: Insufficient privileges
=#=#=#= End test: l33t-haxor: Create a resource meta attribute - Insufficient privileges (4) =#=#=#=
* Passed: crm_resource - l33t-haxor: Create a resource meta attribute
=#=#=#= Begin test: l33t-haxor: Query a resource meta attribute =#=#=#=
crm_resource: Error performing operation: Insufficient privileges
=#=#=#= End test: l33t-haxor: Query a resource meta attribute - Insufficient privileges (4) =#=#=#=
* Passed: crm_resource - l33t-haxor: Query a resource meta attribute
=#=#=#= Begin test: l33t-haxor: Remove a resource meta attribute =#=#=#=
crm_resource: Error performing operation: Insufficient privileges
=#=#=#= End test: l33t-haxor: Remove a resource meta attribute - Insufficient privileges (4) =#=#=#=
* Passed: crm_resource - l33t-haxor: Remove a resource meta attribute
=#=#=#= Begin test: niceguy: Create a resource meta attribute =#=#=#=
unpack_resources error: Resource start-up disabled since no STONITH resources have been defined
unpack_resources error: Either configure some or disable STONITH with the stonith-enabled option
unpack_resources error: NOTE: Clusters with shared data need STONITH to ensure data integrity
pcmk__apply_creation_acl trace: Creation of scaffolding with id="dummy-meta_attributes" is implicitly allowed
pcmk__apply_creation_acl trace: ACLs allow creation of with id="dummy-meta_attributes-target-role"
Set 'dummy' option: id=dummy-meta_attributes-target-role set=dummy-meta_attributes name=target-role value=Stopped
=#=#=#= Current cib after: niceguy: Create a resource meta attribute =#=#=#=
=#=#=#= End test: niceguy: Create a resource meta attribute - OK (0) =#=#=#=
* Passed: crm_resource - niceguy: Create a resource meta attribute
=#=#=#= Begin test: niceguy: Query a resource meta attribute =#=#=#=
unpack_resources error: Resource start-up disabled since no STONITH resources have been defined
unpack_resources error: Either configure some or disable STONITH with the stonith-enabled option
unpack_resources error: NOTE: Clusters with shared data need STONITH to ensure data integrity
Stopped
=#=#=#= Current cib after: niceguy: Query a resource meta attribute =#=#=#=
=#=#=#= End test: niceguy: Query a resource meta attribute - OK (0) =#=#=#=
* Passed: crm_resource - niceguy: Query a resource meta attribute
=#=#=#= Begin test: niceguy: Remove a resource meta attribute =#=#=#=
unpack_resources error: Resource start-up disabled since no STONITH resources have been defined
unpack_resources error: Either configure some or disable STONITH with the stonith-enabled option
unpack_resources error: NOTE: Clusters with shared data need STONITH to ensure data integrity
Deleted 'dummy' option: id=dummy-meta_attributes-target-role name=target-role
=#=#=#= Current cib after: niceguy: Remove a resource meta attribute =#=#=#=
=#=#=#= End test: niceguy: Remove a resource meta attribute - OK (0) =#=#=#=
* Passed: crm_resource - niceguy: Remove a resource meta attribute
=#=#=#= Begin test: niceguy: Create a resource meta attribute =#=#=#=
unpack_resources error: Resource start-up disabled since no STONITH resources have been defined
unpack_resources error: Either configure some or disable STONITH with the stonith-enabled option
unpack_resources error: NOTE: Clusters with shared data need STONITH to ensure data integrity
pcmk__apply_creation_acl trace: ACLs allow creation of with id="dummy-meta_attributes-target-role"
Set 'dummy' option: id=dummy-meta_attributes-target-role set=dummy-meta_attributes name=target-role value=Started
=#=#=#= Current cib after: niceguy: Create a resource meta attribute =#=#=#=
=#=#=#= End test: niceguy: Create a resource meta attribute - OK (0) =#=#=#=
* Passed: crm_resource - niceguy: Create a resource meta attribute
=#=#=#= Begin test: badidea: Query configuration - implied deny =#=#=#=
=#=#=#= End test: badidea: Query configuration - implied deny - OK (0) =#=#=#=
* Passed: cibadmin - badidea: Query configuration - implied deny
=#=#=#= Begin test: betteridea: Query configuration - explicit deny =#=#=#=
=#=#=#= End test: betteridea: Query configuration - explicit deny - OK (0) =#=#=#=
* Passed: cibadmin - betteridea: Query configuration - explicit deny
=#=#=#= Begin test: niceguy: Replace - remove acls =#=#=#=
pcmk__check_acl trace: Default ACL denies user 'niceguy' read/write access to /cib[@epoch]
pcmk__check_acl trace: Default ACL denies user 'niceguy' read/write access to /cib/configuration/acls
Call failed: Permission denied
=#=#=#= End test: niceguy: Replace - remove acls - Insufficient privileges (4) =#=#=#=
* Passed: cibadmin - niceguy: Replace - remove acls
=#=#=#= Begin test: niceguy: Replace - create resource =#=#=#=
pcmk__check_acl trace: Default ACL denies user 'niceguy' read/write access to /cib[@epoch]
pcmk__check_acl trace: Default ACL denies user 'niceguy' read/write access to /cib/configuration/resources/primitive[@id='dummy2']
pcmk__apply_creation_acl trace: ACLs disallow creation of with id="dummy2"
Call failed: Permission denied
=#=#=#= End test: niceguy: Replace - create resource - Insufficient privileges (4) =#=#=#=
* Passed: cibadmin - niceguy: Replace - create resource
=#=#=#= Begin test: niceguy: Replace - modify attribute (deny) =#=#=#=
pcmk__check_acl trace: Default ACL denies user 'niceguy' read/write access to /cib[@epoch]
pcmk__check_acl trace: Default ACL denies user 'niceguy' read/write access to /cib/configuration/crm_config/cluster_property_set[@id='cib-bootstrap-options']/nvpair[@id='cib-bootstrap-options-enable-acl'][@value]
Call failed: Permission denied
=#=#=#= End test: niceguy: Replace - modify attribute (deny) - Insufficient privileges (4) =#=#=#=
* Passed: cibadmin - niceguy: Replace - modify attribute (deny)
=#=#=#= Begin test: niceguy: Replace - delete attribute (deny) =#=#=#=
pcmk__check_acl trace: Default ACL denies user 'niceguy' read/write access to /cib[@epoch]
pcmk__check_acl trace: Default ACL denies user 'niceguy' read/write access to /cib/configuration/crm_config/cluster_property_set[@id='cib-bootstrap-options']/nvpair[@id='cib-bootstrap-options-enable-acl']
Call failed: Permission denied
=#=#=#= End test: niceguy: Replace - delete attribute (deny) - Insufficient privileges (4) =#=#=#=
* Passed: cibadmin - niceguy: Replace - delete attribute (deny)
=#=#=#= Begin test: niceguy: Replace - create attribute (deny) =#=#=#=
pcmk__check_acl trace: Default ACL denies user 'niceguy' read/write access to /cib[@epoch]
pcmk__check_acl trace: Default ACL denies user 'niceguy' read/write access to /cib/configuration/resources/primitive[@id='dummy'][@description]
Call failed: Permission denied
=#=#=#= End test: niceguy: Replace - create attribute (deny) - Insufficient privileges (4) =#=#=#=
* Passed: cibadmin - niceguy: Replace - create attribute (deny)
=#=#=#= Begin test: bob: Replace - create attribute (direct allow) =#=#=#=
=#=#=#= End test: bob: Replace - create attribute (direct allow) - OK (0) =#=#=#=
* Passed: cibadmin - bob: Replace - create attribute (direct allow)
=#=#=#= Begin test: bob: Replace - modify attribute (direct allow) =#=#=#=
=#=#=#= End test: bob: Replace - modify attribute (direct allow) - OK (0) =#=#=#=
* Passed: cibadmin - bob: Replace - modify attribute (direct allow)
=#=#=#= Begin test: bob: Replace - delete attribute (direct allow) =#=#=#=
=#=#=#= End test: bob: Replace - delete attribute (direct allow) - OK (0) =#=#=#=
* Passed: cibadmin - bob: Replace - delete attribute (direct allow)
=#=#=#= Begin test: joe: Replace - create attribute (inherited allow) =#=#=#=
=#=#=#= End test: joe: Replace - create attribute (inherited allow) - OK (0) =#=#=#=
* Passed: cibadmin - joe: Replace - create attribute (inherited allow)
=#=#=#= Begin test: joe: Replace - modify attribute (inherited allow) =#=#=#=
=#=#=#= End test: joe: Replace - modify attribute (inherited allow) - OK (0) =#=#=#=
* Passed: cibadmin - joe: Replace - modify attribute (inherited allow)
=#=#=#= Begin test: joe: Replace - delete attribute (inherited allow) =#=#=#=
=#=#=#= End test: joe: Replace - delete attribute (inherited allow) - OK (0) =#=#=#=
* Passed: cibadmin - joe: Replace - delete attribute (inherited allow)
=#=#=#= Begin test: mike: Replace - create attribute (allow overrides deny) =#=#=#=
=#=#=#= End test: mike: Replace - create attribute (allow overrides deny) - OK (0) =#=#=#=
* Passed: cibadmin - mike: Replace - create attribute (allow overrides deny)
=#=#=#= Begin test: mike: Replace - modify attribute (allow overrides deny) =#=#=#=
=#=#=#= End test: mike: Replace - modify attribute (allow overrides deny) - OK (0) =#=#=#=
* Passed: cibadmin - mike: Replace - modify attribute (allow overrides deny)
=#=#=#= Begin test: mike: Replace - delete attribute (allow overrides deny) =#=#=#=
=#=#=#= End test: mike: Replace - delete attribute (allow overrides deny) - OK (0) =#=#=#=
* Passed: cibadmin - mike: Replace - delete attribute (allow overrides deny)
=#=#=#= Begin test: chris: Replace - create attribute (deny overrides allow) =#=#=#=
pcmk__check_acl trace: Parent ACL denies user 'chris' read/write access to /cib/configuration/resources/primitive[@id='dummy'][@description]
Call failed: Permission denied
=#=#=#= End test: chris: Replace - create attribute (deny overrides allow) - Insufficient privileges (4) =#=#=#=
* Passed: cibadmin - chris: Replace - create attribute (deny overrides allow)
=#=#=#= Begin test: chris: Replace - modify attribute (deny overrides allow) =#=#=#=
pcmk__check_acl trace: Parent ACL denies user 'chris' read/write access to /cib/configuration/resources/primitive[@id='dummy'][@description]
Call failed: Permission denied
=#=#=#= End test: chris: Replace - modify attribute (deny overrides allow) - Insufficient privileges (4) =#=#=#=
* Passed: cibadmin - chris: Replace - modify attribute (deny overrides allow)
=#=#=#= Begin test: chris: Replace - delete attribute (deny overrides allow) =#=#=#=
pcmk__check_acl trace: Parent ACL denies user 'chris' read/write access to /cib/configuration/resources/primitive[@id='dummy']
Call failed: Permission denied
=#=#=#= End test: chris: Replace - delete attribute (deny overrides allow) - Insufficient privileges (4) =#=#=#=
* Passed: cibadmin - chris: Replace - delete attribute (deny overrides allow)
diff --git a/include/crm/common/xml.h b/include/crm/common/xml.h
index 9182f55361..300006f334 100644
--- a/include/crm/common/xml.h
+++ b/include/crm/common/xml.h
@@ -1,307 +1,317 @@
/*
* Copyright 2004-2022 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__H
# define PCMK__CRM_COMMON_XML__H
# include
# include
# include
# include
# include
# include
# include
# include
# include
# include
#ifdef __cplusplus
extern "C" {
#endif
/**
* \file
* \brief Wrappers for and extensions to libxml2
* \ingroup core
*/
/* Define compression parameters for IPC messages
*
* Compression costs a LOT, so we don't want to do it unless we're hitting
* message limits. Currently, we use 128KB as the threshold, because higher
* values don't play well with the heartbeat stack. With an earlier limit of
* 10KB, compressing 184 of 1071 messages accounted for 23% of the total CPU
* used by the cib.
*/
# define CRM_BZ2_BLOCKS 4
# define CRM_BZ2_WORK 20
# define CRM_BZ2_THRESHOLD 128 * 1024
typedef const xmlChar *pcmkXmlStr;
gboolean add_message_xml(xmlNode * msg, const char *field, xmlNode * xml);
xmlNode *get_message_xml(xmlNode * msg, const char *field);
xmlDoc *getDocPtr(xmlNode * node);
/*
- * Replacement function for xmlCopyPropList which at the very least,
- * doesn't work the way *I* would expect it to.
+ * \brief xmlCopyPropList ACLs-sensitive replacement expading i++ notation
*
- * Copy all the attributes/properties from src into target.
+ * The gist is the same as with \c{xmlCopyPropList(target, src->properties)}.
+ * The function exits prematurely when any attribute cannot be copied for
+ * ACLs violation. Even without bailing out, the result can possibly be
+ * incosistent with expectations in that case, hence the caller shall,
+ * aposteriori, verify that no document-level-tracked denial was indicated
+ * with \c{xml_acl_denied(target)} and drop whole such intermediate object.
*
- * Not recursive, does not return anything.
+ * \param[in,out] target Element to receive attributes from #src element
+ * \param[in] src Element carrying attributes to copy over to #target
*
+ * \note Original commit 1c632c506 sadly haven't stated which otherwise
+ * assumed behaviours of xmlCopyPropList were missing beyond otherwise
+ * custom extensions like said ACLs and "atomic increment" (that landed
+ * later on, anyway).
*/
-void copy_in_properties(xmlNode * target, xmlNode * src);
+void copy_in_properties(xmlNode *target, xmlNode *src);
+
void expand_plus_plus(xmlNode * target, const char *name, const char *value);
void fix_plus_plus_recursive(xmlNode * target);
/*
* Create a node named "name" as a child of "parent"
* If parent is NULL, creates an unconnected node.
*
* Returns the created node
*
*/
xmlNode *create_xml_node(xmlNode * parent, const char *name);
/*
* Create a node named "name" as a child of "parent", giving it the provided
* text content.
* If parent is NULL, creates an unconnected node.
*
* Returns the created node
*
*/
xmlNode *pcmk_create_xml_text_node(xmlNode * parent, const char *name, const char *content);
/*
* Create a new HTML node named "element_name" as a child of "parent", giving it the
* provided text content. Optionally, apply a CSS #id and #class.
*
* Returns the created node.
*/
xmlNode *pcmk_create_html_node(xmlNode * parent, const char *element_name, const char *id,
const char *class_name, const char *text);
/*
*
*/
void purge_diff_markers(xmlNode * a_node);
/*
* Returns a deep copy of src_node
*
*/
xmlNode *copy_xml(xmlNode * src_node);
/*
* Add a copy of xml_node to new_parent
*/
xmlNode *add_node_copy(xmlNode * new_parent, xmlNode * xml_node);
int add_node_nocopy(xmlNode * parent, const char *name, xmlNode * child);
/*
* XML I/O Functions
*
* Whitespace between tags is discarded.
*/
xmlNode *filename2xml(const char *filename);
xmlNode *stdin2xml(void);
xmlNode *string2xml(const char *input);
int write_xml_fd(xmlNode * xml_node, const char *filename, int fd, gboolean compress);
int write_xml_file(xmlNode * xml_node, const char *filename, gboolean compress);
char *dump_xml_formatted(xmlNode * msg);
/* Also dump the text node with xml_log_option_text enabled */
char *dump_xml_formatted_with_text(xmlNode * msg);
char *dump_xml_unformatted(xmlNode * msg);
/*
* Diff related Functions
*/
xmlNode *diff_xml_object(xmlNode * left, xmlNode * right, gboolean suppress);
xmlNode *subtract_xml_object(xmlNode * parent, xmlNode * left, xmlNode * right,
gboolean full, gboolean * changed, const char *marker);
gboolean can_prune_leaf(xmlNode * xml_node);
/*
* Searching & Modifying
*/
xmlNode *find_xml_node(xmlNode * cib, const char *node_path, gboolean must_find);
void xml_remove_prop(xmlNode * obj, const char *name);
gboolean replace_xml_child(xmlNode * parent, xmlNode * child, xmlNode * update,
gboolean delete_only);
gboolean update_xml_child(xmlNode * child, xmlNode * to_update);
int find_xml_children(xmlNode ** children, xmlNode * root,
const char *tag, const char *field, const char *value,
gboolean search_matches);
xmlNode *get_xpath_object(const char *xpath, xmlNode * xml_obj, int error_level);
xmlNode *get_xpath_object_relative(const char *xpath, xmlNode * xml_obj, int error_level);
static inline const char *
crm_element_name(const xmlNode *xml)
{
return xml? (const char *)(xml->name) : NULL;
}
static inline const char *
crm_map_element_name(const xmlNode *xml)
{
const char *name = crm_element_name(xml);
if (strcmp(name, "master") == 0) {
return "clone";
} else {
return name;
}
}
gboolean xml_has_children(const xmlNode * root);
char *calculate_on_disk_digest(xmlNode * local_cib);
char *calculate_operation_digest(xmlNode * local_cib, const char *version);
char *calculate_xml_versioned_digest(xmlNode * input, gboolean sort, gboolean do_filter,
const char *version);
/* schema-related functions (from schemas.c) */
gboolean validate_xml(xmlNode * xml_blob, const char *validation, gboolean to_logs);
gboolean validate_xml_verbose(xmlNode * xml_blob);
/*!
* \brief Update CIB XML to most recent schema version
*
* "Update" means either actively employ XSLT-based transformation(s)
* (if intermediate product to transform valid per its declared schema version,
* transformation available, proceeded successfully with a result valid per
* expectated newer schema version), or just try to bump the marked validating
* schema until all gradually rising schema versions attested or the first
* such attempt subsequently fails to validate. Which of the two styles will
* be used depends on \p transform parameter (positive/negative, respectively).
*
* \param[in,out] xml_blob XML tree representing CIB, may be swapped with
* an "updated" one
* \param[out] best The highest configuration version (per its index
* in the global schemas table) it was possible to
* reach during the update steps while ensuring
* the validity of the result; if no validation
* success was observed against possibly multiple
* schemas, the value is less or equal the result
* of \c get_schema_version applied on the input
* \p xml_blob value (unless that function maps it
* to -1, then 0 would be used instead)
* \param[in] max When \p transform is positive, this allows to
* set upper boundary schema (per its index in the
* global schemas table) beyond which it's forbidden
* to update by the means of XSLT transformation
* \param[in] transform Whether to employ XSLT-based transformation so
* as to allow overcoming possible incompatibilities
* between major schema versions (see above)
* \param[in] to_logs If true, output notable progress info to
* internal log streams; if false, to stderr
*
* \return \c pcmk_ok if no non-recoverable error encountered (up to
* caller to evaluate if the update satisfies the requirements
* per returned \p best value), negative value carrying the reason
* otherwise
*/
int update_validation(xmlNode **xml_blob, int *best, int max,
gboolean transform, gboolean to_logs);
int get_schema_version(const char *name);
const char *get_schema_name(int version);
const char *xml_latest_schema(void);
gboolean cli_config_update(xmlNode ** xml, int *best_version, gboolean to_logs);
/*!
* \brief Initialize the CRM XML subsystem
*
* This method sets global XML settings and loads pacemaker schemas into the cache.
*/
void crm_xml_init(void);
void crm_xml_cleanup(void);
void pcmk_free_xml_subtree(xmlNode *xml);
void free_xml(xmlNode * child);
xmlNode *first_named_child(const xmlNode *parent, const char *name);
xmlNode *crm_next_same_xml(const xmlNode *sibling);
xmlNode *sorted_xml(xmlNode * input, xmlNode * parent, gboolean recursive);
xmlXPathObjectPtr xpath_search(xmlNode * xml_top, const char *path);
void crm_foreach_xpath_result(xmlNode *xml, const char *xpath,
void (*helper)(xmlNode*, void*), void *user_data);
xmlNode *expand_idref(xmlNode * input, xmlNode * top);
void freeXpathObject(xmlXPathObjectPtr xpathObj);
xmlNode *getXpathResult(xmlXPathObjectPtr xpathObj, int index);
void dedupXpathResults(xmlXPathObjectPtr xpathObj);
static inline int numXpathResults(xmlXPathObjectPtr xpathObj)
{
if(xpathObj == NULL || xpathObj->nodesetval == NULL) {
return 0;
}
return xpathObj->nodesetval->nodeNr;
}
bool xml_tracking_changes(xmlNode * xml);
bool xml_document_dirty(xmlNode *xml);
void xml_track_changes(xmlNode * xml, const char *user, xmlNode *acl_source, bool enforce_acls);
void xml_calculate_changes(xmlNode *old_xml, xmlNode *new_xml);
void xml_calculate_significant_changes(xmlNode *old_xml, xmlNode *new_xml);
void xml_accept_changes(xmlNode * xml);
void xml_log_changes(uint8_t level, const char *function, xmlNode *xml);
void xml_log_patchset(uint8_t level, const char *function, xmlNode *xml);
bool xml_patch_versions(xmlNode *patchset, int add[3], int del[3]);
xmlNode *xml_create_patchset(
int format, xmlNode *source, xmlNode *target, bool *config, bool manage_version);
int xml_apply_patchset(xmlNode *xml, xmlNode *patchset, bool check_version);
void patchset_process_digest(xmlNode *patch, xmlNode *source, xmlNode *target, bool with_digest);
void save_xml_to_file(xmlNode * xml, const char *desc, const char *filename);
char *xml_get_path(xmlNode *xml);
char * crm_xml_escape(const char *text);
void crm_xml_sanitize_id(char *id);
void crm_xml_set_id(xmlNode *xml, const char *format, ...) G_GNUC_PRINTF(2, 3);
/*!
* \brief xmlNode destructor which can be used in glib collections
*/
void crm_destroy_xml(gpointer data);
#if !defined(PCMK_ALLOW_DEPRECATED) || (PCMK_ALLOW_DEPRECATED == 1)
#include
#endif
#ifdef __cplusplus
}
#endif
#endif
diff --git a/lib/cib/cib_ops.c b/lib/cib/cib_ops.c
index ce8c00ad6e..b91a00fe4e 100644
--- a/lib/cib/cib_ops.c
+++ b/lib/cib/cib_ops.c
@@ -1,849 +1,854 @@
/*
* Copyright 2004-2022 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU Lesser General Public License
* version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
int
cib_process_query(const char *op, int options, const char *section, xmlNode * req, xmlNode * input,
xmlNode * existing_cib, xmlNode ** result_cib, xmlNode ** answer)
{
xmlNode *obj_root = NULL;
int result = pcmk_ok;
crm_trace("Processing %s for %s section",
op, pcmk__s(section, "unspecified"));
if (options & cib_xpath) {
return cib_process_xpath(op, options, section, req, input,
existing_cib, result_cib, answer);
}
CRM_CHECK(*answer == NULL, free_xml(*answer));
*answer = NULL;
if (pcmk__str_eq(XML_CIB_TAG_SECTION_ALL, section, pcmk__str_casei)) {
section = NULL;
}
obj_root = pcmk_find_cib_element(existing_cib, section);
if (obj_root == NULL) {
result = -ENXIO;
} else if (options & cib_no_children) {
const char *tag = TYPE(obj_root);
xmlNode *shallow = create_xml_node(*answer, tag);
copy_in_properties(shallow, obj_root);
*answer = shallow;
} else {
*answer = obj_root;
}
if (result == pcmk_ok && *answer == NULL) {
crm_err("Error creating query response");
result = -ENOMSG;
}
return result;
}
int
cib_process_erase(const char *op, int options, const char *section, xmlNode * req, xmlNode * input,
xmlNode * existing_cib, xmlNode ** result_cib, xmlNode ** answer)
{
int result = pcmk_ok;
crm_trace("Processing \"%s\" event", op);
*answer = NULL;
free_xml(*result_cib);
*result_cib = createEmptyCib(0);
copy_in_properties(*result_cib, existing_cib);
cib_update_counter(*result_cib, XML_ATTR_GENERATION_ADMIN, FALSE);
return result;
}
int
cib_process_upgrade(const char *op, int options, const char *section, xmlNode * req,
xmlNode * input, xmlNode * existing_cib, xmlNode ** result_cib,
xmlNode ** answer)
{
int rc = 0;
int new_version = 0;
int current_version = 0;
int max_version = 0;
const char *max = crm_element_value(req, F_CIB_SCHEMA_MAX);
const char *value = crm_element_value(existing_cib, XML_ATTR_VALIDATION);
*answer = NULL;
crm_trace("Processing \"%s\" event with max=%s", op, max);
if (value != NULL) {
current_version = get_schema_version(value);
}
if (max) {
max_version = get_schema_version(max);
}
rc = update_validation(result_cib, &new_version, max_version, TRUE,
!(options & cib_verbose));
if (new_version > current_version) {
cib_update_counter(*result_cib, XML_ATTR_GENERATION_ADMIN, FALSE);
cib_update_counter(*result_cib, XML_ATTR_GENERATION, TRUE);
cib_update_counter(*result_cib, XML_ATTR_NUMUPDATES, TRUE);
return pcmk_ok;
}
return rc;
}
int
cib_process_bump(const char *op, int options, const char *section, xmlNode * req, xmlNode * input,
xmlNode * existing_cib, xmlNode ** result_cib, xmlNode ** answer)
{
int result = pcmk_ok;
crm_trace("Processing %s for epoch='%s'", op,
pcmk__s(crm_element_value(existing_cib, XML_ATTR_GENERATION), ""));
*answer = NULL;
cib_update_counter(*result_cib, XML_ATTR_GENERATION, FALSE);
return result;
}
int
cib_update_counter(xmlNode * xml_obj, const char *field, gboolean reset)
{
char *new_value = NULL;
char *old_value = NULL;
int int_value = -1;
if (reset == FALSE && crm_element_value(xml_obj, field) != NULL) {
old_value = crm_element_value_copy(xml_obj, field);
}
if (old_value != NULL) {
int_value = atoi(old_value);
new_value = pcmk__itoa(++int_value);
} else {
new_value = strdup("1");
CRM_ASSERT(new_value != NULL);
}
crm_trace("Update %s from %s to %s",
field, pcmk__s(old_value, "unset"), new_value);
crm_xml_add(xml_obj, field, new_value);
free(new_value);
free(old_value);
return pcmk_ok;
}
int
cib_process_replace(const char *op, int options, const char *section, xmlNode * req,
xmlNode * input, xmlNode * existing_cib, xmlNode ** result_cib,
xmlNode ** answer)
{
const char *tag = NULL;
int result = pcmk_ok;
crm_trace("Processing %s for %s section",
op, pcmk__s(section, "unspecified"));
if (options & cib_xpath) {
return cib_process_xpath(op, options, section, req, input,
existing_cib, result_cib, answer);
}
*answer = NULL;
if (input == NULL) {
return -EINVAL;
}
tag = crm_element_name(input);
if (pcmk__str_eq(XML_CIB_TAG_SECTION_ALL, section, pcmk__str_casei)) {
section = NULL;
} else if (pcmk__str_eq(tag, section, pcmk__str_casei)) {
section = NULL;
}
if (pcmk__str_eq(tag, XML_TAG_CIB, pcmk__str_casei)) {
int updates = 0;
int epoch = 0;
int admin_epoch = 0;
int replace_updates = 0;
int replace_epoch = 0;
int replace_admin_epoch = 0;
const char *reason = NULL;
const char *peer = crm_element_value(req, F_ORIG);
const char *digest = crm_element_value(req, XML_ATTR_DIGEST);
if (digest) {
const char *version = crm_element_value(req, XML_ATTR_CRM_VERSION);
char *digest_verify = calculate_xml_versioned_digest(input, FALSE, TRUE,
version ? version :
CRM_FEATURE_SET);
if (!pcmk__str_eq(digest_verify, digest, pcmk__str_casei)) {
crm_err("Digest mis-match on replace from %s: %s vs. %s (expected)", peer,
digest_verify, digest);
reason = "digest mismatch";
} else {
crm_info("Digest matched on replace from %s: %s", peer, digest);
}
free(digest_verify);
} else {
crm_trace("No digest to verify");
}
cib_version_details(existing_cib, &admin_epoch, &epoch, &updates);
cib_version_details(input, &replace_admin_epoch, &replace_epoch, &replace_updates);
if (replace_admin_epoch < admin_epoch) {
reason = XML_ATTR_GENERATION_ADMIN;
} else if (replace_admin_epoch > admin_epoch) {
/* no more checks */
} else if (replace_epoch < epoch) {
reason = XML_ATTR_GENERATION;
} else if (replace_epoch > epoch) {
/* no more checks */
} else if (replace_updates < updates) {
reason = XML_ATTR_NUMUPDATES;
}
if (reason != NULL) {
crm_info("Replacement %d.%d.%d from %s not applied to %d.%d.%d:"
" current %s is greater than the replacement",
replace_admin_epoch, replace_epoch,
replace_updates, peer, admin_epoch, epoch, updates, reason);
result = -pcmk_err_old_data;
} else {
crm_info("Replaced %d.%d.%d with %d.%d.%d from %s",
admin_epoch, epoch, updates,
replace_admin_epoch, replace_epoch, replace_updates, peer);
}
free_xml(*result_cib);
*result_cib = copy_xml(input);
} else {
xmlNode *obj_root = NULL;
gboolean ok = TRUE;
obj_root = pcmk_find_cib_element(*result_cib, section);
ok = replace_xml_child(NULL, obj_root, input, FALSE);
if (ok == FALSE) {
crm_trace("No matching object to replace");
result = -ENXIO;
}
}
return result;
}
int
cib_process_delete(const char *op, int options, const char *section, xmlNode * req, xmlNode * input,
xmlNode * existing_cib, xmlNode ** result_cib, xmlNode ** answer)
{
xmlNode *obj_root = NULL;
crm_trace("Processing \"%s\" event", op);
if (options & cib_xpath) {
return cib_process_xpath(op, options, section, req, input,
existing_cib, result_cib, answer);
}
if (input == NULL) {
crm_err("Cannot perform modification with no data");
return -EINVAL;
}
obj_root = pcmk_find_cib_element(*result_cib, section);
if(pcmk__str_eq(crm_element_name(input), section, pcmk__str_casei)) {
xmlNode *child = NULL;
for (child = pcmk__xml_first_child(input); child;
child = pcmk__xml_next(child)) {
if (replace_xml_child(NULL, obj_root, child, TRUE) == FALSE) {
crm_trace("No matching object to delete: %s=%s", child->name, ID(child));
}
}
} else if (replace_xml_child(NULL, obj_root, input, TRUE) == FALSE) {
crm_trace("No matching object to delete: %s=%s", input->name, ID(input));
}
return pcmk_ok;
}
int
cib_process_modify(const char *op, int options, const char *section, xmlNode * req, xmlNode * input,
xmlNode * existing_cib, xmlNode ** result_cib, xmlNode ** answer)
{
xmlNode *obj_root = NULL;
crm_trace("Processing \"%s\" event", op);
if (options & cib_xpath) {
return cib_process_xpath(op, options, section, req, input,
existing_cib, result_cib, answer);
}
if (input == NULL) {
crm_err("Cannot perform modification with no data");
return -EINVAL;
}
obj_root = pcmk_find_cib_element(*result_cib, section);
if (obj_root == NULL) {
xmlNode *tmp_section = NULL;
const char *path = pcmk_cib_parent_name_for(section);
if (path == NULL) {
return -EINVAL;
}
tmp_section = create_xml_node(NULL, section);
cib_process_xpath(CIB_OP_CREATE, 0, path, NULL, tmp_section, NULL, result_cib, answer);
free_xml(tmp_section);
obj_root = pcmk_find_cib_element(*result_cib, section);
}
CRM_CHECK(obj_root != NULL, return -EINVAL);
if (update_xml_child(obj_root, input) == FALSE) {
if (options & cib_can_create) {
add_node_copy(obj_root, input);
} else {
return -ENXIO;
}
}
if(options & cib_mixed_update) {
int max = 0, lpc;
xmlXPathObjectPtr xpathObj = xpath_search(*result_cib, "//@__delete__");
if (xpathObj) {
max = numXpathResults(xpathObj);
crm_log_xml_trace(*result_cib, "Mixed result");
}
for (lpc = 0; lpc < max; lpc++) {
xmlNode *match = getXpathResult(xpathObj, lpc);
xmlChar *match_path = xmlGetNodePath(match);
crm_debug("Destroying %s", match_path);
free(match_path);
free_xml(match);
}
freeXpathObject(xpathObj);
}
return pcmk_ok;
}
static int
update_cib_object(xmlNode * parent, xmlNode * update)
{
int result = pcmk_ok;
xmlNode *target = NULL;
xmlNode *a_child = NULL;
const char *replace = NULL;
const char *object_id = NULL;
const char *object_name = NULL;
CRM_CHECK(update != NULL, return -EINVAL);
CRM_CHECK(parent != NULL, return -EINVAL);
object_name = crm_element_name(update);
CRM_CHECK(object_name != NULL, return -EINVAL);
object_id = ID(update);
crm_trace("Processing update for <%s%s%s%s>", object_name,
((object_id == NULL)? "" : " id='"), pcmk__s(object_id, ""),
((object_id == NULL)? "" : "'"));
if (object_id == NULL) {
/* placeholder object */
target = find_xml_node(parent, object_name, FALSE);
} else {
target = pcmk__xe_match(parent, object_name, XML_ATTR_ID, object_id);
}
if (target == NULL) {
target = create_xml_node(parent, object_name);
}
crm_trace("Found node <%s%s%s%s> to update", object_name,
((object_id == NULL)? "" : " id='"), pcmk__s(object_id, ""),
((object_id == NULL)? "" : "'"));
replace = crm_element_value(update, XML_CIB_ATTR_REPLACE);
if (replace != NULL) {
xmlNode *remove = NULL;
int last = 0, lpc = 0, len = 0;
len = strlen(replace);
while (lpc <= len) {
if (replace[lpc] == ',' || replace[lpc] == 0) {
char *replace_item = NULL;
if (last == lpc) {
/* nothing to do */
last = lpc + 1;
goto incr;
}
replace_item = strndup(replace + last, lpc - last);
remove = find_xml_node(target, replace_item, FALSE);
if (remove != NULL) {
crm_trace("Replacing node <%s> in <%s>",
replace_item, crm_element_name(target));
free_xml(remove);
remove = NULL;
}
free(replace_item);
last = lpc + 1;
}
incr:
lpc++;
}
xml_remove_prop(update, XML_CIB_ATTR_REPLACE);
xml_remove_prop(target, XML_CIB_ATTR_REPLACE);
}
copy_in_properties(target, update);
+ if (xml_acl_denied(target)) {
+ crm_notice("Cannot update <%s id=%s>", pcmk__s(object_name, ""), pcmk__s(object_id, ""));
+ return -EACCES;
+ }
+
crm_trace("Processing children of <%s%s%s%s>", object_name,
((object_id == NULL)? "" : " id='"), pcmk__s(object_id, ""),
((object_id == NULL)? "" : "'"));
for (a_child = pcmk__xml_first_child(update); a_child != NULL;
a_child = pcmk__xml_next(a_child)) {
int tmp_result = 0;
crm_trace("Updating child <%s%s%s%s>", crm_element_name(a_child),
((ID(a_child) == NULL)? "" : " id='"),
pcmk__s(ID(a_child), ""), ((ID(a_child) == NULL)? "" : "'"));
tmp_result = update_cib_object(target, a_child);
/* only the first error is likely to be interesting */
if (tmp_result != pcmk_ok) {
crm_err("Error updating child <%s%s%s%s>",
crm_element_name(a_child),
((ID(a_child) == NULL)? "" : " id='"),
pcmk__s(ID(a_child), ""),
((ID(a_child) == NULL)? "" : "'"));
if (result == pcmk_ok) {
result = tmp_result;
}
}
}
crm_trace("Finished handling update for <%s%s%s%s>", object_name,
((object_id == NULL)? "" : " id='"), pcmk__s(object_id, ""),
((object_id == NULL)? "" : "'"));
return result;
}
static int
add_cib_object(xmlNode * parent, xmlNode * new_obj)
{
const char *object_name = NULL;
const char *object_id = NULL;
xmlNode *equiv_node = NULL;
if ((parent == NULL) || (new_obj == NULL)) {
return -EINVAL;
}
object_name = crm_element_name(new_obj);
if (object_name == NULL) {
return -EINVAL;
}
object_id = ID(new_obj);
crm_trace("Processing creation of <%s%s%s%s>", object_name,
((object_id == NULL)? "" : " id='"), pcmk__s(object_id, ""),
((object_id == NULL)? "" : "'"));
if (object_id == NULL) {
equiv_node = find_xml_node(parent, object_name, FALSE);
} else {
equiv_node = pcmk__xe_match(parent, object_name, XML_ATTR_ID,
object_id);
}
if (equiv_node != NULL) {
return -EEXIST;
}
return update_cib_object(parent, new_obj);
}
int
cib_process_create(const char *op, int options, const char *section, xmlNode * req, xmlNode * input,
xmlNode * existing_cib, xmlNode ** result_cib, xmlNode ** answer)
{
xmlNode *failed = NULL;
int result = pcmk_ok;
xmlNode *update_section = NULL;
crm_trace("Processing %s for %s section",
op, pcmk__s(section, "unspecified"));
if (pcmk__str_eq(XML_CIB_TAG_SECTION_ALL, section, pcmk__str_casei)) {
section = NULL;
} else if (pcmk__str_eq(XML_TAG_CIB, section, pcmk__str_casei)) {
section = NULL;
} else if (pcmk__str_eq(crm_element_name(input), XML_TAG_CIB, pcmk__str_casei)) {
section = NULL;
}
CRM_CHECK(strcasecmp(CIB_OP_CREATE, op) == 0, return -EINVAL);
if (input == NULL) {
crm_err("Cannot perform modification with no data");
return -EINVAL;
}
if (section == NULL) {
return cib_process_modify(op, options, section, req, input, existing_cib, result_cib,
answer);
}
failed = create_xml_node(NULL, XML_TAG_FAILED);
update_section = pcmk_find_cib_element(*result_cib, section);
if (pcmk__str_eq(crm_element_name(input), section, pcmk__str_casei)) {
xmlNode *a_child = NULL;
for (a_child = pcmk__xml_first_child(input); a_child != NULL;
a_child = pcmk__xml_next(a_child)) {
result = add_cib_object(update_section, a_child);
if (update_results(failed, a_child, op, result)) {
break;
}
}
} else {
result = add_cib_object(update_section, input);
update_results(failed, input, op, result);
}
if ((result == pcmk_ok) && xml_has_children(failed)) {
result = -EINVAL;
}
if (result != pcmk_ok) {
crm_log_xml_err(failed, "CIB Update failures");
*answer = failed;
} else {
free_xml(failed);
}
return result;
}
int
cib_process_diff(const char *op, int options, const char *section, xmlNode * req, xmlNode * input,
xmlNode * existing_cib, xmlNode ** result_cib, xmlNode ** answer)
{
const char *originator = NULL;
if (req != NULL) {
originator = crm_element_value(req, F_ORIG);
}
crm_trace("Processing \"%s\" event from %s%s",
op, originator,
(pcmk_is_set(options, cib_force_diff)? " (global update)" : ""));
free_xml(*result_cib);
*result_cib = copy_xml(existing_cib);
return xml_apply_patchset(*result_cib, input, TRUE);
}
gboolean
cib_config_changed(xmlNode * last, xmlNode * next, xmlNode ** diff)
{
int lpc = 0, max = 0;
gboolean config_changes = FALSE;
xmlXPathObject *xpathObj = NULL;
int format = 1;
CRM_ASSERT(diff != NULL);
if (*diff == NULL && last != NULL && next != NULL) {
*diff = diff_xml_object(last, next, FALSE);
}
if (*diff == NULL) {
goto done;
}
crm_element_value_int(*diff, "format", &format);
/* This function only applies to v1 diffs. */
CRM_LOG_ASSERT(format == 1);
xpathObj = xpath_search(*diff, "//" XML_CIB_TAG_CONFIGURATION);
if (numXpathResults(xpathObj) > 0) {
config_changes = TRUE;
goto done;
}
freeXpathObject(xpathObj);
/*
* Do not check XML_TAG_DIFF_ADDED "//" XML_TAG_CIB
* This always contains every field and would produce a false positive
* every time if the checked value existed
*/
xpathObj = xpath_search(*diff, "//" XML_TAG_DIFF_REMOVED "//" XML_TAG_CIB);
max = numXpathResults(xpathObj);
for (lpc = 0; lpc < max; lpc++) {
xmlNode *top = getXpathResult(xpathObj, lpc);
if (crm_element_value(top, XML_ATTR_GENERATION) != NULL) {
config_changes = TRUE;
goto done;
}
if (crm_element_value(top, XML_ATTR_GENERATION_ADMIN) != NULL) {
config_changes = TRUE;
goto done;
}
if (crm_element_value(top, XML_ATTR_VALIDATION) != NULL) {
config_changes = TRUE;
goto done;
}
if (crm_element_value(top, XML_ATTR_CRM_VERSION) != NULL) {
config_changes = TRUE;
goto done;
}
if (crm_element_value(top, "remote-clear-port") != NULL) {
config_changes = TRUE;
goto done;
}
if (crm_element_value(top, "remote-tls-port") != NULL) {
config_changes = TRUE;
goto done;
}
}
done:
freeXpathObject(xpathObj);
return config_changes;
}
int
cib_process_xpath(const char *op, int options, const char *section, xmlNode * req, xmlNode * input,
xmlNode * existing_cib, xmlNode ** result_cib, xmlNode ** answer)
{
int lpc = 0;
int max = 0;
int rc = pcmk_ok;
gboolean is_query = pcmk__str_eq(op, CIB_OP_QUERY, pcmk__str_casei);
xmlXPathObjectPtr xpathObj = NULL;
crm_trace("Processing \"%s\" event", op);
if (is_query) {
xpathObj = xpath_search(existing_cib, section);
} else {
xpathObj = xpath_search(*result_cib, section);
}
max = numXpathResults(xpathObj);
if (max < 1 && pcmk__str_eq(op, CIB_OP_DELETE, pcmk__str_casei)) {
crm_debug("%s was already removed", section);
} else if (max < 1) {
crm_debug("%s: %s does not exist", op, section);
rc = -ENXIO;
} else if (is_query) {
if (max > 1) {
*answer = create_xml_node(NULL, "xpath-query");
}
}
if (pcmk__str_eq(op, CIB_OP_DELETE, pcmk__str_casei) && (options & cib_multiple)) {
dedupXpathResults(xpathObj);
}
for (lpc = 0; lpc < max; lpc++) {
xmlChar *path = NULL;
xmlNode *match = getXpathResult(xpathObj, lpc);
if (match == NULL) {
continue;
}
path = xmlGetNodePath(match);
crm_debug("Processing %s op for %s with %s", op, section, path);
free(path);
if (pcmk__str_eq(op, CIB_OP_DELETE, pcmk__str_casei)) {
if (match == *result_cib) {
/* Attempting to delete the whole "/cib" */
crm_warn("Cannot perform %s for %s: The xpath is addressing the whole /cib", op, section);
rc = -EINVAL;
break;
}
free_xml(match);
if ((options & cib_multiple) == 0) {
break;
}
} else if (pcmk__str_eq(op, CIB_OP_MODIFY, pcmk__str_casei)) {
if (update_xml_child(match, input) == FALSE) {
rc = -ENXIO;
} else if ((options & cib_multiple) == 0) {
break;
}
} else if (pcmk__str_eq(op, CIB_OP_CREATE, pcmk__str_casei)) {
add_node_copy(match, input);
break;
} else if (pcmk__str_eq(op, CIB_OP_QUERY, pcmk__str_casei)) {
if (options & cib_no_children) {
const char *tag = TYPE(match);
xmlNode *shallow = create_xml_node(*answer, tag);
copy_in_properties(shallow, match);
if (*answer == NULL) {
*answer = shallow;
}
} else if (options & cib_xpath_address) {
char *path = NULL;
xmlNode *parent = match;
while (parent && parent->type == XML_ELEMENT_NODE) {
const char *id = crm_element_value(parent, XML_ATTR_ID);
char *new_path = NULL;
if (id) {
new_path = crm_strdup_printf("/%s[@id='%s']%s",
parent->name, id,
(path? path : ""));
} else {
new_path = crm_strdup_printf("/%s%s", parent->name,
(path? path : ""));
}
free(path);
path = new_path;
parent = parent->parent;
}
crm_trace("Got: %s", path);
if (*answer == NULL) {
*answer = create_xml_node(NULL, "xpath-query");
}
parent = create_xml_node(*answer, "xpath-query-path");
crm_xml_add(parent, XML_ATTR_ID, path);
free(path);
} else if (*answer) {
add_node_copy(*answer, match);
} else {
*answer = match;
}
} else if (pcmk__str_eq(op, CIB_OP_REPLACE, pcmk__str_casei)) {
xmlNode *parent = match->parent;
free_xml(match);
if (input != NULL) {
add_node_copy(parent, input);
}
if ((options & cib_multiple) == 0) {
break;
}
}
}
freeXpathObject(xpathObj);
return rc;
}
/* remove this function */
gboolean
update_results(xmlNode * failed, xmlNode * target, const char *operation, int return_code)
{
xmlNode *xml_node = NULL;
gboolean was_error = FALSE;
const char *error_msg = NULL;
if (return_code != pcmk_ok) {
error_msg = pcmk_strerror(return_code);
was_error = TRUE;
xml_node = create_xml_node(failed, XML_FAIL_TAG_CIB);
add_node_copy(xml_node, target);
crm_xml_add(xml_node, XML_FAILCIB_ATTR_ID, ID(target));
crm_xml_add(xml_node, XML_FAILCIB_ATTR_OBJTYPE, TYPE(target));
crm_xml_add(xml_node, XML_FAILCIB_ATTR_OP, operation);
crm_xml_add(xml_node, XML_FAILCIB_ATTR_REASON, error_msg);
crm_warn("Action %s failed: %s (cde=%d)", operation, error_msg, return_code);
}
return was_error;
}
diff --git a/lib/common/acl.c b/lib/common/acl.c
index b9f7472ee3..a6f93d9337 100644
--- a/lib/common/acl.c
+++ b/lib/common/acl.c
@@ -1,815 +1,844 @@
/*
* Copyright 2004-2022 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU Lesser General Public License
* version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "crmcommon_private.h"
#define MAX_XPATH_LEN 4096
typedef struct xml_acl_s {
enum xml_private_flags mode;
char *xpath;
} xml_acl_t;
static void
free_acl(void *data)
{
if (data) {
xml_acl_t *acl = data;
free(acl->xpath);
free(acl);
}
}
void
pcmk__free_acls(GList *acls)
{
g_list_free_full(acls, free_acl);
}
static GList *
create_acl(xmlNode *xml, GList *acls, enum xml_private_flags mode)
{
xml_acl_t *acl = NULL;
const char *tag = crm_element_value(xml, XML_ACL_ATTR_TAG);
const char *ref = crm_element_value(xml, XML_ACL_ATTR_REF);
const char *xpath = crm_element_value(xml, XML_ACL_ATTR_XPATH);
const char *attr = crm_element_value(xml, XML_ACL_ATTR_ATTRIBUTE);
if (tag == NULL) {
// @COMPAT rolling upgrades <=1.1.11
tag = crm_element_value(xml, XML_ACL_ATTR_TAGv1);
}
if (ref == NULL) {
// @COMPAT rolling upgrades <=1.1.11
ref = crm_element_value(xml, XML_ACL_ATTR_REFv1);
}
if ((tag == NULL) && (ref == NULL) && (xpath == NULL)) {
// Schema should prevent this, but to be safe ...
crm_trace("Ignoring ACL <%s> element without selection criteria",
crm_element_name(xml));
return NULL;
}
acl = calloc(1, sizeof (xml_acl_t));
CRM_ASSERT(acl != NULL);
acl->mode = mode;
if (xpath) {
acl->xpath = strdup(xpath);
CRM_ASSERT(acl->xpath != NULL);
crm_trace("Unpacked ACL <%s> element using xpath: %s",
crm_element_name(xml), acl->xpath);
} else {
int offset = 0;
char buffer[MAX_XPATH_LEN];
if (tag) {
offset += snprintf(buffer + offset, MAX_XPATH_LEN - offset,
"//%s", tag);
} else {
offset += snprintf(buffer + offset, MAX_XPATH_LEN - offset,
"//*");
}
if (ref || attr) {
offset += snprintf(buffer + offset, MAX_XPATH_LEN - offset,
"[");
}
if (ref) {
offset += snprintf(buffer + offset, MAX_XPATH_LEN - offset,
"@id='%s'", ref);
}
// NOTE: schema currently does not allow this
if (ref && attr) {
offset += snprintf(buffer + offset, MAX_XPATH_LEN - offset,
" and ");
}
if (attr) {
offset += snprintf(buffer + offset, MAX_XPATH_LEN - offset,
"@%s", attr);
}
if (ref || attr) {
offset += snprintf(buffer + offset, MAX_XPATH_LEN - offset,
"]");
}
CRM_LOG_ASSERT(offset > 0);
acl->xpath = strdup(buffer);
CRM_ASSERT(acl->xpath != NULL);
crm_trace("Unpacked ACL <%s> element as xpath: %s",
crm_element_name(xml), 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(xmlNode *acl_top, xmlNode *acl_entry, GList *acls)
{
xmlNode *child = NULL;
for (child = pcmk__xe_first_child(acl_entry); child;
child = pcmk__xe_next(child)) {
const char *tag = crm_element_name(child);
const char *kind = crm_element_value(child, XML_ACL_ATTR_KIND);
if (strcmp(XML_ACL_TAG_PERMISSION, tag) == 0){
CRM_ASSERT(kind != NULL);
crm_trace("Unpacking ACL <%s> element of kind '%s'", tag, kind);
tag = kind;
} else {
crm_trace("Unpacking ACL <%s> element", tag);
}
if (strcmp(XML_ACL_TAG_ROLE_REF, tag) == 0
|| strcmp(XML_ACL_TAG_ROLE_REFv1, tag) == 0) {
const char *ref_role = crm_element_value(child, XML_ATTR_ID);
if (ref_role) {
xmlNode *role = NULL;
for (role = pcmk__xe_first_child(acl_top); role;
role = pcmk__xe_next(role)) {
if (!strcmp(XML_ACL_TAG_ROLE, (const char *) role->name)) {
const char *role_id = crm_element_value(role,
XML_ATTR_ID);
if (role_id && strcmp(ref_role, role_id) == 0) {
crm_trace("Unpacking referenced role '%s' in ACL <%s> element",
role_id, crm_element_name(acl_entry));
acls = parse_acl_entry(acl_top, role, acls);
break;
}
}
}
}
} else if (strcmp(XML_ACL_TAG_READ, tag) == 0) {
acls = create_acl(child, acls, pcmk__xf_acl_read);
} else if (strcmp(XML_ACL_TAG_WRITE, tag) == 0) {
acls = create_acl(child, acls, pcmk__xf_acl_write);
} else if (strcmp(XML_ACL_TAG_DENY, tag) == 0) {
acls = create_acl(child, acls, pcmk__xf_acl_deny);
} else {
crm_warn("Ignoring unknown ACL %s '%s'",
(kind? "kind" : "element"), tag);
}
}
return acls;
}
/*
*/
static const char *
acl_to_text(enum xml_private_flags flags)
{
if (pcmk_is_set(flags, pcmk__xf_acl_deny)) {
return "deny";
} else if (pcmk_any_flags_set(flags, pcmk__xf_acl_write|pcmk__xf_acl_create)) {
return "read/write";
} else if (pcmk_is_set(flags, pcmk__xf_acl_read)) {
return "read";
}
return "none";
}
void
pcmk__apply_acl(xmlNode *xml)
{
GList *aIter = NULL;
xml_private_t *p = xml->doc->_private;
xmlXPathObjectPtr xpathObj = NULL;
if (!xml_acl_enabled(xml)) {
crm_trace("Skipping ACLs for user '%s' because not enabled for this XML",
p->user);
return;
}
for (aIter = p->acls; aIter != NULL; aIter = aIter->next) {
int max = 0, lpc = 0;
xml_acl_t *acl = aIter->data;
xpathObj = xpath_search(xml, acl->xpath);
max = numXpathResults(xpathObj);
for (lpc = 0; lpc < max; lpc++) {
xmlNode *match = getXpathResult(xpathObj, lpc);
char *path = xml_get_path(match);
p = match->_private;
crm_trace("Applying %s ACL to %s matched by %s",
acl_to_text(acl->mode), path, acl->xpath);
pcmk__set_xml_flags(p, acl->mode);
free(path);
}
crm_trace("Applied %s ACL %s (%d match%s)",
acl_to_text(acl->mode), acl->xpath, max,
((max == 1)? "" : "es"));
freeXpathObject(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_private_t *p = NULL;
if ((target == NULL) || (target->doc == NULL)
|| (target->doc->_private == NULL)) {
return;
}
p = target->doc->_private;
if (!pcmk_acl_required(user)) {
crm_trace("Not unpacking ACLs because not required for user '%s'",
user);
} else if (p->acls == NULL) {
xmlNode *acls = get_xpath_object("//" XML_CIB_TAG_ACLS,
source, LOG_NEVER);
pcmk__str_update(&p->user, user);
if (acls) {
xmlNode *child = NULL;
for (child = pcmk__xe_first_child(acls); child;
child = pcmk__xe_next(child)) {
const char *tag = crm_element_name(child);
if (!strcmp(tag, XML_ACL_TAG_USER)
|| !strcmp(tag, XML_ACL_TAG_USERv1)) {
const char *id = crm_element_value(child, XML_ATTR_NAME);
if (id == NULL) {
id = crm_element_value(child, XML_ATTR_ID);
}
if (id && strcmp(id, user) == 0) {
crm_debug("Unpacking ACLs for user '%s'", id);
p->acls = parse_acl_entry(acls, child, p->acls);
}
} else if (!strcmp(tag, XML_ACL_TAG_GROUP)) {
const char *id = crm_element_value(child, XML_ATTR_NAME);
if (id == NULL) {
id = crm_element_value(child, XML_ATTR_ID);
}
if (id && pcmk__is_user_in_group(user,id)) {
crm_debug("Unpacking ACLs for group '%s'", id);
p->acls = parse_acl_entry(acls, child, p->acls);
}
}
}
}
}
}
/*!
* \internal
* \brief Copy source to target and set xf_acl_enabled flag in target
*
* \param[in] acl_source XML with ACL definitions
* \param[in,out] target XML that ACLs will be applied to
* \param[in] user Username whose ACLs need to be set
*/
void
pcmk__enable_acl(xmlNode *acl_source, xmlNode *target, const char *user)
{
pcmk__unpack_acl(acl_source, target, user);
pcmk__set_xml_doc_flag(target, pcmk__xf_acl_enabled);
pcmk__apply_acl(target);
}
static inline bool
test_acl_mode(enum xml_private_flags allowed, enum xml_private_flags requested)
{
if (pcmk_is_set(allowed, pcmk__xf_acl_deny)) {
return false;
} else if (pcmk_all_flags_set(allowed, requested)) {
return true;
} else if (pcmk_is_set(requested, pcmk__xf_acl_read)
&& pcmk_is_set(allowed, pcmk__xf_acl_write)) {
return true;
} else if (pcmk_is_set(requested, pcmk__xf_acl_create)
&& pcmk_any_flags_set(allowed, pcmk__xf_acl_write|pcmk__xf_created)) {
return true;
}
return false;
}
+/*!
+ * \internal
+ * \brief Rid XML tree of all unreadable nodes and node properties
+ *
+ * \param[in] xml root 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_private_t *p = xml->_private;
if (test_acl_mode(p->flags, pcmk__xf_acl_read)) {
crm_trace("%s[@id=%s] is readable", crm_element_name(xml), 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, XML_ATTR_ID) == 0) {
continue;
}
xmlUnsetProp(xml, tmp->name);
}
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) {
free_xml(xml); /* Nothing readable under here, purge completely */
}
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_private_t *doc = 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 = copy_xml(xml);
if (target == NULL) {
return true;
}
pcmk__enable_acl(acl_source, target, user);
doc = target->doc->_private;
for(aIter = doc->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;
xmlXPathObjectPtr xpathObj = xpath_search(target, acl->xpath);
max = numXpathResults(xpathObj);
for(lpc = 0; lpc < max; lpc++) {
xmlNode *match = getXpathResult(xpathObj, lpc);
if (!purge_xml_attributes(match) && (match == target)) {
crm_trace("ACLs deny user '%s' access to entire XML document",
user);
freeXpathObject(xpathObj);
return true;
}
}
crm_trace("ACLs deny user '%s' access to %s (%d %s)",
user, acl->xpath, max,
pcmk__plural_alt(max, "match", "matches"));
freeXpathObject(xpathObj);
}
}
if (!purge_xml_attributes(target)) {
crm_trace("ACLs deny user '%s' access to entire XML document", user);
return true;
}
if (doc->acls) {
g_list_free_full(doc->acls, free_acl);
doc->acls = NULL;
} else {
crm_trace("User '%s' without ACLs denied access to entire XML document",
user);
free_xml(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 "id").
*
* \param[in] xml XML element to check
*
* \return true if XML element is implicitly allowed, false otherwise
*/
static bool
implicitly_allowed(xmlNode *xml)
{
char *path = NULL;
for (xmlAttr *prop = xml->properties; prop != NULL; prop = prop->next) {
if (strcmp((const char *) prop->name, XML_ATTR_ID) != 0) {
return false;
}
}
path = xml_get_path(xml);
if (strstr(path, "/" XML_CIB_TAG_ACLS "/") != NULL) {
free(path);
return false;
}
free(path);
return true;
}
#define display_id(xml) (ID(xml)? ID(xml) : "")
/*!
* \internal
* \brief Drop XML nodes created in violation of ACLs
*
* Given an XML element, free all of its descendent 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
* "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_private_t *p = xml->_private;
if (pcmk_is_set(p->flags, pcmk__xf_created)) {
if (implicitly_allowed(xml)) {
crm_trace("Creation of <%s> scaffolding with id=\"%s\""
" is implicitly allowed",
crm_element_name(xml), display_id(xml));
} else if (pcmk__check_acl(xml, NULL, pcmk__xf_acl_write)) {
crm_trace("ACLs allow creation of <%s> with id=\"%s\"",
crm_element_name(xml), display_id(xml));
} else if (check_top) {
crm_trace("ACLs disallow creation of <%s> with id=\"%s\"",
crm_element_name(xml), display_id(xml));
pcmk_free_xml_subtree(xml);
return;
} else {
crm_notice("ACLs would disallow creation of %s<%s> with id=\"%s\" ",
((xml == xmlDocGetRootElement(xml->doc))? "root element " : ""),
crm_element_name(xml), 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(xmlNode *xml)
{
if (xml && xml->doc && xml->doc->_private){
xml_private_t *p = xml->doc->_private;
return pcmk_is_set(p->flags, pcmk__xf_acl_denied);
}
return false;
}
void
xml_acl_disable(xmlNode *xml)
{
if (xml_acl_enabled(xml)) {
xml_private_t *p = 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(p, 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(xmlNode *xml)
{
if (xml && xml->doc && xml->doc->_private){
xml_private_t *p = xml->doc->_private;
return pcmk_is_set(p->flags, pcmk__xf_acl_enabled);
}
return false;
}
bool
pcmk__check_acl(xmlNode *xml, const char *name, enum xml_private_flags mode)
{
CRM_ASSERT(xml);
CRM_ASSERT(xml->doc);
CRM_ASSERT(xml->doc->_private);
if (pcmk__tracking_xml_changes(xml, false) && xml_acl_enabled(xml)) {
int offset = 0;
xmlNode *parent = xml;
char buffer[MAX_XPATH_LEN];
xml_private_t *docp = xml->doc->_private;
offset = pcmk__element_xpath(NULL, xml, buffer, offset,
sizeof(buffer));
if (name) {
offset += snprintf(buffer + offset, MAX_XPATH_LEN - offset,
"[@%s]", name);
}
CRM_LOG_ASSERT(offset > 0);
if (docp->acls == NULL) {
crm_trace("User '%s' without ACLs denied %s access to %s",
docp->user, acl_to_text(mode), buffer);
pcmk__set_xml_doc_flag(xml, pcmk__xf_acl_denied);
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 (name) {
xmlAttr *attr = xmlHasProp(xml, (pcmkXmlStr) name);
if (attr && mode == pcmk__xf_acl_create) {
mode = pcmk__xf_acl_write;
}
}
while (parent && parent->_private) {
xml_private_t *p = parent->_private;
if (test_acl_mode(p->flags, mode)) {
return true;
} else if (pcmk_is_set(p->flags, pcmk__xf_acl_deny)) {
crm_trace("%sACL denies user '%s' %s access to %s",
(parent != xml) ? "Parent " : "", docp->user,
acl_to_text(mode), buffer);
pcmk__set_xml_doc_flag(xml, pcmk__xf_acl_denied);
return false;
}
parent = parent->parent;
}
crm_trace("Default ACL denies user '%s' %s access to %s",
docp->user, acl_to_text(mode), buffer);
pcmk__set_xml_doc_flag(xml, pcmk__xf_acl_denied);
return false;
}
return true;
}
/*!
* \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 strdup(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 = strdup("#unprivileged");
CRM_CHECK(effective_user != NULL, return NULL);
crm_err("Unable to determine effective user, assuming unprivileged for ACLs");
}
}
requested_user = crm_element_value(request, XML_ACL_TAG_USER);
if (requested_user == NULL) {
/* @COMPAT rolling upgrades <=1.1.11
*
* field is checked for backward compatibility with older versions that
* did not use XML_ACL_TAG_USER.
*/
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 $XML_ACL_TAG_USER
*/
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 $XML_ACL_TAG_USER
*/
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, XML_ACL_TAG_USER)) {
crm_xml_add(request, XML_ACL_TAG_USER, user);
}
if (field != NULL && user != crm_element_value(request, field)) {
crm_xml_add(request, field, user);
}
return requested_user;
}
diff --git a/lib/common/tests/operations/Makefile.am b/lib/common/tests/operations/Makefile.am
index e4d6fb32b8..7ae4aff8cc 100644
--- a/lib/common/tests/operations/Makefile.am
+++ b/lib/common/tests/operations/Makefile.am
@@ -1,24 +1,27 @@
#
# Copyright 2020-2022 the Pacemaker project contributors
#
# The version control history for this file may have further details.
#
# This source code is licensed under the GNU General Public License version 2
# or later (GPLv2+) WITHOUT ANY WARRANTY.
#
AM_CPPFLAGS = -I$(top_srcdir)/include \
-I$(top_builddir)/include \
-I$(top_srcdir)/lib/common
LDADD = $(top_builddir)/lib/common/libcrmcommon_test.la
AM_LDFLAGS = $(LDFLAGS_WRAP)
include $(top_srcdir)/mk/tap.mk
# Add "_test" to the end of all test program names to simplify .gitignore.
-check_PROGRAMS = parse_op_key_test \
+check_PROGRAMS = copy_in_properties_test \
+ expand_plus_plus_test \
+ fix_plus_plus_recursive_test \
+ parse_op_key_test \
pcmk_is_probe_test \
pcmk_xe_is_probe_test \
pcmk_xe_mask_probe_failure_test
TESTS = $(check_PROGRAMS)
diff --git a/lib/common/tests/operations/copy_in_properties_test.c b/lib/common/tests/operations/copy_in_properties_test.c
new file mode 100644
index 0000000000..3a652e7233
--- /dev/null
+++ b/lib/common/tests/operations/copy_in_properties_test.c
@@ -0,0 +1,73 @@
+ /*
+ * Copyright 2022 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU Lesser General Public License
+ * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
+ */
+
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+static void
+target_is_NULL(void **state)
+{
+ xmlNode *test_xml_1 = create_xml_node(NULL, "test_xml_1");
+ xmlNode *test_xml_2 = NULL;
+
+ pcmk__xe_set_props(test_xml_1, "test_prop", "test_value", NULL);
+
+ copy_in_properties(test_xml_2, test_xml_1);
+
+ assert_ptr_equal(test_xml_2, NULL);
+}
+
+static void
+src_is_NULL(void **state)
+{
+ xmlNode *test_xml_1 = NULL;
+ xmlNode *test_xml_2 = create_xml_node(NULL, "test_xml_2");
+
+ copy_in_properties(test_xml_2, test_xml_1);
+
+ assert_ptr_equal(test_xml_2->properties, NULL);
+}
+
+static void
+copying_is_successful(void **state)
+{
+ const char *xml_1_value;
+ const char *xml_2_value;
+
+ xmlNode *test_xml_1 = create_xml_node(NULL, "test_xml_1");
+ xmlNode *test_xml_2 = create_xml_node(NULL, "test_xml_2");
+
+ pcmk__xe_set_props(test_xml_1, "test_prop", "test_value", NULL);
+
+ copy_in_properties(test_xml_2, test_xml_1);
+
+ xml_1_value = crm_element_value(test_xml_1, "test_prop");
+ xml_2_value = crm_element_value(test_xml_2, "test_prop");
+
+ assert_string_equal(xml_1_value, xml_2_value);
+}
+
+int main(int argc, char **argv)
+{
+ const struct CMUnitTest tests[] = {
+ cmocka_unit_test(target_is_NULL),
+ cmocka_unit_test(src_is_NULL),
+ cmocka_unit_test(copying_is_successful),
+ };
+
+ cmocka_set_message_output(CM_OUTPUT_TAP);
+ return cmocka_run_group_tests(tests, NULL, NULL);
+}
diff --git a/lib/common/tests/operations/expand_plus_plus_test.c b/lib/common/tests/operations/expand_plus_plus_test.c
new file mode 100644
index 0000000000..cbc1382c32
--- /dev/null
+++ b/lib/common/tests/operations/expand_plus_plus_test.c
@@ -0,0 +1,267 @@
+/*
+ * Copyright 2022 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU Lesser General Public License
+ * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
+ */
+
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+static void
+value_is_name_plus_plus(void **state)
+{
+ const char *new_value;
+ xmlNode *test_xml = create_xml_node(NULL, "test_xml");
+ crm_xml_add(test_xml, "X", "5");
+ expand_plus_plus(test_xml, "X", "X++");
+ new_value = crm_element_value(test_xml, "X");
+ assert_string_equal(new_value, "6");
+}
+
+static void
+value_is_name_plus_equals_integer(void **state)
+{
+ const char *new_value;
+ xmlNode *test_xml = create_xml_node(NULL, "test_xml");
+ crm_xml_add(test_xml, "X", "5");
+ expand_plus_plus(test_xml, "X", "X+=2");
+ new_value = crm_element_value(test_xml, "X");
+ assert_string_equal(new_value, "7");
+}
+
+// NULL input
+
+static void
+target_is_NULL(void **state)
+{
+
+ const char *new_value;
+ xmlNode *test_xml = create_xml_node(NULL, "test_xml");
+ crm_xml_add(test_xml, "X", "5");
+ expand_plus_plus(NULL, "X", "X++");
+ new_value = crm_element_value(test_xml, "X");
+ assert_string_equal(new_value, "5");
+}
+
+static void
+name_is_NULL(void **state)
+{
+ const char *new_value;
+ xmlNode *test_xml = create_xml_node(NULL, "test_xml");
+ crm_xml_add(test_xml, "X", "5");
+ expand_plus_plus(test_xml, NULL, "X++");
+ new_value = crm_element_value(test_xml, "X");
+ assert_string_equal(new_value, "5");
+}
+
+static void
+value_is_NULL(void **state)
+{
+ const char *new_value;
+ xmlNode *test_xml = create_xml_node(NULL, "test_xml");
+ crm_xml_add(test_xml, "X", "5");
+ expand_plus_plus(test_xml, "X", NULL);
+ new_value = crm_element_value(test_xml, "X");
+ assert_string_equal(new_value, "5");
+}
+
+// the value input doesn't start with the name input
+
+static void
+value_is_wrong_name(void **state)
+{
+ const char *new_value;
+ xmlNode *test_xml = create_xml_node(NULL, "test_xml");
+ crm_xml_add(test_xml, "X", "5");
+ expand_plus_plus(test_xml, "X", "Y++");
+ new_value = crm_element_value(test_xml, "X");
+ assert_string_equal(new_value, "Y++");
+}
+
+static void
+value_is_only_an_integer(void **state)
+{
+ const char *new_value;
+ xmlNode *test_xml = create_xml_node(NULL, "test_xml");
+ crm_xml_add(test_xml, "X", "5");
+ expand_plus_plus(test_xml, "X", "2");
+ new_value = crm_element_value(test_xml, "X");
+ assert_string_equal(new_value, "2");
+}
+
+// non-integers
+
+static void
+variable_is_initialized_to_be_NULL(void **state)
+{
+ const char *new_value;
+ xmlNode *test_xml = create_xml_node(NULL, "test_xml");
+ crm_xml_add(test_xml, "X", NULL);
+ expand_plus_plus(test_xml, "X", "X++");
+ new_value = crm_element_value(test_xml, "X");
+ assert_string_equal(new_value, "X++");
+}
+
+static void
+variable_is_initialized_to_be_non_numeric(void **state)
+{
+ const char *new_value;
+ xmlNode *test_xml = create_xml_node(NULL, "test_xml");
+ crm_xml_add(test_xml, "X", "hello");
+ expand_plus_plus(test_xml, "X", "X++");
+ new_value = crm_element_value(test_xml, "X");
+ assert_string_equal(new_value, "1");
+}
+
+static void
+variable_is_initialized_to_be_non_numeric_2(void **state)
+{
+ const char *new_value;
+ xmlNode *test_xml = create_xml_node(NULL, "test_xml");
+ crm_xml_add(test_xml, "X", "hello");
+ expand_plus_plus(test_xml, "X", "X+=2");
+ new_value = crm_element_value(test_xml, "X");
+ assert_string_equal(new_value, "2");
+}
+
+static void
+variable_is_initialized_to_be_numeric_and_decimal_point_containing(void **state)
+{
+ const char *new_value;
+ xmlNode *test_xml = create_xml_node(NULL, "test_xml");
+ crm_xml_add(test_xml, "X", "5.01");
+ expand_plus_plus(test_xml, "X", "X++");
+ new_value = crm_element_value(test_xml, "X");
+ assert_string_equal(new_value, "6");
+}
+
+static void
+variable_is_initialized_to_be_numeric_and_decimal_point_containing_2(void **state)
+{
+ const char *new_value;
+ xmlNode *test_xml = create_xml_node(NULL, "test_xml");
+ crm_xml_add(test_xml, "X", "5.50");
+ expand_plus_plus(test_xml, "X", "X++");
+ new_value = crm_element_value(test_xml, "X");
+ assert_string_equal(new_value, "6");
+}
+
+static void
+variable_is_initialized_to_be_numeric_and_decimal_point_containing_3(void **state)
+{
+ const char *new_value;
+ xmlNode *test_xml = create_xml_node(NULL, "test_xml");
+ crm_xml_add(test_xml, "X", "5.99");
+ expand_plus_plus(test_xml, "X", "X++");
+ new_value = crm_element_value(test_xml, "X");
+ assert_string_equal(new_value, "6");
+}
+
+static void
+value_is_non_numeric(void **state)
+{
+ const char *new_value;
+ xmlNode *test_xml = create_xml_node(NULL, "test_xml");
+ crm_xml_add(test_xml, "X", "5");
+ expand_plus_plus(test_xml, "X", "X+=hello");
+ new_value = crm_element_value(test_xml, "X");
+ assert_string_equal(new_value, "5");
+}
+
+static void
+value_is_numeric_and_decimal_point_containing(void **state)
+{
+ const char *new_value;
+ xmlNode *test_xml = create_xml_node(NULL, "test_xml");
+ crm_xml_add(test_xml, "X", "5");
+ expand_plus_plus(test_xml, "X", "X+=2.01");
+ new_value = crm_element_value(test_xml, "X");
+ assert_string_equal(new_value, "7");
+}
+
+static void
+value_is_numeric_and_decimal_point_containing_2(void **state)
+{
+ const char *new_value;
+ xmlNode *test_xml = create_xml_node(NULL, "test_xml");
+ crm_xml_add(test_xml, "X", "5");
+ expand_plus_plus(test_xml, "X", "X+=1.50");
+ new_value = crm_element_value(test_xml, "X");
+ assert_string_equal(new_value, "6");
+}
+
+static void
+value_is_numeric_and_decimal_point_containing_3(void **state)
+{
+ const char *new_value;
+ xmlNode *test_xml = create_xml_node(NULL, "test_xml");
+ crm_xml_add(test_xml, "X", "5");
+ expand_plus_plus(test_xml, "X", "X+=1.99");
+ new_value = crm_element_value(test_xml, "X");
+ assert_string_equal(new_value, "6");
+}
+
+// undefined input
+
+static void
+name_is_undefined(void **state)
+{
+ const char *new_value;
+ xmlNode *test_xml = create_xml_node(NULL, "test_xml");
+ crm_xml_add(test_xml, "Y", "5");
+ expand_plus_plus(test_xml, "X", "X++");
+ new_value = crm_element_value(test_xml, "X");
+ assert_string_equal(new_value, "X++");
+}
+
+// large input
+
+static void
+assignment_result_is_too_large(void **state)
+{
+ const char *new_value;
+ xmlNode *test_xml = create_xml_node(NULL, "test_xml");
+ crm_xml_add(test_xml, "X", "5");
+ expand_plus_plus(test_xml, "X", "X+=100000000000");
+ new_value = crm_element_value(test_xml, "X");
+ printf("assignment result is too large %s\n", new_value);
+ assert_string_equal(new_value, "1000000");
+}
+
+int main(int argc, char **argv)
+{
+ const struct CMUnitTest tests[] = {
+ cmocka_unit_test(value_is_name_plus_plus),
+ cmocka_unit_test(value_is_name_plus_equals_integer),
+ cmocka_unit_test(target_is_NULL),
+ cmocka_unit_test(name_is_NULL),
+ cmocka_unit_test(value_is_NULL),
+ cmocka_unit_test(value_is_wrong_name),
+ cmocka_unit_test(value_is_only_an_integer),
+ cmocka_unit_test(variable_is_initialized_to_be_NULL),
+ cmocka_unit_test(variable_is_initialized_to_be_non_numeric),
+ cmocka_unit_test(variable_is_initialized_to_be_non_numeric_2),
+ cmocka_unit_test(variable_is_initialized_to_be_numeric_and_decimal_point_containing),
+ cmocka_unit_test(variable_is_initialized_to_be_numeric_and_decimal_point_containing_2),
+ cmocka_unit_test(variable_is_initialized_to_be_numeric_and_decimal_point_containing_3),
+ cmocka_unit_test(value_is_non_numeric),
+ cmocka_unit_test(value_is_numeric_and_decimal_point_containing),
+ cmocka_unit_test(value_is_numeric_and_decimal_point_containing_2),
+ cmocka_unit_test(value_is_numeric_and_decimal_point_containing_3),
+ cmocka_unit_test(name_is_undefined),
+ cmocka_unit_test(assignment_result_is_too_large),
+ };
+
+ cmocka_set_message_output(CM_OUTPUT_TAP);
+ return cmocka_run_group_tests(tests, NULL, NULL);
+}
diff --git a/lib/common/tests/operations/fix_plus_plus_recursive_test.c b/lib/common/tests/operations/fix_plus_plus_recursive_test.c
new file mode 100644
index 0000000000..9ad48b58e4
--- /dev/null
+++ b/lib/common/tests/operations/fix_plus_plus_recursive_test.c
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2022 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU Lesser General Public License
+ * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
+ */
+
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+static void
+element_nodes(void **state)
+{
+ const char *new_value_root;
+ const char *new_value_child;
+ const char *new_value_grandchild;
+
+ xmlNode *test_xml_root = create_xml_node(NULL, "test_xml_root");
+ xmlNode *test_xml_child = create_xml_node(test_xml_root, "test_xml_child");
+ xmlNode *test_xml_grandchild = create_xml_node(test_xml_child, "test_xml_grandchild");
+ xmlNode *test_xml_text = pcmk_create_xml_text_node(test_xml_root, "text_xml_text", "content");
+ xmlNode *test_xml_comment = string2xml("");
+
+ crm_xml_add(test_xml_root, "X", "5");
+ crm_xml_add(test_xml_child, "X", "X++");
+ crm_xml_add(test_xml_grandchild, "X", "X+=2");
+ crm_xml_add(test_xml_text, "X", "X++");
+
+ fix_plus_plus_recursive(test_xml_root);
+ fix_plus_plus_recursive(test_xml_comment);
+
+ new_value_root = crm_element_value(test_xml_root, "X");
+ new_value_child = crm_element_value(test_xml_child, "X");
+ new_value_grandchild = crm_element_value(test_xml_grandchild, "X");
+
+ assert_string_equal(new_value_root, "5");
+ assert_string_equal(new_value_child, "1");
+ assert_string_equal(new_value_grandchild, "2");
+}
+
+int main(int argc, char **argv)
+{
+ const struct CMUnitTest tests[] = {
+ cmocka_unit_test(element_nodes),
+ };
+
+ cmocka_set_message_output(CM_OUTPUT_TAP);
+ return cmocka_run_group_tests(tests, NULL, NULL);
+}
diff --git a/lib/common/xml.c b/lib/common/xml.c
index 6f6a9c199b..33810c6de1 100644
--- a/lib/common/xml.c
+++ b/lib/common/xml.c
@@ -1,2996 +1,3024 @@
/*
* Copyright 2004-2022 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU Lesser General Public License
* version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include /* xmlAllocOutputBuffer */
#include
#include
#include
#include // PCMK__XML_LOG_BASE, etc.
#include "crmcommon_private.h"
// Define this as 1 in development to get insanely verbose trace messages
#ifndef XML_PARSER_DEBUG
#define XML_PARSER_DEBUG 0
#endif
/* @TODO XML_PARSE_RECOVER allows some XML errors to be silently worked around
* by libxml2, which is potentially ambiguous and dangerous. We should drop it
* when we can break backward compatibility with configurations that might be
* relying on it (i.e. pacemaker 3.0.0).
*
* It might be a good idea to have a transitional period where we first try
* parsing without XML_PARSE_RECOVER, and if that fails, try parsing again with
* it, logging a warning if it succeeds.
*/
#define PCMK__XML_PARSE_OPTS (XML_PARSE_NOBLANKS | XML_PARSE_RECOVER)
#define CHUNK_SIZE 1024
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_private_t *)xml->doc->_private)->flags,
pcmk__xf_tracking)) {
return FALSE;
} else if (lazy && !pcmk_is_set(((xml_private_t *)xml->doc->_private)->flags,
pcmk__xf_lazy)) {
return FALSE;
}
return TRUE;
}
#define buffer_print(buffer, max, offset, fmt, args...) do { \
int rc = (max); \
if(buffer) { \
rc = snprintf((buffer) + (offset), (max) - (offset), fmt, ##args); \
} \
if(buffer && rc < 0) { \
crm_perror(LOG_ERR, "snprintf failed at offset %d", offset); \
(buffer)[(offset)] = 0; \
break; \
} else if(rc >= ((max) - (offset))) { \
char *tmp = NULL; \
(max) = QB_MAX(CHUNK_SIZE, (max) * 2); \
tmp = pcmk__realloc((buffer), (max)); \
CRM_ASSERT(tmp); \
(buffer) = tmp; \
} else { \
offset += rc; \
break; \
} \
} while(1);
static void
insert_prefix(int options, char **buffer, int *offset, int *max, int depth)
{
if (options & xml_log_option_formatted) {
size_t spaces = 2 * depth;
if ((*buffer) == NULL || spaces >= ((*max) - (*offset))) {
(*max) = QB_MAX(CHUNK_SIZE, (*max) * 2);
(*buffer) = pcmk__realloc((*buffer), (*max));
}
memset((*buffer) + (*offset), ' ', spaces);
(*offset) += spaces;
}
}
static void
set_parent_flag(xmlNode *xml, long flag)
{
for(; xml; xml = xml->parent) {
xml_private_t *p = xml->_private;
if(p == NULL) {
/* During calls to xmlDocCopyNode(), _private will be unset for parent nodes */
} else {
pcmk__set_xml_flags(p, flag);
}
}
}
void
pcmk__set_xml_doc_flag(xmlNode *xml, enum xml_private_flags flag)
{
if(xml && xml->doc && xml->doc->_private){
/* During calls to xmlDocCopyNode(), xml->doc may be unset */
xml_private_t *p = xml->doc->_private;
pcmk__set_xml_flags(p, flag);
}
}
// Mark document, element, and all element's parents as changed
static void
mark_xml_node_dirty(xmlNode *xml)
{
pcmk__set_xml_doc_flag(xml, pcmk__xf_dirty);
set_parent_flag(xml, pcmk__xf_dirty);
}
// Clear flags on XML node and its children
static void
reset_xml_node_flags(xmlNode *xml)
{
xmlNode *cIter = NULL;
xml_private_t *p = xml->_private;
if (p) {
p->flags = 0;
}
for (cIter = pcmk__xml_first_child(xml); cIter != NULL;
cIter = pcmk__xml_next(cIter)) {
reset_xml_node_flags(cIter);
}
}
// Set xpf_created flag on XML node and any children
void
pcmk__mark_xml_created(xmlNode *xml)
{
xmlNode *cIter = NULL;
xml_private_t *p = xml->_private;
if (p && pcmk__tracking_xml_changes(xml, FALSE)) {
if (!pcmk_is_set(p->flags, pcmk__xf_created)) {
pcmk__set_xml_flags(p, pcmk__xf_created);
mark_xml_node_dirty(xml);
}
for (cIter = pcmk__xml_first_child(xml); cIter != NULL;
cIter = pcmk__xml_next(cIter)) {
pcmk__mark_xml_created(cIter);
}
}
}
void
pcmk__mark_xml_attr_dirty(xmlAttr *a)
{
xmlNode *parent = a->parent;
xml_private_t *p = NULL;
p = a->_private;
pcmk__set_xml_flags(p, pcmk__xf_dirty|pcmk__xf_modified);
pcmk__clear_xml_flags(p, pcmk__xf_deleted);
mark_xml_node_dirty(parent);
}
#define XML_PRIVATE_MAGIC (long) 0x81726354
// Free an XML object previously marked as deleted
static void
free_deleted_object(void *data)
{
if(data) {
pcmk__deleted_xml_t *deleted_obj = data;
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_private_t *p)
{
if(p) {
CRM_ASSERT(p->check == XML_PRIVATE_MAGIC);
free(p->user);
p->user = NULL;
if(p->acls) {
pcmk__free_acls(p->acls);
p->acls = NULL;
}
if(p->deleted_objs) {
g_list_free_full(p->deleted_objs, free_deleted_object);
p->deleted_objs = NULL;
}
}
}
// Free all private data associated with an XML node
static void
free_private_data(xmlNode *node)
{
/* need to explicitly avoid our custom _private field cleanup when
called from internal XSLT cleanup (xsltApplyStylesheetInternal
-> xsltFreeTransformContext -> xsltFreeRVTs -> xmlFreeDoc)
onto result tree fragments, represented as standalone documents
with otherwise infeasible space-prefixed name (xsltInternals.h:
XSLT_MARK_RES_TREE_FRAG) and carrying it's own load at _private
field -- later assert on the XML_PRIVATE_MAGIC would explode */
if (node->type != XML_DOCUMENT_NODE || node->name == NULL
|| node->name[0] != ' ') {
reset_xml_private_data(node->_private);
free(node->_private);
}
}
// Allocate and initialize private data for an XML node
static void
new_private_data(xmlNode *node)
{
xml_private_t *p = NULL;
switch(node->type) {
case XML_ELEMENT_NODE:
case XML_DOCUMENT_NODE:
case XML_ATTRIBUTE_NODE:
case XML_COMMENT_NODE:
p = calloc(1, sizeof(xml_private_t));
p->check = XML_PRIVATE_MAGIC;
/* Flags will be reset if necessary when tracking is enabled */
pcmk__set_xml_flags(p, pcmk__xf_dirty|pcmk__xf_created);
node->_private = p;
break;
case XML_TEXT_NODE:
case XML_DTD_NODE:
case XML_CDATA_SECTION_NODE:
break;
default:
/* Ignore */
crm_trace("Ignoring %p %d", node, node->type);
CRM_LOG_ASSERT(node->type == XML_ELEMENT_NODE);
break;
}
if(p && pcmk__tracking_xml_changes(node, FALSE)) {
/* XML_ELEMENT_NODE doesn't get picked up here, node->doc is
* not hooked up at the point we are called
*/
mark_xml_node_dirty(node);
}
}
void
xml_track_changes(xmlNode * xml, const char *user, xmlNode *acl_source, bool enforce_acls)
{
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);
if(enforce_acls) {
if(acl_source == NULL) {
acl_source = xml;
}
pcmk__set_xml_doc_flag(xml, 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_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_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(xmlNode *xml, enum xml_private_flags ignore_if_set)
{
int position = 0;
xmlNode *cIter = NULL;
for(cIter = xml; cIter->prev; cIter = cIter->prev) {
xml_private_t *p = ((xmlNode*)cIter->prev)->_private;
if (!pcmk_is_set(p->flags, ignore_if_set)) {
position++;
}
}
return position;
}
// This also clears attribute's flags if not marked as deleted
static bool
marked_as_deleted(xmlAttrPtr a, void *user_data)
{
xml_private_t *p = a->_private;
if (pcmk_is_set(p->flags, pcmk__xf_deleted)) {
return true;
}
p->flags = pcmk__xf_none;
return false;
}
// Remove all attributes marked as deleted from an XML node
static void
accept_attr_deletions(xmlNode *xml)
{
// Clear XML node's flags
((xml_private_t *) xml->_private)->flags = pcmk__xf_none;
// Remove this XML node's attributes that were marked as deleted
pcmk__xe_remove_matching_attrs(xml, marked_as_deleted, NULL);
// Recursively do the same for this XML node's children
for (xmlNodePtr cIter = pcmk__xml_first_child(xml); cIter != NULL;
cIter = pcmk__xml_next(cIter)) {
accept_attr_deletions(cIter);
}
}
/*!
* \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(xmlNode *haystack, 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 = ID(needle);
const char *attr = (id == NULL)? NULL : XML_ATTR_ID;
return pcmk__xe_match(haystack, crm_element_name(needle), attr, id);
}
}
void
xml_log_changes(uint8_t log_level, const char *function, xmlNode * xml)
{
GList *gIter = NULL;
xml_private_t *doc = NULL;
if (log_level == LOG_NEVER) {
return;
}
CRM_ASSERT(xml);
CRM_ASSERT(xml->doc);
doc = xml->doc->_private;
if (!pcmk_is_set(doc->flags, pcmk__xf_dirty)) {
return;
}
for(gIter = doc->deleted_objs; gIter; gIter = gIter->next) {
pcmk__deleted_xml_t *deleted_obj = gIter->data;
if (deleted_obj->position >= 0) {
do_crm_log_alias(log_level, __FILE__, function, __LINE__, "-- %s (%d)",
deleted_obj->path, deleted_obj->position);
} else {
do_crm_log_alias(log_level, __FILE__, function, __LINE__, "-- %s",
deleted_obj->path);
}
}
log_data_element(log_level, __FILE__, function, __LINE__, "+ ", xml, 0,
xml_log_option_formatted|xml_log_option_dirty_add);
}
void
xml_accept_changes(xmlNode * xml)
{
xmlNode *top = NULL;
xml_private_t *doc = NULL;
if(xml == NULL) {
return;
}
crm_trace("Accepting changes to %p", xml);
doc = xml->doc->_private;
top = xmlDocGetRootElement(xml->doc);
reset_xml_private_data(xml->doc->_private);
if (!pcmk_is_set(doc->flags, pcmk__xf_dirty)) {
doc->flags = pcmk__xf_none;
return;
}
doc->flags = pcmk__xf_none;
accept_attr_deletions(top);
}
xmlNode *
find_xml_node(xmlNode * root, const char *search_path, gboolean must_find)
{
xmlNode *a_child = NULL;
const char *name = "NULL";
if (root != NULL) {
name = crm_element_name(root);
}
if (search_path == NULL) {
crm_warn("Will never find ");
return NULL;
}
for (a_child = pcmk__xml_first_child(root); a_child != NULL;
a_child = pcmk__xml_next(a_child)) {
if (strcmp((const char *)a_child->name, search_path) == 0) {
/* crm_trace("returning node (%s).", crm_element_name(a_child)); */
return a_child;
}
}
if (must_find) {
crm_warn("Could not find %s in %s.", search_path, name);
} else if (root != NULL) {
crm_trace("Could not find %s in %s.", search_path, name);
} else {
crm_trace("Could not find %s in .", search_path);
}
return NULL;
}
#define attr_matches(c, n, v) pcmk__str_eq(crm_element_value((c), (n)), \
(v), pcmk__str_none)
/*!
* \internal
* \brief Find first XML child element matching given criteria
*
* \param[in] parent XML element to search
* \param[in] node_name If not NULL, only match children of this type
* \param[in] attr_n If not NULL, only match children with an attribute
* of this name and a value of \p attr_v
* \param[in] attr_v If \p attr_n and this are not NULL, only match children
* with an attribute named \p attr_n and this value
*
* \return Matching XML child element, or NULL if none found
*/
xmlNode *
pcmk__xe_match(xmlNode *parent, const char *node_name,
const char *attr_n, const char *attr_v)
{
/* ensure attr_v specified when attr_n is */
CRM_CHECK(attr_n == NULL || attr_v != NULL, return NULL);
for (xmlNode *child = pcmk__xml_first_child(parent); child != NULL;
child = pcmk__xml_next(child)) {
if (pcmk__str_eq(node_name, (const char *) (child->name),
pcmk__str_null_matches)
&& ((attr_n == NULL) || attr_matches(child, attr_n, attr_v))) {
return child;
}
}
crm_trace("XML child node <%s%s%s%s%s> not found in %s",
(node_name? node_name : "(any)"),
(attr_n? " " : ""),
(attr_n? attr_n : ""),
(attr_n? "=" : ""),
(attr_n? attr_v : ""),
crm_element_name(parent));
return NULL;
}
void
copy_in_properties(xmlNode * target, xmlNode * src)
{
if (src == NULL) {
crm_warn("No node to copy properties from");
} else if (target == NULL) {
crm_err("No node to copy properties into");
} else {
for (xmlAttrPtr a = pcmk__xe_first_attr(src); a != NULL; a = a->next) {
const char *p_name = (const char *) a->name;
const char *p_value = pcmk__xml_attr_value(a);
expand_plus_plus(target, p_name, p_value);
+ if (xml_acl_denied(target)) {
+ crm_trace("Cannot copy %s=%s to %s", p_name, p_value, target->name);
+ return;
+ }
}
}
return;
}
+/*!
+ * \brief Parse integer assignment statements on this node and all its child
+ * nodes
+ *
+ * \param[in] target root XML node to be processed
+ *
+ * \note This function is recursive
+ */
void
fix_plus_plus_recursive(xmlNode * target)
{
/* TODO: Remove recursion and use xpath searches for value++ */
xmlNode *child = NULL;
for (xmlAttrPtr a = pcmk__xe_first_attr(target); a != NULL; a = a->next) {
const char *p_name = (const char *) a->name;
const char *p_value = pcmk__xml_attr_value(a);
expand_plus_plus(target, p_name, p_value);
}
for (child = pcmk__xml_first_child(target); child != NULL;
child = pcmk__xml_next(child)) {
fix_plus_plus_recursive(child);
}
}
+/*!
+ * \brief Update current XML attribute value per parsed integer assignment
+ statement
+ *
+ * \param[in,out] target an XML node, containing a XML attribute that is
+ * initialized to some numeric value, to be processed
+ * \param[in] name name of the XML attribute, e.g. X, whose value
+ * should be updated
+ * \param[in] value assignment statement, e.g. "X++" or
+ * "X+=5", to be applied to the initialized value.
+ *
+ * \note The original XML attribute value is treated as 0 if non-numeric and
+ * truncated to be an integer if decimal-point-containing.
+ * \note The final XML attribute value is truncated to not exceed 1000000.
+ * \note Undefined behavior if unexpected input.
+ */
void
expand_plus_plus(xmlNode * target, const char *name, const char *value)
{
int offset = 1;
int name_len = 0;
int int_value = 0;
int value_len = 0;
const char *old_value = NULL;
- if (value == NULL || name == NULL) {
+ if (target == NULL || value == NULL || name == NULL) {
return;
}
old_value = crm_element_value(target, name);
if (old_value == NULL) {
/* if no previous value, set unexpanded */
goto set_unexpanded;
} else if (strstr(value, name) != value) {
goto set_unexpanded;
}
name_len = strlen(name);
value_len = strlen(value);
if (value_len < (name_len + 2)
|| value[name_len] != '+' || (value[name_len + 1] != '+' && value[name_len + 1] != '=')) {
goto set_unexpanded;
}
/* if we are expanding ourselves,
* then no previous value was set and leave int_value as 0
*/
if (old_value != value) {
int_value = char2score(old_value);
}
if (value[name_len + 1] != '+') {
const char *offset_s = value + (name_len + 2);
offset = char2score(offset_s);
}
int_value += offset;
if (int_value > INFINITY) {
int_value = (int)INFINITY;
}
crm_xml_add_int(target, name, int_value);
return;
set_unexpanded:
if (old_value == value) {
/* the old value is already set, nothing to do */
return;
}
crm_xml_add(target, name, value);
return;
}
/*!
* \internal
* \brief Remove an XML element's attributes that match some criteria
*
* \param[in,out] element XML element to modify
* \param[in] match If not NULL, only remove attributes for which
* this function returns true
* \param[in] user_data Data to pass to \p match
*/
void
pcmk__xe_remove_matching_attrs(xmlNode *element,
bool (*match)(xmlAttrPtr, void *),
void *user_data)
{
xmlAttrPtr next = NULL;
for (xmlAttrPtr a = pcmk__xe_first_attr(element); a != NULL; a = next) {
next = a->next; // Grab now because attribute might get removed
if ((match == NULL) || match(a, user_data)) {
if (!pcmk__check_acl(element, NULL, pcmk__xf_acl_write)) {
crm_trace("ACLs prevent removal of attributes (%s and "
"possibly others) from %s element",
(const char *) a->name, (const char *) element->name);
return; // ACLs apply to element, not particular attributes
}
if (pcmk__tracking_xml_changes(element, false)) {
// Leave (marked for removal) until after diff is calculated
set_parent_flag(element, pcmk__xf_dirty);
pcmk__set_xml_flags((xml_private_t *) a->_private,
pcmk__xf_deleted);
} else {
xmlRemoveProp(a);
}
}
}
}
xmlDoc *
getDocPtr(xmlNode * node)
{
xmlDoc *doc = NULL;
CRM_CHECK(node != NULL, return NULL);
doc = node->doc;
if (doc == NULL) {
doc = xmlNewDoc((pcmkXmlStr) "1.0");
xmlDocSetRootElement(doc, node);
xmlSetTreeDoc(node, doc);
}
return doc;
}
xmlNode *
add_node_copy(xmlNode * parent, xmlNode * src_node)
{
xmlNode *child = NULL;
xmlDoc *doc = getDocPtr(parent);
CRM_CHECK(src_node != NULL, return NULL);
child = xmlDocCopyNode(src_node, doc, 1);
xmlAddChild(parent, child);
pcmk__mark_xml_created(child);
return child;
}
int
add_node_nocopy(xmlNode * parent, const char *name, xmlNode * child)
{
add_node_copy(parent, child);
free_xml(child);
return 1;
}
xmlNode *
create_xml_node(xmlNode * parent, const char *name)
{
xmlDoc *doc = NULL;
xmlNode *node = NULL;
if (pcmk__str_empty(name)) {
CRM_CHECK(name != NULL && name[0] == 0, return NULL);
return NULL;
}
if (parent == NULL) {
doc = xmlNewDoc((pcmkXmlStr) "1.0");
node = xmlNewDocRawNode(doc, NULL, (pcmkXmlStr) name, NULL);
xmlDocSetRootElement(doc, node);
} else {
doc = getDocPtr(parent);
node = xmlNewDocRawNode(doc, NULL, (pcmkXmlStr) name, NULL);
xmlAddChild(parent, node);
}
pcmk__mark_xml_created(node);
return node;
}
xmlNode *
pcmk_create_xml_text_node(xmlNode * parent, const char *name, const char *content)
{
xmlNode *node = create_xml_node(parent, name);
if (node != NULL) {
xmlNodeSetContent(node, (pcmkXmlStr) content);
}
return node;
}
xmlNode *
pcmk_create_html_node(xmlNode * parent, const char *element_name, const char *id,
const char *class_name, const char *text)
{
xmlNode *node = pcmk_create_xml_text_node(parent, element_name, text);
if (class_name != NULL) {
crm_xml_add(node, "class", class_name);
}
if (id != NULL) {
crm_xml_add(node, "id", id);
}
return node;
}
/*!
* Free an XML element and all of its children, removing it from its parent
*
* \param[in] xml XML element to free
*/
void
pcmk_free_xml_subtree(xmlNode *xml)
{
xmlUnlinkNode(xml); // Detaches from parent and siblings
xmlFreeNode(xml); // Frees
}
static void
free_xml_with_position(xmlNode * child, int position)
{
if (child != NULL) {
xmlNode *top = NULL;
xmlDoc *doc = child->doc;
xml_private_t *p = child->_private;
if (doc != NULL) {
top = xmlDocGetRootElement(doc);
}
if (doc != NULL && top == child) {
/* Free everything */
xmlFreeDoc(doc);
} else if (pcmk__check_acl(child, NULL, pcmk__xf_acl_write) == FALSE) {
int offset = 0;
char buffer[PCMK__BUFFER_SIZE];
pcmk__element_xpath(NULL, child, buffer, offset, sizeof(buffer));
crm_trace("Cannot remove %s %x", buffer, p->flags);
return;
} else {
if (doc && pcmk__tracking_xml_changes(child, FALSE)
&& !pcmk_is_set(p->flags, pcmk__xf_created)) {
int offset = 0;
char buffer[PCMK__BUFFER_SIZE];
if (pcmk__element_xpath(NULL, child, buffer, offset,
sizeof(buffer)) > 0) {
pcmk__deleted_xml_t *deleted_obj = NULL;
crm_trace("Deleting %s %p from %p", buffer, child, doc);
deleted_obj = calloc(1, sizeof(pcmk__deleted_xml_t));
deleted_obj->path = strdup(buffer);
deleted_obj->position = -1;
/* Record the "position" only for XML comments for now */
if (child->type == XML_COMMENT_NODE) {
if (position >= 0) {
deleted_obj->position = position;
} else {
deleted_obj->position = pcmk__xml_position(child,
pcmk__xf_skip);
}
}
p = doc->_private;
p->deleted_objs = g_list_append(p->deleted_objs, deleted_obj);
pcmk__set_xml_doc_flag(child, pcmk__xf_dirty);
}
}
pcmk_free_xml_subtree(child);
}
}
}
void
free_xml(xmlNode * child)
{
free_xml_with_position(child, -1);
}
xmlNode *
copy_xml(xmlNode * src)
{
xmlDoc *doc = xmlNewDoc((pcmkXmlStr) "1.0");
xmlNode *copy = xmlDocCopyNode(src, doc, 1);
xmlDocSetRootElement(doc, copy);
xmlSetTreeDoc(copy, doc);
return copy;
}
static void
log_xmllib_err(void *ctx, const char *fmt, ...)
G_GNUC_PRINTF(2, 3);
// Log an XML library error
static void
log_xmllib_err(void *ctx, const char *fmt, ...)
{
va_list ap;
static struct qb_log_callsite *xml_error_cs = NULL;
if (xml_error_cs == NULL) {
xml_error_cs = qb_log_callsite_get(
__func__, __FILE__, "xml library error", LOG_TRACE, __LINE__, crm_trace_nonlog);
}
va_start(ap, fmt);
if (xml_error_cs && xml_error_cs->targets) {
PCMK__XML_LOG_BASE(LOG_ERR, TRUE,
crm_abort(__FILE__, __PRETTY_FUNCTION__, __LINE__, "xml library error",
TRUE, TRUE),
"XML Error: ", fmt, ap);
} else {
PCMK__XML_LOG_BASE(LOG_ERR, TRUE, 0, "XML Error: ", fmt, ap);
}
va_end(ap);
}
xmlNode *
string2xml(const char *input)
{
xmlNode *xml = NULL;
xmlDocPtr output = NULL;
xmlParserCtxtPtr ctxt = NULL;
xmlErrorPtr last_error = NULL;
if (input == NULL) {
crm_err("Can't parse NULL input");
return NULL;
}
/* create a parser context */
ctxt = xmlNewParserCtxt();
CRM_CHECK(ctxt != NULL, return NULL);
xmlCtxtResetLastError(ctxt);
xmlSetGenericErrorFunc(ctxt, log_xmllib_err);
output = xmlCtxtReadDoc(ctxt, (pcmkXmlStr) input, NULL, NULL,
PCMK__XML_PARSE_OPTS);
if (output) {
xml = xmlDocGetRootElement(output);
}
last_error = xmlCtxtGetLastError(ctxt);
if (last_error && last_error->code != XML_ERR_OK) {
/* crm_abort(__FILE__,__func__,__LINE__, "last_error->code != XML_ERR_OK", TRUE, TRUE); */
/*
* http://xmlsoft.org/html/libxml-xmlerror.html#xmlErrorLevel
* http://xmlsoft.org/html/libxml-xmlerror.html#xmlParserErrors
*/
crm_warn("Parsing failed (domain=%d, level=%d, code=%d): %s",
last_error->domain, last_error->level, last_error->code, last_error->message);
if (last_error->code == XML_ERR_DOCUMENT_EMPTY) {
CRM_LOG_ASSERT("Cannot parse an empty string");
} else if (last_error->code != XML_ERR_DOCUMENT_END) {
crm_err("Couldn't%s parse %d chars: %s", xml ? " fully" : "", (int)strlen(input),
input);
if (xml != NULL) {
crm_log_xml_err(xml, "Partial");
}
} else {
int len = strlen(input);
int lpc = 0;
while(lpc < len) {
crm_warn("Parse error[+%.3d]: %.80s", lpc, input+lpc);
lpc += 80;
}
CRM_LOG_ASSERT("String parsing error");
}
}
xmlFreeParserCtxt(ctxt);
return xml;
}
xmlNode *
stdin2xml(void)
{
size_t data_length = 0;
size_t read_chars = 0;
char *xml_buffer = NULL;
xmlNode *xml_obj = NULL;
do {
xml_buffer = pcmk__realloc(xml_buffer, data_length + PCMK__BUFFER_SIZE);
read_chars = fread(xml_buffer + data_length, 1, PCMK__BUFFER_SIZE,
stdin);
data_length += read_chars;
} while (read_chars == PCMK__BUFFER_SIZE);
if (data_length == 0) {
crm_warn("No XML supplied on stdin");
free(xml_buffer);
return NULL;
}
xml_buffer[data_length] = '\0';
xml_obj = string2xml(xml_buffer);
free(xml_buffer);
crm_log_xml_trace(xml_obj, "Created fragment");
return xml_obj;
}
static char *
decompress_file(const char *filename)
{
char *buffer = NULL;
int rc = 0;
size_t length = 0, read_len = 0;
BZFILE *bz_file = NULL;
FILE *input = fopen(filename, "r");
if (input == NULL) {
crm_perror(LOG_ERR, "Could not open %s for reading", filename);
return NULL;
}
bz_file = BZ2_bzReadOpen(&rc, input, 0, 0, NULL, 0);
if (rc != BZ_OK) {
crm_err("Could not prepare to read compressed %s: %s "
CRM_XS " bzerror=%d", filename, bz2_strerror(rc), rc);
BZ2_bzReadClose(&rc, bz_file);
fclose(input);
return NULL;
}
rc = BZ_OK;
// cppcheck seems not to understand the abort-logic in pcmk__realloc
// cppcheck-suppress memleak
while (rc == BZ_OK) {
buffer = pcmk__realloc(buffer, PCMK__BUFFER_SIZE + length + 1);
read_len = BZ2_bzRead(&rc, bz_file, buffer + length, PCMK__BUFFER_SIZE);
crm_trace("Read %ld bytes from file: %d", (long)read_len, rc);
if (rc == BZ_OK || rc == BZ_STREAM_END) {
length += read_len;
}
}
buffer[length] = '\0';
if (rc != BZ_STREAM_END) {
crm_err("Could not read compressed %s: %s "
CRM_XS " bzerror=%d", filename, bz2_strerror(rc), rc);
free(buffer);
buffer = NULL;
}
BZ2_bzReadClose(&rc, bz_file);
fclose(input);
return buffer;
}
/*!
* \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:
/* Remove it */
pcmk_free_xml_subtree(iter);
break;
case XML_ELEMENT_NODE:
/* Search it */
pcmk__strip_xml_text(iter);
break;
default:
/* Leave it */
break;
}
iter = next;
}
}
xmlNode *
filename2xml(const char *filename)
{
xmlNode *xml = NULL;
xmlDocPtr output = NULL;
bool uncompressed = true;
xmlParserCtxtPtr ctxt = NULL;
xmlErrorPtr last_error = NULL;
/* create a parser context */
ctxt = xmlNewParserCtxt();
CRM_CHECK(ctxt != NULL, return NULL);
xmlCtxtResetLastError(ctxt);
xmlSetGenericErrorFunc(ctxt, log_xmllib_err);
if (filename) {
uncompressed = !pcmk__ends_with_ext(filename, ".bz2");
}
if (pcmk__str_eq(filename, "-", pcmk__str_null_matches)) {
/* STDIN_FILENO == fileno(stdin) */
output = xmlCtxtReadFd(ctxt, STDIN_FILENO, "unknown.xml", NULL,
PCMK__XML_PARSE_OPTS);
} else if (uncompressed) {
output = xmlCtxtReadFile(ctxt, filename, NULL, PCMK__XML_PARSE_OPTS);
} else {
char *input = decompress_file(filename);
output = xmlCtxtReadDoc(ctxt, (pcmkXmlStr) input, NULL, NULL,
PCMK__XML_PARSE_OPTS);
free(input);
}
if (output && (xml = xmlDocGetRootElement(output))) {
pcmk__strip_xml_text(xml);
}
last_error = xmlCtxtGetLastError(ctxt);
if (last_error && last_error->code != XML_ERR_OK) {
/* crm_abort(__FILE__,__func__,__LINE__, "last_error->code != XML_ERR_OK", TRUE, TRUE); */
/*
* http://xmlsoft.org/html/libxml-xmlerror.html#xmlErrorLevel
* http://xmlsoft.org/html/libxml-xmlerror.html#xmlParserErrors
*/
crm_err("Parsing failed (domain=%d, level=%d, code=%d): %s",
last_error->domain, last_error->level, last_error->code, last_error->message);
if (last_error && last_error->code != XML_ERR_OK) {
crm_err("Couldn't%s parse %s", xml ? " fully" : "", filename);
if (xml != NULL) {
crm_log_xml_err(xml, "Partial");
}
}
}
xmlFreeParserCtxt(ctxt);
return xml;
}
/*!
* \internal
* \brief Add a "last written" attribute to an XML element, set to current time
*
* \param[in] xe XML element to add attribute to
*
* \return Value that was set, or NULL on error
*/
const char *
pcmk__xe_add_last_written(xmlNode *xe)
{
const char *now_str = pcmk__epoch2str(NULL);
return crm_xml_add(xe, XML_CIB_ATTR_WRITTEN,
now_str ? now_str : "Could not determine current time");
}
/*!
* \brief Sanitize a string so it is usable as an XML ID
*
* \param[in,out] id String to sanitize
*/
void
crm_xml_sanitize_id(char *id)
{
char *c;
for (c = id; *c; ++c) {
/* @TODO Sanitize more comprehensively */
switch (*c) {
case ':':
case '#':
*c = '.';
}
}
}
/*!
* \brief Set the ID of an XML element using a format
*
* \param[in,out] xml XML element
* \param[in] fmt printf-style format
* \param[in] ... any arguments required by format
*/
void
crm_xml_set_id(xmlNode *xml, const char *format, ...)
{
va_list ap;
int len = 0;
char *id = NULL;
/* equivalent to crm_strdup_printf() */
va_start(ap, format);
len = vasprintf(&id, format, ap);
va_end(ap);
CRM_ASSERT(len > 0);
crm_xml_sanitize_id(id);
crm_xml_add(xml, XML_ATTR_ID, id);
free(id);
}
/*!
* \internal
* \brief Write XML to a file stream
*
* \param[in] xml_node XML to write
* \param[in] filename Name of file being written (for logging only)
* \param[in] stream Open file stream corresponding to filename
* \param[in] compress Whether to compress XML before writing
* \param[out] nbytes Number of bytes written
*
* \return Standard Pacemaker return code
*/
static int
write_xml_stream(xmlNode *xml_node, const char *filename, FILE *stream,
bool compress, unsigned int *nbytes)
{
int rc = pcmk_rc_ok;
char *buffer = NULL;
*nbytes = 0;
crm_log_xml_trace(xml_node, "writing");
buffer = dump_xml_formatted(xml_node);
CRM_CHECK(buffer && strlen(buffer),
crm_log_xml_warn(xml_node, "formatting failed");
rc = pcmk_rc_error;
goto bail);
if (compress) {
unsigned int in = 0;
BZFILE *bz_file = NULL;
rc = BZ_OK;
bz_file = BZ2_bzWriteOpen(&rc, stream, 5, 0, 30);
if (rc != BZ_OK) {
crm_warn("Not compressing %s: could not prepare file stream: %s "
CRM_XS " bzerror=%d", filename, bz2_strerror(rc), rc);
} else {
BZ2_bzWrite(&rc, bz_file, buffer, strlen(buffer));
if (rc != BZ_OK) {
crm_warn("Not compressing %s: could not compress data: %s "
CRM_XS " bzerror=%d errno=%d",
filename, bz2_strerror(rc), rc, errno);
}
}
if (rc == BZ_OK) {
BZ2_bzWriteClose(&rc, bz_file, 0, &in, nbytes);
if (rc != BZ_OK) {
crm_warn("Not compressing %s: could not write compressed data: %s "
CRM_XS " bzerror=%d errno=%d",
filename, bz2_strerror(rc), rc, errno);
*nbytes = 0; // retry without compression
} else {
crm_trace("Compressed XML for %s from %u bytes to %u",
filename, in, *nbytes);
}
}
rc = pcmk_rc_ok; // Either true, or we'll retry without compression
}
if (*nbytes == 0) {
rc = fprintf(stream, "%s", buffer);
if (rc < 0) {
rc = errno;
crm_perror(LOG_ERR, "writing %s", filename);
} else {
*nbytes = (unsigned int) rc;
rc = pcmk_rc_ok;
}
}
bail:
if (fflush(stream) != 0) {
rc = errno;
crm_perror(LOG_ERR, "flushing %s", filename);
}
/* Don't report error if the file does not support synchronization */
if (fsync(fileno(stream)) < 0 && errno != EROFS && errno != EINVAL) {
rc = errno;
crm_perror(LOG_ERR, "synchronizing %s", filename);
}
fclose(stream);
crm_trace("Saved %d bytes to %s as XML", *nbytes, filename);
free(buffer);
return rc;
}
/*!
* \brief Write XML to a file descriptor
*
* \param[in] xml_node XML to write
* \param[in] filename Name of file being written (for logging only)
* \param[in] fd Open file descriptor corresponding to filename
* \param[in] compress Whether to compress XML before writing
*
* \return Number of bytes written on success, -errno otherwise
*/
int
write_xml_fd(xmlNode * xml_node, const char *filename, int fd, gboolean compress)
{
FILE *stream = NULL;
unsigned int nbytes = 0;
int rc = pcmk_rc_ok;
CRM_CHECK(xml_node && (fd > 0), return -EINVAL);
stream = fdopen(fd, "w");
if (stream == NULL) {
return -errno;
}
rc = write_xml_stream(xml_node, filename, stream, compress, &nbytes);
if (rc != pcmk_rc_ok) {
return pcmk_rc2legacy(rc);
}
return (int) nbytes;
}
/*!
* \brief Write XML to a file
*
* \param[in] xml_node XML to write
* \param[in] filename Name of file to write
* \param[in] compress Whether to compress XML before writing
*
* \return Number of bytes written on success, -errno otherwise
*/
int
write_xml_file(xmlNode * xml_node, const char *filename, gboolean compress)
{
FILE *stream = NULL;
unsigned int nbytes = 0;
int rc = pcmk_rc_ok;
CRM_CHECK(xml_node && filename, return -EINVAL);
stream = fopen(filename, "w");
if (stream == NULL) {
return -errno;
}
rc = write_xml_stream(xml_node, filename, stream, compress, &nbytes);
if (rc != pcmk_rc_ok) {
return pcmk_rc2legacy(rc);
}
return (int) nbytes;
}
// Replace a portion of a dynamically allocated string (reallocating memory)
static char *
replace_text(char *text, int start, size_t *length, const char *replace)
{
size_t offset = strlen(replace) - 1; // We have space for 1 char already
*length += offset;
text = pcmk__realloc(text, *length);
for (size_t lpc = (*length) - 1; lpc > (start + offset); lpc--) {
text[lpc] = text[lpc - offset];
}
memcpy(text + start, replace, offset + 1);
return text;
}
/*!
* \brief Replace special characters with their XML escape sequences
*
* \param[in] text Text to escape
*
* \return Newly allocated string equivalent to \p text but with special
* characters replaced with XML escape sequences (or NULL if \p text
* is NULL)
*/
char *
crm_xml_escape(const char *text)
{
size_t length;
char *copy;
/*
* When xmlCtxtReadDoc() parses < and friends in a
* value, it converts them to their human readable
* form.
*
* If one uses xmlNodeDump() to convert it back to a
* string, all is well, because special characters are
* converted back to their escape sequences.
*
* However xmlNodeDump() is randomly dog slow, even with the same
* input. So we need to replicate the escaping in our custom
* version so that the result can be re-parsed by xmlCtxtReadDoc()
* when necessary.
*/
if (text == NULL) {
return NULL;
}
length = 1 + strlen(text);
copy = strdup(text);
CRM_ASSERT(copy != NULL);
for (size_t index = 0; index < length; index++) {
if(copy[index] & 0x80 && copy[index+1] & 0x80){
index++;
break;
}
switch (copy[index]) {
case 0:
break;
case '<':
copy = replace_text(copy, index, &length, "<");
break;
case '>':
copy = replace_text(copy, index, &length, ">");
break;
case '"':
copy = replace_text(copy, index, &length, """);
break;
case '\'':
copy = replace_text(copy, index, &length, "'");
break;
case '&':
copy = replace_text(copy, index, &length, "&");
break;
case '\t':
/* Might as well just expand to a few spaces... */
copy = replace_text(copy, index, &length, " ");
break;
case '\n':
copy = replace_text(copy, index, &length, "\\n");
break;
case '\r':
copy = replace_text(copy, index, &length, "\\r");
break;
default:
/* Check for and replace non-printing characters with their octal equivalent */
if(copy[index] < ' ' || copy[index] > '~') {
char *replace = crm_strdup_printf("\\%.3o", copy[index]);
copy = replace_text(copy, index, &length, replace);
free(replace);
}
}
}
return copy;
}
static inline void
dump_xml_attr(xmlAttrPtr attr, int options, char **buffer, int *offset, int *max)
{
char *p_value = NULL;
const char *p_name = NULL;
xml_private_t *p = NULL;
CRM_ASSERT(buffer != NULL);
if (attr == NULL || attr->children == NULL) {
return;
}
p = attr->_private;
if (p && pcmk_is_set(p->flags, pcmk__xf_deleted)) {
return;
}
p_name = (const char *)attr->name;
p_value = crm_xml_escape((const char *)attr->children->content);
buffer_print(*buffer, *max, *offset, " %s=\"%s\"",
p_name, pcmk__s(p_value, ""));
free(p_value);
}
// Log an XML element (and any children) in a formatted way
void
pcmk__xe_log(int log_level, const char *file, const char *function, int line,
const char *prefix, xmlNode *data, int depth, int options)
{
int max = 0;
int offset = 0;
const char *name = NULL;
const char *hidden = NULL;
xmlNode *child = NULL;
if ((data == NULL) || (log_level == LOG_NEVER)) {
return;
}
name = crm_element_name(data);
if (pcmk_is_set(options, xml_log_option_open)) {
char *buffer = NULL;
insert_prefix(options, &buffer, &offset, &max, depth);
if (data->type == XML_COMMENT_NODE) {
buffer_print(buffer, max, offset, "", data->content);
} else {
buffer_print(buffer, max, offset, "<%s", name);
hidden = crm_element_value(data, "hidden");
for (xmlAttrPtr a = pcmk__xe_first_attr(data); a != NULL;
a = a->next) {
xml_private_t *p = a->_private;
const char *p_name = (const char *) a->name;
const char *p_value = pcmk__xml_attr_value(a);
char *p_copy = NULL;
if (pcmk_is_set(p->flags, pcmk__xf_deleted)) {
continue;
} else if (pcmk_any_flags_set(options,
xml_log_option_diff_plus
|xml_log_option_diff_minus)
&& (strcmp(XML_DIFF_MARKER, p_name) == 0)) {
continue;
} else if (hidden != NULL && p_name[0] != 0 && strstr(hidden, p_name) != NULL) {
p_copy = strdup("*****");
} else {
p_copy = crm_xml_escape(p_value);
}
buffer_print(buffer, max, offset, " %s=\"%s\"",
p_name, pcmk__s(p_copy, ""));
free(p_copy);
}
if(xml_has_children(data) == FALSE) {
buffer_print(buffer, max, offset, "/>");
} else if (pcmk_is_set(options, xml_log_option_children)) {
buffer_print(buffer, max, offset, ">");
} else {
buffer_print(buffer, max, offset, "/>");
}
}
do_crm_log_alias(log_level, file, function, line, "%s %s", prefix, buffer);
free(buffer);
}
if(data->type == XML_COMMENT_NODE) {
return;
} else if(xml_has_children(data) == FALSE) {
return;
} else if (pcmk_is_set(options, xml_log_option_children)) {
offset = 0;
max = 0;
for (child = pcmk__xml_first_child(data); child != NULL;
child = pcmk__xml_next(child)) {
pcmk__xe_log(log_level, file, function, line, prefix, child,
depth + 1,
options|xml_log_option_open|xml_log_option_close);
}
}
if (pcmk_is_set(options, xml_log_option_close)) {
char *buffer = NULL;
insert_prefix(options, &buffer, &offset, &max, depth);
buffer_print(buffer, max, offset, "%s>", name);
do_crm_log_alias(log_level, file, function, line, "%s %s", prefix, buffer);
free(buffer);
}
}
// Log XML portions that have been marked as changed
static void
log_xml_changes(int log_level, const char *file, const char *function, int line,
const char *prefix, xmlNode *data, int depth, int options)
{
xml_private_t *p;
char *prefix_m = NULL;
xmlNode *child = NULL;
if ((data == NULL) || (log_level == LOG_NEVER)) {
return;
}
p = data->_private;
prefix_m = strdup(prefix);
prefix_m[1] = '+';
if (pcmk_all_flags_set(p->flags, pcmk__xf_dirty|pcmk__xf_created)) {
/* Continue and log full subtree */
pcmk__xe_log(log_level, file, function, line, prefix_m, data, depth,
options|xml_log_option_open|xml_log_option_close
|xml_log_option_children);
} else if (pcmk_is_set(p->flags, pcmk__xf_dirty)) {
char *spaces = calloc(80, 1);
int s_count = 0, s_max = 80;
char *prefix_del = NULL;
char *prefix_moved = NULL;
const char *flags = prefix;
insert_prefix(options, &spaces, &s_count, &s_max, depth);
prefix_del = strdup(prefix);
prefix_del[0] = '-';
prefix_del[1] = '-';
prefix_moved = strdup(prefix);
prefix_moved[1] = '~';
if (pcmk_is_set(p->flags, pcmk__xf_moved)) {
flags = prefix_moved;
} else {
flags = prefix;
}
pcmk__xe_log(log_level, file, function, line, flags, data, depth,
options|xml_log_option_open);
for (xmlAttrPtr a = pcmk__xe_first_attr(data); a != NULL; a = a->next) {
const char *aname = (const char*) a->name;
p = a->_private;
if (pcmk_is_set(p->flags, pcmk__xf_deleted)) {
const char *value = crm_element_value(data, aname);
flags = prefix_del;
do_crm_log_alias(log_level, file, function, line,
"%s %s @%s=%s", flags, spaces, aname, value);
} else if (pcmk_is_set(p->flags, pcmk__xf_dirty)) {
const char *value = crm_element_value(data, aname);
if (pcmk_is_set(p->flags, pcmk__xf_created)) {
flags = prefix_m;
} else if (pcmk_is_set(p->flags, pcmk__xf_modified)) {
flags = prefix;
} else if (pcmk_is_set(p->flags, pcmk__xf_moved)) {
flags = prefix_moved;
} else {
flags = prefix;
}
do_crm_log_alias(log_level, file, function, line,
"%s %s @%s=%s", flags, spaces, aname, value);
}
}
free(prefix_moved);
free(prefix_del);
free(spaces);
for (child = pcmk__xml_first_child(data); child != NULL;
child = pcmk__xml_next(child)) {
log_xml_changes(log_level, file, function, line, prefix, child,
depth + 1, options);
}
pcmk__xe_log(log_level, file, function, line, prefix, data, depth,
options|xml_log_option_close);
} else {
for (child = pcmk__xml_first_child(data); child != NULL;
child = pcmk__xml_next(child)) {
log_xml_changes(log_level, file, function, line, prefix, child,
depth + 1, options);
}
}
free(prefix_m);
}
void
log_data_element(int log_level, const char *file, const char *function, int line,
const char *prefix, xmlNode * data, int depth, int options)
{
xmlNode *a_child = NULL;
char *prefix_m = NULL;
if (log_level == LOG_NEVER) {
return;
}
if (prefix == NULL) {
prefix = "";
}
/* Since we use the same file and line, to avoid confusing libqb, we need to use the same format strings */
if (data == NULL) {
do_crm_log_alias(log_level, file, function, line, "%s: %s", prefix,
"No data to dump as XML");
return;
}
if (pcmk_is_set(options, xml_log_option_dirty_add)) {
log_xml_changes(log_level, file, function, line, prefix, data, depth,
options);
return;
}
if (pcmk_is_set(options, xml_log_option_formatted)) {
if (pcmk_is_set(options, xml_log_option_diff_plus)
&& (data->children == NULL || crm_element_value(data, XML_DIFF_MARKER))) {
options |= xml_log_option_diff_all;
prefix_m = strdup(prefix);
prefix_m[1] = '+';
prefix = prefix_m;
} else if (pcmk_is_set(options, xml_log_option_diff_minus)
&& (data->children == NULL || crm_element_value(data, XML_DIFF_MARKER))) {
options |= xml_log_option_diff_all;
prefix_m = strdup(prefix);
prefix_m[1] = '-';
prefix = prefix_m;
}
}
if (pcmk_is_set(options, xml_log_option_diff_short)
&& !pcmk_is_set(options, xml_log_option_diff_all)) {
/* Still searching for the actual change */
for (a_child = pcmk__xml_first_child(data); a_child != NULL;
a_child = pcmk__xml_next(a_child)) {
log_data_element(log_level, file, function, line, prefix, a_child, depth + 1, options);
}
} else {
pcmk__xe_log(log_level, file, function, line, prefix, data, depth,
options|xml_log_option_open|xml_log_option_close
|xml_log_option_children);
}
free(prefix_m);
}
static void
dump_filtered_xml(xmlNode * data, int options, char **buffer, int *offset, int *max)
{
for (xmlAttrPtr a = pcmk__xe_first_attr(data); a != NULL; a = a->next) {
if (!pcmk__xa_filterable((const char *) (a->name))) {
dump_xml_attr(a, options, buffer, offset, max);
}
}
}
static void
dump_xml_element(xmlNode * data, int options, char **buffer, int *offset, int *max, int depth)
{
const char *name = NULL;
CRM_ASSERT(max != NULL);
CRM_ASSERT(offset != NULL);
CRM_ASSERT(buffer != NULL);
if (data == NULL) {
crm_trace("Nothing to dump");
return;
}
if (*buffer == NULL) {
*offset = 0;
*max = 0;
}
name = crm_element_name(data);
CRM_ASSERT(name != NULL);
insert_prefix(options, buffer, offset, max, depth);
buffer_print(*buffer, *max, *offset, "<%s", name);
if (options & xml_log_option_filtered) {
dump_filtered_xml(data, options, buffer, offset, max);
} else {
for (xmlAttrPtr a = pcmk__xe_first_attr(data); a != NULL; a = a->next) {
dump_xml_attr(a, options, buffer, offset, max);
}
}
if (data->children == NULL) {
buffer_print(*buffer, *max, *offset, "/>");
} else {
buffer_print(*buffer, *max, *offset, ">");
}
if (options & xml_log_option_formatted) {
buffer_print(*buffer, *max, *offset, "\n");
}
if (data->children) {
xmlNode *xChild = NULL;
for(xChild = data->children; xChild != NULL; xChild = xChild->next) {
pcmk__xml2text(xChild, options, buffer, offset, max, depth + 1);
}
insert_prefix(options, buffer, offset, max, depth);
buffer_print(*buffer, *max, *offset, "%s>", name);
if (options & xml_log_option_formatted) {
buffer_print(*buffer, *max, *offset, "\n");
}
}
}
static void
dump_xml_text(xmlNode * data, int options, char **buffer, int *offset, int *max, int depth)
{
CRM_ASSERT(max != NULL);
CRM_ASSERT(offset != NULL);
CRM_ASSERT(buffer != NULL);
if (data == NULL) {
crm_trace("Nothing to dump");
return;
}
if (*buffer == NULL) {
*offset = 0;
*max = 0;
}
insert_prefix(options, buffer, offset, max, depth);
buffer_print(*buffer, *max, *offset, "%s", data->content);
if (options & xml_log_option_formatted) {
buffer_print(*buffer, *max, *offset, "\n");
}
}
static void
dump_xml_cdata(xmlNode * data, int options, char **buffer, int *offset, int *max, int depth)
{
CRM_ASSERT(max != NULL);
CRM_ASSERT(offset != NULL);
CRM_ASSERT(buffer != NULL);
if (data == NULL) {
crm_trace("Nothing to dump");
return;
}
if (*buffer == NULL) {
*offset = 0;
*max = 0;
}
insert_prefix(options, buffer, offset, max, depth);
buffer_print(*buffer, *max, *offset, "content);
buffer_print(*buffer, *max, *offset, "]]>");
if (options & xml_log_option_formatted) {
buffer_print(*buffer, *max, *offset, "\n");
}
}
static void
dump_xml_comment(xmlNode * data, int options, char **buffer, int *offset, int *max, int depth)
{
CRM_ASSERT(max != NULL);
CRM_ASSERT(offset != NULL);
CRM_ASSERT(buffer != NULL);
if (data == NULL) {
crm_trace("Nothing to dump");
return;
}
if (*buffer == NULL) {
*offset = 0;
*max = 0;
}
insert_prefix(options, buffer, offset, max, depth);
buffer_print(*buffer, *max, *offset, "");
if (options & xml_log_option_formatted) {
buffer_print(*buffer, *max, *offset, "\n");
}
}
#define PCMK__XMLDUMP_STATS 0
/*!
* \internal
* \brief Create a text representation of an XML object
*
* \param[in] data XML to convert
* \param[in] options Group of enum xml_log_options flags
* \param[in,out] buffer Buffer to store text in (may be reallocated)
* \param[in,out] offset Current position of null terminator within \p buffer
* \param[in,out] max Current size of \p buffer in bytes
* \param[in] depth Current indentation level
*/
void
pcmk__xml2text(xmlNode *data, int options, char **buffer, int *offset,
int *max, int depth)
{
if(data == NULL) {
*offset = 0;
*max = 0;
return;
}
if (!pcmk_is_set(options, xml_log_option_filtered)
&& pcmk_is_set(options, xml_log_option_full_fledged)) {
/* libxml's serialization reuse is a good idea, sadly we cannot
apply it for the filtered cases (preceding filtering pass
would preclude further reuse of such in-situ modified XML
in generic context and is likely not a win performance-wise),
and there's also a historically unstable throughput argument
(likely stemming from memory allocation overhead, eventhough
that shall be minimized with defaults preset in crm_xml_init) */
#if (PCMK__XMLDUMP_STATS - 0)
time_t next, new = time(NULL);
#endif
xmlDoc *doc;
xmlOutputBuffer *xml_buffer;
doc = getDocPtr(data);
/* doc will only be NULL if data is */
CRM_CHECK(doc != NULL, return);
xml_buffer = xmlAllocOutputBuffer(NULL);
CRM_ASSERT(xml_buffer != NULL);
/* XXX we could setup custom allocation scheme for the particular
buffer, but it's subsumed with crm_xml_init that needs to
be invoked prior to entering this function as such, since
its other branch vitally depends on it -- what can be done
about this all is to have a facade parsing functions that
would 100% mark entering libxml code for us, since we don't
do anything as crazy as swapping out the binary form of the
parsed tree (but those would need to be strictly used as
opposed to libxml's raw functions) */
xmlNodeDumpOutput(xml_buffer, doc, data, 0,
(options & xml_log_option_formatted), NULL);
/* attempt adding final NL - failing shouldn't be fatal here */
(void) xmlOutputBufferWrite(xml_buffer, sizeof("\n") - 1, "\n");
if (xml_buffer->buffer != NULL) {
buffer_print(*buffer, *max, *offset, "%s",
(char *) xmlBufContent(xml_buffer->buffer));
}
#if (PCMK__XMLDUMP_STATS - 0)
next = time(NULL);
if ((now + 1) < next) {
crm_log_xml_trace(data, "Long time");
crm_err("xmlNodeDump() -> %dbytes took %ds", *max, next - now);
}
#endif
/* asserted allocation before so there should be something to remove */
(void) xmlOutputBufferClose(xml_buffer);
return;
}
switch(data->type) {
case XML_ELEMENT_NODE:
/* Handle below */
dump_xml_element(data, options, buffer, offset, max, depth);
break;
case XML_TEXT_NODE:
/* if option xml_log_option_text is enabled, then dump XML_TEXT_NODE */
if (options & xml_log_option_text) {
dump_xml_text(data, options, buffer, offset, max, depth);
}
return;
case XML_COMMENT_NODE:
dump_xml_comment(data, options, buffer, offset, max, depth);
break;
case XML_CDATA_SECTION_NODE:
dump_xml_cdata(data, options, buffer, offset, max, depth);
break;
default:
crm_warn("Unhandled type: %d", data->type);
return;
/*
XML_ATTRIBUTE_NODE = 2
XML_ENTITY_REF_NODE = 5
XML_ENTITY_NODE = 6
XML_PI_NODE = 7
XML_DOCUMENT_NODE = 9
XML_DOCUMENT_TYPE_NODE = 10
XML_DOCUMENT_FRAG_NODE = 11
XML_NOTATION_NODE = 12
XML_HTML_DOCUMENT_NODE = 13
XML_DTD_NODE = 14
XML_ELEMENT_DECL = 15
XML_ATTRIBUTE_DECL = 16
XML_ENTITY_DECL = 17
XML_NAMESPACE_DECL = 18
XML_XINCLUDE_START = 19
XML_XINCLUDE_END = 20
XML_DOCB_DOCUMENT_NODE = 21
*/
}
}
/*!
* \internal
* \brief Add a single character to a dynamically allocated buffer
*
* \param[in,out] buffer Buffer to store text in (may be reallocated)
* \param[in,out] offset Current position of null terminator within \p buffer
* \param[in,out] max Current size of \p buffer in bytes
* \param[in] c Character to add to \p buffer
*/
void
pcmk__buffer_add_char(char **buffer, int *offset, int *max, char c)
{
buffer_print(*buffer, *max, *offset, "%c", c);
}
char *
dump_xml_formatted_with_text(xmlNode * an_xml_node)
{
char *buffer = NULL;
int offset = 0, max = 0;
pcmk__xml2text(an_xml_node,
xml_log_option_formatted|xml_log_option_full_fledged,
&buffer, &offset, &max, 0);
return buffer;
}
char *
dump_xml_formatted(xmlNode * an_xml_node)
{
char *buffer = NULL;
int offset = 0, max = 0;
pcmk__xml2text(an_xml_node, xml_log_option_formatted, &buffer, &offset,
&max, 0);
return buffer;
}
char *
dump_xml_unformatted(xmlNode * an_xml_node)
{
char *buffer = NULL;
int offset = 0, max = 0;
pcmk__xml2text(an_xml_node, 0, &buffer, &offset, &max, 0);
return buffer;
}
gboolean
xml_has_children(const xmlNode * xml_root)
{
if (xml_root != NULL && xml_root->children != NULL) {
return TRUE;
}
return FALSE;
}
void
xml_remove_prop(xmlNode * obj, const char *name)
{
if (pcmk__check_acl(obj, NULL, pcmk__xf_acl_write) == FALSE) {
crm_trace("Cannot remove %s from %s", name, obj->name);
} else if (pcmk__tracking_xml_changes(obj, FALSE)) {
/* Leave in place (marked for removal) until after the diff is calculated */
xml_private_t *p = NULL;
xmlAttr *attr = xmlHasProp(obj, (pcmkXmlStr) name);
p = attr->_private;
set_parent_flag(obj, pcmk__xf_dirty);
pcmk__set_xml_flags(p, pcmk__xf_deleted);
} else {
xmlUnsetProp(obj, (pcmkXmlStr) name);
}
}
void
save_xml_to_file(xmlNode * xml, const char *desc, const char *filename)
{
char *f = NULL;
if (filename == NULL) {
char *uuid = crm_generate_uuid();
f = crm_strdup_printf("%s/%s", pcmk__get_tmpdir(), uuid);
filename = f;
free(uuid);
}
crm_info("Saving %s to %s", desc, filename);
write_xml_file(xml, filename, FALSE);
free(f);
}
/*!
* \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_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.
*/
static void
mark_attr_deleted(xmlNode *new_xml, const char *element, const char *attr_name,
const char *old_value)
{
xml_private_t *p = new_xml->doc->_private;
xmlAttr *attr = NULL;
// Prevent the dirty flag being set recursively upwards
pcmk__clear_xml_flags(p, pcmk__xf_tracking);
// Restore the old value (and the tracking flag)
attr = xmlSetProp(new_xml, (pcmkXmlStr) attr_name, (pcmkXmlStr) old_value);
pcmk__set_xml_flags(p, pcmk__xf_tracking);
// Reset flags (so the attribute doesn't appear as newly created)
p = attr->_private;
p->flags = 0;
// Check ACLs and mark restored value for later removal
xml_remove_prop(new_xml, attr_name);
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)
{
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
xmlSetProp(new_xml, (pcmkXmlStr) attr_name, (pcmkXmlStr) old_value);
// 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
*/
static void
mark_attr_moved(xmlNode *new_xml, const char *element, xmlAttr *old_attr,
xmlAttr *new_attr, int p_old, int p_new)
{
xml_private_t *p = 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
mark_xml_node_dirty(new_xml);
// Mark attribute as changed
pcmk__set_xml_flags(p, pcmk__xf_dirty|pcmk__xf_moved);
p = (p_old > p_new)? old_attr->_private : new_attr->_private;
pcmk__set_xml_flags(p, pcmk__xf_skip);
}
/*!
* \internal
* \brief Calculate differences in all previously existing XML attributes
*/
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) {
xmlAttr *old_attr = attr_iter;
xmlAttr *new_attr = xmlHasProp(new_xml, attr_iter->name);
const char *name = (const char *) attr_iter->name;
const char *old_value = crm_element_value(old_xml, name);
attr_iter = attr_iter->next;
if (new_attr == NULL) {
mark_attr_deleted(new_xml, (const char *) old_xml->name, name,
old_value);
} else {
xml_private_t *p = 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(p, 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
*/
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_private_t *p = attr_iter->_private;
attr_iter = attr_iter->next;
if (pcmk_is_set(p->flags, pcmk__xf_created)) {
const char *attr_name = (const char *) new_attr->name;
crm_trace("Created new attribute %s=%s in %s",
attr_name, crm_element_value(new_xml, attr_name),
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
xmlUnsetProp(new_xml, new_attr->name);
}
}
}
}
/*!
* \internal
* \brief Calculate differences in attributes between two XML nodes
*/
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.
*/
static void
mark_child_deleted(xmlNode *old_child, xmlNode *new_parent)
{
// Re-create the child element so we can check ACLs
xmlNode *candidate = add_node_copy(new_parent, old_child);
// Clear flags on new child and its children
reset_xml_node_flags(candidate);
// 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_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_private_t *p = new_child->_private;
crm_trace("Child element %s with id='%s' moved from position %d to %d under %s",
new_child->name, (ID(new_child)? ID(new_child) : ""),
p_old, p_new, new_parent->name);
mark_xml_node_dirty(new_parent);
pcmk__set_xml_flags(p, pcmk__xf_moved);
if (p_old > p_new) {
p = old_child->_private;
} else {
p = new_child->_private;
}
pcmk__set_xml_flags(p, 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 *cIter = NULL;
xml_private_t *p = NULL;
CRM_CHECK(new_xml != NULL, return);
if (old_xml == NULL) {
pcmk__mark_xml_created(new_xml);
pcmk__apply_creation_acl(new_xml, check_top);
return;
}
p = new_xml->_private;
CRM_CHECK(p != NULL, return);
if(p->flags & pcmk__xf_processed) {
/* Avoid re-comparing nodes */
return;
}
pcmk__set_xml_flags(p, pcmk__xf_processed);
xml_diff_attrs(old_xml, new_xml);
// Check for differences in the original children
for (cIter = pcmk__xml_first_child(old_xml); cIter != NULL; ) {
xmlNode *old_child = cIter;
xmlNode *new_child = pcmk__xml_match(new_xml, cIter, true);
cIter = pcmk__xml_next(cIter);
if(new_child) {
mark_xml_changes(old_child, new_child, TRUE);
} else {
mark_child_deleted(old_child, new_xml);
}
}
// Check for moved or created children
for (cIter = pcmk__xml_first_child(new_xml); cIter != NULL; ) {
xmlNode *new_child = cIter;
xmlNode *old_child = pcmk__xml_match(old_xml, cIter, true);
cIter = pcmk__xml_next(cIter);
if(old_child == NULL) {
// This is a newly created child
p = new_child->_private;
pcmk__set_xml_flags(p, pcmk__xf_skip);
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);
}
}
}
}
void
xml_calculate_significant_changes(xmlNode *old_xml, xmlNode *new_xml)
{
pcmk__set_xml_doc_flag(new_xml, pcmk__xf_lazy);
xml_calculate_changes(old_xml, new_xml);
}
void
xml_calculate_changes(xmlNode *old_xml, xmlNode *new_xml)
{
CRM_CHECK(pcmk__str_eq(crm_element_name(old_xml), crm_element_name(new_xml), pcmk__str_casei),
return);
CRM_CHECK(pcmk__str_eq(ID(old_xml), ID(new_xml), pcmk__str_casei), return);
if(xml_tracking_changes(new_xml) == FALSE) {
xml_track_changes(new_xml, NULL, NULL, FALSE);
}
mark_xml_changes(old_xml, new_xml, FALSE);
}
gboolean
can_prune_leaf(xmlNode * xml_node)
{
xmlNode *cIter = NULL;
gboolean can_prune = TRUE;
const char *name = crm_element_name(xml_node);
if (pcmk__strcase_any_of(name, XML_TAG_RESOURCE_REF, XML_CIB_TAG_OBJ_REF,
XML_ACL_TAG_ROLE_REF, XML_ACL_TAG_ROLE_REFv1, NULL)) {
return FALSE;
}
for (xmlAttrPtr a = pcmk__xe_first_attr(xml_node); a != NULL; a = a->next) {
const char *p_name = (const char *) a->name;
if (strcmp(p_name, XML_ATTR_ID) == 0) {
continue;
}
can_prune = FALSE;
}
cIter = pcmk__xml_first_child(xml_node);
while (cIter) {
xmlNode *child = cIter;
cIter = pcmk__xml_next(cIter);
if (can_prune_leaf(child)) {
free_xml(child);
} else {
can_prune = FALSE;
}
}
return can_prune;
}
/*!
* \internal
* \brief Find a comment with matching content in specified XML
*
* \param[in] root XML to search
* \param[in] search_comment Comment whose content should be searched for
* \param[in] exact If true, comment must also be at same position
*/
xmlNode *
pcmk__xc_match(xmlNode *root, xmlNode *search_comment, bool exact)
{
xmlNode *a_child = NULL;
int search_offset = pcmk__xml_position(search_comment, pcmk__xf_skip);
CRM_CHECK(search_comment->type == XML_COMMENT_NODE, return NULL);
for (a_child = pcmk__xml_first_child(root); a_child != NULL;
a_child = pcmk__xml_next(a_child)) {
if (exact) {
int offset = pcmk__xml_position(a_child, pcmk__xf_skip);
xml_private_t *p = a_child->_private;
if (offset < search_offset) {
continue;
} else if (offset > search_offset) {
return NULL;
}
if (pcmk_is_set(p->flags, pcmk__xf_skip)) {
continue;
}
}
if (a_child->type == XML_COMMENT_NODE
&& pcmk__str_eq((const char *)a_child->content, (const char *)search_comment->content, pcmk__str_casei)) {
return a_child;
} else if (exact) {
return NULL;
}
}
return NULL;
}
/*!
* \internal
* \brief Make one XML comment match another (in content)
*
* \param[in,out] parent If \p target is NULL and this is not, add or update
* comment child of this XML node that matches \p update
* \param[in,out] target If not NULL, update this XML comment node
* \param[in] update Make comment content match this (must not be NULL)
*
* \note At least one of \parent and \target must be non-NULL
*/
void
pcmk__xc_update(xmlNode *parent, xmlNode *target, xmlNode *update)
{
CRM_CHECK(update != NULL, return);
CRM_CHECK(update->type == XML_COMMENT_NODE, return);
if (target == NULL) {
target = pcmk__xc_match(parent, update, false);
}
if (target == NULL) {
add_node_copy(parent, update);
} else if (!pcmk__str_eq((const char *)target->content, (const char *)update->content, pcmk__str_casei)) {
xmlFree(target->content);
target->content = xmlStrdup(update->content);
}
}
/*!
* \internal
* \brief Make one XML tree match another (in children and attributes)
*
* \param[in,out] parent If \p target is NULL and this is not, add or update
* child of this XML node that matches \p update
* \param[in,out] target If not NULL, update this XML
* \param[in] update Make the desired XML match this (must not be NULL)
* \param[in] as_diff If false, expand "++" when making attributes match
*
* \note At least one of \parent and \target must be non-NULL
*/
void
pcmk__xml_update(xmlNode *parent, xmlNode *target, xmlNode *update,
bool as_diff)
{
xmlNode *a_child = NULL;
const char *object_name = NULL,
*object_href = NULL,
*object_href_val = NULL;
#if XML_PARSER_DEBUG
crm_log_xml_trace(update, "update:");
crm_log_xml_trace(target, "target:");
#endif
CRM_CHECK(update != NULL, return);
if (update->type == XML_COMMENT_NODE) {
pcmk__xc_update(parent, target, update);
return;
}
object_name = crm_element_name(update);
object_href_val = ID(update);
if (object_href_val != NULL) {
object_href = XML_ATTR_ID;
} else {
object_href_val = crm_element_value(update, XML_ATTR_IDREF);
object_href = (object_href_val == NULL) ? NULL : XML_ATTR_IDREF;
}
CRM_CHECK(object_name != NULL, return);
CRM_CHECK(target != NULL || parent != NULL, return);
if (target == NULL) {
target = pcmk__xe_match(parent, object_name,
object_href, object_href_val);
}
if (target == NULL) {
target = create_xml_node(parent, object_name);
CRM_CHECK(target != NULL, return);
#if XML_PARSER_DEBUG
crm_trace("Added <%s%s%s%s%s/>", pcmk__s(object_name, ""),
object_href ? " " : "",
object_href ? object_href : "",
object_href ? "=" : "",
object_href ? object_href_val : "");
} else {
crm_trace("Found node <%s%s%s%s%s/> to update",
pcmk__s(object_name, ""),
object_href ? " " : "",
object_href ? object_href : "",
object_href ? "=" : "",
object_href ? object_href_val : "");
#endif
}
CRM_CHECK(pcmk__str_eq(crm_element_name(target), crm_element_name(update),
pcmk__str_casei),
return);
if (as_diff == FALSE) {
/* So that expand_plus_plus() gets called */
copy_in_properties(target, update);
} else {
/* No need for expand_plus_plus(), just raw speed */
for (xmlAttrPtr a = pcmk__xe_first_attr(update); a != NULL;
a = a->next) {
const char *p_value = pcmk__xml_attr_value(a);
/* Remove it first so the ordering of the update is preserved */
xmlUnsetProp(target, a->name);
xmlSetProp(target, a->name, (pcmkXmlStr) p_value);
}
}
for (a_child = pcmk__xml_first_child(update); a_child != NULL;
a_child = pcmk__xml_next(a_child)) {
#if XML_PARSER_DEBUG
crm_trace("Updating child <%s%s%s%s%s/>",
pcmk__s(object_name, ""),
object_href ? " " : "",
object_href ? object_href : "",
object_href ? "=" : "",
object_href ? object_href_val : "");
#endif
pcmk__xml_update(target, NULL, a_child, as_diff);
}
#if XML_PARSER_DEBUG
crm_trace("Finished with <%s%s%s%s%s/>", pcmk__s(object_name, ""),
object_href ? " " : "",
object_href ? object_href : "",
object_href ? "=" : "",
object_href ? object_href_val : "");
#endif
}
gboolean
update_xml_child(xmlNode * child, xmlNode * to_update)
{
gboolean can_update = TRUE;
xmlNode *child_of_child = NULL;
CRM_CHECK(child != NULL, return FALSE);
CRM_CHECK(to_update != NULL, return FALSE);
if (!pcmk__str_eq(crm_element_name(to_update), crm_element_name(child), pcmk__str_none)) {
can_update = FALSE;
} else if (!pcmk__str_eq(ID(to_update), ID(child), pcmk__str_none)) {
can_update = FALSE;
} else if (can_update) {
#if XML_PARSER_DEBUG
crm_log_xml_trace(child, "Update match found...");
#endif
pcmk__xml_update(NULL, child, to_update, false);
}
for (child_of_child = pcmk__xml_first_child(child); child_of_child != NULL;
child_of_child = pcmk__xml_next(child_of_child)) {
/* only update the first one */
if (can_update) {
break;
}
can_update = update_xml_child(child_of_child, to_update);
}
return can_update;
}
int
find_xml_children(xmlNode ** children, xmlNode * root,
const char *tag, const char *field, const char *value, gboolean search_matches)
{
int match_found = 0;
CRM_CHECK(root != NULL, return FALSE);
CRM_CHECK(children != NULL, return FALSE);
if (tag != NULL && !pcmk__str_eq(tag, crm_element_name(root), pcmk__str_casei)) {
} else if (value != NULL && !pcmk__str_eq(value, crm_element_value(root, field), pcmk__str_casei)) {
} else {
if (*children == NULL) {
*children = create_xml_node(NULL, __func__);
}
add_node_copy(*children, root);
match_found = 1;
}
if (search_matches || match_found == 0) {
xmlNode *child = NULL;
for (child = pcmk__xml_first_child(root); child != NULL;
child = pcmk__xml_next(child)) {
match_found += find_xml_children(children, child, tag, field, value, search_matches);
}
}
return match_found;
}
gboolean
replace_xml_child(xmlNode * parent, xmlNode * child, xmlNode * update, gboolean delete_only)
{
gboolean can_delete = FALSE;
xmlNode *child_of_child = NULL;
const char *up_id = NULL;
const char *child_id = NULL;
const char *right_val = NULL;
CRM_CHECK(child != NULL, return FALSE);
CRM_CHECK(update != NULL, return FALSE);
up_id = ID(update);
child_id = ID(child);
if (up_id == NULL || (child_id && strcmp(child_id, up_id) == 0)) {
can_delete = TRUE;
}
if (!pcmk__str_eq(crm_element_name(update), crm_element_name(child), pcmk__str_casei)) {
can_delete = FALSE;
}
if (can_delete && delete_only) {
for (xmlAttrPtr a = pcmk__xe_first_attr(update); a != NULL;
a = a->next) {
const char *p_name = (const char *) a->name;
const char *p_value = pcmk__xml_attr_value(a);
right_val = crm_element_value(child, p_name);
if (!pcmk__str_eq(p_value, right_val, pcmk__str_casei)) {
can_delete = FALSE;
}
}
}
if (can_delete && parent != NULL) {
crm_log_xml_trace(child, "Delete match found...");
if (delete_only || update == NULL) {
free_xml(child);
} else {
xmlNode *tmp = copy_xml(update);
xmlDoc *doc = tmp->doc;
xmlNode *old = NULL;
xml_accept_changes(tmp);
old = xmlReplaceNode(child, tmp);
if(xml_tracking_changes(tmp)) {
/* Replaced sections may have included relevant ACLs */
pcmk__apply_acl(tmp);
}
xml_calculate_changes(old, tmp);
xmlDocSetRootElement(doc, old);
free_xml(old);
}
child = NULL;
return TRUE;
} else if (can_delete) {
crm_log_xml_debug(child, "Cannot delete the search root");
can_delete = FALSE;
}
child_of_child = pcmk__xml_first_child(child);
while (child_of_child) {
xmlNode *next = pcmk__xml_next(child_of_child);
can_delete = replace_xml_child(child, child_of_child, update, delete_only);
/* only delete the first one */
if (can_delete) {
child_of_child = NULL;
} else {
child_of_child = next;
}
}
return can_delete;
}
xmlNode *
sorted_xml(xmlNode *input, xmlNode *parent, gboolean recursive)
{
xmlNode *child = NULL;
GSList *nvpairs = NULL;
xmlNode *result = NULL;
const char *name = NULL;
CRM_CHECK(input != NULL, return NULL);
name = crm_element_name(input);
CRM_CHECK(name != NULL, return NULL);
result = create_xml_node(parent, name);
nvpairs = pcmk_xml_attrs2nvpairs(input);
nvpairs = pcmk_sort_nvpairs(nvpairs);
pcmk_nvpairs2xml_attrs(nvpairs, result);
pcmk_free_nvpairs(nvpairs);
for (child = pcmk__xml_first_child(input); child != NULL;
child = pcmk__xml_next(child)) {
if (recursive) {
sorted_xml(child, result, recursive);
} else {
add_node_copy(result, child);
}
}
return result;
}
xmlNode *
first_named_child(const xmlNode *parent, const char *name)
{
xmlNode *match = NULL;
for (match = pcmk__xe_first_child(parent); match != NULL;
match = pcmk__xe_next(match)) {
/*
* name == NULL gives first child regardless of name; this is
* semantically incorrect in this function, but may be necessary
* due to prior use of xml_child_iter_filter
*/
if (pcmk__str_eq(name, (const char *)match->name, pcmk__str_null_matches)) {
return match;
}
}
return NULL;
}
/*!
* \brief Get next instance of same XML tag
*
* \param[in] sibling XML tag to start from
*
* \return Next sibling XML tag with same name
*/
xmlNode *
crm_next_same_xml(const xmlNode *sibling)
{
xmlNode *match = pcmk__xe_next(sibling);
const char *name = crm_element_name(sibling);
while (match != NULL) {
if (!strcmp(crm_element_name(match), name)) {
return match;
}
match = pcmk__xe_next(match);
}
return NULL;
}
void
crm_xml_init(void)
{
static bool init = true;
if(init) {
init = false;
/* The default allocator XML_BUFFER_ALLOC_EXACT does far too many
* pcmk__realloc()s and it can take upwards of 18 seconds (yes, seconds)
* to dump a 28kb tree which XML_BUFFER_ALLOC_DOUBLEIT can do in
* less than 1 second.
*/
xmlSetBufferAllocationScheme(XML_BUFFER_ALLOC_DOUBLEIT);
/* Populate and free the _private field when nodes are created and destroyed */
xmlDeregisterNodeDefault(free_private_data);
xmlRegisterNodeDefault(new_private_data);
crm_schema_init();
}
}
void
crm_xml_cleanup(void)
{
crm_schema_cleanup();
xmlCleanupParser();
}
#define XPATH_MAX 512
xmlNode *
expand_idref(xmlNode * input, xmlNode * top)
{
const char *tag = NULL;
const char *ref = NULL;
xmlNode *result = input;
if (result == NULL) {
return NULL;
} else if (top == NULL) {
top = input;
}
tag = crm_element_name(result);
ref = crm_element_value(result, XML_ATTR_IDREF);
if (ref != NULL) {
char *xpath_string = crm_strdup_printf("//%s[@id='%s']", tag, ref);
result = get_xpath_object(xpath_string, top, LOG_ERR);
if (result == NULL) {
char *nodePath = (char *)xmlGetNodePath(top);
crm_err("No match for %s found in %s: Invalid configuration",
xpath_string, pcmk__s(nodePath, "unrecognizable path"));
free(nodePath);
}
free(xpath_string);
}
return result;
}
void
crm_destroy_xml(gpointer data)
{
free_xml(data);
}
char *
pcmk__xml_artefact_root(enum pcmk__xml_artefact_ns ns)
{
static const char *base = NULL;
char *ret = NULL;
if (base == NULL) {
base = getenv("PCMK_schema_directory");
}
if (pcmk__str_empty(base)) {
base = CRM_SCHEMA_DIRECTORY;
}
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;
}
char *
pcmk__xml_artefact_path(enum pcmk__xml_artefact_ns ns, const char *filespec)
{
char *base = pcmk__xml_artefact_root(ns), *ret = NULL;
switch (ns) {
case pcmk__xml_artefact_ns_legacy_rng:
case pcmk__xml_artefact_ns_base_rng:
ret = crm_strdup_printf("%s/%s.rng", base, filespec);
break;
case pcmk__xml_artefact_ns_legacy_xslt:
case pcmk__xml_artefact_ns_base_xslt:
ret = crm_strdup_printf("%s/%s.xsl", base, filespec);
break;
default:
crm_err("XML artefact family specified as %u not recognized", ns);
}
free(base);
return ret;
}
void
pcmk__xe_set_propv(xmlNodePtr node, va_list pairs)
{
while (true) {
const char *name, *value;
name = va_arg(pairs, const char *);
if (name == NULL) {
return;
}
value = va_arg(pairs, const char *);
if (value != NULL) {
crm_xml_add(node, name, value);
}
}
}
void
pcmk__xe_set_props(xmlNodePtr node, ...)
{
va_list pairs;
va_start(pairs, node);
pcmk__xe_set_propv(node, pairs);
va_end(pairs);
}
// Deprecated functions kept only for backward API compatibility
// LCOV_EXCL_START
#include
xmlNode *
find_entity(xmlNode *parent, const char *node_name, const char *id)
{
return pcmk__xe_match(parent, node_name,
((id == NULL)? id : XML_ATTR_ID), id);
}
// LCOV_EXCL_STOP
// End deprecated API
diff --git a/lib/pacemaker/pcmk_acl.c b/lib/pacemaker/pcmk_acl.c
index b79ed0480e..468d6ddf8e 100644
--- a/lib/pacemaker/pcmk_acl.c
+++ b/lib/pacemaker/pcmk_acl.c
@@ -1,356 +1,358 @@
/*
* Copyright 2004-2022 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU Lesser General Public License
* version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#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 This function takes some XML, and annotates it with XML
* namespaces to indicate the ACL permissions.
*
* \param[in,out] xml_modify
*
* \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
pcmk__acl_annotate_permissions_recursive(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);
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);
/* XXX recursion can be turned into plain 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 |= pcmk__acl_annotate_permissions_recursive((xmlNodePtr) i_node->properties);
}
if (i_node->children != NULL) {
ret |= pcmk__acl_annotate_permissions_recursive(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, xmlDoc *cib_doc,
xmlDoc **acl_evaled_doc)
{
int ret, version;
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),
XML_ATTR_VALIDATION);
version = get_schema_version(validation);
if (get_schema_version(PCMK__COMPAT_ACL_2_MIN_INCL) > version) {
return pcmk_rc_schema_validation;
}
target = copy_xml(xmlDocGetRootElement(cib_doc));
if (target == NULL) {
return EINVAL;
}
pcmk__enable_acl(target, target, cred);
ret = pcmk__acl_annotate_permissions_recursive(target);
if (ret == pcmk_rc_ok) {
char* credentials = crm_strdup_printf("ACLs as evaluated for user %s", cred);
comment = xmlNewDocComment(target->doc, (pcmkXmlStr) credentials);
free(credentials);
if (comment == NULL) {
xmlFreeNode(target);
return EINVAL;
}
xmlAddPrevSibling(xmlDocGetRootElement(target->doc), comment);
*acl_evaled_doc = target->doc;
return pcmk_rc_ok;
} else {
xmlFreeNode(target);
return ret; //for now, it should be some kind of error
}
}
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 ret;
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;
xmlDocDumpFormatMemory(annotated_doc, &annotated_dump, &dump_size, 1);
res = xmlReadDoc(annotated_dump, "on-the-fly-access-render", NULL,
XML_PARSE_NONET);
CRM_ASSERT(res != NULL);
xmlFree(annotated_dump);
xmlFreeDoc(annotated_doc);
annotated_doc = res;
sfile = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_base_xslt,
"access-render-2");
parser_ctxt = xmlNewParserCtxt();
CRM_ASSERT(sfile != NULL);
CRM_ASSERT(parser_ctxt != NULL);
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);
return EINVAL;
}
free(sfile);
sfile = NULL;
xmlFreeParserCtxt(parser_ctxt);
xslt_ctxt = xsltNewTransformContext(xslt, annotated_doc);
CRM_ASSERT(xslt_ctxt != NULL);
if (how == pcmk__acl_render_text) {
params = params_noansi;
} else if (how == pcmk__acl_render_namespace) {
params = params_namespace;
} else {
params = params_useansi;
}
xsltQuoteUserParams(xslt_ctxt, params);
res = xsltApplyStylesheetUser(xslt, annotated_doc, NULL,
NULL, NULL, xslt_ctxt);
xmlFreeDoc(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) {
ret = EINVAL;
} else {
int doc_txt_len;
int temp = xsltSaveResultToString(doc_txt_ptr, &doc_txt_len, res, xslt);
xmlFreeDoc(res);
if (temp == 0) {
ret = pcmk_rc_ok;
} else {
ret = EINVAL;
}
}
xsltFreeStylesheet(xslt);
return ret;
}