Line data Source code
1 : /*
2 : * Copyright 2004-2024 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/parser.h>
20 : #include <libxml/tree.h>
21 : #include <libxml/xpath.h>
22 : #include <libxslt/transform.h>
23 : #include <libxslt/variables.h>
24 : #include <libxslt/xsltutils.h>
25 :
26 : #include <crm/crm.h>
27 : #include <crm/common/xml.h>
28 : #include <crm/common/xml_internal.h>
29 : #include <crm/common/internal.h>
30 :
31 : #include <pacemaker-internal.h>
32 :
33 : #define ACL_NS_PREFIX "http://clusterlabs.org/ns/pacemaker/access/"
34 : #define ACL_NS_Q_PREFIX "pcmk-access-"
35 : #define ACL_NS_Q_WRITABLE (const xmlChar *) ACL_NS_Q_PREFIX "writable"
36 : #define ACL_NS_Q_READABLE (const xmlChar *) ACL_NS_Q_PREFIX "readable"
37 : #define ACL_NS_Q_DENIED (const xmlChar *) ACL_NS_Q_PREFIX "denied"
38 :
39 : static const xmlChar *NS_WRITABLE = (const xmlChar *) ACL_NS_PREFIX "writable";
40 : static const xmlChar *NS_READABLE = (const xmlChar *) ACL_NS_PREFIX "readable";
41 : static const xmlChar *NS_DENIED = (const xmlChar *) ACL_NS_PREFIX "denied";
42 :
43 : /*!
44 : * \brief This function takes a node and marks it with the namespace
45 : * given in the ns parameter.
46 : *
47 : * \param[in,out] i_node
48 : * \param[in] ns
49 : * \param[in,out] ret
50 : * \param[in,out] ns_recycle_writable
51 : * \param[in,out] ns_recycle_readable
52 : * \param[in,out] ns_recycle_denied
53 : */
54 : static void
55 0 : pcmk__acl_mark_node_with_namespace(xmlNode *i_node, const xmlChar *ns, int *ret,
56 : xmlNs **ns_recycle_writable,
57 : xmlNs **ns_recycle_readable,
58 : xmlNs **ns_recycle_denied)
59 : {
60 0 : if (ns == NS_WRITABLE)
61 : {
62 0 : if (*ns_recycle_writable == NULL)
63 : {
64 0 : *ns_recycle_writable = xmlNewNs(xmlDocGetRootElement(i_node->doc),
65 : NS_WRITABLE, ACL_NS_Q_WRITABLE);
66 : }
67 0 : xmlSetNs(i_node, *ns_recycle_writable);
68 0 : *ret = pcmk_rc_ok;
69 : }
70 0 : else if (ns == NS_READABLE)
71 : {
72 0 : if (*ns_recycle_readable == NULL)
73 : {
74 0 : *ns_recycle_readable = xmlNewNs(xmlDocGetRootElement(i_node->doc),
75 : NS_READABLE, ACL_NS_Q_READABLE);
76 : }
77 0 : xmlSetNs(i_node, *ns_recycle_readable);
78 0 : *ret = pcmk_rc_ok;
79 : }
80 0 : else if (ns == NS_DENIED)
81 : {
82 0 : if (*ns_recycle_denied == NULL)
83 : {
84 0 : *ns_recycle_denied = xmlNewNs(xmlDocGetRootElement(i_node->doc),
85 : NS_DENIED, ACL_NS_Q_DENIED);
86 : };
87 0 : xmlSetNs(i_node, *ns_recycle_denied);
88 0 : *ret = pcmk_rc_ok;
89 : }
90 0 : }
91 :
92 : /*!
93 : * \brief Annotate a given XML element or property and its siblings with
94 : * XML namespaces to indicate ACL permissions
95 : *
96 : * \param[in,out] xml_modify XML to annotate
97 : *
98 : * \return A standard Pacemaker return code
99 : * Namely:
100 : * - pcmk_rc_ok upon success,
101 : * - pcmk_rc_already if ACLs were not applicable,
102 : * - pcmk_rc_schema_validation if the validation schema version
103 : * is unsupported (see note), or
104 : * - EINVAL or ENOMEM as appropriate;
105 : *
106 : * \note This function is recursive
107 : */
108 : static int
109 0 : annotate_with_siblings(xmlNode *xml_modify)
110 : {
111 :
112 : static xmlNs *ns_recycle_writable = NULL,
113 : *ns_recycle_readable = NULL,
114 : *ns_recycle_denied = NULL;
115 : static const xmlDoc *prev_doc = NULL;
116 :
117 0 : xmlNode *i_node = NULL;
118 : const xmlChar *ns;
119 0 : int ret = EINVAL; // nodes have not been processed yet
120 :
121 0 : if (prev_doc == NULL || prev_doc != xml_modify->doc) {
122 0 : prev_doc = xml_modify->doc;
123 0 : ns_recycle_writable = ns_recycle_readable = ns_recycle_denied = NULL;
124 : }
125 :
126 0 : for (i_node = xml_modify; i_node != NULL; i_node = i_node->next) {
127 0 : switch (i_node->type) {
128 0 : case XML_ELEMENT_NODE:
129 0 : pcmk__set_xml_doc_flag(i_node, pcmk__xf_tracking);
130 :
131 0 : if (!pcmk__check_acl(i_node, NULL, pcmk__xf_acl_read)) {
132 0 : ns = NS_DENIED;
133 0 : } else if (!pcmk__check_acl(i_node, NULL, pcmk__xf_acl_write)) {
134 0 : ns = NS_READABLE;
135 : } else {
136 0 : ns = NS_WRITABLE;
137 : }
138 0 : pcmk__acl_mark_node_with_namespace(i_node, ns, &ret,
139 : &ns_recycle_writable,
140 : &ns_recycle_readable,
141 : &ns_recycle_denied);
142 : // @TODO Could replace recursion with iteration to save stack
143 0 : if (i_node->properties != NULL) {
144 : /* This is not entirely clear, but relies on the very same
145 : * class-hierarchy emulation that libxml2 has firmly baked
146 : * in its API/ABI
147 : */
148 0 : ret |= annotate_with_siblings((xmlNodePtr)
149 0 : i_node->properties);
150 : }
151 0 : if (i_node->children != NULL) {
152 0 : ret |= annotate_with_siblings(i_node->children);
153 : }
154 0 : break;
155 :
156 0 : case XML_ATTRIBUTE_NODE:
157 : // We can utilize that parent has already been assigned the ns
158 0 : if (!pcmk__check_acl(i_node->parent,
159 0 : (const char *) i_node->name,
160 : pcmk__xf_acl_read)) {
161 0 : ns = NS_DENIED;
162 0 : } else if (!pcmk__check_acl(i_node,
163 0 : (const char *) i_node->name,
164 : pcmk__xf_acl_write)) {
165 0 : ns = NS_READABLE;
166 : } else {
167 0 : ns = NS_WRITABLE;
168 : }
169 0 : pcmk__acl_mark_node_with_namespace(i_node, ns, &ret,
170 : &ns_recycle_writable,
171 : &ns_recycle_readable,
172 : &ns_recycle_denied);
173 0 : break;
174 :
175 0 : case XML_COMMENT_NODE:
176 : // We can utilize that parent has already been assigned the ns
177 0 : if (!pcmk__check_acl(i_node->parent,
178 0 : (const char *) i_node->name,
179 : pcmk__xf_acl_read)) {
180 0 : ns = NS_DENIED;
181 0 : } else if (!pcmk__check_acl(i_node->parent,
182 0 : (const char *) i_node->name,
183 : pcmk__xf_acl_write)) {
184 0 : ns = NS_READABLE;
185 : } else {
186 0 : ns = NS_WRITABLE;
187 : }
188 0 : pcmk__acl_mark_node_with_namespace(i_node, ns, &ret,
189 : &ns_recycle_writable,
190 : &ns_recycle_readable,
191 : &ns_recycle_denied);
192 0 : break;
193 :
194 0 : default:
195 0 : break;
196 : }
197 : }
198 :
199 0 : return ret;
200 : }
201 :
202 : int
203 0 : pcmk__acl_annotate_permissions(const char *cred, const xmlDoc *cib_doc,
204 : xmlDoc **acl_evaled_doc)
205 : {
206 : int ret;
207 : xmlNode *target, *comment;
208 : const char *validation;
209 :
210 0 : CRM_CHECK(cred != NULL, return EINVAL);
211 0 : CRM_CHECK(cib_doc != NULL, return EINVAL);
212 0 : CRM_CHECK(acl_evaled_doc != NULL, return EINVAL);
213 :
214 : /* avoid trivial accidental XML injection */
215 0 : if (strpbrk(cred, "<>&") != NULL) {
216 0 : return EINVAL;
217 : }
218 :
219 0 : if (!pcmk_acl_required(cred)) {
220 : /* nothing to evaluate */
221 0 : return pcmk_rc_already;
222 : }
223 :
224 : // @COMPAT xmlDocGetRootElement() requires non-const in libxml2 < 2.9.2
225 0 : validation = crm_element_value(xmlDocGetRootElement((xmlDoc *) cib_doc),
226 : PCMK_XA_VALIDATE_WITH);
227 :
228 0 : if (pcmk__cmp_schemas_by_name(PCMK__COMPAT_ACL_2_MIN_INCL,
229 : validation) > 0) {
230 0 : return pcmk_rc_schema_validation;
231 : }
232 :
233 0 : target = pcmk__xml_copy(NULL, xmlDocGetRootElement((xmlDoc *) cib_doc));
234 0 : if (target == NULL) {
235 0 : return EINVAL;
236 : }
237 :
238 0 : pcmk__enable_acl(target, target, cred);
239 :
240 0 : ret = annotate_with_siblings(target);
241 :
242 0 : if (ret == pcmk_rc_ok) {
243 0 : char *credentials = crm_strdup_printf("ACLs as evaluated for user %s",
244 : cred);
245 :
246 0 : comment = xmlNewDocComment(target->doc, (pcmkXmlStr) credentials);
247 0 : free(credentials);
248 0 : if (comment == NULL) {
249 0 : xmlFreeNode(target);
250 0 : return EINVAL;
251 : }
252 0 : xmlAddPrevSibling(xmlDocGetRootElement(target->doc), comment);
253 0 : *acl_evaled_doc = target->doc;
254 0 : return pcmk_rc_ok;
255 : } else {
256 0 : xmlFreeNode(target);
257 0 : return ret; //for now, it should be some kind of error
258 : }
259 : }
260 :
261 : int
262 0 : pcmk__acl_evaled_render(xmlDoc *annotated_doc, enum pcmk__acl_render_how how,
263 : xmlChar **doc_txt_ptr)
264 : {
265 : xmlDoc *xslt_doc;
266 : xsltStylesheet *xslt;
267 : xsltTransformContext *xslt_ctxt;
268 : xmlDoc *res;
269 : char *sfile;
270 : static const char *params_namespace[] = {
271 : "accessrendercfg:c-writable", ACL_NS_Q_PREFIX "writable:",
272 : "accessrendercfg:c-readable", ACL_NS_Q_PREFIX "readable:",
273 : "accessrendercfg:c-denied", ACL_NS_Q_PREFIX "denied:",
274 : "accessrendercfg:c-reset", "",
275 : "accessrender:extra-spacing", "no",
276 : "accessrender:self-reproducing-prefix", ACL_NS_Q_PREFIX,
277 : NULL
278 : }, *params_useansi[] = {
279 : /* start with hard-coded defaults, then adapt per the template ones */
280 : "accessrendercfg:c-writable", "\x1b[32m",
281 : "accessrendercfg:c-readable", "\x1b[34m",
282 : "accessrendercfg:c-denied", "\x1b[31m",
283 : "accessrendercfg:c-reset", "\x1b[0m",
284 : "accessrender:extra-spacing", "no",
285 : "accessrender:self-reproducing-prefix", ACL_NS_Q_PREFIX,
286 : NULL
287 : }, *params_noansi[] = {
288 : "accessrendercfg:c-writable", "vvv---[ WRITABLE ]---vvv",
289 : "accessrendercfg:c-readable", "vvv---[ READABLE ]---vvv",
290 : "accessrendercfg:c-denied", "vvv---[ ~DENIED~ ]---vvv",
291 : "accessrendercfg:c-reset", "",
292 : "accessrender:extra-spacing", "yes",
293 : "accessrender:self-reproducing-prefix", "",
294 : NULL
295 : };
296 : const char **params;
297 : int ret;
298 : xmlParserCtxtPtr parser_ctxt;
299 :
300 : /* unfortunately, the input (coming from CIB originally) was parsed with
301 : blanks ignored, and since the output is a conversion of XML to text
302 : format (we would be covered otherwise thanks to implicit
303 : pretty-printing), we need to dump the tree to string output first,
304 : only to subsequently reparse it -- this time with blanks honoured */
305 : xmlChar *annotated_dump;
306 : int dump_size;
307 :
308 0 : CRM_ASSERT(how != pcmk__acl_render_none);
309 :
310 : // Color is the default render mode for terminals; text is default otherwise
311 0 : if (how == pcmk__acl_render_default) {
312 0 : if (isatty(STDOUT_FILENO)) {
313 0 : how = pcmk__acl_render_color;
314 : } else {
315 0 : how = pcmk__acl_render_text;
316 : }
317 : }
318 :
319 0 : xmlDocDumpFormatMemory(annotated_doc, &annotated_dump, &dump_size, 1);
320 0 : res = xmlReadDoc(annotated_dump, "on-the-fly-access-render", NULL,
321 : XML_PARSE_NONET);
322 0 : CRM_ASSERT(res != NULL);
323 0 : xmlFree(annotated_dump);
324 0 : xmlFreeDoc(annotated_doc);
325 0 : annotated_doc = res;
326 :
327 0 : sfile = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_base_xslt,
328 : "access-render-2");
329 0 : parser_ctxt = xmlNewParserCtxt();
330 :
331 0 : CRM_ASSERT(sfile != NULL);
332 0 : pcmk__mem_assert(parser_ctxt);
333 :
334 0 : xslt_doc = xmlCtxtReadFile(parser_ctxt, sfile, NULL, XML_PARSE_NONET);
335 :
336 0 : xslt = xsltParseStylesheetDoc(xslt_doc); /* acquires xslt_doc! */
337 0 : if (xslt == NULL) {
338 0 : crm_crit("Problem in parsing %s", sfile);
339 0 : return EINVAL;
340 : }
341 0 : free(sfile);
342 0 : sfile = NULL;
343 0 : xmlFreeParserCtxt(parser_ctxt);
344 :
345 0 : xslt_ctxt = xsltNewTransformContext(xslt, annotated_doc);
346 0 : pcmk__mem_assert(xslt_ctxt);
347 :
348 0 : switch (how) {
349 0 : case pcmk__acl_render_namespace:
350 0 : params = params_namespace;
351 0 : break;
352 0 : case pcmk__acl_render_text:
353 0 : params = params_noansi;
354 0 : break;
355 0 : default:
356 : /* pcmk__acl_render_color is the only remaining option.
357 : * The compiler complains about params possibly uninitialized if we
358 : * don't use default here.
359 : */
360 0 : params = params_useansi;
361 0 : break;
362 : }
363 :
364 0 : xsltQuoteUserParams(xslt_ctxt, params);
365 :
366 0 : res = xsltApplyStylesheetUser(xslt, annotated_doc, NULL,
367 : NULL, NULL, xslt_ctxt);
368 :
369 0 : xmlFreeDoc(annotated_doc);
370 0 : annotated_doc = NULL;
371 0 : xsltFreeTransformContext(xslt_ctxt);
372 0 : xslt_ctxt = NULL;
373 :
374 0 : if (how == pcmk__acl_render_color && params != params_useansi) {
375 0 : char **param_i = (char **) params;
376 : do {
377 0 : free(*param_i);
378 0 : } while (*param_i++ != NULL);
379 0 : free(params);
380 : }
381 :
382 0 : if (res == NULL) {
383 0 : ret = EINVAL;
384 : } else {
385 : int doc_txt_len;
386 0 : int temp = xsltSaveResultToString(doc_txt_ptr, &doc_txt_len, res, xslt);
387 0 : xmlFreeDoc(res);
388 0 : if (temp == 0) {
389 0 : ret = pcmk_rc_ok;
390 : } else {
391 0 : ret = EINVAL;
392 : }
393 : }
394 0 : xsltFreeStylesheet(xslt);
395 0 : return ret;
396 : }
|