Refactored YANG min/max validation code, created new clixon_validate_minmax.[ch]

Added new recursive minmax check for non-presence containers
This makes validation checks stricter, including check of incoming RPCs
Renamed xml_yang_check_list_unique_minmax() to xml_yang_minmax_recurse()
Fixed again: [YANG min-elements within non-presence container does not work](https://github.com/clicon/clixon/issues/355)
This commit is contained in:
Olof hagsand 2022-09-01 10:51:06 +02:00
parent c8bf718db8
commit 2eb9c6cda1
17 changed files with 867 additions and 614 deletions

View file

@ -34,9 +34,7 @@
***** END LICENSE BLOCK *****
*
* XML code
*
* "api-path" is "URI-encoded path expression" definition in RFC8040 3.5.3
* Check YANG validation
*/
#ifdef HAVE_CONFIG_H
#include "clixon_config.h" /* generated by config & autoconf */
@ -79,6 +77,7 @@
#include "clixon_yang_type.h"
#include "clixon_xml_map.h"
#include "clixon_xml_bind.h"
#include "clixon_validate_minmax.h"
#include "clixon_validate.h"
/*! Validate xml node of type leafref, ensure the value is one of that path's reference
@ -871,582 +870,6 @@ check_mandatory(cxobj *xt,
goto done;
}
/*! New element last in list, check if already exists if sp return -1
* @param[in] vec Vector of existing entries (new is last)
* @param[in] i1 The new entry is placed at vec[i1]
* @param[in] vlen Lenght of entry
* @param[in] sorted Sorted by system, ie sorted by key, otherwise no assumption
* @retval 0 OK, entry is unique
* @retval -1 Duplicate detected
* @note This is currently quadratic complexity. It could be improved by inserting new element sorted and binary search.
* @retval 1 Validation OK
* @retval 0 Validation failed (cbret set)
* @retval -1 Error
*/
static int
unique_search_xpath(cxobj *x,
char *xpath,
cvec *nsc,
char ***svec,
size_t *slen)
{
int retval = -1;
cxobj **xvec = NULL;
size_t xveclen;
int i;
int s;
cxobj *xi;
char *bi;
/* Collect tuples */
if (xpath_vec(x, nsc, "%s", &xvec, &xveclen, xpath) < 0)
goto done;
for (i=0; i<xveclen; i++){
xi = xvec[i];
if ((bi = xml_body(xi)) == NULL)
break;
/* Check if bi is duplicate?
* XXX: sort svec?
*/
for (s=0; s<(*slen); s++){
if (strcmp(bi, (*svec)[s]) == 0){
goto fail;
}
}
(*slen) ++;
if (((*svec) = realloc((*svec), (*slen)*sizeof(char*))) == NULL){
clicon_err(OE_UNIX, errno, "realloc");
goto done;
}
(*svec)[(*slen)-1] = bi;
} /* i search results */
retval = 1;
done:
if (xvec)
free(xvec);
return retval;
fail:
retval = 0;
goto done;
}
/*! New element last in list, check if already exists if sp return -1
* @param[in] vec Vector of existing entries (new is last)
* @param[in] i1 The new entry is placed at vec[i1]
* @param[in] vlen Lenght of entry
* @param[in] sorted Sorted by system, ie sorted by key, otherwise no assumption
* @retval 0 OK, entry is unique
* @retval -1 Duplicate detected
* @note This is currently quadratic complexity. It could be improved by inserting new element sorted and binary search.
* @retval 1 Validation OK
* @retval 0 Validation failed (cbret set)
* @retval -1 Error
*/
static int
check_insert_duplicate(char **vec,
int i1,
int vlen,
int sorted)
{
int i;
int v;
char *b;
if (sorted){
/* Just go look at previous element to see if it is duplicate (sorted by system) */
if (i1 == 0)
return 0;
i = i1-1;
for (v=0; v<vlen; v++){
b = vec[i*vlen+v];
if (b == NULL || strcmp(b, vec[i1*vlen+v]))
return 0;
}
/* here we have passed thru all keys of previous element and they are all equal */
return -1;
}
else{
for (i=0; i<i1; i++){
for (v=0; v<vlen; v++){
b = vec[i*vlen+v];
if (b == NULL || strcmp(b, vec[i1*vlen+v]))
break;
}
if (v==vlen) /* duplicate */
break;
}
return i==i1?0:-1;
}
}
/*! Given a list with unique constraint, detect duplicates
* @param[in] x The first element in the list (on return the last)
* @param[in] xt The parent of x (a list)
* @param[in] y Its yang spec (Y_LIST)
* @param[in] yu A yang unique (Y_UNIQUE) for unique schema node ids or (Y_LIST) for list keys
* @param[out] xret Error XML tree. Free with xml_free after use
* @retval 1 Validation OK
* @retval 0 Validation failed (cbret set)
* @retval -1 Error
* Discussion: the RFC 7950 Sec 7.8.3: "constraints on valid list entries"
* The arguments are "descendant schema node identifiers". A direct interpretation is that
* this is for "direct" descendants, but it does not rule out transient descendants.
* The implementation supports two variants:
* 1) list of direct descendants, eg "a b"
* 2) single transient schema node identifier, eg "a/b"
* The problem with combining (1) and (2) is that (2) results in a potential set of results, what
* would unique "a/b c/d" mean if both a/b and c/d returns a set?
* For (1):
* All key leafs MUST be present for all list entries.
* The combined values of all the leafs specified in the key are used to
* uniquely identify a list entry. All key leafs MUST be given values
* when a list entry is created.
*/
static int
check_unique_list_direct(cxobj *x,
cxobj *xt,
yang_stmt *y,
yang_stmt *yu,
cxobj **xret)
{
int retval = -1;
cg_var *cvi; /* unique node name */
cxobj *xi;
char **vec = NULL; /* 2xmatrix */
int clen;
int i;
int v;
char *bi;
int sorted;
char *str;
cvec *cvk;
cvk = yang_cvec_get(yu);
/* If list and is sorted by system, then it is assumed elements are in key-order which is optimized
* Other cases are "unique" constraint or list sorted by user which is quadratic in complexity
* This second case COULD be optimized if binary insert is made on the vec vector.
*/
sorted = (yang_keyword_get(yu) == Y_LIST &&
yang_find(y, Y_ORDERED_BY, "user") == NULL);
cvk = yang_cvec_get(yu);
/* nr of unique elements to check */
if ((clen = cvec_len(cvk)) == 0){
/* No keys: no checks necessary */
goto ok;
}
if ((vec = calloc(clen*xml_child_nr(xt), sizeof(char*))) == NULL){
clicon_err(OE_UNIX, errno, "calloc");
goto done;
}
/* A vector is built with key-values, for each iteration check "backward" in the vector
* for duplicates
*/
i = 0; /* x element index */
do {
cvi = NULL;
v = 0; /* index in each tuple */
/* XXX Quadratic if clen > 1 */
while ((cvi = cvec_each(cvk, cvi)) != NULL){
/* RFC7950: Sec 7.8.3.1: entries that do not have value for all
* referenced leafs are not taken into account */
str = cv_string_get(cvi);
if (index(str, '/') != NULL){
clicon_err(OE_YANG, 0, "Multiple descendant nodes not allowed (w /)");
goto done;
}
if ((xi = xml_find(x, str)) == NULL)
break;
if ((bi = xml_body(xi)) == NULL)
break;
vec[i*clen + v++] = bi;
}
if (cvi==NULL){
/* Last element (i) is newly inserted, see if it is already there */
if (check_insert_duplicate(vec, i, clen, sorted) < 0){
if (xret && netconf_data_not_unique_xml(xret, x, cvk) < 0)
goto done;
goto fail;
}
}
x = xml_child_each(xt, x, CX_ELMNT);
i++;
} while (x && y == xml_spec(x)); /* stop if list ends, others may follow */
ok:
/* It would be possible to cache vec here as an optimization */
retval = 1;
done:
if (vec)
free(vec);
return retval;
fail:
retval = 0;
goto done;
}
/*! Given a list with unique constraint, detect duplicates
* @param[in] x The first element in the list (on return the last)
* @param[in] xt The parent of x (a list)
* @param[in] y Its yang spec (Y_LIST)
* @param[in] yu A yang unique (Y_UNIQUE) for unique schema node ids or (Y_LIST) for list keys
* @param[out] xret Error XML tree. Free with xml_free after use
* @retval 1 Validation OK
* @retval 0 Validation failed (xret set)
* @retval -1 Error
* Discussion: the RFC 7950 Sec 7.8.3: "constraints on valid list entries"
* The arguments are "descendant schema node identifiers". A direct interpretation is that
* this is for "direct" descendants, but it does not rule out transient descendants.
* The implementation supports two variants:
* 1) list of direct descendants, eg "a b"
* 2) single transient schema node identifier, eg "a/b"
* The problem with combining (1) and (2) is that (2) results in a potential set of results, what
* would unique "a/b c/d" mean if both a/b and c/d returns a set?
* For (1):
* All key leafs MUST be present for all list entries.
* The combined values of all the leafs specified in the key are used to
* uniquely identify a list entry. All key leafs MUST be given values
* when a list entry is created.
*/
static int
check_unique_list(cxobj *x,
cxobj *xt,
yang_stmt *y,
yang_stmt *yu,
cxobj **xret)
{
int retval = -1;
cg_var *cvi; /* unique node name */
char **svec = NULL; /* vector of search results */
size_t slen = 0;
char *xpath0 = NULL;
char *xpath1 = NULL;
int ret;
cvec *cvk;
cvec *nsc0 = NULL;
cvec *nsc1 = NULL;
/* Check if multiple direct children */
cvk = yang_cvec_get(yu);
if (cvec_len(cvk) > 1){
retval = check_unique_list_direct(x, xt, y, yu, xret);
goto done;
}
cvi = cvec_i(cvk, 0);
if (cvi == NULL || (xpath0 = cv_string_get(cvi)) == NULL){
clicon_err(OE_YANG, 0, "No descendant schemanode");
goto done;
}
/* Check if direct schmeanode-id , ie not xpath */
if (index(xpath0, '/') == NULL){
retval = check_unique_list_direct(x, xt, y, yu, xret);
goto done;
}
/* Here proper xpath with at least one slash (can there be a descendant schemanodeid w/o slash?) */
if (xml_nsctx_yang(yu, &nsc0) < 0)
goto done;
if ((ret = xpath2canonical(xpath0, nsc0, ys_spec(y),
&xpath1, &nsc1, NULL)) < 0)
goto done;
if (ret == 0)
goto fail; // XXX set xret
do {
/* Collect search results from one */
if ((ret = unique_search_xpath(x, xpath1, nsc1, &svec, &slen)) < 0)
goto done;
if (ret == 0){
if (xret && netconf_data_not_unique_xml(xret, x, cvk) < 0)
goto done;
goto fail;
}
x = xml_child_each(xt, x, CX_ELMNT);
} while (x && y == xml_spec(x)); /* stop if list ends, others may follow */
// ok:
/* It would be possible to cache vec here as an optimization */
retval = 1;
done:
if (nsc0)
cvec_free(nsc0);
if (nsc1)
cvec_free(nsc1);
if (xpath1)
free(xpath1);
if (svec)
free(svec);
return retval;
fail:
retval = 0;
goto done;
}
/*! Given a list, check if any min/max-elemants constraints apply
* @param[in] xp Parent of the xml list there are too few/many
* @param[in] y Yang spec of the failing list
* @param[in] nr Number of elements (like x) in the list
* @param[out] xret Error XML tree. Free with xml_free after use
* @retval 1 Validation OK
* @retval 0 Validation failed (cbret set)
* @retval -1 Error
* @see RFC7950 7.7.5
*/
static int
check_min_max(cxobj *xp,
yang_stmt *y,
int nr,
cxobj **xret)
{
int retval = -1;
yang_stmt *ymin; /* yang min */
yang_stmt *ymax; /* yang max */
cg_var *cv;
if ((ymin = yang_find(y, Y_MIN_ELEMENTS, NULL)) != NULL){
cv = yang_cv_get(ymin);
if (nr < cv_uint32_get(cv)){
if (xret && netconf_minmax_elements_xml(xret, xp, yang_argument_get(y), 0) < 0)
goto done;
goto fail;
}
}
if ((ymax = yang_find(y, Y_MAX_ELEMENTS, NULL)) != NULL){
cv = yang_cv_get(ymax);
if (cv_uint32_get(cv) > 0 && /* 0 means unbounded */
nr > cv_uint32_get(cv)){
if (xret && netconf_minmax_elements_xml(xret, xp, yang_argument_get(y), 1) < 0)
goto done;
goto fail;
}
}
retval = 1;
done:
return retval;
fail:
retval = 0;
goto done;
}
/*! Check if there is any empty list (no x elements) and check min-elements
* Note recurse for non-presence container
* @param[in] xt XML node
* @param[in] yt YANG node
* @param[out] xret Error XML tree. Free with xml_free after use
* @retval 1 Validation OK
* @retval 0 Validation failed (xret set)
* @retval -1 Error
*/
static int
check_empty_list_minmax(cxobj *xt,
yang_stmt *ye,
cxobj **xret)
{
int retval = -1;
int ret;
yang_stmt *yprev = NULL;
if (yang_config(ye) == 1){
if(yang_keyword_get(ye) == Y_CONTAINER &&
yang_find(ye, Y_PRESENCE, NULL) == NULL){
yprev = NULL;
while ((yprev = yn_each(ye, yprev)) != NULL) {
if ((ret = check_empty_list_minmax(xt, yprev, xret)) < 0)
goto done;
if (ret == 0)
goto fail;
}
}
else if (yang_keyword_get(ye) == Y_LIST ||
yang_keyword_get(ye) == Y_LEAF_LIST){
/* Check if the list length violates min/max */
if ((ret = check_min_max(xt, ye, 0, xret)) < 0)
goto done;
if (ret == 0)
goto fail;
}
}
retval = 1;
done:
return retval;
fail:
retval = 0;
goto done;
}
/*! Detect unique constraint for duplicates from parent node and minmax
* @param[in] xt XML parent (may have lists w unique constraints as child)
* @param[out] xret Error XML tree. Free with xml_free after use
* @retval 1 Validation OK
* @retval 0 Validation failed (xret set)
* @retval -1 Error
* Assume xt:s children are sorted and yang populated.
* The function does two different things of the children of an XML node:
* (1) Check min/max element constraints
* (2) Check unique constraints
*
* The routine uses a node traversing mechanism as the following example, where
* two lists [x1,..] and [x2,..] are embedded:
* xt: {a, b, [x1, x1, x1], d, e, f, [x2, x2, x2], g}
* The function does this using a single iteration and uses the fact that the
* xml symbols share yang symbols: ie [x1..] has yang y1 and d has yd.
*
* Unique constraints:
* Lists are identified, then check_unique_list is called on each list.
* Example, x has an associated yang list node with list of unique constraints
* y-list->y-unique - "a"
* xt->x -> ab
* x -> bc
* x -> ab
*
* Min-max constraints:
* Find upper and lower bound of existing lists and report violations
* Somewhat tricky to find violation of min-elements of empty
* lists, but this is done by a "gap-detection" mechanism, which detects
* gaps in the xml nodes given the ancestor Yang structure.
* But no gap analysis is done if the yang spec of the top-level xml is unknown
* Example:
* Yang structure: y1, y2, y3,
* XML structure: [x1, x1], [x3, x3] where [x2] list is missing
* @note min-element constraints on empty lists are not detected on top-level.
* Or more specifically, if no yang spec if associated with the top-level
* XML node. This may not be a large problem since it would mean empty configs
* are not allowed.
*/
int
xml_yang_check_list_unique_minmax(cxobj *xt,
cxobj **xret)
{
int retval = -1;
cxobj *x = NULL;
yang_stmt *y;
yang_stmt *yt;
yang_stmt *yprev = NULL; /* previous in list */
yang_stmt *ye = NULL; /* yang each list to catch emtpy */
yang_stmt *ych; /* y:s parent node (if choice that ye can compare to) */
yang_stmt *yu; /* yang unique */
int ret;
int nr=0; /* Nr of list elements for min/max check */
enum rfc_6020 keyw;
/* RFC 7950 7.7.5: regarding min-max elements check
* The behavior of the constraint depends on the type of the
* leaf-list's or list's closest ancestor node in the schema tree
* that is not a non-presence container (see Section 7.5.1):
* o If no such ancestor exists in the schema tree, the constraint
* is enforced.
* o Otherwise, if this ancestor is a case node, the constraint is
* enforced if any other node from the case exists.
* o Otherwise, it is enforced if the ancestor node exists.
*/
yt = xml_spec(xt); /* If yt == NULL, then no gap-analysis is done */
/* Traverse all elements */
while ((x = xml_child_each(xt, x, CX_ELMNT)) != NULL) {
if ((y = xml_spec(x)) == NULL)
continue;
if ((ych = yang_choice(y)) == NULL)
ych = y;
keyw = yang_keyword_get(y);
if (keyw != Y_LIST && keyw != Y_LEAF_LIST){
if (yprev != NULL && y == yprev){
/* Only lists and leaf-lists are allowed to be many
* This checks duplicate container and leafs
*/
if (xret && netconf_minmax_elements_xml(xret, xt, xml_name(x), 1) < 0)
goto done;
goto fail;
}
yprev = y; /* Restart min/max count */
continue;
}
/* Here only (leaf)lists */
if (yprev != NULL){ /* There exists a previous (leaf)list */
if (y == yprev){ /* If same yang as previous x, then skip (eg same list) */
nr++;
continue;
}
else {
/* Check if the list length violates min/max */
if ((ret = check_min_max(xt, yprev, nr, xret)) < 0)
goto done;
if (ret == 0)
goto fail;
}
}
/* Here is only new list / leaf-list */
yprev = y; /* Restart min/max count */
nr = 1;
/* Gap analysis: Check if there is any empty list between y and yprev
* Note, does not detect empty choice list (too complicated)
*/
if (yt != NULL && ych != ye){
/* Skip analysis if Yang spec is unknown OR
* if we are still iterating the same Y_CASE w multiple lists
*/
ye = yn_each(yt, ye);
if (ye && ych != ye)
do {
if ((ret = check_empty_list_minmax(xt, ye, xret)) < 0)
goto done;
if (ret == 0)
goto fail;
ye = yn_each(yt, ye);
} while(ye != NULL && /* to avoid livelock (shouldnt happen) */
ye != ych);
}
if (keyw != Y_LIST)
continue;
/* Here new (first element) of lists only
* First check unique keys direct children
*/
if ((ret = check_unique_list_direct(x, xt, y, y, xret)) < 0)
goto done;
if (ret == 0)
goto fail;
/* Check if there is a unique constraint on the list
*/
yu = NULL;
while ((yu = yn_each(y, yu)) != NULL) {
if (yang_keyword_get(yu) != Y_UNIQUE)
continue;
/* Here is a list w unique constraints identified by:
* its first element x, its yang spec y, its parent xt, and
* a unique yang spec yu,
* Two cases:
* 1) multiple direct children (no prefixes), eg "a b"
* 2) single xpath with canonical prefixes, eg "/ex:a/ex:b"
*/
if ((ret = check_unique_list(x, xt, y, yu, xret)) < 0)
goto done;
if (ret == 0)
goto fail;
}
} /* while x */
/* yprev if set, is a list that has been traversed
* This check is made in the loop as well - this is for the last list
*/
if (yprev){
/* Check if the list length violates min/max */
if ((ret = check_min_max(xt, yprev, nr, xret)) < 0)
goto done;
if (ret == 0)
goto fail;
}
/* Check if there is any empty list between after last non-empty list
* Note, does not detect empty lists within choice/case (too complicated)
*/
if ((ye = yn_each(yt, ye)) != NULL){
do {
if ((ret = check_empty_list_minmax(xt, ye, xret)) < 0)
goto done;
if (ret == 0)
goto fail;
} while((ye = yn_each(yt, ye)) != NULL);
}
retval = 1;
done:
return retval;
fail:
retval = 0;
goto done;
}
/*! Validate a single XML node with yang specification for added entry
* 1. Check if mandatory leafs present as subs.
* 2. Check leaf values, eg int ranges and string regexps.
@ -1808,7 +1231,7 @@ xml_yang_validate_all(clicon_handle h,
/* Check unique and min-max after choice test for example*/
if (yang_config(yt) != 0){
/* Checks if next level contains any unique list constraints */
if ((ret = xml_yang_check_list_unique_minmax(xt, xret)) < 0)
if ((ret = xml_yang_minmax_recurse(xt, xret)) < 0)
goto done;
if (ret == 0)
goto fail;
@ -1845,7 +1268,7 @@ xml_yang_validate_all_top(clicon_handle h,
if ((ret = xml_yang_validate_all(h, x, xret)) < 1)
return ret;
}
if ((ret = xml_yang_check_list_unique_minmax(xt, xret)) < 1)
if ((ret = xml_yang_minmax_recurse(xt, xret)) < 1)
return ret;
return 1;
}