/* 
 * Copyright (C) 2009 Yan Gao <ygao@novell.com>
 * 
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

#include <crm_internal.h>

#include <pwd.h>

#include <crm/cib.h>
#include <cib_private.h>
#include <crm/common/xml.h>

typedef struct acl_obj_s
{
	const char *mode;
	const char *tag;
	const char *ref;
	const char *xpath;
	const char *attribute;
} acl_obj_t;

typedef struct xml_perm_s
{
	const char *mode;
	GHashTable *attribute_perms;
} xml_perm_t;

static gboolean req_by_privileged(xmlNode *request);
static xmlNode *diff_xml_object_orig(xmlNode *old, xmlNode *new, gboolean suppress, xmlNode *new_diff);

static gboolean unpack_user_acl(xmlNode *xml_acls, const char *user, GListPtr *user_acl);
static gboolean user_match(const char *user, const char *uid);
static gboolean unpack_acl(xmlNode *xml_acls, xmlNode *xml_acl, GListPtr *acl);
static gboolean unpack_role_acl(xmlNode *xml_acls, const char *role, GListPtr *acl);
static gboolean acl_append(xmlNode *acl_child, GListPtr *acl);
static void free_acl(GListPtr acl);
static gboolean parse_acl_xpath(xmlNode *xml, GListPtr acl, GListPtr *parsed_acl);

static gboolean gen_xml_perms(xmlNode *xml, GListPtr acl, GHashTable **xml_perms);
static int search_xml_children(GListPtr *children, xmlNode *root,
                  const char *tag, const char *field, const char *value,
                  gboolean search_matches);
static int search_xpath_objects(GListPtr *objects, xmlNode *xml_obj, const char *xpath);
static gboolean update_xml_perms(xmlNode *xml, acl_obj_t *acl_obj, GHashTable *xml_perms);
static gboolean update_xml_children_perms(xmlNode *xml, const char *mode, GHashTable *xml_perms);
static void free_xml_perm(gpointer xml_perm);

static gboolean acl_filter_xml(xmlNode *xml, GHashTable *xml_perms);
static gboolean acl_check_diff_xml(xmlNode *xml, GHashTable *xml_perms);

gboolean
acl_enabled(GHashTable *config_hash)
{
	const char *value = NULL;
	gboolean rc = FALSE;

	value = cib_pref(config_hash, "enable-acl");
	rc = crm_is_true(value);

	crm_debug("CIB ACL is %s", rc?"enabled":"disabled");
	return rc;
}

/* rc = TRUE if orig_cib has been filtered*/
/* That means *filtered_cib rather than orig_cib should be exploited afterwards*/
gboolean
acl_filter_cib(xmlNode *request, xmlNode *current_cib, xmlNode *orig_cib, xmlNode **filtered_cib)
{
	const char *user = NULL;
	xmlNode *xml_acls = NULL;
	xmlNode *tmp_cib = NULL;
	GListPtr user_acl = NULL;
	GHashTable *xml_perms = NULL;

	*filtered_cib = NULL;

	if (req_by_privileged(request)) {
		return FALSE;
	}

	if (orig_cib == NULL) {
		return FALSE;
	}

	if (current_cib == NULL) {
		return TRUE;
	}

	xml_acls = get_object_root(XML_CIB_TAG_ACLS, current_cib);
	if (xml_acls == NULL) {
		crm_warn("Ordinary users cannot access the CIB without any defined ACLs: '%s'", user);
		return TRUE;
	}

	user = crm_element_value(request, F_CIB_USER);
	unpack_user_acl(xml_acls, user, &user_acl);

	tmp_cib = copy_xml(orig_cib);

	gen_xml_perms(tmp_cib, user_acl, &xml_perms);

	if (acl_filter_xml(tmp_cib, xml_perms)) {
		crm_warn("User '%s' doesn't have the permission for the whole CIB", user);
		tmp_cib = NULL;
	}

	g_hash_table_destroy(xml_perms);
	free_acl(user_acl);
	
	*filtered_cib = tmp_cib;
	return TRUE;
}

/* rc = TRUE if the request passes the ACL check */
/* rc = FALSE if the permission is denied */
gboolean
acl_check_diff(xmlNode *request, xmlNode *current_cib, xmlNode *result_cib, xmlNode *diff)
{
	const char *user = NULL;
	xmlNode *xml_acls = NULL;
	GListPtr user_acl = NULL;
	xmlNode *orig_diff = NULL;
	int rc = FALSE;

	if (req_by_privileged(request)) {
		return TRUE;
	}

	if (diff == NULL) {
		return TRUE;
	}

	if (current_cib == NULL) {
		return FALSE;
	}

	xml_acls = get_object_root(XML_CIB_TAG_ACLS, current_cib);
	if (xml_acls == NULL) {
		crm_warn("Ordinary users cannot access the CIB without any defined ACLs: '%s'", user);
		return FALSE;
	}

	user = crm_element_value(request, F_CIB_USER);
	unpack_user_acl(xml_acls, user, &user_acl);

	orig_diff = diff_xml_object_orig(current_cib, result_cib, FALSE, diff);

	for (diff_child = __xml_first_child(orig_diff); diff_child; diff_child = __xml_next(diff_child)) {
		const char *tag = crm_element_name(diff_child);
		GListPtr parsed_acl = NULL;

		crm_debug("Preparing ACL checking on '%s'", tag);

		if (crm_str_eq(tag, XML_TAG_DIFF_REMOVED, TRUE)) {
			crm_debug("Parsing any xpaths under the ACL according to the current CIB");
			parse_acl_xpath(current_cib, user_acl, &parsed_acl);
		} else if (crm_str_eq(tag, XML_TAG_DIFF_ADDED, TRUE)) {
			crm_debug("Parsing any xpaths under the ACL according to the result CIB");
			parse_acl_xpath(result_cib, user_acl, &parsed_acl);
		} else {
			continue;
		}

		for (diff_cib = __xml_first_child(diff_child); diff_cib; diff_child = __xml_next(diff_cib)) {
			GHashTable *xml_perms = NULL;

			gen_xml_perms(diff_cib, parsed_acl, &xml_perms);
			rc = acl_check_diff_xml(diff_cib, xml_perms);
			g_hash_table_destroy(xml_perms);

			if (rc == FALSE) {
				crm_warn("User '%s' doesn't have enough permission to modify the CIB objects", user);
				goto done;
			}
		}
		free_acl(parsed_acl);
	}

done:
	free_xml(orig_diff);
	free_acl(user_acl);
	return rc;
}

static gboolean
req_by_privileged(xmlNode *request)
{
	const char *user = crm_element_value(request, F_CIB_USER);

	if (user == NULL || strcmp(user, "") == 0) {
		crm_debug("Request without an explicit client user: op=%s, origin=%s, client=%s",
				crm_element_value(request, F_CIB_OPERATION),
				crm_element_value(request, F_ORIG)?crm_element_value(request, F_ORIG):"local",
				crm_element_value(request, F_CIB_CLIENTNAME));
		return TRUE;
	} 

	if (is_privileged(user)) {
		return TRUE;
	}
	return FALSE;
}

/* Borrowed from lib/common/xml.c: diff_xml_object() */
/* But if a new format of diff ("new_diff") exists, we could reuse its "diff-removed" part */
/* So it would be more time-saving than generating the diff from start */
static xmlNode *
diff_xml_object_orig(xmlNode *old, xmlNode *new, gboolean suppress, xmlNode *new_diff)
{
    xmlNode *tmp1    = NULL;
    xmlNode *diff    = create_xml_node(NULL, "diff");
    xmlNode *removed = NULL;
    xmlNode *added   = NULL;

    crm_xml_add(diff, XML_ATTR_CRM_VERSION, CRM_FEATURE_SET);
	
    if (new_diff && (tmp1 = find_xml_node(new_diff, "diff-removed", FALSE))) {
	removed = add_node_copy(diff, tmp1);

    } else {
	removed = create_xml_node(diff, "diff-removed");

	tmp1 = subtract_xml_object(removed, old, new, FALSE, "removed:top");
	if(suppress && tmp1 != NULL && can_prune_leaf(tmp1)) {
	    free_xml_from_parent(removed, tmp1);
	}
    }

    added = create_xml_node(diff, "diff-added");
	
    tmp1 = subtract_xml_object(added, new, old, FALSE, "added:top");
    if(suppress && tmp1 != NULL && can_prune_leaf(tmp1)) {
	free_xml_from_parent(added, tmp1);
    }
	
    if(added->children == NULL && removed->children == NULL) {
	free_xml(diff);
	diff = NULL;
    }
	
    return diff;
}

static gboolean
unpack_user_acl(xmlNode *xml_acls, const char *user, GListPtr *user_acl)
{
	if (xml_acls == NULL) {
		return FALSE;
	}
	
	for (xml_acl = __xml_first_child(xml_acls); xml_acl; xml_acl = __xml_next(xml_acl)) {
		const char *tag = crm_element_name(xml_acl);
		const char *id = crm_element_value(xml_acl, XML_ATTR_ID);

		if (crm_str_eq(tag, XML_ACL_TAG_USER, TRUE)) {
			if (user_match(user, id)) {
				crm_debug("Unpacking ACL of user: '%s'", id);
				unpack_acl(xml_acls, xml_acl, user_acl);
				return TRUE;
			}
		}
	}
	return FALSE;
}

static gboolean
user_match(const char *user, const char *uid)
{
	CRM_CHECK(user != NULL && user[0] != '\0'
		&& uid != NULL && uid[0] != '\0', return FALSE);

	if (crm_str_eq(user, uid, TRUE)) {
		return TRUE;
	}

	return FALSE;
}

static gboolean
unpack_acl(xmlNode *xml_acls, xmlNode *xml_acl, GListPtr *acl)
{
	for (acl_child = __xml_first_child(xml_acl); acl_child; acl_child = __xml_next(acl_child)) {
		const char *tag = crm_element_name(acl_child);

		if (crm_str_eq(XML_ACL_TAG_ROLE_REF, tag, TRUE)) {
			const char *ref_role = crm_element_value(acl_child, XML_ATTR_ID);
			if (ref_role) {
				unpack_role_acl(xml_acls, ref_role, acl);
			}
		} else if (crm_str_eq(XML_ACL_TAG_READ, tag, TRUE)
			|| crm_str_eq(XML_ACL_TAG_WRITE, tag, TRUE)
			|| crm_str_eq(XML_ACL_TAG_DENY, tag, TRUE))
				acl_append(acl_child, acl);
		);
	return TRUE;
}

static gboolean
unpack_role_acl(xmlNode *xml_acls, const char *role, GListPtr *acl)
{
	for (xml_acl = __xml_first_child(xml_acls); xml_acl; xml_acl = __xml_next(xml_acl)) {
	    if(crm_str_eq(XML_ACL_TAG_ROLE, (const char *)child->name, TRUE)) {
		const char *role_id = crm_element_value(xml_acl, XML_ATTR_ID);

		if (role_id && crm_str_eq(role, role_id, TRUE)) {
			crm_debug("Unpacking ACL of the referenced role: '%s'", role);
			unpack_acl(xml_acls, xml_acl, acl);
			return TRUE;
		}
	    }
	}
	return FALSE;
}

static gboolean
acl_append(xmlNode *acl_child, GListPtr *acl)
{
	acl_obj_t *acl_obj = NULL;

	const char *tag = crm_element_value(acl_child, XML_ACL_ATTR_TAG);
	const char *ref = crm_element_value(acl_child, XML_ACL_ATTR_REF);
	const char *xpath = crm_element_value(acl_child, XML_ACL_ATTR_XPATH);

	if (tag == NULL && ref == NULL && xpath == NULL) {
		return FALSE;
	}

	crm_malloc0(acl_obj, sizeof(acl_obj_t));
	if (acl_obj == NULL) {
		return FALSE;
	}

	acl_obj->mode = crm_element_name(acl_child);
	acl_obj->tag = tag;
	acl_obj->ref = ref;
	acl_obj->xpath = xpath;
	acl_obj->attribute = crm_element_value(acl_child, XML_ACL_ATTR_ATTRIBUTE);

	*acl = g_list_append(*acl, acl_obj);

	crm_debug_3("ACL object appended: mode=%s, tag=%s, ref=%s, xpath=%s, attribute=%s",
			acl_obj->mode, acl_obj->tag, acl_obj->ref, acl_obj->xpath, acl_obj->attribute);

	return TRUE;
}

static void
free_acl(GListPtr acl)
{
	GListPtr iterator = acl;
	while(iterator != NULL) {
		crm_free(iterator->data);
		iterator = iterator->next;
	}
	if(acl != NULL) {
		g_list_free(acl);
	}
}

static gboolean
parse_acl_xpath(xmlNode *xml, GListPtr acl, GListPtr *parsed_acl)
{
	GListPtr acl_iterator = acl;
	acl_obj_t *new_acl_obj = NULL;
	
	*parsed_acl = NULL;

	while (acl_iterator != NULL) {
		acl_obj_t *acl_obj = acl_iterator->data;

		if (acl_obj->tag || acl_obj->ref) {
			crm_malloc0(new_acl_obj, sizeof(acl_obj_t));
			if (new_acl_obj == NULL) {
				return  FALSE;
			}

			memcpy(new_acl_obj, acl_obj, sizeof(acl_obj_t));

			*parsed_acl = g_list_append(*parsed_acl, new_acl_obj);

			crm_debug_3("Copied ACL object: mode=%s, tag=%s, ref=%s, xpath=%s, attribute=%s",
					new_acl_obj->mode, new_acl_obj->tag, new_acl_obj->ref,
					new_acl_obj->xpath, new_acl_obj->attribute);

		} else if (acl_obj->xpath) {
			GListPtr children = NULL;
			GListPtr children_iterator = NULL;

			search_xpath_objects(&children, xml, acl_obj->xpath);

			children_iterator = children;
			while (children_iterator != NULL) {
				crm_malloc0(new_acl_obj, sizeof(acl_obj_t));
				if (new_acl_obj == NULL) {
					return  FALSE;
				}

				new_acl_obj->mode = acl_obj->mode;
				new_acl_obj->tag = crm_element_name((xmlNode*)children_iterator->data);
				new_acl_obj->ref = crm_element_value(children_iterator->data, XML_ATTR_ID);
				new_acl_obj->attribute = acl_obj->attribute;

				*parsed_acl = g_list_append(*parsed_acl, new_acl_obj);

				crm_debug_3("Parsed the ACL object with xpath '%s' to: mode=%s, tag=%s, ref=%s, xpath=%s, attribute=%s",
						acl_obj->xpath, new_acl_obj->mode, new_acl_obj->tag,
						new_acl_obj->ref, new_acl_obj->xpath, new_acl_obj->attribute);

				children_iterator = children_iterator->next;
			}
			g_list_free(children);
		}
		acl_iterator = acl_iterator->next;
	}

	return TRUE;
}

static gboolean
gen_xml_perms(xmlNode *xml, GListPtr acl, GHashTable **xml_perms)
{
	GListPtr acl_iterator = acl;

	if (*xml_perms == NULL) {
		*xml_perms = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, free_xml_perm);
	}

	while (acl_iterator != NULL) {
		acl_obj_t *acl_obj = acl_iterator->data;
		GListPtr children = NULL;
		GListPtr children_iterator = NULL;

		crm_debug("Generating permissions with ACL: mode=%s, tag=%s, ref=%s, xpath=%s, attribute=%s",
				acl_obj->mode, acl_obj->tag, acl_obj->ref, acl_obj->xpath, acl_obj->attribute);
		if (acl_obj->tag || acl_obj->ref) {
			search_xml_children(&children, xml, acl_obj->tag, XML_ATTR_ID, acl_obj->ref, TRUE);

		} else if (acl_obj->xpath) {
			/* Never be here for a modification operation */
			/* Already parse_acl_xpath() previously */
			search_xpath_objects(&children, xml, acl_obj->xpath);
		}

		children_iterator = children;
		while (children_iterator != NULL) {
			update_xml_perms(children_iterator->data, acl_obj, *xml_perms);

			children_iterator = children_iterator->next;
		}
		g_list_free(children);

		acl_iterator = acl_iterator->next;
	}
	
	return TRUE;
}

/* Borrowed from lib/common/xml.c: find_xml_children() */
/* But adding the original xmlNode pointers into a GList */
static int
search_xml_children(GListPtr *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 && safe_str_neq(tag, crm_element_name(root))) {

	} else if(value != NULL
		  && safe_str_neq(value, crm_element_value(root, field))) {

	} else {
		*children = g_list_append(*children, root);
		match_found = 1;
	}

	if(search_matches || match_found == 0) {
	    for (child = __xml_first_child(root); child; child = __xml_next(child)) {
			match_found += search_xml_children(
				children, child, tag, field, value,
				search_matches);
	    }
	}
	
	return match_found;
}

static int
search_xpath_objects(GListPtr *objects, xmlNode *xml_obj, const char *xpath)
{
	int match_found = 0;
	xmlXPathObjectPtr xpathObj = NULL;

	if(xpath == NULL) {
		return 0;
	}

	xpathObj = xpath_search(xml_obj, xpath);

	if(xpathObj == NULL || xpathObj->nodesetval == NULL || xpathObj->nodesetval->nodeNr < 1) {
		crm_debug("No match for %s in %s", xpath, xmlGetNodePath(xml_obj));

	} else if(xpathObj->nodesetval->nodeNr > 0) {
		int lpc = 0, max = xpathObj->nodesetval->nodeNr;

		for(lpc = 0; lpc < max; lpc++) {
			xmlNode *match = getXpathResult(xpathObj, lpc);
			if (match == NULL) {
				continue;
			}

			*objects = g_list_append(*objects, match);
			match_found++;
		}
	}

	if(xpathObj) {
		xmlXPathFreeObject(xpathObj);
	}
	return match_found;
}

static gboolean
update_xml_perms(xmlNode *xml, acl_obj_t *acl_obj, GHashTable *xml_perms)
{
	xml_perm_t *perm = NULL;

	if (g_hash_table_lookup_extended(xml_perms, xml, NULL, (gpointer)&perm)) {
		if (perm->mode != NULL) {
			return FALSE;
		}
	} else {
		crm_malloc0(perm, sizeof(xml_perm_t));
		if (perm == NULL) {
			return FALSE;
		}
		g_hash_table_insert(xml_perms, xml, perm);
	}

	if (acl_obj->attribute == NULL) {
		perm->mode = acl_obj->mode;
		crm_debug_3("Permission for element: element_mode=%s, tag=%s, id=%s",
                                perm->mode, crm_element_name(xml), crm_element_value(xml, XML_ATTR_ID));

		for (child = __xml_first_child(root); child; child = __xml_next(child)) {
			update_xml_children_perms(child, perm->mode, xml_perms);
		}

	} else {
		if (perm->attribute_perms == NULL
				|| (g_hash_table_lookup_extended(perm->attribute_perms, 
					acl_obj->attribute, NULL, NULL) == FALSE)) {

			if (perm->attribute_perms == NULL) {
				perm->attribute_perms = g_hash_table_new_full(
						crm_str_hash, g_str_equal, g_hash_destroy_str, g_hash_destroy_str);
			}

			g_hash_table_insert(perm->attribute_perms,
					crm_strdup(acl_obj->attribute), crm_strdup(acl_obj->mode));
			crm_debug_3("Permission for attribute: attribute_mode=%s, tag=%s, id=%s attribute=%s",
                                	acl_obj->mode, crm_element_name(xml),
					crm_element_value(xml, XML_ATTR_ID), acl_obj->attribute);
		}
	}

	return TRUE;
}

static gboolean
update_xml_children_perms(xmlNode *xml, const char *mode, GHashTable *xml_perms)
{
	xml_perm_t *perm = NULL;

	if (g_hash_table_lookup_extended(xml_perms, xml, NULL, (gpointer)&perm)) {
		if (perm->mode != NULL) {
			return FALSE;
		}
	} else {
		crm_malloc0(perm, sizeof(xml_perm_t));
		if (perm == NULL) {
			return FALSE;
		}
		g_hash_table_insert(xml_perms, xml, perm);
	}

	perm->mode = mode;
	crm_debug_4("Permission for child element: element_mode=%s, tag=%s, id=%s",
			mode, crm_element_name(xml), crm_element_value(xml, XML_ATTR_ID));

	for (child = __xml_first_child(root); child; child = __xml_next(child)) {
		update_xml_children_perms(child, mode, xml_perms);
	}

	return TRUE;
}

static void
free_xml_perm(gpointer xml_perm)
{
	xml_perm_t *perm = xml_perm;

	if (perm == NULL) {
		return;
	}

	if (perm->attribute_perms != NULL) {
		g_hash_table_destroy(perm->attribute_perms);
	}

	crm_free(perm);
}

#define can_read(mode) (crm_str_eq(mode, XML_ACL_TAG_READ, TRUE) \
			|| crm_str_eq(mode, XML_ACL_TAG_WRITE, TRUE))

#define can_write(mode) crm_str_eq(mode, XML_ACL_TAG_WRITE, TRUE)

/* rc = TRUE if the xml is filtered out*/
static gboolean
acl_filter_xml(xmlNode *xml, GHashTable *xml_perms)
{
	int children_counter = 0;
	xml_perm_t *perm = NULL;
	int allow_counter = 0;

	for (child = __xml_first_child(xml); child; child = __xml_next(child)) {
		if (acl_filter_xml(child, xml_perms) == FALSE) {
			children_counter++;
		}
	}

	g_hash_table_lookup_extended(xml_perms, xml, NULL, (gpointer)&perm);

	if (perm == NULL) {
		crm_debug_4("No ACL defined to read the element: tag=%s, id=%s",
				crm_element_name(xml), crm_element_value(xml, XML_ATTR_ID));
		goto end_filter;
	}

	if (perm->attribute_perms == NULL) {
		if (can_read(perm->mode)) {
			return FALSE;
		} else {
			crm_debug_4("No enough permission to read the element: element_mode=%s, tag=%s, id=%s",
					 perm->mode, crm_element_name(xml), crm_element_value(xml, XML_ATTR_ID));
			goto end_filter;
		}
	}

	xml_prop_iter(xml, prop_name, prop_value,
		gpointer mode = NULL;

		if (g_hash_table_lookup_extended(perm->attribute_perms, prop_name, NULL, &mode)) {
			if (can_read(mode)) {
				allow_counter++;
			} else {
				xml_remove_prop(xml, prop_name);
				crm_debug_4("Filtered out the attribute: attribute_mode=%s, tag=%s, id=%s, attribute=%s",
						 (char *)mode, crm_element_name(xml), crm_element_value(xml, XML_ATTR_ID), prop_name);
			}
		} else {
			if (can_read(perm->mode)) {
				allow_counter++;
			} else if (crm_str_eq(prop_name, XML_ATTR_ID, TRUE) == FALSE) {
				xml_remove_prop(xml, prop_name);
				crm_debug_4("Filtered out the attribute: element_mode=%s, tag=%s, id=%s, attribute=%s",
						 perm->mode, crm_element_name(xml), crm_element_value(xml, XML_ATTR_ID), prop_name);
			}
		}
		);

	if (allow_counter) {
		return FALSE;
	}

	if (can_read(perm->mode)) {
		return FALSE;
	}

end_filter:
	if (children_counter) {
		crm_debug_4("Don't filter out the element (tag=%s, id=%s) because user can read its children",
				crm_element_name(xml), crm_element_value(xml, XML_ATTR_ID));
		return FALSE;
	}

	free_xml_from_parent(NULL, xml);
	crm_debug_4("Filtered out the element: tag=%s, id=%s",
			crm_element_name(xml), crm_element_value(xml, XML_ATTR_ID));
	return TRUE;
}

static gboolean
acl_check_diff_xml(xmlNode *xml, GHashTable *xml_perms)
{
	xml_perm_t *perm = NULL;

	for (child = __xml_first_child(xml); child; child = __xml_next(child)) {
		if (acl_check_diff_xml(child, xml_perms) == FALSE) {
			return FALSE;
		}
	}

	g_hash_table_lookup_extended(xml_perms, xml, NULL, (gpointer)&perm);

	xml_prop_iter(xml, prop_name, prop_value,
		gpointer mode = NULL;

		if (crm_str_eq(crm_element_name(xml), XML_TAG_CIB, TRUE)) {
			if (crm_str_eq(prop_name, XML_ATTR_GENERATION, TRUE)
					|| crm_str_eq(prop_name, XML_ATTR_NUMUPDATES, TRUE)
					|| crm_str_eq(prop_name, XML_ATTR_GENERATION_ADMIN, TRUE)) {
				continue;
			}
		}

		if (crm_str_eq(prop_name, XML_ATTR_ID, TRUE)) {
			continue;
		}

		if (crm_str_eq(prop_name, XML_DIFF_MARKER, TRUE) && xml_has_children(xml)) {
			continue;
		}

		if (perm == NULL) {
			crm_warn("No ACL defined to modify the element: tag=%s, id=%s, attribute=%s",
					 crm_element_name(xml), crm_element_value(xml, XML_ATTR_ID), prop_name);
			return FALSE;
		}

		if (perm->attribute_perms == NULL) {
			if (can_write(perm->mode)) {
				return TRUE;
			} else {
				crm_warn("No enough permission to modify the element: element_mode=%s, tag=%s, id=%s, attribute=%s",
						 perm->mode, crm_element_name(xml), crm_element_value(xml, XML_ATTR_ID), prop_name);
				return FALSE;
			}
		}

		if (g_hash_table_lookup_extended(perm->attribute_perms, prop_name, NULL, &mode)) {
			if (can_write(mode) == FALSE) {
				crm_warn("No enough permission to modify the attribute: attribute_mode=%s, tag=%s, id=%s, attribute=%s",
						 (char *)mode, crm_element_name(xml), crm_element_value(xml, XML_ATTR_ID), prop_name);
				return FALSE;
			}
		} else if (can_write(perm->mode) == FALSE) {
			crm_warn("No enough permission to modify the element and the attribute: element_mode=%s, tag=%s, id=%s, attribute=%s",
					 perm->mode, crm_element_name(xml), crm_element_value(xml, XML_ATTR_ID), prop_name);
			return FALSE;
		}

		);

	return TRUE;
}

