NACM datnode read path implementation revised

This commit is contained in:
Olof hagsand 2020-04-15 16:55:22 +02:00
parent ba59e22fd7
commit 1fb3dcc686
6 changed files with 804 additions and 179 deletions

View file

@ -27,8 +27,8 @@ Expected: May 2020
### Major New features
* NACM RFC341 datanode paths
* Read operations now implemented
* Create/Write/Update not yet
* NACM datanode read paths
* NYI: NACM datanode paths for create/delete/update
### API changes on existing protocol/config features (You may have have to change how you use Clixon)

View file

@ -321,34 +321,7 @@ nacm_rpc(char *rpc,
* @retval -1 Error
* @retval 0 No Match
* @retval 1 Match
* @retval 1 SubMatch
o For HEAD and GET requests, any data nodes that are ancestor nodes
of the target resource are considered to be part of the retrieval
request for access control purposes.
Data nodes to which the client does not have read access are silently
omitted, along with any descendants,
the selection criteria are applied to the subset of nodes that the user
is authorized to read, not the entire datastore.
* Assume tree:
* tree
* |
* a
* / \
* b c
* access: /
* rule1: /a/b deny
* rule2: / permit
* returned tree:
* tree
* |
* a
* \
* c
* Algorithm:
* Traverse all rules and access the tree:
* rule1: purge b
* rule2: accept rest.
* @note not used for read
*/
static int
nacm_rule_datanode_path(clicon_handle h,
@ -361,7 +334,7 @@ nacm_rule_datanode_path(clicon_handle h,
cxobj *xp; /* Node specified by path */
cxobj *xa; /* Ancestor */
char *path0; /* Non-canonical path */
char *path; /* Canonical path */
char *path=NULL; /* Canonical path */
yang_stmt *yspec;
cxobj **xvec = NULL;
size_t xlen = 0;
@ -396,6 +369,10 @@ nacm_rule_datanode_path(clicon_handle h,
match:
retval = 1;
done:
if (nsc0)
cvec_free(nsc0);
if (path)
free(path);
if (xvec)
free(xvec);
return retval;
@ -504,32 +481,35 @@ nacm_rule_datanode(clicon_handle h,
goto done;
}
/*! Perform NACM action: mark if permit, del if deny
* @param[in] xrule NACM rule
* @param[in] xn XML node (requested node)
* @retval -1 Error
* @retval 0 OK
*/
static int
nacm_data_read_action(cxobj *xrule,
cxobj *xp,
int default_permit)
cxobj *xn)
{
int retval = -1;
char *action;
if ((action = xml_find_body(xrule, "action")) != NULL){
if (strcmp(action, "deny")==0){
if (xml_purge(xp) < 0)
goto done;
}
else if (strcmp(action, "permit")==0){
if (default_permit)
;
else
xml_flag_set(xp, XML_FLAG_MARK);
}
if (strcmp(action, "deny")==0)
xml_flag_set(xn, XML_FLAG_DEL);
else if (strcmp(action, "permit")==0)
xml_flag_set(xn, XML_FLAG_MARK);
}
retval = 0;
done:
//done:
return retval;
}
/*
/*! Match specific rule to specific requested node
* @param[in] xt XML root tree with "config" label
* @param[in] xn XML node (requested node)
* @param[in] xrule NACM rule
* @param[in] yspec YANG spec
* @retval -1 Error
* @retval 0 OK and rule does not match
* @retval 1 OK and rule matches
@ -539,12 +519,11 @@ nacm_data_read_action(cxobj *xrule,
* (2) read_default is deny:
* mark all permit rules and ancestors, remove everything else
*/
// xml_apply_ancestor(xn, (xml_applyfn_t*)xml_flag_set, (void*)XML_FLAG_CHANGE);
static int
nacm_data_read_xrule(clicon_handle h,
cxobj *xt,
nacm_data_read_xrule_xml(cxobj *xt,
cxobj *xn,
cxobj *xrule,
int default_permit)
yang_stmt *yspec)
{
int retval = -1;
cxobj *xp;
@ -553,18 +532,23 @@ nacm_data_read_xrule(clicon_handle h,
char *access_operations;
cvec *nsc0 = NULL; /* Non-canonical namespace context */
char *path0; /* Non-canonical path */
char *path; /* Canonical path */
yang_stmt *yspec;
char *path=NULL; /* Canonical path */
yang_stmt *ymod;
cxobj **xvec = NULL;
size_t xlen = 0;
char *module_rule; /* rule module name */
int i;
yspec = clicon_dbspec_yang(h);
if ((module_rule = xml_find_body(xrule, "module-name")) == NULL)
goto nomatch;
/* 6a) The rule's "module-name" leaf is "*" or equals the name of
* the YANG module where the requested data node is defined.
*/
if (strcmp(module_rule, "*") != 0){
if (ys_module_by_xml(yspec, xn, &ymod) < 0)
goto done;
if (strcmp(yang_argument_get(ymod), module_rule) != 0)
goto nomatch;
}
/* 6b) Either (1) the rule does not have a "rule-type" defined or
(2) the "rule-type" is "data-node" and the "path" matches the
requested data node, action node, or notification node. */
@ -578,25 +562,8 @@ nacm_data_read_xrule(clicon_handle h,
if (!match_access(access_operations, "read", NULL))
goto nomatch;
if (pathobj == NULL){
/* 6a) The rule's "module-name" leaf is "*" or equals the name of
* the YANG module where the requested data node is defined.
* Go thru all children of xt,
*/
i = 0;
xp = NULL;
while ((xp = xml_child_each(xt, xp, CX_ELMNT)) != NULL) {
if (strcmp(module_rule, "*") != 0){
if (ys_module_by_xml(yspec, xp, &ymod) < 0)
if (nacm_data_read_action(xrule, xn) < 0)
goto done;
if (strcmp(yang_argument_get(ymod), module_rule) != 0)
continue;
}
i++;
if (nacm_data_read_action(xrule, xp, default_permit) < 0)
goto done;
}
if (i==0)
goto nomatch;
goto match;
}
path0 = clixon_trim2(xml_body(pathobj), " \t\n");
@ -606,7 +573,13 @@ nacm_data_read_xrule(clicon_handle h,
/* instance-id requires canonical paths */
if (xpath2canonical(path0, nsc0, yspec, &path, NULL) < 0)
goto done;
/* XXX path may not be yang-consistent gives an error,... but retval =0 back,... */
/* path may not be yang-consistent gives an error,... but retval =0 back,...
* Here I may need another function.
* I have a top-level node "xt", a node "xn" and a "path".
* I want to know if "xn" is or is a descendant of the matches of path on xt.
* The brute force is to evaluate the result in xvec and then see if ancestors of
* xn is in xvec.
*/
if ((ret = clixon_xml_find_instance_id(xt, yspec, &xvec, &xlen, "%s", path)) < 0)
goto done;
switch (ret){
@ -618,21 +591,20 @@ nacm_data_read_xrule(clicon_handle h,
goto nomatch;
assert(xlen == 1);
xp = xvec[0]; /* XXX: loop? */
/* 6a) The rule's "module-name" leaf is "*" or equals the name of
* the YANG module where the requested data node is defined. */
if (strcmp(module_rule, "*") != 0){
if (ys_module_by_xml(yspec, xp, &ymod) < 0)
goto done;
if (strcmp(yang_argument_get(ymod), module_rule) != 0)
/* Check if ancestor is xp */
if (xn != xp && !xml_isancestor(xn, xp))
goto nomatch;
}
if (nacm_data_read_action(xrule, xp, default_permit) < 0)
if (nacm_data_read_action(xrule, xn) < 0)
goto done;
break;
} /* switch */
match:
retval = 1; /* match */
done:
if (path)
free(path);
if (nsc0)
cvec_free(nsc0);
if (xvec)
free(xvec);
return retval;
@ -641,8 +613,109 @@ nacm_data_read_xrule(clicon_handle h,
goto done;
}
/*! Recursive check for NACM read rules among all XML nodes
* @param[in] h Clicon handle
* @param[in] xt XML root tree with "config" label
* @param[in] xn XML node (requested node)
* @param[in] gvec Vector of groups
* @param[in] glen Length of group vector
* @param[in] rlistvec Vector of rules
* @param[in] rlistlen Length of rule vector
* @param[in] nsc Namespace context
* @param[in] yspec YANG spec
* @retval 0 OK
* @retval -1 Error
*/
static int
nacm_datanode_read_recurse(clicon_handle h,
cxobj *xt,
cxobj *xn,
cxobj **gvec,
size_t glen,
cxobj **rlistvec,
size_t rlistlen,
cvec *nsc,
yang_stmt *yspec)
{
int retval=-1;
cxobj *rlist;
cxobj **rvec = NULL; /* rules */
size_t rlen;
cxobj *x;
int i;
int j;
char *gname;
cxobj *xrule;
cxobj *xprev;
int match;
if (xml_spec(xn)){ /* Check this node */
for (i=0; i<rlistlen; i++){ /* Loop through rule list */
rlist = rlistvec[i];
/* Loop through user's group to find match in this rule-list */
for (j=0; j<glen; j++){
gname = xml_find_body(gvec[j], "name");
if (xpath_first(rlist, nsc, ".[group='%s']", gname)!=NULL)
break; /* found */
}
if (j==glen) /* not found */
continue;
/* 6. For each rule-list entry found, process all rules, in order,
until a rule that matches the requested access operation is
found. (see 6 sub rules in nacm_rule_datanode
*/
if (xpath_vec(rlist, nsc, "rule", &rvec, &rlen) < 0)
goto done;
match = 0;
for (j=0; j<rlen; j++){ /* Loop through rules */
xrule = rvec[j];
if ((match = nacm_data_read_xrule_xml(xt, xn, xrule, yspec)) < 0)
goto done;
if (match == 1){ /* match */
break;
}
}
if (rvec){
free(rvec);
rvec=NULL;
}
if (match)
break;
}
#if 0 /* 6(A) in algorithm
* If N did not match any rule R, and default rule is deny, remove that subtree */
if (strcmp(read_default, "deny") == 0)
if (xml_tree_prune_flagged_sub(xt, XML_FLAG_MARK, 1, NULL) < 0)
goto done;
#endif
}
/* If node should be purged, dont recurse and defewr removal to caller */
if (xml_flag(xn, XML_FLAG_DEL) == 0){
x = NULL; /* Recursively check XML */
xprev = NULL;
while ((x = xml_child_each(xn, x, CX_ELMNT)) != NULL) {
if (nacm_datanode_read_recurse(h, xt, x, gvec, glen, rlistvec, rlistlen, nsc, yspec) < 0)
goto done;
/* check for delayed remove */
if (xml_flag(x, XML_FLAG_DEL)){
if (xml_purge(x) < 0)
goto done;
x = xprev;
}
}
}
retval = 0;
done:
if (rvec){
free(rvec);
rvec=NULL;
}
return retval;
}
/*! Make nacm datanode and module rule read access validation
* Just purge nodes that fail validation (dont send netconf error message)
* @param[in] h Clicon handle
* @param[in] xt XML root tree with "config" label
* @param[in] xrvec Vector of requested nodes (sub-part of xt)
* @param[in] xrlen Length of requsted node vector
@ -667,51 +740,23 @@ nacm_data_read_xrule(clicon_handle h,
* - rule r2 to permit/deny /a/b
* - rule r3 to permit/deny /a/b/c
* - rule r4 to permit/deny /d
* - read access on /a/b which returns <a><b><c/><d/></b></a>?
* permit - t; deny - f
* r1 | r2 | r3 | result (r0 and r4 are dont cares - dont match)
* ------+------+------+---------
* t | t | t | <a><b><c/><d/></b></a>
* t | t | f | <a><b><d/></b></a>
* t | f | t | <a/>
* t | f | f | <a/>
* f | t | t |
* f | t | f |
* f | f | t |
* f | f | f |
*
* - read access on / which returns <d/><e/>?
* permit - t; deny - f
* r0 | r4 | result
* ------+------+---------
* t | t | <d/><e/>
* t | f | <e/>
* f | t | <d/>
* f | f |
* Algorithm 1, based on an xml tree XT:
* The special variable ACTION can have values:
* permit/deny/default
* 1. Traverse through all nodes x in xt. Set ACTION to default
* - Find first exact matching rule r of non-default rules r1-rn on x
* - if found set ACTION to r->action (permit/deny).
* - if ACTION is deny purge x and all descendants
* - if ACTION is permit, mark x as PERMIT
* - continue traverse
* 2. If default action is
* 2. Traverse through all nodes x in xt. Set ACTION to default r0->action
* - if x is marked as PERMIT
* - if ACTION is deny deny purge x and all descendants
* Some observations:
* 1. The requested node is a set of nodes in a tree (not just the top-node)
* 2. Any node descendants of a deny is denied (except default)
* 3. First rule matching a node is the active rule
*
* Algorithm 2 (based on requested node set XRS which are sub-trees in XT).
* For each XR in XRS, check match with rule r1-rn, r0.
* 1. XR is PERMIT
* - Recursively match subtree to find reject sub-trees and purge.
* 2. XR is REJECT. Purge XR.
* Module-rule w no path is implicit rule on top node.
* Algorithm: Select either (A) or (B)
*
* 1. Select next node N in the requested node tree:
* 2. Select next R rule in the set of applicable rules:
* 3. If N does not match R, and remaining rules, goto 2.
* 4. If N matches R as deny, remove that subtree
* 5. If N matches R as accept, mark that node
* 6(A). If N did not match any rule R, and default rule is deny, remove that subtree
* 7. If remaining nodes, goto 1
* 8(B) If default rule is deny, recursively remove all subtrees that are not marked
*
* A module rule has the "module-name" leaf set but no nodes from the
* "rule-type" choice set.
* @see RFC8341 3.4.5. Data Node Access Validation
* @see nacm_datanode_write
* @see nacm_rpc
@ -727,16 +772,11 @@ nacm_datanode_read(clicon_handle h,
int retval = -1;
cxobj **gvec = NULL; /* groups */
size_t glen;
char *gname;
cxobj **rlistvec = NULL; /* rule-list */
size_t rlistlen;
int i;
int j;
char *read_default = NULL;
int matches = 0;
cvec *nsc = NULL;
cxobj *xrule;
int ret;
/* Create namespace context for with nacm namespace as default */
if ((nsc = xml_nsctx_init(NULL, NACM_NS)) == NULL)
@ -764,53 +804,21 @@ nacm_datanode_read(clicon_handle h,
clicon_err(OE_XML, EINVAL, "No nacm read-default rule");
goto done;
}
matches = 0;
for (i=0; i<rlistlen; i++){ /* Loop through rule list */
cxobj *rlist;
cxobj **rvec = NULL; /* rules */
size_t rlen;
rlist = rlistvec[i];
/* Loop through user's group to find match in this rule-list */
for (j=0; j<glen; j++){
gname = xml_find_body(gvec[j], "name");
if (xpath_first(rlist, nsc, ".[group='%s']", gname)!=NULL)
break; /* found */
}
if (j==glen) /* not found */
continue;
/* 6. For each rule-list entry found, process all rules, in order,
until a rule that matches the requested access operation is
found. (see 6 sub rules in nacm_rule_datanode
if (nacm_datanode_read_recurse(h, xt, xt, gvec, glen, rlistvec, rlistlen, nsc,
clicon_dbspec_yang(h)) < 0)
goto done;
#if 1
/* Step 8(B) above:
* If default rule is deny, recursively remove all subtrees that are not marked
*/
if (xpath_vec(rlist, nsc, "rule", &rvec, &rlen) < 0)
goto done;
for (j=0; j<rlen; j++){ /* Loop through rules */
xrule = rvec[j];
if ((ret = nacm_data_read_xrule(h, xt, xrule, strcmp(read_default,"permit")==0)) < 0)
goto done;
if (ret > 0)
matches++;
}
if (rvec){
free(rvec);
rvec=NULL;
}
}
/* If there were accpet rules in some matches, remove everything else */
if (matches && strcmp(read_default, "deny") == 0)
if (strcmp(read_default, "deny") == 0)
if (xml_tree_prune_flagged_sub(xt, XML_FLAG_MARK, 1, NULL) < 0)
goto done;
if (strcmp(read_default, "deny") == 0){
if (matches == 0)
goto step9; /* delete all */
else /* Accept rules that override default delete: delete everything not marked) */
if (xml_tree_prune_flagged_sub(xt, XML_FLAG_MARK, 1, NULL) < 0)
goto done;
}
#endif
/* reset flag */
if (xml_apply(xt, CX_ELMNT, (xml_applyfn_t*)xml_flag_reset, (void*)XML_FLAG_MARK) < 0)
goto done;
goto ok;
/* 8. At this point, no matching rule was found in any rule-list
entry. */
@ -821,7 +829,7 @@ nacm_datanode_read(clicon_handle h,
"nacm:default-deny-all" statement, then the requested data node
and all its descendants are not included in the reply.
*/
for (i=0; i<xrlen; i++) /* Loop through requested nodes */
for (i=0; i<xrlen; i++) /* Loop through requested nodes, safe since vector not children */
if (xml_purge(xrvec[i]) < 0)
goto done;
ok:

View file

@ -1440,6 +1440,7 @@ clixon_path_search(cxobj *xt,
yc = cp->cp_yang;
if ((modns = yang_find_mynamespace(yc)) == NULL)
goto fail;
if (xvecp)
for (i=0; i<clixon_xvec_len(xvecp); i++){
xp = clixon_xvec_i(xvecp, i); /* Iterate over parent set */
xvecc = NULL;

View file

@ -827,6 +827,19 @@ xml_child_order(cxobj *xp,
* @note Never manipulate the child-list during operation or using the
* same object recursively, the function uses an internal field to remember the
* index used. It works as long as the same object is not iterated concurrently.
* If you need to delete a node you can do somethjing like:
* @code
* cxobj *xprev = NULL;
* cxobj *x = NULL;
* while ((x = xml_child_each(x_top, x, -1)) != NULL) {
* if (something){
* if (xml_purge(x) < 0)
* goto done;
* x = xprev;
* continue;
* }
* }
* @endcode
*/
cxobj *
xml_child_each(cxobj *xparent,

303
test/test_nacm_datanode.sh Executable file
View file

@ -0,0 +1,303 @@
#!/usr/bin/env bash
# Authentication and authorization and IETF NACM
# NACM data node rules
# See RFC 8341 A.4. Quote:
#
# Data node rules are used to control access to specific (config and
# non-config) data nodes within the NETCONF content provided by the
# server.
# This example shows four data node rules:
#
# deny-nacm: This rule denies the "guest" group any access to the
# /nacm subtree.
#
# permit-acme-config: This rule gives the "limited" group read-write
# access to the acme <config-parameters>.
#
# permit-dummy-interface: This rule gives the "limited" and "guest"
# groups read-update access to the acme <interface> entry named
# "dummy". This entry cannot be created or deleted by these groups;
# it can only be altered.
#
# permit-interface: This rule gives the "admin" group read-write
# access to all acme <interface> entries.
#
# Test as follows:
# 1. limit can read /nacm
# 2. guest cannot read /nacm
# 3. limited can read and set /config-parameters
# 4. guest cannot set /config-parametesr
# 5. guest|limit cannot POST dummy interface
# 6. admin can POST dummy interface
# 7. guest|limit can read and PUT dummy interface
# 8. guest|limit cannot DELETE dummy interface
# 9. admin can DELETE dummy interface
# Magic line must be first in script (see README.md)
s="$_" ; . ./lib.sh || if [ "$s" = $0 ]; then exit 0; else return 0; fi
APPNAME=example
# Common NACM scripts
. ./nacm.sh
cfg=$dir/conf_yang.xml
fyang=$dir/nacm-example.yang
fyang2=$dir/itf.yang
cat <<EOF > $cfg
<clixon-config xmlns="http://clicon.org/config">
<CLICON_CONFIGFILE>$cfg</CLICON_CONFIGFILE>
<CLICON_YANG_DIR>/usr/local/share/clixon</CLICON_YANG_DIR>
<CLICON_YANG_DIR>$IETFRFC</CLICON_YANG_DIR>
<CLICON_YANG_DIR>$dir</CLICON_YANG_DIR>
<CLICON_YANG_MAIN_FILE>$fyang</CLICON_YANG_MAIN_FILE>
<CLICON_CLISPEC_DIR>/usr/local/lib/$APPNAME/clispec</CLICON_CLISPEC_DIR>
<CLICON_RESTCONF_DIR>/usr/local/lib/$APPNAME/restconf</CLICON_RESTCONF_DIR>
<CLICON_CLI_DIR>/usr/local/lib/$APPNAME/cli</CLICON_CLI_DIR>
<CLICON_CLI_MODE>$APPNAME</CLICON_CLI_MODE>
<CLICON_SOCK>/usr/local/var/$APPNAME/$APPNAME.sock</CLICON_SOCK>
<CLICON_BACKEND_DIR>/usr/local/lib/$APPNAME/backend</CLICON_BACKEND_DIR>
<CLICON_BACKEND_PIDFILE>/usr/local/var/$APPNAME/$APPNAME.pidfile</CLICON_BACKEND_PIDFILE>
<CLICON_XMLDB_DIR>/usr/local/var/$APPNAME</CLICON_XMLDB_DIR>
<CLICON_RESTCONF_PRETTY>false</CLICON_RESTCONF_PRETTY>
<CLICON_NACM_MODE>internal</CLICON_NACM_MODE>
<CLICON_NACM_CREDENTIALS>none</CLICON_NACM_CREDENTIALS>
</clixon-config>
EOF
# There are two implicit modules defined by RFC 8341
# This is a try to define them
cat <<EOF > $fyang
module nacm-example{
yang-version 1.1;
namespace "http://example.com/ns/netconf";
prefix ex;
import ietf-netconf-acm {
prefix nacm;
}
import itf {
prefix acme;
}
container acme-netconf{
container config-parameters{
list parameter{
key name;
leaf name{
type string;
}
leaf value{
type string;
}
}
}
}
}
EOF
cat <<EOF > $fyang2
module itf{
yang-version 1.1;
namespace "http://example.com/ns/itf";
prefix acme;
container interfaces{
list interface{
key name;
leaf key{
type string;
}
leaf value{
type string;
}
}
}
}
EOF
# The groups are slightly modified from RFC8341 A.1 ($USER added in admin group)
# The rule-list is from A.4
# Note read-default is set to permit to ensure that deny-nacm is meaningful
RULES=$(cat <<EOF
<nacm xmlns="urn:ietf:params:xml:ns:yang:ietf-netconf-acm">
<enable-nacm>false</enable-nacm>
<read-default>permit</read-default>
<write-default>deny</write-default>
<exec-default>permit</exec-default>
$NGROUPS
<rule-list>
<name>guest-acl</name>
<group>guest</group>
<rule>
<name>deny-nacm</name>
<path xmlns:n="urn:ietf:params:xml:ns:yang:ietf-netconf-acm">
/n:nacm
</path>
<access-operations>*</access-operations>
<action>deny</action>
<comment>
Deny the 'guest' group any access to the /nacm data.
</comment>
</rule>
</rule-list>
<rule-list>
<name>limited-acl</name>
<group>limited</group>
<rule>
<name>permit-acme-config</name>
<path xmlns:acme="http://example.com/ns/netconf">
/acme:acme-netconf/acme:config-parameters
</path>
<access-operations>
read create update delete
</access-operations>
<action>permit</action>
<comment>
Allow the 'limited' group complete access to the acme
NETCONF configuration parameters. Showing long form
of 'access-operations' instead of shorthand.
</comment>
</rule>
</rule-list>
<rule-list>
<name>guest-limited-acl</name>
<group>guest</group>
<group>limited</group>
<rule>
<name>permit-dummy-interface</name>
<path xmlns:acme="http://example.com/ns/itf">
/acme:interfaces/acme:interface[acme:name='dummy']
</path>
<access-operations>read update</access-operations>
<action>permit</action>
<comment>
Allow the 'limited' and 'guest' groups read
and update access to the dummy interface.
</comment>
</rule>
</rule-list>
<rule-list>
<name>admin-acl</name>
<group>admin</group>
<rule>
<name>permit-interface</name>
<path xmlns:acme="http://example.com/ns/itf">
/acme:interfaces/acme:interface
</path>
<access-operations>*</access-operations>
<action>permit</action>
<comment>
Allow the 'admin' group full access to all acme interfaces.
</comment>
</rule>
</rule-list>
</nacm>
EOF
)
CONFIG=$(cat <<EOF
<acme-netconf xmlns="http://example.com/ns/netconf">
<config-parameters>
<parameter>
<name>a</name>
<value>72</value>
</parameter>
</config-parameters>
</acme-netconf>
EOF
)
new "test params: -f $cfg"
if [ $BE -ne 0 ]; then
new "kill old backend"
sudo clixon_backend -zf $cfg
if [ $? -ne 0 ]; then
err
fi
new "start backend -s init -f $cfg"
start_backend -s init -f $cfg
fi
new "waiting"
wait_backend
if [ $RC -ne 0 ]; then
new "kill old restconf daemon"
sudo pkill -u $wwwuser -f clixon_restconf
new "start restconf daemon (-a is enable basic authentication)"
start_restconf -f $cfg -- -a
new "waiting"
wait_restconf
fi
new "auth set authentication config"
expecteof "$clixon_netconf -qf $cfg" 0 "<rpc><edit-config><target><candidate/></target><config>$RULES</config></edit-config></rpc>]]>]]>" "^<rpc-reply><ok/></rpc-reply>]]>]]>$"
new "set app config"
expecteof "$clixon_netconf -qf $cfg" 0 "<rpc><edit-config><target><candidate/></target><config>$CONFIG</config></edit-config></rpc>]]>]]>" "^<rpc-reply><ok/></rpc-reply>]]>]]>$"
new "commit it"
expecteof "$clixon_netconf -qf $cfg" 0 "<rpc><commit/></rpc>]]>]]>" "^<rpc-reply><ok/></rpc-reply>]]>]]>$"
new "enable nacm"
expectpart "$(curl -u andy:bar -siS -X PUT -H "Content-Type: application/yang-data+json" -d '{"ietf-netconf-acm:enable-nacm": true}' http://localhost/restconf/data/ietf-netconf-acm:nacm/enable-nacm)" 0 "HTTP/1.1 204 No Content"
#--------------- nacm enabled
#user:admin,wilma,guest
new "admin can read /nacm"
expectpart "$(curl -u andy:bar -siS -X GET http://localhost/restconf/data/ietf-netconf-acm:nacm/enable-nacm)" 0 'HTTP/1.1 200 OK' '{"ietf-netconf-acm:enable-nacm":true}'
new "1. limit can read /nacm"
expectpart "$(curl -u wilma:bar -siS -X GET http://localhost/restconf/data/ietf-netconf-acm:nacm/enable-nacm)" 0 'HTTP/1.1 200 OK' '{"ietf-netconf-acm:enable-nacm":true}'
# Comment: it should NOT be access-denied, it should be not found, since then you say it exists but you
# dont have access to it which is insecure
new "2. guest cannot read /nacm"
expectpart "$(curl -u guest:bar -siS -X GET http://localhost/restconf/data/ietf-netconf-acm:nacm/enable-nacm)" 0 'HTTP/1.1 404 Not Found' '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"invalid-value","error-severity":"error","error-message":"Instance does not exist"}}}'
# 3. limited can read and set /config-parameters
new "3. limited can read config-parameters"
expectpart "$(curl -u wilma:bar -siS -X GET http://localhost/restconf/data/nacm-example:acme-netconf/config-parameters)" 0 'HTTP/1.1 200 OK' '{"nacm-example:config-parameters":{"parameter":\[{"name":"a","value":"72"}\]}}'
if false; then # notyet
new "3. limited can set config-parameters"
expectpart "$(curl -u wilma:bar -siS -X PUT -H "Content-Type: application/yang-data+json" http://localhost/restconf/data/nacm-example:acme-netconf/config-parameters/parameter=a -d '{"nacm-example:parameter":[{"name":"a","value":"93"}]}')" 0 'HTTP/1.1 200 OK'
fi
new "4. guest cannot set /config-parameter"
expectpart "$(curl -u wilma:bar -siS -X PUT -H "Content-Type: application/yang-data+json" http://localhost/restconf/data/nacm-example:acme-netconf/config-parameters/parameter=a -d '{"nacm-example:parameter":[{"name":"a","value":"93"}]}')" 0 'HTTP/1.1 403 Forbidden' '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"access-denied","error-severity":"error","error-message":"default deny"}}}'
# 5. guest|limit cannot POST dummy interface
# 6. admin can POST dummy interface
# 7. guest|limit can read and PUT dummy interface
# 8. guest|limit cannot DELETE dummy interface
# 9. admin can DELETE dummy interface
if [ $RC -ne 0 ]; then
new "Kill restconf daemon"
stop_restconf
fi
if [ $BE -eq 0 ]; then
exit # BE
fi
new "Kill backend"
# Check if premature kill
pid=$(pgrep -u root -f clixon_backend)
if [ -z "$pid" ]; then
err "backend already dead"
fi
# kill backend
stop_backend -f $cfg
rm -rf $dir

300
test/test_nacm_datanode_read.sh Executable file
View file

@ -0,0 +1,300 @@
#!/usr/bin/env bash
# Authentication and authorization and IETF NACM
# NACM data node rules
# The RFC 8341 examples in the appendix are very limited.
# Here focus on datanode paths from a read perspective.
# The limit user is used for this
# The following shows a tree of the paths where permit/deny actions can be set:
# read-default
# module
# /table
# /table/parameters/parameter
#
# The test goes through all permutations and checks the expected behaviour
#
# Magic line must be first in script (see README.md)
s="$_" ; . ./lib.sh || if [ "$s" = $0 ]; then exit 0; else return 0; fi
APPNAME=example
# Common NACM scripts
. ./nacm.sh
cfg=$dir/conf_yang.xml
fyang=$dir/nacm-example.yang
fyang2=$dir/nacm-example2.yang
cat <<EOF > $cfg
<clixon-config xmlns="http://clicon.org/config">
<CLICON_CONFIGFILE>$cfg</CLICON_CONFIGFILE>
<CLICON_YANG_DIR>/usr/local/share/clixon</CLICON_YANG_DIR>
<CLICON_YANG_DIR>$IETFRFC</CLICON_YANG_DIR>
<CLICON_YANG_DIR>$dir</CLICON_YANG_DIR>
<CLICON_YANG_MAIN_FILE>$fyang</CLICON_YANG_MAIN_FILE>
<CLICON_CLISPEC_DIR>/usr/local/lib/$APPNAME/clispec</CLICON_CLISPEC_DIR>
<CLICON_RESTCONF_DIR>/usr/local/lib/$APPNAME/restconf</CLICON_RESTCONF_DIR>
<CLICON_CLI_DIR>/usr/local/lib/$APPNAME/cli</CLICON_CLI_DIR>
<CLICON_CLI_MODE>$APPNAME</CLICON_CLI_MODE>
<CLICON_SOCK>/usr/local/var/$APPNAME/$APPNAME.sock</CLICON_SOCK>
<CLICON_BACKEND_DIR>/usr/local/lib/$APPNAME/backend</CLICON_BACKEND_DIR>
<CLICON_BACKEND_PIDFILE>/usr/local/var/$APPNAME/$APPNAME.pidfile</CLICON_BACKEND_PIDFILE>
<CLICON_XMLDB_DIR>/usr/local/var/$APPNAME</CLICON_XMLDB_DIR>
<CLICON_RESTCONF_PRETTY>false</CLICON_RESTCONF_PRETTY>
<CLICON_NACM_MODE>internal</CLICON_NACM_MODE>
<CLICON_NACM_CREDENTIALS>none</CLICON_NACM_CREDENTIALS>
</clixon-config>
EOF
cat <<EOF > $fyang
module nacm-example{
yang-version 1.1;
namespace "urn:example:nacm";
prefix ex;
import ietf-netconf-acm {
prefix nacm;
}
import nacm-example2 {
prefix ex2;
}
container table{
container parameters{
list parameter{
key name;
leaf name{
type string;
}
leaf value{
type string;
}
}
}
}
container other{
leaf value{
type string;
}
}
}
EOF
cat <<EOF > $fyang2
module nacm-example2{
yang-version 1.1;
namespace "urn:example:nacm2";
prefix ex2;
container other2{
leaf value{
type string;
}
}
}
EOF
RULES=$(cat <<EOF
<nacm xmlns="urn:ietf:params:xml:ns:yang:ietf-netconf-acm">
<enable-nacm>false</enable-nacm>
<read-default>deny</read-default>
<write-default>deny</write-default>
<exec-default>permit</exec-default>
$NGROUPS
<rule-list>
<name>limited-acl</name>
<group>limited</group>
<rule>
<name>parameter</name>
<module-name>*</module-name>
<access-operations>read</access-operations>
<path xmlns:ex="urn:example:nacm">/ex:table/ex:parameters/ex:parameter</path>
<action>permit</action>
</rule>
<rule>
<name>table</name>
<module-name>*</module-name>
<access-operations>read</access-operations>
<path xmlns:ex="urn:example:nacm">/ex:table</path>
<action>permit</action>
</rule>
<rule>
<name>module</name>
<module-name>nacm-example</module-name>
<access-operations>read</access-operations>
<action>permit</action>
</rule>
</rule-list>
$NADMIN
</nacm>
EOF
)
CONFIG=$(cat <<EOF
<table xmlns="urn:example:nacm">
<parameters>
<parameter>
<name>a</name>
<value>72</value>
</parameter>
</parameters>
</table>
<other xmlns="urn:example:nacm">
<value>99</value>
</other>
<other2 xmlns="urn:example:nacm2">
<value>88</value>
</other2>
EOF
)
#
# Arguments, permit/deny on different levels:
# Configs (permit/deny):
# - read-default
# - module as a whole
# - table: root symbol
# - param: sub symbol
# Tests, epxect true/false:
# - read other module
# - read other in same module
# - read table
# - read parameter
testrun(){
readdefault=$1
module=$2
table=$3
parameter=$4
test1=$5
test2=$6
test3=$7
test4=$8
new "read-default:$readdefault module:$module table:$table parameter:$parameter"
new "set read-default $readdefault"
expectpart "$(curl -u andy:bar -siS -X PUT -H "Content-Type: application/yang-data+json" http://localhost/restconf/data/ietf-netconf-acm:nacm/read-default -d "{\"ietf-netconf-acm:read-default\":\"$readdefault\"}" )" 0 "HTTP/1.1 204 No Content"
new "set module rule $module"
expectpart "$(curl -u andy:bar -siS -X PUT -H "Content-Type: application/yang-data+json" http://localhost/restconf/data/ietf-netconf-acm:nacm/rule-list=limited-acl/rule=module/action -d "{\"ietf-netconf-acm:action\":\"$module\"}" )" 0 "HTTP/1.1 204 No Content"
new "set table rule $table"
expectpart "$(curl -u andy:bar -siS -X PUT -H "Content-Type: application/yang-data+json" http://localhost/restconf/data/ietf-netconf-acm:nacm/rule-list=limited-acl/rule=table/action -d "{\"ietf-netconf-acm:action\":\"$table\"}" )" 0 "HTTP/1.1 204 No Content"
new "set parameter rule $parameter"
expectpart "$(curl -u andy:bar -siS -X PUT -H "Content-Type: application/yang-data+json" http://localhost/restconf/data/ietf-netconf-acm:nacm/rule-list=limited-acl/rule=parameter/action -d "{\"ietf-netconf-acm:action\":\"$parameter\"}" )" 0 "HTTP/1.1 204 No Content"
#--------------- Here check
new "get other module"
if $test1; then
expectpart "$(curl -u wilma:bar -siS -X GET http://localhost/restconf/data/nacm-example2:other2/value)" 0 'HTTP/1.1 200 OK' '{"nacm-example2:value":"88"}'
else
expectpart "$(curl -u wilma:bar -siS -X GET http://localhost/restconf/data/nacm-example2:other2/value)" 0 'HTTP/1.1 404 Not Found' '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"invalid-value","error-severity":"error","error-message":"Instance does not exist"}}}'
fi
new "get other in same module"
if $test2; then
expectpart "$(curl -u wilma:bar -siS -X GET http://localhost/restconf/data/nacm-example:other/value)" 0 'HTTP/1.1 200 OK' '{"nacm-example:value":"99"}'
else
expectpart "$(curl -u wilma:bar -siS -X GET http://localhost/restconf/data/nacm-example:other/value)" 0 'HTTP/1.1 404 Not Found' '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"invalid-value","error-severity":"error","error-message":"Instance does not exist"}}}'
fi
new "get table"
if $test3; then
expectpart "$(curl -u wilma:bar -siS -X GET http://localhost/restconf/data/nacm-example:table?depth=1)" 0 'HTTP/1.1 200 OK' '{"nacm-example:table":{}}'
else
expectpart "$(curl -u wilma:bar -siS -X GET http://localhost/restconf/data/nacm-example:table?depth=1)" 0 'HTTP/1.1 404 Not Found' '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"invalid-value","error-severity":"error","error-message":"Instance does not exist"}}}'
fi
new "get parameter"
if $test4; then
expectpart "$(curl -u wilma:bar -siS -X GET http://localhost/restconf/data/nacm-example:table/parameters/parameter=a)" 0 'HTTP/1.1 200 OK' '{"nacm-example:parameter":\[{"name":"a","value":"72"}\]}'
else
expectpart "$(curl -u wilma:bar -siS -X GET http://localhost/restconf/data/nacm-example:table/parameters/parameter=a)" 0 'HTTP/1.1 404 Not Found' '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"invalid-value","error-severity":"error","error-message":"Instance does not exist"}}}'
fi
} # testrun
new "test params: -f $cfg"
if [ $BE -ne 0 ]; then
new "kill old backend"
sudo clixon_backend -zf $cfg
if [ $? -ne 0 ]; then
err
fi
new "start backend -s init -f $cfg"
start_backend -s init -f $cfg
fi
new "waiting"
wait_backend
if [ $RC -ne 0 ]; then
new "kill old restconf daemon"
sudo pkill -u $wwwuser -f clixon_restconf
new "start restconf daemon (-a is enable basic authentication)"
start_restconf -f $cfg -- -a
new "waiting"
wait_restconf
fi
new "auth set authentication config"
expecteof "$clixon_netconf -qf $cfg" 0 "<rpc><edit-config><target><candidate/></target><config>$RULES</config></edit-config></rpc>]]>]]>" "^<rpc-reply><ok/></rpc-reply>]]>]]>$"
new "set app config"
expecteof "$clixon_netconf -qf $cfg" 0 "<rpc><edit-config><target><candidate/></target><config>$CONFIG</config></edit-config></rpc>]]>]]>" "^<rpc-reply><ok/></rpc-reply>]]>]]>$"
new "commit it"
expecteof "$clixon_netconf -qf $cfg" 0 "<rpc><commit/></rpc>]]>]]>" "^<rpc-reply><ok/></rpc-reply>]]>]]>$"
new "enable nacm"
expectpart "$(curl -u andy:bar -siS -X PUT -H "Content-Type: application/yang-data+json" -d '{"ietf-netconf-acm:enable-nacm": true}' http://localhost/restconf/data/ietf-netconf-acm:nacm/enable-nacm)" 0 "HTTP/1.1 204 No Content"
#--------------- nacm enabled
# config: def module table parameter
# test: mod other table parameter
# default deny
testrun deny deny deny deny false false false false
testrun deny deny deny permit false false false false
testrun deny deny permit deny false false true false
testrun deny deny permit permit false false true true
testrun deny permit deny deny false true false false
testrun deny permit deny permit false true false false
testrun deny permit permit deny false true true false
testrun deny permit permit permit false true true true
# default permit
testrun permit deny deny deny true false false false
testrun permit deny deny permit true false false false
testrun permit deny permit deny true false true false
testrun permit deny permit permit true false true true
testrun permit permit deny deny true true false false
testrun permit permit deny permit true true false false
testrun permit permit permit deny true true true false
testrun permit permit permit permit true true true true
if [ $RC -ne 0 ]; then
new "Kill restconf daemon"
stop_restconf
fi
if [ $BE -ne 0 ]; then # Bring your own backend
new "Kill backend"
# Check if premature kill
pid=$(pgrep -u root -f clixon_backend)
if [ -z "$pid" ]; then
err "backend already dead"
fi
# kill backend
stop_backend -f $cfg
fi
rm -rf $dir