/src/pacemaker/lib/common/acl.c
Line | Count | Source |
1 | | /* |
2 | | * Copyright 2004-2025 the Pacemaker project contributors |
3 | | * |
4 | | * The version control history for this file may have further details. |
5 | | * |
6 | | * This source code is licensed under the GNU Lesser General Public License |
7 | | * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. |
8 | | */ |
9 | | |
10 | | #include <crm_internal.h> |
11 | | |
12 | | #include <stdio.h> |
13 | | #include <sys/types.h> |
14 | | #include <pwd.h> |
15 | | #include <string.h> |
16 | | #include <stdlib.h> |
17 | | #include <stdarg.h> |
18 | | |
19 | | #include <libxml/tree.h> // xmlNode, etc. |
20 | | #include <libxml/xmlstring.h> // xmlChar |
21 | | #include <libxml/xpath.h> // xmlXPathObject, etc. |
22 | | |
23 | | #include <crm/crm.h> |
24 | | #include <crm/common/xml.h> |
25 | | #include <crm/common/xml_internal.h> |
26 | | #include "crmcommon_private.h" |
27 | | |
28 | | typedef struct xml_acl_s { |
29 | | enum pcmk__xml_flags mode; |
30 | | gchar *xpath; |
31 | | } xml_acl_t; |
32 | | |
33 | | static void |
34 | | free_acl(void *data) |
35 | 0 | { |
36 | 0 | if (data) { |
37 | 0 | xml_acl_t *acl = data; |
38 | |
|
39 | 0 | g_free(acl->xpath); |
40 | 0 | free(acl); |
41 | 0 | } |
42 | 0 | } |
43 | | |
44 | | void |
45 | | pcmk__free_acls(GList *acls) |
46 | 0 | { |
47 | 0 | g_list_free_full(acls, free_acl); |
48 | 0 | } |
49 | | |
50 | | static GList * |
51 | | create_acl(const xmlNode *xml, GList *acls, enum pcmk__xml_flags mode) |
52 | 0 | { |
53 | 0 | xml_acl_t *acl = NULL; |
54 | |
|
55 | 0 | const char *tag = pcmk__xe_get(xml, PCMK_XA_OBJECT_TYPE); |
56 | 0 | const char *ref = pcmk__xe_get(xml, PCMK_XA_REFERENCE); |
57 | 0 | const char *xpath = pcmk__xe_get(xml, PCMK_XA_XPATH); |
58 | 0 | const char *attr = pcmk__xe_get(xml, PCMK_XA_ATTRIBUTE); |
59 | |
|
60 | 0 | if ((tag == NULL) && (ref == NULL) && (xpath == NULL)) { |
61 | | // Schema should prevent this, but to be safe ... |
62 | 0 | crm_trace("Ignoring ACL <%s> element without selection criteria", |
63 | 0 | xml->name); |
64 | 0 | return NULL; |
65 | 0 | } |
66 | | |
67 | 0 | acl = pcmk__assert_alloc(1, sizeof (xml_acl_t)); |
68 | |
|
69 | 0 | acl->mode = mode; |
70 | 0 | if (xpath) { |
71 | 0 | acl->xpath = g_strdup(xpath); |
72 | 0 | crm_trace("Unpacked ACL <%s> element using xpath: %s", |
73 | 0 | xml->name, acl->xpath); |
74 | |
|
75 | 0 | } else { |
76 | 0 | GString *buf = g_string_sized_new(128); |
77 | |
|
78 | 0 | if ((ref != NULL) && (attr != NULL)) { |
79 | | // NOTE: schema currently does not allow this |
80 | 0 | pcmk__g_strcat(buf, "//", pcmk__s(tag, "*"), "[@" PCMK_XA_ID "='", |
81 | 0 | ref, "' and @", attr, "]", NULL); |
82 | |
|
83 | 0 | } else if (ref != NULL) { |
84 | 0 | pcmk__g_strcat(buf, "//", pcmk__s(tag, "*"), "[@" PCMK_XA_ID "='", |
85 | 0 | ref, "']", NULL); |
86 | |
|
87 | 0 | } else if (attr != NULL) { |
88 | 0 | pcmk__g_strcat(buf, "//", pcmk__s(tag, "*"), "[@", attr, "]", NULL); |
89 | |
|
90 | 0 | } else { |
91 | 0 | pcmk__g_strcat(buf, "//", pcmk__s(tag, "*"), NULL); |
92 | 0 | } |
93 | |
|
94 | 0 | acl->xpath = buf->str; |
95 | |
|
96 | 0 | g_string_free(buf, FALSE); |
97 | 0 | crm_trace("Unpacked ACL <%s> element as xpath: %s", |
98 | 0 | xml->name, acl->xpath); |
99 | 0 | } |
100 | | |
101 | 0 | return g_list_append(acls, acl); |
102 | 0 | } |
103 | | |
104 | | /*! |
105 | | * \internal |
106 | | * \brief Unpack a user, group, or role subtree of the ACLs section |
107 | | * |
108 | | * \param[in] acl_top XML of entire ACLs section |
109 | | * \param[in] acl_entry XML of ACL element being unpacked |
110 | | * \param[in,out] acls List of ACLs unpacked so far |
111 | | * |
112 | | * \return New head of (possibly modified) acls |
113 | | * |
114 | | * \note This function is recursive |
115 | | */ |
116 | | static GList * |
117 | | parse_acl_entry(const xmlNode *acl_top, const xmlNode *acl_entry, GList *acls) |
118 | 0 | { |
119 | 0 | for (const xmlNode *child = pcmk__xe_first_child(acl_entry, NULL, NULL, |
120 | 0 | NULL); |
121 | 0 | child != NULL; child = pcmk__xe_next(child, NULL)) { |
122 | |
|
123 | 0 | if (pcmk__xe_is(child, PCMK_XE_ACL_PERMISSION)) { |
124 | 0 | const char *kind = pcmk__xe_get(child, PCMK_XA_KIND); |
125 | |
|
126 | 0 | pcmk__assert(kind != NULL); |
127 | 0 | crm_trace("Unpacking <" PCMK_XE_ACL_PERMISSION "> element of " |
128 | 0 | "kind '%s'", |
129 | 0 | kind); |
130 | | |
131 | 0 | if (pcmk__str_eq(kind, PCMK_VALUE_READ, pcmk__str_none)) { |
132 | 0 | acls = create_acl(child, acls, pcmk__xf_acl_read); |
133 | |
|
134 | 0 | } else if (pcmk__str_eq(kind, PCMK_VALUE_WRITE, pcmk__str_none)) { |
135 | 0 | acls = create_acl(child, acls, pcmk__xf_acl_write); |
136 | |
|
137 | 0 | } else if (pcmk__str_eq(kind, PCMK_VALUE_DENY, pcmk__str_none)) { |
138 | 0 | acls = create_acl(child, acls, pcmk__xf_acl_deny); |
139 | |
|
140 | 0 | } else { |
141 | 0 | crm_warn("Ignoring unknown ACL kind '%s'", kind); |
142 | 0 | } |
143 | |
|
144 | 0 | } else if (pcmk__xe_is(child, PCMK_XE_ROLE)) { |
145 | 0 | const char *ref_role = pcmk__xe_get(child, PCMK_XA_ID); |
146 | |
|
147 | 0 | crm_trace("Unpacking <" PCMK_XE_ROLE "> element"); |
148 | | |
149 | 0 | if (ref_role == NULL) { |
150 | 0 | continue; |
151 | 0 | } |
152 | | |
153 | 0 | for (xmlNode *role = pcmk__xe_first_child(acl_top, NULL, NULL, |
154 | 0 | NULL); |
155 | 0 | role != NULL; role = pcmk__xe_next(role, NULL)) { |
156 | |
|
157 | 0 | const char *role_id = NULL; |
158 | |
|
159 | 0 | if (!pcmk__xe_is(role, PCMK_XE_ACL_ROLE)) { |
160 | 0 | continue; |
161 | 0 | } |
162 | | |
163 | 0 | role_id = pcmk__xe_get(role, PCMK_XA_ID); |
164 | |
|
165 | 0 | if (pcmk__str_eq(ref_role, role_id, pcmk__str_none)) { |
166 | 0 | crm_trace("Unpacking referenced role '%s' in <%s> element", |
167 | 0 | role_id, acl_entry->name); |
168 | 0 | acls = parse_acl_entry(acl_top, role, acls); |
169 | 0 | break; |
170 | 0 | } |
171 | 0 | } |
172 | 0 | } |
173 | 0 | } |
174 | | |
175 | 0 | return acls; |
176 | 0 | } |
177 | | |
178 | | /* |
179 | | <acls> |
180 | | <acl_target id="l33t-haxor"><role id="auto-l33t-haxor"/></acl_target> |
181 | | <acl_role id="auto-l33t-haxor"> |
182 | | <acl_permission id="crook-nothing" kind="deny" xpath="/cib"/> |
183 | | </acl_role> |
184 | | <acl_target id="niceguy"> |
185 | | <role id="observer"/> |
186 | | </acl_target> |
187 | | <acl_role id="observer"> |
188 | | <acl_permission id="observer-read-1" kind="read" xpath="/cib"/> |
189 | | <acl_permission id="observer-write-1" kind="write" xpath="//nvpair[@name='fencing-enabled']"/> |
190 | | <acl_permission id="observer-write-2" kind="write" xpath="//nvpair[@name='target-role']"/> |
191 | | </acl_role> |
192 | | <acl_target id="badidea"><role id="auto-badidea"/></acl_target> |
193 | | <acl_role id="auto-badidea"> |
194 | | <acl_permission id="badidea-resources" kind="read" xpath="//meta_attributes"/> |
195 | | <acl_permission id="badidea-resources-2" kind="deny" reference="dummy-meta_attributes"/> |
196 | | </acl_role> |
197 | | </acls> |
198 | | */ |
199 | | |
200 | | static const char * |
201 | | acl_to_text(enum pcmk__xml_flags flags) |
202 | 0 | { |
203 | 0 | if (pcmk__is_set(flags, pcmk__xf_acl_deny)) { |
204 | 0 | return "deny"; |
205 | |
|
206 | 0 | } else if (pcmk__any_flags_set(flags, |
207 | 0 | pcmk__xf_acl_write|pcmk__xf_acl_create)) { |
208 | 0 | return "read/write"; |
209 | |
|
210 | 0 | } else if (pcmk__is_set(flags, pcmk__xf_acl_read)) { |
211 | 0 | return "read"; |
212 | 0 | } |
213 | 0 | return "none"; |
214 | 0 | } |
215 | | |
216 | | void |
217 | | pcmk__apply_acl(xmlNode *xml) |
218 | 0 | { |
219 | 0 | GList *aIter = NULL; |
220 | 0 | xml_doc_private_t *docpriv = NULL; |
221 | 0 | xml_node_private_t *nodepriv = NULL; |
222 | 0 | xmlXPathObject *xpathObj = NULL; |
223 | |
|
224 | 0 | pcmk__assert(xml != NULL); |
225 | |
|
226 | 0 | docpriv = xml->doc->_private; |
227 | |
|
228 | 0 | if (!pcmk__xml_doc_all_flags_set(xml->doc, pcmk__xf_acl_enabled)) { |
229 | 0 | crm_trace("Skipping ACLs for user '%s' because not enabled for this XML", |
230 | 0 | pcmk__s(docpriv->acl_user, "(unknown)")); |
231 | 0 | return; |
232 | 0 | } |
233 | | |
234 | 0 | for (aIter = docpriv->acls; aIter != NULL; aIter = aIter->next) { |
235 | 0 | int max = 0, lpc = 0; |
236 | 0 | xml_acl_t *acl = aIter->data; |
237 | |
|
238 | 0 | xpathObj = pcmk__xpath_search(xml->doc, acl->xpath); |
239 | 0 | max = pcmk__xpath_num_results(xpathObj); |
240 | |
|
241 | 0 | for (lpc = 0; lpc < max; lpc++) { |
242 | 0 | xmlNode *match = pcmk__xpath_result(xpathObj, lpc); |
243 | |
|
244 | 0 | if (match == NULL) { |
245 | 0 | continue; |
246 | 0 | } |
247 | | |
248 | | /* @COMPAT If the ACL's XPath matches a node that is neither an |
249 | | * element nor a document, we apply the ACL to the parent element |
250 | | * rather than to the matched node. For example, if the XPath |
251 | | * matches a "score" attribute, then it applies to every element |
252 | | * that contains a "score" attribute. That is, the XPath expression |
253 | | * "//@score" matches all attributes named "score", but we apply the |
254 | | * ACL to all elements containing such an attribute. |
255 | | * |
256 | | * This behavior is incorrect from an XPath standpoint and is thus |
257 | | * confusing and counterintuitive. The correct way to match all |
258 | | * elements containing a "score" attribute is to use an XPath |
259 | | * predicate: "// *[@score]". (Space inserted after slashes so that |
260 | | * GCC doesn't throw an error about nested comments.) |
261 | | * |
262 | | * Additionally, if an XPath expression matches the entire document |
263 | | * (for example, "/"), then the ACL applies to the document's root |
264 | | * element if it exists. |
265 | | * |
266 | | * These behaviors should be changed so that the ACL applies to the |
267 | | * nodes matched by the XPath expression, or so that it doesn't |
268 | | * apply at all if applying an ACL to an attribute doesn't make |
269 | | * sense. |
270 | | * |
271 | | * Unfortunately, we document in Pacemaker Explained that matching |
272 | | * attributes is a valid way to match elements: "Attributes may be |
273 | | * specified in the XPath to select particular elements, but the |
274 | | * permissions apply to the entire element." |
275 | | * |
276 | | * So we have to keep this behavior at least until a compatibility |
277 | | * break. Even then, it's not feasible in the general case to |
278 | | * transform such XPath expressions using XSLT. |
279 | | */ |
280 | 0 | match = pcmk__xpath_match_element(match); |
281 | 0 | if (match == NULL) { |
282 | 0 | continue; |
283 | 0 | } |
284 | | |
285 | 0 | nodepriv = match->_private; |
286 | 0 | pcmk__set_xml_flags(nodepriv, acl->mode); |
287 | | |
288 | | // Build a GString only if tracing is enabled |
289 | 0 | pcmk__if_tracing( |
290 | 0 | { |
291 | 0 | GString *path = pcmk__element_xpath(match); |
292 | 0 | crm_trace("Applying %s ACL to %s matched by %s", |
293 | 0 | acl_to_text(acl->mode), path->str, acl->xpath); |
294 | 0 | g_string_free(path, TRUE); |
295 | 0 | }, |
296 | 0 | {} |
297 | 0 | ); |
298 | 0 | } |
299 | 0 | crm_trace("Applied %s ACL %s (%d match%s)", |
300 | 0 | acl_to_text(acl->mode), acl->xpath, max, |
301 | 0 | ((max == 1)? "" : "es")); |
302 | 0 | xmlXPathFreeObject(xpathObj); |
303 | 0 | } |
304 | 0 | } |
305 | | |
306 | | /*! |
307 | | * \internal |
308 | | * \brief Unpack ACLs for a given user into the |
309 | | * metadata of the target XML tree |
310 | | * |
311 | | * Taking the description of ACLs from the source XML tree and |
312 | | * marking up the target XML tree with access information for the |
313 | | * given user by tacking it onto the relevant nodes |
314 | | * |
315 | | * \param[in] source XML with ACL definitions |
316 | | * \param[in,out] target XML that ACLs will be applied to |
317 | | * \param[in] user Username whose ACLs need to be unpacked |
318 | | */ |
319 | | void |
320 | | pcmk__unpack_acl(xmlNode *source, xmlNode *target, const char *user) |
321 | 0 | { |
322 | 0 | xml_doc_private_t *docpriv = NULL; |
323 | |
|
324 | 0 | if ((target == NULL) || (target->doc == NULL) |
325 | 0 | || (target->doc->_private == NULL)) { |
326 | 0 | return; |
327 | 0 | } |
328 | | |
329 | 0 | docpriv = target->doc->_private; |
330 | 0 | if (!pcmk_acl_required(user)) { |
331 | 0 | crm_trace("Not unpacking ACLs because not required for user '%s'", |
332 | 0 | user); |
333 | |
|
334 | 0 | } else if (docpriv->acls == NULL) { |
335 | 0 | xmlNode *acls = pcmk__xpath_find_one(source->doc, "//" PCMK_XE_ACLS, |
336 | 0 | LOG_NEVER); |
337 | |
|
338 | 0 | pcmk__str_update(&(docpriv->acl_user), user); |
339 | |
|
340 | 0 | if (acls) { |
341 | 0 | xmlNode *child = NULL; |
342 | |
|
343 | 0 | for (child = pcmk__xe_first_child(acls, NULL, NULL, NULL); |
344 | 0 | child != NULL; child = pcmk__xe_next(child, NULL)) { |
345 | |
|
346 | 0 | if (pcmk__xe_is(child, PCMK_XE_ACL_TARGET)) { |
347 | 0 | const char *id = pcmk__xe_get(child, PCMK_XA_NAME); |
348 | |
|
349 | 0 | if (id == NULL) { |
350 | 0 | id = pcmk__xe_get(child, PCMK_XA_ID); |
351 | 0 | } |
352 | |
|
353 | 0 | if (id && strcmp(id, user) == 0) { |
354 | 0 | crm_debug("Unpacking ACLs for user '%s'", id); |
355 | 0 | docpriv->acls = parse_acl_entry(acls, child, docpriv->acls); |
356 | 0 | } |
357 | 0 | } else if (pcmk__xe_is(child, PCMK_XE_ACL_GROUP)) { |
358 | 0 | const char *id = pcmk__xe_get(child, PCMK_XA_NAME); |
359 | |
|
360 | 0 | if (id == NULL) { |
361 | 0 | id = pcmk__xe_get(child, PCMK_XA_ID); |
362 | 0 | } |
363 | |
|
364 | 0 | if (id && pcmk__is_user_in_group(user,id)) { |
365 | 0 | crm_debug("Unpacking ACLs for group '%s'", id); |
366 | 0 | docpriv->acls = parse_acl_entry(acls, child, docpriv->acls); |
367 | 0 | } |
368 | 0 | } |
369 | 0 | } |
370 | 0 | } |
371 | 0 | } |
372 | 0 | } |
373 | | |
374 | | /*! |
375 | | * \internal |
376 | | * \brief Copy source to target and set xf_acl_enabled flag in target |
377 | | * |
378 | | * \param[in] acl_source XML with ACL definitions |
379 | | * \param[in,out] target XML that ACLs will be applied to |
380 | | * \param[in] user Username whose ACLs need to be set |
381 | | */ |
382 | | void |
383 | | pcmk__enable_acl(xmlNode *acl_source, xmlNode *target, const char *user) |
384 | 0 | { |
385 | 0 | if (target == NULL) { |
386 | 0 | return; |
387 | 0 | } |
388 | 0 | pcmk__unpack_acl(acl_source, target, user); |
389 | 0 | pcmk__xml_doc_set_flags(target->doc, pcmk__xf_acl_enabled); |
390 | 0 | pcmk__apply_acl(target); |
391 | 0 | } |
392 | | |
393 | | static inline bool |
394 | | test_acl_mode(enum pcmk__xml_flags allowed, enum pcmk__xml_flags requested) |
395 | 0 | { |
396 | 0 | if (pcmk__is_set(allowed, pcmk__xf_acl_deny)) { |
397 | 0 | return false; |
398 | |
|
399 | 0 | } else if (pcmk__all_flags_set(allowed, requested)) { |
400 | 0 | return true; |
401 | |
|
402 | 0 | } else if (pcmk__is_set(requested, pcmk__xf_acl_read) |
403 | 0 | && pcmk__is_set(allowed, pcmk__xf_acl_write)) { |
404 | 0 | return true; |
405 | |
|
406 | 0 | } else if (pcmk__is_set(requested, pcmk__xf_acl_create) |
407 | 0 | && pcmk__any_flags_set(allowed, |
408 | 0 | pcmk__xf_acl_write|pcmk__xf_created)) { |
409 | 0 | return true; |
410 | 0 | } |
411 | 0 | return false; |
412 | 0 | } |
413 | | |
414 | | /*! |
415 | | * \internal |
416 | | * \brief Rid XML tree of all unreadable nodes and node properties |
417 | | * |
418 | | * \param[in,out] xml Root XML node to be purged of attributes |
419 | | * |
420 | | * \return true if this node or any of its children are readable |
421 | | * if false is returned, xml will be freed |
422 | | * |
423 | | * \note This function is recursive |
424 | | */ |
425 | | static bool |
426 | | purge_xml_attributes(xmlNode *xml) |
427 | 0 | { |
428 | 0 | xmlNode *child = NULL; |
429 | 0 | xmlAttr *xIter = NULL; |
430 | 0 | bool readable_children = false; |
431 | 0 | xml_node_private_t *nodepriv = xml->_private; |
432 | |
|
433 | 0 | if (test_acl_mode(nodepriv->flags, pcmk__xf_acl_read)) { |
434 | 0 | crm_trace("%s[@" PCMK_XA_ID "=%s] is readable", |
435 | 0 | xml->name, pcmk__xe_id(xml)); |
436 | 0 | return true; |
437 | 0 | } |
438 | | |
439 | 0 | xIter = xml->properties; |
440 | 0 | while (xIter != NULL) { |
441 | 0 | xmlAttr *tmp = xIter; |
442 | 0 | const char *prop_name = (const char *)xIter->name; |
443 | |
|
444 | 0 | xIter = xIter->next; |
445 | 0 | if (strcmp(prop_name, PCMK_XA_ID) == 0) { |
446 | 0 | continue; |
447 | 0 | } |
448 | | |
449 | 0 | pcmk__xa_remove(tmp, true); |
450 | 0 | } |
451 | |
|
452 | 0 | child = pcmk__xml_first_child(xml); |
453 | 0 | while ( child != NULL ) { |
454 | 0 | xmlNode *tmp = child; |
455 | |
|
456 | 0 | child = pcmk__xml_next(child); |
457 | 0 | readable_children |= purge_xml_attributes(tmp); |
458 | 0 | } |
459 | |
|
460 | 0 | if (!readable_children) { |
461 | | // Nothing readable under here, so purge completely |
462 | 0 | pcmk__xml_free(xml); |
463 | 0 | } |
464 | 0 | return readable_children; |
465 | 0 | } |
466 | | |
467 | | /*! |
468 | | * \brief Copy ACL-allowed portions of specified XML |
469 | | * |
470 | | * \param[in] user Username whose ACLs should be used |
471 | | * \param[in] acl_source XML containing ACLs |
472 | | * \param[in] xml XML to be copied |
473 | | * \param[out] result Copy of XML portions readable via ACLs |
474 | | * |
475 | | * \return true if xml exists and ACLs are required for user, false otherwise |
476 | | * \note If this returns true, caller should use \p result rather than \p xml |
477 | | */ |
478 | | bool |
479 | | xml_acl_filtered_copy(const char *user, xmlNode *acl_source, xmlNode *xml, |
480 | | xmlNode **result) |
481 | 0 | { |
482 | 0 | GList *aIter = NULL; |
483 | 0 | xmlNode *target = NULL; |
484 | 0 | xml_doc_private_t *docpriv = NULL; |
485 | |
|
486 | 0 | *result = NULL; |
487 | 0 | if ((xml == NULL) || !pcmk_acl_required(user)) { |
488 | 0 | crm_trace("Not filtering XML because ACLs not required for user '%s'", |
489 | 0 | user); |
490 | 0 | return false; |
491 | 0 | } |
492 | | |
493 | 0 | crm_trace("Filtering XML copy using user '%s' ACLs", user); |
494 | 0 | target = pcmk__xml_copy(NULL, xml); |
495 | 0 | if (target == NULL) { |
496 | 0 | return true; |
497 | 0 | } |
498 | | |
499 | 0 | pcmk__enable_acl(acl_source, target, user); |
500 | |
|
501 | 0 | docpriv = target->doc->_private; |
502 | 0 | for(aIter = docpriv->acls; aIter != NULL && target; aIter = aIter->next) { |
503 | 0 | int max = 0; |
504 | 0 | xml_acl_t *acl = aIter->data; |
505 | |
|
506 | 0 | if (acl->mode != pcmk__xf_acl_deny) { |
507 | | /* Nothing to do */ |
508 | |
|
509 | 0 | } else if (acl->xpath) { |
510 | 0 | int lpc = 0; |
511 | 0 | xmlXPathObject *xpathObj = pcmk__xpath_search(target->doc, |
512 | 0 | acl->xpath); |
513 | |
|
514 | 0 | max = pcmk__xpath_num_results(xpathObj); |
515 | 0 | for(lpc = 0; lpc < max; lpc++) { |
516 | 0 | xmlNode *match = pcmk__xpath_result(xpathObj, lpc); |
517 | |
|
518 | 0 | if (match == NULL) { |
519 | 0 | continue; |
520 | 0 | } |
521 | | |
522 | | // @COMPAT See COMPAT comment in pcmk__apply_acl() |
523 | 0 | match = pcmk__xpath_match_element(match); |
524 | 0 | if (match == NULL) { |
525 | 0 | continue; |
526 | 0 | } |
527 | | |
528 | 0 | if (!purge_xml_attributes(match) && (match == target)) { |
529 | 0 | crm_trace("ACLs deny user '%s' access to entire XML document", |
530 | 0 | user); |
531 | 0 | xmlXPathFreeObject(xpathObj); |
532 | 0 | return true; |
533 | 0 | } |
534 | 0 | } |
535 | 0 | crm_trace("ACLs deny user '%s' access to %s (%d %s)", |
536 | 0 | user, acl->xpath, max, |
537 | 0 | pcmk__plural_alt(max, "match", "matches")); |
538 | 0 | xmlXPathFreeObject(xpathObj); |
539 | 0 | } |
540 | 0 | } |
541 | | |
542 | 0 | if (!purge_xml_attributes(target)) { |
543 | 0 | crm_trace("ACLs deny user '%s' access to entire XML document", user); |
544 | 0 | return true; |
545 | 0 | } |
546 | | |
547 | 0 | if (docpriv->acls) { |
548 | 0 | g_list_free_full(docpriv->acls, free_acl); |
549 | 0 | docpriv->acls = NULL; |
550 | |
|
551 | 0 | } else { |
552 | 0 | crm_trace("User '%s' without ACLs denied access to entire XML document", |
553 | 0 | user); |
554 | 0 | pcmk__xml_free(target); |
555 | 0 | target = NULL; |
556 | 0 | } |
557 | | |
558 | 0 | if (target) { |
559 | 0 | *result = target; |
560 | 0 | } |
561 | |
|
562 | 0 | return true; |
563 | 0 | } |
564 | | |
565 | | /*! |
566 | | * \internal |
567 | | * \brief Check whether creation of an XML element is implicitly allowed |
568 | | * |
569 | | * Check whether XML is a "scaffolding" element whose creation is implicitly |
570 | | * allowed regardless of ACLs (that is, it is not in the ACL section and has |
571 | | * no attributes other than \c PCMK_XA_ID). |
572 | | * |
573 | | * \param[in] xml XML element to check |
574 | | * |
575 | | * \return true if XML element is implicitly allowed, false otherwise |
576 | | */ |
577 | | static bool |
578 | | implicitly_allowed(const xmlNode *xml) |
579 | 0 | { |
580 | 0 | GString *path = NULL; |
581 | |
|
582 | 0 | for (xmlAttr *prop = xml->properties; prop != NULL; prop = prop->next) { |
583 | 0 | if (strcmp((const char *) prop->name, PCMK_XA_ID) != 0) { |
584 | 0 | return false; |
585 | 0 | } |
586 | 0 | } |
587 | | |
588 | 0 | path = pcmk__element_xpath(xml); |
589 | 0 | pcmk__assert(path != NULL); |
590 | |
|
591 | 0 | if (strstr((const char *) path->str, "/" PCMK_XE_ACLS "/") != NULL) { |
592 | 0 | g_string_free(path, TRUE); |
593 | 0 | return false; |
594 | 0 | } |
595 | | |
596 | 0 | g_string_free(path, TRUE); |
597 | 0 | return true; |
598 | 0 | } |
599 | | |
600 | 0 | #define display_id(xml) pcmk__s(pcmk__xe_id(xml), "<unset>") |
601 | | |
602 | | /*! |
603 | | * \internal |
604 | | * \brief Drop XML nodes created in violation of ACLs |
605 | | * |
606 | | * Given an XML element, free all of its descendant nodes created in violation |
607 | | * of ACLs, with the exception of allowing "scaffolding" elements (i.e. those |
608 | | * that aren't in the ACL section and don't have any attributes other than |
609 | | * \c PCMK_XA_ID). |
610 | | * |
611 | | * \param[in,out] xml XML to check |
612 | | * \param[in] check_top Whether to apply checks to argument itself |
613 | | * (if true, xml might get freed) |
614 | | * |
615 | | * \note This function is recursive |
616 | | */ |
617 | | void |
618 | | pcmk__apply_creation_acl(xmlNode *xml, bool check_top) |
619 | 0 | { |
620 | 0 | xml_node_private_t *nodepriv = xml->_private; |
621 | |
|
622 | 0 | if (pcmk__is_set(nodepriv->flags, pcmk__xf_created)) { |
623 | 0 | if (implicitly_allowed(xml)) { |
624 | 0 | crm_trace("Creation of <%s> scaffolding with " PCMK_XA_ID "=\"%s\"" |
625 | 0 | " is implicitly allowed", |
626 | 0 | xml->name, display_id(xml)); |
627 | |
|
628 | 0 | } else if (pcmk__check_acl(xml, NULL, pcmk__xf_acl_write)) { |
629 | 0 | crm_trace("ACLs allow creation of <%s> with " PCMK_XA_ID "=\"%s\"", |
630 | 0 | xml->name, display_id(xml)); |
631 | |
|
632 | 0 | } else if (check_top) { |
633 | | /* is_root=true should be impossible with check_top=true, but check |
634 | | * for sanity |
635 | | */ |
636 | 0 | bool is_root = (xmlDocGetRootElement(xml->doc) == xml); |
637 | 0 | xml_doc_private_t *docpriv = xml->doc->_private; |
638 | |
|
639 | 0 | crm_trace("ACLs disallow creation of %s<%s> with " |
640 | 0 | PCMK_XA_ID "=\"%s\"", |
641 | 0 | (is_root? "root element " : ""), xml->name, |
642 | 0 | display_id(xml)); |
643 | | |
644 | | // pcmk__xml_free() checks ACLs if enabled, which would fail |
645 | 0 | pcmk__clear_xml_flags(docpriv, pcmk__xf_acl_enabled); |
646 | 0 | pcmk__xml_free(xml); |
647 | |
|
648 | 0 | if (!is_root) { |
649 | | // If root, the document was freed. Otherwise re-enable ACLs. |
650 | 0 | pcmk__set_xml_flags(docpriv, pcmk__xf_acl_enabled); |
651 | 0 | } |
652 | 0 | return; |
653 | |
|
654 | 0 | } else { |
655 | 0 | crm_notice("ACLs would disallow creation of %s<%s> with " |
656 | 0 | PCMK_XA_ID "=\"%s\"", |
657 | 0 | ((xml == xmlDocGetRootElement(xml->doc))? "root element " : ""), |
658 | 0 | xml->name, display_id(xml)); |
659 | 0 | } |
660 | 0 | } |
661 | | |
662 | 0 | for (xmlNode *cIter = pcmk__xml_first_child(xml); cIter != NULL; ) { |
663 | 0 | xmlNode *child = cIter; |
664 | 0 | cIter = pcmk__xml_next(cIter); /* In case it is free'd */ |
665 | 0 | pcmk__apply_creation_acl(child, true); |
666 | 0 | } |
667 | 0 | } |
668 | | |
669 | | /*! |
670 | | * \brief Check whether or not an XML node is ACL-denied |
671 | | * |
672 | | * \param[in] xml node to check |
673 | | * |
674 | | * \return true if XML node exists and is ACL-denied, false otherwise |
675 | | */ |
676 | | bool |
677 | | xml_acl_denied(const xmlNode *xml) |
678 | 0 | { |
679 | 0 | if (xml && xml->doc && xml->doc->_private){ |
680 | 0 | xml_doc_private_t *docpriv = xml->doc->_private; |
681 | |
|
682 | 0 | return pcmk__is_set(docpriv->flags, pcmk__xf_acl_denied); |
683 | 0 | } |
684 | 0 | return false; |
685 | 0 | } |
686 | | |
687 | | void |
688 | | xml_acl_disable(xmlNode *xml) |
689 | 0 | { |
690 | 0 | if ((xml != NULL) |
691 | 0 | && pcmk__xml_doc_all_flags_set(xml->doc, pcmk__xf_acl_enabled)) { |
692 | |
|
693 | 0 | xml_doc_private_t *docpriv = xml->doc->_private; |
694 | | |
695 | | /* Catch anything that was created but shouldn't have been */ |
696 | 0 | pcmk__apply_acl(xml); |
697 | 0 | pcmk__apply_creation_acl(xml, false); |
698 | 0 | pcmk__clear_xml_flags(docpriv, pcmk__xf_acl_enabled); |
699 | 0 | } |
700 | 0 | } |
701 | | |
702 | | /*! |
703 | | * \internal |
704 | | * \brief Deny access to an XML tree's document based on ACLs |
705 | | * |
706 | | * \param[in,out] xml XML tree |
707 | | * \param[in] attr_name Name of attribute being accessed in \p xml (for |
708 | | * logging only) |
709 | | * \param[in] prefix Prefix describing ACL that denied access (for |
710 | | * logging only) |
711 | | * \param[in] user User accessing \p xml (for logging only) |
712 | | * \param[in] mode Access mode (for logging only) |
713 | | */ |
714 | 0 | #define check_acl_deny(xml, attr_name, prefix, user, mode) do { \ |
715 | 0 | xmlNode *tree = xml; \ |
716 | 0 | \ |
717 | 0 | pcmk__xml_doc_set_flags(tree->doc, pcmk__xf_acl_denied); \ |
718 | 0 | pcmk__if_tracing( \ |
719 | 0 | { \ |
720 | 0 | GString *xpath = pcmk__element_xpath(tree); \ |
721 | 0 | \ |
722 | 0 | if ((attr_name) != NULL) { \ |
723 | 0 | pcmk__g_strcat(xpath, "[@", attr_name, "]", NULL); \ |
724 | 0 | } \ |
725 | 0 | qb_log_from_external_source(__func__, __FILE__, \ |
726 | 0 | "%sACL denies user '%s' %s " \ |
727 | 0 | "access to %s", \ |
728 | 0 | LOG_TRACE, __LINE__, 0 , \ |
729 | 0 | prefix, user, \ |
730 | 0 | acl_to_text(mode), xpath->str); \ |
731 | 0 | g_string_free(xpath, TRUE); \ |
732 | 0 | }, \ |
733 | 0 | {} \ |
734 | 0 | ); \ |
735 | 0 | } while (false); |
736 | | |
737 | | bool |
738 | | pcmk__check_acl(xmlNode *xml, const char *attr_name, enum pcmk__xml_flags mode) |
739 | 0 | { |
740 | 0 | xml_doc_private_t *docpriv = NULL; |
741 | |
|
742 | 0 | pcmk__assert((xml != NULL) && (xml->doc->_private != NULL)); |
743 | |
|
744 | 0 | if (!pcmk__xml_doc_all_flags_set(xml->doc, |
745 | 0 | pcmk__xf_tracking|pcmk__xf_acl_enabled)) { |
746 | 0 | return true; |
747 | 0 | } |
748 | | |
749 | 0 | docpriv = xml->doc->_private; |
750 | 0 | if (docpriv->acls == NULL) { |
751 | 0 | check_acl_deny(xml, attr_name, "Lack of ", docpriv->acl_user, mode); |
752 | 0 | return false; |
753 | 0 | } |
754 | | |
755 | | /* Walk the tree upwards looking for xml_acl_* flags |
756 | | * - Creating an attribute requires write permissions for the node |
757 | | * - Creating a child requires write permissions for the parent |
758 | | */ |
759 | | |
760 | 0 | if (attr_name != NULL) { |
761 | 0 | xmlAttr *attr = xmlHasProp(xml, (const xmlChar *) attr_name); |
762 | |
|
763 | 0 | if ((attr != NULL) && (mode == pcmk__xf_acl_create)) { |
764 | 0 | mode = pcmk__xf_acl_write; |
765 | 0 | } |
766 | 0 | } |
767 | |
|
768 | 0 | for (const xmlNode *parent = xml; |
769 | 0 | (parent != NULL) && (parent->_private != NULL); |
770 | 0 | parent = parent->parent) { |
771 | |
|
772 | 0 | const xml_node_private_t *nodepriv = parent->_private; |
773 | |
|
774 | 0 | if (test_acl_mode(nodepriv->flags, mode)) { |
775 | 0 | return true; |
776 | 0 | } |
777 | | |
778 | 0 | if (pcmk__is_set(nodepriv->flags, pcmk__xf_acl_deny)) { |
779 | 0 | const char *pfx = (parent != xml)? "Parent " : ""; |
780 | |
|
781 | 0 | check_acl_deny(xml, attr_name, pfx, docpriv->acl_user, mode); |
782 | 0 | return false; |
783 | 0 | } |
784 | 0 | } |
785 | | |
786 | 0 | check_acl_deny(xml, attr_name, "Default ", docpriv->acl_user, mode); |
787 | 0 | return false; |
788 | 0 | } |
789 | | |
790 | | /*! |
791 | | * \brief Check whether ACLs are required for a given user |
792 | | * |
793 | | * \param[in] User name to check |
794 | | * |
795 | | * \return true if the user requires ACLs, false otherwise |
796 | | */ |
797 | | bool |
798 | | pcmk_acl_required(const char *user) |
799 | 0 | { |
800 | 0 | if (pcmk__str_empty(user)) { |
801 | 0 | crm_trace("ACLs not required because no user set"); |
802 | 0 | return false; |
803 | |
|
804 | 0 | } else if (!strcmp(user, CRM_DAEMON_USER) || !strcmp(user, "root")) { |
805 | 0 | crm_trace("ACLs not required for privileged user %s", user); |
806 | 0 | return false; |
807 | 0 | } |
808 | 0 | crm_trace("ACLs required for %s", user); |
809 | 0 | return true; |
810 | 0 | } |
811 | | |
812 | | char * |
813 | | pcmk__uid2username(uid_t uid) |
814 | 0 | { |
815 | 0 | struct passwd *pwent = NULL; |
816 | |
|
817 | 0 | errno = 0; |
818 | 0 | pwent = getpwuid(uid); |
819 | |
|
820 | 0 | if (pwent == NULL) { |
821 | 0 | crm_err("Cannot get name from password database for user ID %lld: %s", |
822 | 0 | (long long) uid, |
823 | 0 | ((errno != 0)? strerror(errno) : "No matching entry found")); |
824 | 0 | return NULL; |
825 | 0 | } |
826 | | |
827 | 0 | return pcmk__str_copy(pwent->pw_name); |
828 | 0 | } |
829 | | |
830 | | /*! |
831 | | * \internal |
832 | | * \brief Set the ACL user field properly on an XML request |
833 | | * |
834 | | * Multiple user names are potentially involved in an XML request: the effective |
835 | | * user of the current process; the user name known from an IPC client |
836 | | * connection; and the user name obtained from the request itself, whether by |
837 | | * the current standard XML attribute name or an older legacy attribute name. |
838 | | * This function chooses the appropriate one that should be used for ACLs, sets |
839 | | * it in the request (using the standard attribute name, and the legacy name if |
840 | | * given), and returns it. |
841 | | * |
842 | | * \param[in,out] request XML request to update |
843 | | * \param[in] field Alternate name for ACL user name XML attribute |
844 | | * \param[in] peer_user User name as known from IPC connection |
845 | | * |
846 | | * \return ACL user name actually used |
847 | | */ |
848 | | const char * |
849 | | pcmk__update_acl_user(xmlNode *request, const char *field, |
850 | | const char *peer_user) |
851 | 0 | { |
852 | 0 | static const char *effective_user = NULL; |
853 | 0 | const char *requested_user = NULL; |
854 | 0 | const char *user = NULL; |
855 | |
|
856 | 0 | if (effective_user == NULL) { |
857 | 0 | effective_user = pcmk__uid2username(geteuid()); |
858 | 0 | if (effective_user == NULL) { |
859 | 0 | effective_user = pcmk__str_copy("#unprivileged"); |
860 | 0 | crm_err("Unable to determine effective user, assuming unprivileged for ACLs"); |
861 | 0 | } |
862 | 0 | } |
863 | |
|
864 | 0 | requested_user = pcmk__xe_get(request, PCMK__XA_ACL_TARGET); |
865 | 0 | if (requested_user == NULL) { |
866 | | /* Currently, different XML attribute names are used for the ACL user in |
867 | | * different contexts (PCMK__XA_ATTR_USER, PCMK__XA_CIB_USER, etc.). |
868 | | * The caller may specify that name as the field argument. |
869 | | * |
870 | | * @TODO Standardize on PCMK__XA_ACL_TARGET and eventually drop the |
871 | | * others once rolling upgrades from versions older than that are no |
872 | | * longer supported. |
873 | | */ |
874 | 0 | requested_user = pcmk__xe_get(request, field); |
875 | 0 | } |
876 | |
|
877 | 0 | if (!pcmk__is_privileged(effective_user)) { |
878 | | /* We're not running as a privileged user, set or overwrite any existing |
879 | | * value for PCMK__XA_ACL_TARGET |
880 | | */ |
881 | 0 | user = effective_user; |
882 | |
|
883 | 0 | } else if (peer_user == NULL && requested_user == NULL) { |
884 | | /* No user known or requested, use 'effective_user' and make sure one is |
885 | | * set for the request |
886 | | */ |
887 | 0 | user = effective_user; |
888 | |
|
889 | 0 | } else if (peer_user == NULL) { |
890 | | /* No user known, trusting 'requested_user' */ |
891 | 0 | user = requested_user; |
892 | |
|
893 | 0 | } else if (!pcmk__is_privileged(peer_user)) { |
894 | | /* The peer is not a privileged user, set or overwrite any existing |
895 | | * value for PCMK__XA_ACL_TARGET |
896 | | */ |
897 | 0 | user = peer_user; |
898 | |
|
899 | 0 | } else if (requested_user == NULL) { |
900 | | /* Even if we're privileged, make sure there is always a value set */ |
901 | 0 | user = peer_user; |
902 | |
|
903 | 0 | } else { |
904 | | /* Legal delegation to 'requested_user' */ |
905 | 0 | user = requested_user; |
906 | 0 | } |
907 | | |
908 | | // This requires pointer comparison, not string comparison |
909 | 0 | if (user != pcmk__xe_get(request, PCMK__XA_ACL_TARGET)) { |
910 | 0 | pcmk__xe_set(request, PCMK__XA_ACL_TARGET, user); |
911 | 0 | } |
912 | |
|
913 | 0 | if ((field != NULL) && (user != pcmk__xe_get(request, field))) { |
914 | 0 | pcmk__xe_set(request, field, user); |
915 | 0 | } |
916 | |
|
917 | 0 | return requested_user; |
918 | 0 | } |
919 | | |
920 | | // Deprecated functions kept only for backward API compatibility |
921 | | // LCOV_EXCL_START |
922 | | |
923 | | #include <crm/common/acl_compat.h> |
924 | | #include <crm/common/xml_compat.h> |
925 | | |
926 | | bool |
927 | | xml_acl_enabled(const xmlNode *xml) |
928 | 0 | { |
929 | 0 | if (xml && xml->doc && xml->doc->_private){ |
930 | 0 | xml_doc_private_t *docpriv = xml->doc->_private; |
931 | |
|
932 | 0 | return pcmk__is_set(docpriv->flags, pcmk__xf_acl_enabled); |
933 | 0 | } |
934 | 0 | return false; |
935 | 0 | } |
936 | | |
937 | | // LCOV_EXCL_STOP |
938 | | // End deprecated API |