NACM Data node WRITE access module support (RFC8341 3.4.5)
This commit is contained in:
parent
342b94fb76
commit
ef86cda2fe
11 changed files with 687 additions and 88 deletions
|
|
@ -77,12 +77,10 @@
|
||||||
* CLICON_YANG_MAIN_FILE Provides a filename with a single module filename.
|
* CLICON_YANG_MAIN_FILE Provides a filename with a single module filename.
|
||||||
* CLICON_YANG_MAIN_DIR Provides a directory where all yang modules should be loaded.
|
* CLICON_YANG_MAIN_DIR Provides a directory where all yang modules should be loaded.
|
||||||
* NACM extension (RFC8341)
|
* NACM extension (RFC8341)
|
||||||
* NACM Data node READ access module support (RFC8341 3.4.5)
|
* NACM Data node READ and WRITE access module support (RFC8341 3.4.5)
|
||||||
* Access control points added for `get` and `get-config` in addition to incoming rpc.
|
* Access control points added for `get` and `get-config` in addition to incoming rpc.
|
||||||
* RFC 8341 Example A.2 implemented, see: [test/test_nacm_module.sh]
|
* RFC 8341 Example A.2 implemented, see: [test/test_nacm_module.sh]
|
||||||
* Remaining work:
|
* Remaining work: data-node PATH
|
||||||
* data-node module write/create/delete/update
|
|
||||||
* data-node path
|
|
||||||
* Recovery user "_nacm_recovery" added.
|
* Recovery user "_nacm_recovery" added.
|
||||||
* Example use is restconf PUT when NACM edit-config is permitted, then automatic commit and discard are permitted using recovery user.
|
* Example use is restconf PUT when NACM edit-config is permitted, then automatic commit and discard are permitted using recovery user.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -214,7 +214,7 @@ from_client_get_config(clicon_handle h,
|
||||||
goto ok;
|
goto ok;
|
||||||
}
|
}
|
||||||
/* Pre-NACM access step */
|
/* Pre-NACM access step */
|
||||||
if ((ret = nacm_access(h, username, &xnacm)) < 0)
|
if ((ret = nacm_access_h(h, username, &xnacm)) < 0)
|
||||||
goto done;
|
goto done;
|
||||||
if (ret == 0){ /* Do NACM validation */
|
if (ret == 0){ /* Do NACM validation */
|
||||||
if (xpath_vec(xret, "%s", &xvec, &xlen, xpath?xpath:"/") < 0)
|
if (xpath_vec(xret, "%s", &xvec, &xlen, xpath?xpath:"/") < 0)
|
||||||
|
|
@ -407,7 +407,7 @@ from_client_get(clicon_handle h,
|
||||||
goto ok;
|
goto ok;
|
||||||
}
|
}
|
||||||
/* Pre-NACM access step */
|
/* Pre-NACM access step */
|
||||||
if ((ret = nacm_access(h, username, &xnacm)) < 0)
|
if ((ret = nacm_access_h(h, username, &xnacm)) < 0)
|
||||||
goto done;
|
goto done;
|
||||||
if (ret == 0){ /* Do NACM validation */
|
if (ret == 0){ /* Do NACM validation */
|
||||||
if (xpath_vec(xret, "%s", &xvec, &xlen, xpath?xpath:"/") < 0)
|
if (xpath_vec(xret, "%s", &xvec, &xlen, xpath?xpath:"/") < 0)
|
||||||
|
|
@ -1052,7 +1052,7 @@ from_client_msg(clicon_handle h,
|
||||||
clicon_debug(1, "%s module:%s rpc:%s", __FUNCTION__, module, rpc);
|
clicon_debug(1, "%s module:%s rpc:%s", __FUNCTION__, module, rpc);
|
||||||
/* Pre-NACM access step */
|
/* Pre-NACM access step */
|
||||||
xnacm = NULL;
|
xnacm = NULL;
|
||||||
if ((ret = nacm_access(h, username, &xnacm)) < 0)
|
if ((ret = nacm_access_h(h, username, &xnacm)) < 0)
|
||||||
goto done;
|
goto done;
|
||||||
if (ret == 0){ /* Do NACM validation */
|
if (ret == 0){ /* Do NACM validation */
|
||||||
/* NACM rpc operation exec validation */
|
/* NACM rpc operation exec validation */
|
||||||
|
|
|
||||||
|
|
@ -212,6 +212,7 @@ yang2cli_var_sub(clicon_handle h,
|
||||||
if (strcmp(type, "enumeration") == 0 || strcmp(type, "bits") == 0){
|
if (strcmp(type, "enumeration") == 0 || strcmp(type, "bits") == 0){
|
||||||
cprintf(cb, " choice:");
|
cprintf(cb, " choice:");
|
||||||
i = 0;
|
i = 0;
|
||||||
|
yi = NULL;
|
||||||
while ((yi = yn_each((yang_node*)ytype, yi)) != NULL){
|
while ((yi = yn_each((yang_node*)ytype, yi)) != NULL){
|
||||||
if (yi->ys_keyword != Y_ENUM && yi->ys_keyword != Y_BIT)
|
if (yi->ys_keyword != Y_ENUM && yi->ys_keyword != Y_BIT)
|
||||||
continue;
|
continue;
|
||||||
|
|
|
||||||
|
|
@ -615,6 +615,8 @@ text_modify(struct text_handle *th,
|
||||||
cxobj *x0p,
|
cxobj *x0p,
|
||||||
cxobj *x1,
|
cxobj *x1,
|
||||||
enum operation_type op,
|
enum operation_type op,
|
||||||
|
char *username,
|
||||||
|
cxobj *xnacm,
|
||||||
cbuf *cbret)
|
cbuf *cbret)
|
||||||
{
|
{
|
||||||
int retval = -1;
|
int retval = -1;
|
||||||
|
|
@ -626,6 +628,7 @@ text_modify(struct text_handle *th,
|
||||||
cxobj *x0c; /* base child */
|
cxobj *x0c; /* base child */
|
||||||
cxobj *x0b; /* base body */
|
cxobj *x0b; /* base body */
|
||||||
cxobj *x1c; /* mod child */
|
cxobj *x1c; /* mod child */
|
||||||
|
char *x0bstr; /* mod body string */
|
||||||
char *x1bstr; /* mod body string */
|
char *x1bstr; /* mod body string */
|
||||||
yang_stmt *yc; /* yang child */
|
yang_stmt *yc; /* yang child */
|
||||||
cxobj **x0vec = NULL;
|
cxobj **x0vec = NULL;
|
||||||
|
|
@ -679,12 +682,33 @@ text_modify(struct text_handle *th,
|
||||||
}
|
}
|
||||||
if (x1bstr){
|
if (x1bstr){
|
||||||
if ((x0b = xml_body_get(x0)) == NULL){
|
if ((x0b = xml_body_get(x0)) == NULL){
|
||||||
|
if (xnacm){
|
||||||
|
if ((ret = nacm_datanode_write(NULL, x0, NACM_CREATE, username, xnacm, cbret)) < 0)
|
||||||
|
goto done;
|
||||||
|
if (ret == 0)
|
||||||
|
goto fail;
|
||||||
|
}
|
||||||
if ((x0b = xml_new("body", x0, NULL)) == NULL)
|
if ((x0b = xml_new("body", x0, NULL)) == NULL)
|
||||||
goto done;
|
goto done;
|
||||||
xml_type_set(x0b, CX_BODY);
|
xml_type_set(x0b, CX_BODY);
|
||||||
|
|
||||||
|
if (xml_value_set(x0b, x1bstr) < 0)
|
||||||
|
goto done;
|
||||||
}
|
}
|
||||||
if (xml_value_set(x0b, x1bstr) < 0)
|
else{
|
||||||
goto done;
|
x0bstr = xml_value(x0b);
|
||||||
|
if (x0bstr==NULL || strcmp(x0bstr, x1bstr)){
|
||||||
|
if (xnacm){
|
||||||
|
if ((ret = nacm_datanode_write(NULL, x0, NACM_UPDATE, username, xnacm, cbret)) < 0)
|
||||||
|
goto done;
|
||||||
|
if (ret == 0)
|
||||||
|
goto fail;
|
||||||
|
}
|
||||||
|
if (xml_value_set(x0b, x1bstr) < 0)
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case OP_DELETE:
|
case OP_DELETE:
|
||||||
|
|
@ -784,7 +808,8 @@ text_modify(struct text_handle *th,
|
||||||
x1cname = xml_name(x1c);
|
x1cname = xml_name(x1c);
|
||||||
x0c = x0vec[i++];
|
x0c = x0vec[i++];
|
||||||
yc = yang_find_datanode(y0, x1cname);
|
yc = yang_find_datanode(y0, x1cname);
|
||||||
if ((ret = text_modify(th, x0c, (yang_node*)yc, x0, x1c, op, cbret)) < 0)
|
if ((ret = text_modify(th, x0c, (yang_node*)yc, x0, x1c, op,
|
||||||
|
username, xnacm, cbret)) < 0)
|
||||||
goto done;
|
goto done;
|
||||||
/* If xml return - ie netconf error xml tree, then stop and return OK */
|
/* If xml return - ie netconf error xml tree, then stop and return OK */
|
||||||
if (ret == 0)
|
if (ret == 0)
|
||||||
|
|
@ -834,6 +859,8 @@ text_modify_top(struct text_handle *th,
|
||||||
cxobj *x1,
|
cxobj *x1,
|
||||||
yang_spec *yspec,
|
yang_spec *yspec,
|
||||||
enum operation_type op,
|
enum operation_type op,
|
||||||
|
char *username,
|
||||||
|
cxobj *xnacm,
|
||||||
cbuf *cbret)
|
cbuf *cbret)
|
||||||
{
|
{
|
||||||
int retval = -1;
|
int retval = -1;
|
||||||
|
|
@ -860,6 +887,10 @@ text_modify_top(struct text_handle *th,
|
||||||
case OP_DELETE:
|
case OP_DELETE:
|
||||||
case OP_REMOVE:
|
case OP_REMOVE:
|
||||||
case OP_REPLACE:
|
case OP_REPLACE:
|
||||||
|
if ((ret = nacm_datanode_write(NULL, x0, NACM_DELETE, username, xnacm, cbret)) < 0) /* XXX */
|
||||||
|
goto done;
|
||||||
|
if (ret == 0)
|
||||||
|
goto fail;
|
||||||
x0c = NULL;
|
x0c = NULL;
|
||||||
while ((x0c = xml_child_each(x0, x0c, CX_ELMNT)) != NULL)
|
while ((x0c = xml_child_each(x0, x0c, CX_ELMNT)) != NULL)
|
||||||
xml_purge(x0c);
|
xml_purge(x0c);
|
||||||
|
|
@ -914,7 +945,8 @@ text_modify_top(struct text_handle *th,
|
||||||
x0c = NULL;
|
x0c = NULL;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
if ((ret = text_modify(th, x0c, (yang_node*)yc, x0, x1c, op, cbret)) < 0)
|
if ((ret = text_modify(th, x0c, (yang_node*)yc, x0, x1c, op,
|
||||||
|
username,xnacm, cbret)) < 0)
|
||||||
goto done;
|
goto done;
|
||||||
/* If xml return - ie netconf error xml tree, then stop and return OK */
|
/* If xml return - ie netconf error xml tree, then stop and return OK */
|
||||||
if (ret == 0)
|
if (ret == 0)
|
||||||
|
|
@ -997,6 +1029,7 @@ text_put(xmldb_handle xh,
|
||||||
cxobj *x0 = NULL;
|
cxobj *x0 = NULL;
|
||||||
struct db_element *de = NULL;
|
struct db_element *de = NULL;
|
||||||
int ret;
|
int ret;
|
||||||
|
cxobj *xnacm = NULL;
|
||||||
|
|
||||||
if (cbret == NULL){
|
if (cbret == NULL){
|
||||||
clicon_err(OE_XML, EINVAL, "cbret is NULL");
|
clicon_err(OE_XML, EINVAL, "cbret is NULL");
|
||||||
|
|
@ -1056,12 +1089,34 @@ text_put(xmldb_handle xh,
|
||||||
#if 0 /* debug */
|
#if 0 /* debug */
|
||||||
if (xml_apply0(x1, -1, xml_sort_verify, NULL) < 0)
|
if (xml_apply0(x1, -1, xml_sort_verify, NULL) < 0)
|
||||||
clicon_log(LOG_NOTICE, "%s: verify failed #1", __FUNCTION__);
|
clicon_log(LOG_NOTICE, "%s: verify failed #1", __FUNCTION__);
|
||||||
|
#endif
|
||||||
|
#if 1
|
||||||
|
{
|
||||||
|
char *mode;
|
||||||
|
cxobj *xnacm0 = NULL;
|
||||||
|
|
||||||
|
mode = th->th_nacm_mode;
|
||||||
|
if (mode){
|
||||||
|
if (strcmp(mode, "external")==0)
|
||||||
|
xnacm0 = th->th_nacm_xtree;
|
||||||
|
else if (strcmp(mode, "internal")==0)
|
||||||
|
xnacm0 = x0;
|
||||||
|
}
|
||||||
|
if (xnacm0 != NULL &&
|
||||||
|
(xnacm = xpath_first(xnacm0, "nacm")) != NULL){
|
||||||
|
/* Pre-NACM access step */
|
||||||
|
if ((ret = nacm_access(mode, xnacm, username)) < 0)
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
/* Here assume if xnacm is set (actually may be ret==0?) do NACM */
|
||||||
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
/*
|
/*
|
||||||
* Modify base tree x with modification x1. This is where the
|
* Modify base tree x with modification x1. This is where the
|
||||||
* new tree is made.
|
* new tree is made.
|
||||||
*/
|
*/
|
||||||
if ((ret = text_modify_top(th, x0, x1, yspec, op, cbret)) < 0)
|
if ((ret = text_modify_top(th, x0, x1, yspec, op, username, xnacm, cbret)) < 0)
|
||||||
goto done;
|
goto done;
|
||||||
/* If xml return - ie netconf error xml tree, then stop and return OK */
|
/* If xml return - ie netconf error xml tree, then stop and return OK */
|
||||||
if (ret == 0)
|
if (ret == 0)
|
||||||
|
|
|
||||||
|
|
@ -45,11 +45,29 @@
|
||||||
*/
|
*/
|
||||||
#define NACM_RECOVERY_USER "_nacm_recovery"
|
#define NACM_RECOVERY_USER "_nacm_recovery"
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Types
|
||||||
|
*/
|
||||||
|
/* NACM access rights,
|
||||||
|
* Note that these are not the same as netconf operations
|
||||||
|
* @see rfc8341 3.2.2
|
||||||
|
* @see enum operation_type Netconf operations
|
||||||
|
*/
|
||||||
|
enum nacm_access{
|
||||||
|
NACM_CREATE,
|
||||||
|
NACM_READ,
|
||||||
|
NACM_UPDATE,
|
||||||
|
NACM_DELETE,
|
||||||
|
NACM_EXEC
|
||||||
|
};
|
||||||
/*
|
/*
|
||||||
* Prototypes
|
* Prototypes
|
||||||
*/
|
*/
|
||||||
int nacm_rpc(char *rpc, char *module, char *username, cxobj *xnacm, cbuf *cbret);
|
int nacm_rpc(char *rpc, char *module, char *username, cxobj *xnacm, cbuf *cbret);
|
||||||
int nacm_datanode_read(cxobj *xt, cxobj **xvec, size_t xlen, char *username, cxobj *nacm_xtree);
|
int nacm_datanode_read(cxobj *xt, cxobj **xvec, size_t xlen, char *username, cxobj *nacm_xtree);
|
||||||
int nacm_access(clicon_handle h, char *username, cxobj **xnacmp);
|
int nacm_datanode_write(cxobj *xt, cxobj *xr, enum nacm_access access,
|
||||||
|
char *username, cxobj *xnacm, cbuf *cbret);
|
||||||
|
int nacm_access_h(clicon_handle h, char *username, cxobj **xnacmp);
|
||||||
|
int nacm_access(char *mode, cxobj *xnacmin, char *username);
|
||||||
|
|
||||||
#endif /* _CLIXON_NACM_H */
|
#endif /* _CLIXON_NACM_H */
|
||||||
|
|
|
||||||
|
|
@ -72,13 +72,16 @@
|
||||||
* Incoming RPC Message Validation Step 7 (c)
|
* Incoming RPC Message Validation Step 7 (c)
|
||||||
* The rule's "access-operations" leaf has the "exec" bit set or
|
* The rule's "access-operations" leaf has the "exec" bit set or
|
||||||
* has the special value "*".
|
* has the special value "*".
|
||||||
|
* @param[in] mode Primary mode, eg read, create, update, delete, exec
|
||||||
|
* @param[in] mode2 Secondary mode, eg "write"
|
||||||
* @retval 0 No match
|
* @retval 0 No match
|
||||||
* @retval 1 Match
|
* @retval 1 Match
|
||||||
* @note access_operations is bit-fields
|
* @note access_operations is bit-fields
|
||||||
*/
|
*/
|
||||||
static int
|
static int
|
||||||
nacm_match_access(char *access_operations,
|
match_access(char *access_operations,
|
||||||
char *mode)
|
char *mode,
|
||||||
|
char *mode2)
|
||||||
{
|
{
|
||||||
if (access_operations==NULL)
|
if (access_operations==NULL)
|
||||||
return 0;
|
return 0;
|
||||||
|
|
@ -86,6 +89,8 @@ nacm_match_access(char *access_operations,
|
||||||
return 1;
|
return 1;
|
||||||
if (strstr(access_operations, mode)!=NULL)
|
if (strstr(access_operations, mode)!=NULL)
|
||||||
return 1;
|
return 1;
|
||||||
|
if (mode2 && strstr(access_operations, mode2)!=NULL)
|
||||||
|
return 1;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -146,7 +151,7 @@ nacm_rule_rpc(char *rpc,
|
||||||
module_rule, rpc_rule, access_operations, action);
|
module_rule, rpc_rule, access_operations, action);
|
||||||
if (module_rule &&
|
if (module_rule &&
|
||||||
(strcmp(module_rule,"*")==0 || strcmp(module_rule,module)==0)){
|
(strcmp(module_rule,"*")==0 || strcmp(module_rule,module)==0)){
|
||||||
if (nacm_match_access(access_operations, "exec")){
|
if (match_access(access_operations, "exec", NULL)){
|
||||||
if (rpc_rule==NULL ||
|
if (rpc_rule==NULL ||
|
||||||
strcmp(rpc_rule, "*")==0 || strcmp(rpc_rule, rpc)==0){
|
strcmp(rpc_rule, "*")==0 || strcmp(rpc_rule, rpc)==0){
|
||||||
/* Here is a matching rule */
|
/* Here is a matching rule */
|
||||||
|
|
@ -171,13 +176,15 @@ nacm_rule_rpc(char *rpc,
|
||||||
/*! Process nacm incoming RPC message validation steps
|
/*! Process nacm incoming RPC message validation steps
|
||||||
* @param[in] module Yang module name
|
* @param[in] module Yang module name
|
||||||
* @param[in] rpc rpc name
|
* @param[in] rpc rpc name
|
||||||
* @param[in] username User name making access
|
* @param[in] username User name of requestor
|
||||||
* @param[in] xnacm NACM xml tree
|
* @param[in] xnacm NACM xml tree
|
||||||
* @param[out] cbret Cligen buffer result. Set to an error msg if retval=0.
|
* @param[out] cbret Cligen buffer result. Set to an error msg if retval=0.
|
||||||
* @retval -1 Error
|
* @retval -1 Error
|
||||||
* @retval 0 Not access and cbret set
|
* @retval 0 Not access and cbret set
|
||||||
* @retval 1 Access
|
* @retval 1 Access
|
||||||
* @see RFC8341 3.4.4. Incoming RPC Message Validation
|
* @see RFC8341 3.4.4. Incoming RPC Message Validation
|
||||||
|
* @see nacm_datanode_write
|
||||||
|
* @see nacm_datanode_read
|
||||||
*/
|
*/
|
||||||
int
|
int
|
||||||
nacm_rpc(char *rpc,
|
nacm_rpc(char *rpc,
|
||||||
|
|
@ -190,7 +197,7 @@ nacm_rpc(char *rpc,
|
||||||
cxobj *xrule;
|
cxobj *xrule;
|
||||||
cxobj **gvec = NULL; /* groups */
|
cxobj **gvec = NULL; /* groups */
|
||||||
size_t glen;
|
size_t glen;
|
||||||
cxobj *xrlist;
|
cxobj *rlist;
|
||||||
cxobj **rlistvec = NULL; /* rule-list */
|
cxobj **rlistvec = NULL; /* rule-list */
|
||||||
size_t rlistlen;
|
size_t rlistlen;
|
||||||
cxobj **rvec = NULL; /* rules */
|
cxobj **rvec = NULL; /* rules */
|
||||||
|
|
@ -198,6 +205,7 @@ nacm_rpc(char *rpc,
|
||||||
int ret;
|
int ret;
|
||||||
int i, j;
|
int i, j;
|
||||||
char *exec_default = NULL;
|
char *exec_default = NULL;
|
||||||
|
char *gname;
|
||||||
|
|
||||||
/* 3. If the requested operation is the NETCONF <close-session>
|
/* 3. If the requested operation is the NETCONF <close-session>
|
||||||
protocol operation, then the protocol operation is permitted.
|
protocol operation, then the protocol operation is permitted.
|
||||||
|
|
@ -224,12 +232,11 @@ nacm_rpc(char *rpc,
|
||||||
if (xpath_vec(xnacm, "rule-list", &rlistvec, &rlistlen) < 0)
|
if (xpath_vec(xnacm, "rule-list", &rlistvec, &rlistlen) < 0)
|
||||||
goto done;
|
goto done;
|
||||||
for (i=0; i<rlistlen; i++){
|
for (i=0; i<rlistlen; i++){
|
||||||
xrlist = rlistvec[i];
|
rlist = rlistvec[i];
|
||||||
/* Loop through user's group to find match in this rule-list */
|
/* Loop through user's group to find match in this rule-list */
|
||||||
for (j=0; j<glen; j++){
|
for (j=0; j<glen; j++){
|
||||||
char *gname;
|
|
||||||
gname = xml_find_body(gvec[j], "name");
|
gname = xml_find_body(gvec[j], "name");
|
||||||
if (xpath_first(xrlist, ".[group='%s']", gname)!=NULL)
|
if (xpath_first(rlist, ".[group='%s']", gname)!=NULL)
|
||||||
break; /* found */
|
break; /* found */
|
||||||
}
|
}
|
||||||
if (j==glen) /* not found */
|
if (j==glen) /* not found */
|
||||||
|
|
@ -238,7 +245,7 @@ nacm_rpc(char *rpc,
|
||||||
until a rule that matches the requested access operation is
|
until a rule that matches the requested access operation is
|
||||||
found.
|
found.
|
||||||
*/
|
*/
|
||||||
if (xpath_vec(xrlist, "rule", &rvec, &rlen) < 0)
|
if (xpath_vec(rlist, "rule", &rvec, &rlen) < 0)
|
||||||
goto done;
|
goto done;
|
||||||
for (j=0; j<rlen; j++){
|
for (j=0; j<rlen; j++){
|
||||||
xrule = rvec[j];
|
xrule = rvec[j];
|
||||||
|
|
@ -256,6 +263,10 @@ nacm_rpc(char *rpc,
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (rvec){
|
||||||
|
free(rvec);
|
||||||
|
rvec=NULL;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
step10:
|
step10:
|
||||||
/* 10. If the requested protocol operation is defined in a YANG module
|
/* 10. If the requested protocol operation is defined in a YANG module
|
||||||
|
|
@ -296,18 +307,20 @@ nacm_rpc(char *rpc,
|
||||||
}
|
}
|
||||||
|
|
||||||
/*---------------------------------------------------------------
|
/*---------------------------------------------------------------
|
||||||
* Datanode read
|
* Datanode/module read
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/*! We have a rule matching user group. Now match proper read operation and module
|
/*! We have a rule matching user group. Now match proper read operation and module
|
||||||
* @see RFC8341 3.4.5. Data Node Access Validation point (6)
|
* @see RFC8341 3.4.5. Data Node Access Validation point (6)
|
||||||
*
|
* @retval -1 Error
|
||||||
|
* @retval 0 No Match
|
||||||
|
* @retval 1 Match
|
||||||
|
* @see rule_data_write
|
||||||
*/
|
*/
|
||||||
static int
|
static int
|
||||||
rule_datanode_read(cxobj *xrule,
|
rule_data_read(cxobj *xrule,
|
||||||
cxobj *xr,
|
cxobj *xr,
|
||||||
cxobj *xt,
|
cxobj *xt)
|
||||||
int *match)
|
|
||||||
{
|
{
|
||||||
int retval = -1;
|
int retval = -1;
|
||||||
cxobj *xp; /* parent */
|
cxobj *xp; /* parent */
|
||||||
|
|
@ -319,7 +332,6 @@ rule_datanode_read(cxobj *xrule,
|
||||||
char *path;
|
char *path;
|
||||||
cxobj *xpath; /* xpath match */
|
cxobj *xpath; /* xpath match */
|
||||||
|
|
||||||
*match = 0;
|
|
||||||
/* 6b) Either (1) the rule does not have a "rule-type" defined or
|
/* 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
|
(2) the "rule-type" is "data-node" and the "path" matches the
|
||||||
requested data node, action node, or notification node. A
|
requested data node, action node, or notification node. A
|
||||||
|
|
@ -332,7 +344,7 @@ rule_datanode_read(cxobj *xrule,
|
||||||
/* 6c) For a "read" access operation, the rule's "access-operations"
|
/* 6c) For a "read" access operation, the rule's "access-operations"
|
||||||
leaf has the "read" bit set or has the special value "*" */
|
leaf has the "read" bit set or has the special value "*" */
|
||||||
access_operations = xml_find_body(xrule, "access-operations");
|
access_operations = xml_find_body(xrule, "access-operations");
|
||||||
if (!nacm_match_access(access_operations, "read"))
|
if (!match_access(access_operations, "read", NULL))
|
||||||
goto nomatch;
|
goto nomatch;
|
||||||
/* 6a) The rule's "module-name" leaf is "*" or equals the name of
|
/* 6a) The rule's "module-name" leaf is "*" or equals the name of
|
||||||
* the YANG module where the requested data node is defined. */
|
* the YANG module where the requested data node is defined. */
|
||||||
|
|
@ -361,11 +373,12 @@ rule_datanode_read(cxobj *xrule,
|
||||||
} while ((xp = xml_parent(xp)) != NULL);
|
} while ((xp = xml_parent(xp)) != NULL);
|
||||||
}
|
}
|
||||||
match:
|
match:
|
||||||
*match = 1;
|
retval = 1;
|
||||||
|
done:
|
||||||
|
return retval;
|
||||||
nomatch:
|
nomatch:
|
||||||
retval = 0;
|
retval = 0;
|
||||||
// done:
|
goto done;
|
||||||
return retval;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*! Go through all rules for a requested node
|
/*! Go through all rules for a requested node
|
||||||
|
|
@ -378,18 +391,17 @@ rule_datanode_read(cxobj *xrule,
|
||||||
* @param[out] xrulep If set, then points to matching rule
|
* @param[out] xrulep If set, then points to matching rule
|
||||||
*/
|
*/
|
||||||
static int
|
static int
|
||||||
nacm_datanode_read_xr(cxobj *xt,
|
nacm_data_read_xr(cxobj *xt,
|
||||||
cxobj *xr,
|
cxobj *xr,
|
||||||
cxobj **gvec,
|
cxobj **gvec,
|
||||||
size_t glen,
|
size_t glen,
|
||||||
cxobj **rlistvec,
|
cxobj **rlistvec,
|
||||||
size_t rlistlen,
|
size_t rlistlen,
|
||||||
cxobj **xrulep
|
cxobj **xrulep)
|
||||||
)
|
|
||||||
{
|
{
|
||||||
int retval = -1;
|
int retval = -1;
|
||||||
int i, j;
|
int i, j;
|
||||||
cxobj *xrlist;
|
cxobj *rlist;
|
||||||
char *gname;
|
char *gname;
|
||||||
cxobj **rvec = NULL; /* rules */
|
cxobj **rvec = NULL; /* rules */
|
||||||
size_t rlen;
|
size_t rlen;
|
||||||
|
|
@ -397,24 +409,24 @@ nacm_datanode_read_xr(cxobj *xt,
|
||||||
int match = 0;
|
int match = 0;
|
||||||
|
|
||||||
for (i=0; i<rlistlen; i++){ /* Loop through rule list */
|
for (i=0; i<rlistlen; i++){ /* Loop through rule list */
|
||||||
xrlist = rlistvec[i];
|
rlist = rlistvec[i];
|
||||||
/* Loop through user's group to find match in this rule-list */
|
/* Loop through user's group to find match in this rule-list */
|
||||||
for (j=0; j<glen; j++){
|
for (j=0; j<glen; j++){
|
||||||
gname = xml_find_body(gvec[j], "name");
|
gname = xml_find_body(gvec[j], "name");
|
||||||
if (xpath_first(xrlist, ".[group='%s']", gname)!=NULL)
|
if (xpath_first(rlist, ".[group='%s']", gname)!=NULL)
|
||||||
break; /* found */
|
break; /* found */
|
||||||
}
|
}
|
||||||
if (j==glen) /* not found */
|
if (j==glen) /* not found */
|
||||||
continue;
|
continue;
|
||||||
/* 6. For each rule-list entry found, process all rules, in order,
|
/* 6. For each rule-list entry found, process all rules, in order,
|
||||||
until a rule that matches the requested access operation is
|
until a rule that matches the requested access operation is
|
||||||
found. (see 6 sub rules in nacm_match_rule2)
|
found. (see 6 sub rules in rule_data_read)
|
||||||
*/
|
*/
|
||||||
if (xpath_vec(xrlist, "rule", &rvec, &rlen) < 0)
|
if (xpath_vec(rlist, "rule", &rvec, &rlen) < 0)
|
||||||
goto done;
|
goto done;
|
||||||
for (j=0; j<rlen; j++){ /* Loop through rules */
|
for (j=0; j<rlen; j++){ /* Loop through rules */
|
||||||
xrule = rvec[j];
|
xrule = rvec[j];
|
||||||
if (rule_datanode_read(xrule, xr, xt, &match) < 0)
|
if ((match = rule_data_read(xrule, xr, xt)) < 0)
|
||||||
goto done;
|
goto done;
|
||||||
if (match) /* xrule match */
|
if (match) /* xrule match */
|
||||||
break;
|
break;
|
||||||
|
|
@ -443,11 +455,10 @@ nacm_datanode_read_xr(cxobj *xt,
|
||||||
* @param[in] xrvec Vector of requested nodes (sub-part of xt)
|
* @param[in] xrvec Vector of requested nodes (sub-part of xt)
|
||||||
* @param[in] xrlen Length of requsted node vector
|
* @param[in] xrlen Length of requsted node vector
|
||||||
* @param[in] username
|
* @param[in] username
|
||||||
* @param[in] xaxm NACM xml tree
|
* @param[in] xnacm NACM xml tree
|
||||||
* @retval -1 Error
|
* @retval -1 Error
|
||||||
* @retval 0 Not access and cbret set
|
* @retval 0 Not access and cbret set
|
||||||
* @retval 1 Access
|
* @retval 1 Access
|
||||||
* @see RFC8341 3.4.5. Data Node Access Validation
|
|
||||||
* 3.2.4: <get> and <get-config> Operations
|
* 3.2.4: <get> and <get-config> Operations
|
||||||
* Data nodes to which the client does not have read access are silently
|
* Data nodes to which the client does not have read access are silently
|
||||||
* omitted, along with any descendants, from the <rpc-reply> message.
|
* omitted, along with any descendants, from the <rpc-reply> message.
|
||||||
|
|
@ -509,6 +520,9 @@ nacm_datanode_read_xr(cxobj *xt,
|
||||||
*
|
*
|
||||||
* A module rule has the "module-name" leaf set but no nodes from the
|
* A module rule has the "module-name" leaf set but no nodes from the
|
||||||
* "rule-type" choice set.
|
* "rule-type" choice set.
|
||||||
|
* @see RFC8341 3.4.5. Data Node Access Validation
|
||||||
|
* @see nacm_datanode_write
|
||||||
|
* @see nacm_rpc
|
||||||
*/
|
*/
|
||||||
int
|
int
|
||||||
nacm_datanode_read(cxobj *xt,
|
nacm_datanode_read(cxobj *xt,
|
||||||
|
|
@ -523,7 +537,6 @@ nacm_datanode_read(cxobj *xt,
|
||||||
cxobj *xr;
|
cxobj *xr;
|
||||||
cxobj **rlistvec = NULL; /* rule-list */
|
cxobj **rlistvec = NULL; /* rule-list */
|
||||||
size_t rlistlen;
|
size_t rlistlen;
|
||||||
cxobj **rvec = NULL; /* rules */
|
|
||||||
int i;
|
int i;
|
||||||
char *read_default = NULL;
|
char *read_default = NULL;
|
||||||
cxobj *xrule;
|
cxobj *xrule;
|
||||||
|
|
@ -553,8 +566,8 @@ nacm_datanode_read(cxobj *xt,
|
||||||
/* Loop through rule-list (steps 5,6,7) to find match of requested node
|
/* Loop through rule-list (steps 5,6,7) to find match of requested node
|
||||||
*/
|
*/
|
||||||
xrule = NULL;
|
xrule = NULL;
|
||||||
if (nacm_datanode_read_xr(xt, xr, gvec, glen, rlistvec, rlistlen,
|
if (nacm_data_read_xr(xt, xr, gvec, glen, rlistvec, rlistlen,
|
||||||
&xrule) < 0)
|
&xrule) < 0)
|
||||||
goto done;
|
goto done;
|
||||||
if (xrule){ /* xrule match requested node xr */
|
if (xrule){ /* xrule match requested node xr */
|
||||||
if ((action = xml_find_body(xrule, "action")) == NULL)
|
if ((action = xml_find_body(xrule, "action")) == NULL)
|
||||||
|
|
@ -565,7 +578,7 @@ nacm_datanode_read(cxobj *xt,
|
||||||
}
|
}
|
||||||
else if (strcmp(action, "permit")==0)
|
else if (strcmp(action, "permit")==0)
|
||||||
;/* XXX recursively find denies in xr and purge them
|
;/* XXX recursively find denies in xr and purge them
|
||||||
* ie call nacm_datanode_read_xr recursively?
|
* ie call nacm_data_read_xr recursively?
|
||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
else{ /* no rule matching xr, apply default */
|
else{ /* no rule matching xr, apply default */
|
||||||
|
|
@ -596,6 +609,235 @@ nacm_datanode_read(cxobj *xt,
|
||||||
retval = 0;
|
retval = 0;
|
||||||
done:
|
done:
|
||||||
clicon_debug(1, "%s retval:%d", __FUNCTION__, retval);
|
clicon_debug(1, "%s retval:%d", __FUNCTION__, retval);
|
||||||
|
if (gvec)
|
||||||
|
free(gvec);
|
||||||
|
if (rlistvec)
|
||||||
|
free(rlistvec);
|
||||||
|
return retval;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*---------------------------------------------------------------
|
||||||
|
* Datanode/module write (=create, delete, update)
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*! We have a rule matching user group. Now match proper write operation and module
|
||||||
|
* @retval -1 Error
|
||||||
|
* @retval 0 No Match
|
||||||
|
* @retval 1 Match
|
||||||
|
* @see RFC8341 3.4.5. Data Node Access Validation point (6)
|
||||||
|
* @see rule_data_read
|
||||||
|
*/
|
||||||
|
static int
|
||||||
|
rule_data_write(cxobj *xt,
|
||||||
|
cxobj *xr,
|
||||||
|
cxobj *xrule,
|
||||||
|
enum nacm_access access)
|
||||||
|
{
|
||||||
|
int retval = -1;
|
||||||
|
char *path;
|
||||||
|
char *access_operations;
|
||||||
|
char *module_rule; /* rule module name */
|
||||||
|
yang_stmt *ys;
|
||||||
|
yang_stmt *ymod;
|
||||||
|
char *module;
|
||||||
|
cxobj *xpath; /* xpath match */
|
||||||
|
cxobj *xp; /* parent */
|
||||||
|
|
||||||
|
/* 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. A
|
||||||
|
path is considered to match if the requested node is the node
|
||||||
|
specified by the path or is a descendant node of the path.*/
|
||||||
|
if ((path = xml_find_body(xrule, "path")) == NULL){
|
||||||
|
if (xml_find_body(xrule, "rpc-name") ||xml_find_body(xrule, "notification-name"))
|
||||||
|
goto nomatch;
|
||||||
|
}
|
||||||
|
/* 6c) For a "read" access operation, the rule's "access-operations"
|
||||||
|
leaf has the "read" bit set or has the special value "*" */
|
||||||
|
|
||||||
|
/* 6d) For a "create" access operation, the rule's
|
||||||
|
"access-operations" leaf has the "create" bit set or has the
|
||||||
|
special value "*".
|
||||||
|
6e) For a "delete" access operation, the rule's
|
||||||
|
"access-operations" leaf has the "delete" bit set or has the
|
||||||
|
special value "*".
|
||||||
|
6f) For an "update" access operation, the rule's
|
||||||
|
"access-operations" leaf has the "update" bit set or has the
|
||||||
|
special value "*". */
|
||||||
|
access_operations = xml_find_body(xrule, "access-operations");
|
||||||
|
switch (access){
|
||||||
|
case NACM_CREATE:
|
||||||
|
if (!match_access(access_operations, "create", "write"))
|
||||||
|
goto nomatch;
|
||||||
|
break;
|
||||||
|
case NACM_UPDATE:
|
||||||
|
if (!match_access(access_operations, "update", "write"))
|
||||||
|
goto nomatch;
|
||||||
|
break;
|
||||||
|
case NACM_DELETE:
|
||||||
|
if (!match_access(access_operations, "delete", "write"))
|
||||||
|
goto nomatch;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
/* 6a) The rule's "module-name" leaf is "*" or equals the name of
|
||||||
|
* the YANG module where the requested data node is defined. */
|
||||||
|
if ((module_rule = xml_find_body(xrule, "module-name")) == NULL)
|
||||||
|
goto nomatch;
|
||||||
|
if (strcmp(module_rule,"*")!=0){
|
||||||
|
if ((ys = xml_spec(xr)) == NULL)
|
||||||
|
goto nomatch;
|
||||||
|
ymod = ys_module(ys);
|
||||||
|
module = ymod->ys_argument;
|
||||||
|
if (strcmp(module, module_rule) != 0)
|
||||||
|
goto nomatch;
|
||||||
|
}
|
||||||
|
/* Here module is matched, now check for path if any */
|
||||||
|
if (path){
|
||||||
|
if ((xpath = xpath_first(xt, "%s", path)) == NULL)
|
||||||
|
goto nomatch;
|
||||||
|
/* The requested node xr is the node specified by the path or is a
|
||||||
|
* descendant node of the path:
|
||||||
|
* xmatch is one of xvec[] or an ancestor of the xvec[] nodes.
|
||||||
|
*/
|
||||||
|
xp = xr;
|
||||||
|
do {
|
||||||
|
if (xpath == xp)
|
||||||
|
goto match;
|
||||||
|
} while ((xp = xml_parent(xp)) != NULL);
|
||||||
|
}
|
||||||
|
match:
|
||||||
|
retval = 1;
|
||||||
|
done:
|
||||||
|
return retval;
|
||||||
|
nomatch:
|
||||||
|
retval = 0;
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*! Make nacm datanode and module rule write access validation
|
||||||
|
* The operations of NACM are: create, read, update, delete, exec
|
||||||
|
* where write is short-hand for create+delete+update
|
||||||
|
* @param[in] xt XML root tree with "config" label. XXX?
|
||||||
|
* @param[in] xr XML requestor node (part of xt)
|
||||||
|
* @param[in] op NACM access of xr
|
||||||
|
* @param[in] username User making access
|
||||||
|
* @param[in] xnacm NACM xml tree
|
||||||
|
* @param[out] cbret Cligen buffer result. Set to an error msg if retval=0.
|
||||||
|
* @retval -1 Error
|
||||||
|
* @retval 0 Not access and cbret set
|
||||||
|
* @retval 1 Access
|
||||||
|
* @see RFC8341 3.4.5. Data Node Access Validation
|
||||||
|
* @see nacm_datanode_read
|
||||||
|
* @see nacm_rpc
|
||||||
|
*/
|
||||||
|
int
|
||||||
|
nacm_datanode_write(cxobj *xt,
|
||||||
|
cxobj *xr,
|
||||||
|
enum nacm_access access,
|
||||||
|
char *username,
|
||||||
|
cxobj *xnacm,
|
||||||
|
cbuf *cbret)
|
||||||
|
{
|
||||||
|
int retval = -1;
|
||||||
|
cxobj **gvec = NULL; /* groups */
|
||||||
|
size_t glen;
|
||||||
|
cxobj **rlistvec = NULL; /* rule-list */
|
||||||
|
size_t rlistlen;
|
||||||
|
cxobj *rlist;
|
||||||
|
cxobj **rvec = NULL; /* rules */
|
||||||
|
size_t rlen;
|
||||||
|
int i, j;
|
||||||
|
char *gname;
|
||||||
|
cxobj *xrule;
|
||||||
|
int match = 0;
|
||||||
|
char *action;
|
||||||
|
char *write_default;
|
||||||
|
|
||||||
|
if (xnacm == NULL)
|
||||||
|
goto permit;
|
||||||
|
/* 3. Check all the "group" entries to see if any of them contain a
|
||||||
|
"user-name" entry that equals the username for the session
|
||||||
|
making the request. (If the "enable-external-groups" leaf is
|
||||||
|
"true", add to these groups the set of groups provided by the
|
||||||
|
transport layer.) */
|
||||||
|
if (username == NULL)
|
||||||
|
goto step9;
|
||||||
|
/* User's group */
|
||||||
|
if (xpath_vec(xnacm, "groups/group[user-name='%s']", &gvec, &glen, username) < 0)
|
||||||
|
goto done;
|
||||||
|
/* 4. If no groups are found, continue with step 9. */
|
||||||
|
if (glen == 0)
|
||||||
|
goto step9;
|
||||||
|
/* 5. Process all rule-list entries, in the order they appear in the
|
||||||
|
configuration. If a rule-list's "group" leaf-list does not
|
||||||
|
match any of the user's groups, proceed to the next rule-list
|
||||||
|
entry. */
|
||||||
|
if (xpath_vec(xnacm, "rule-list", &rlistvec, &rlistlen) < 0)
|
||||||
|
goto done;
|
||||||
|
for (i=0; i<rlistlen; i++){
|
||||||
|
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, ".[group='%s']", gname)!=NULL)
|
||||||
|
break; /* found */
|
||||||
|
}
|
||||||
|
if (j==glen) /* not found */
|
||||||
|
continue;
|
||||||
|
if (xpath_vec(rlist, "rule", &rvec, &rlen) < 0)
|
||||||
|
goto done;
|
||||||
|
/* 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_match_rule2)
|
||||||
|
*/
|
||||||
|
for (j=0; j<rlen; j++){ /* Loop through rules */
|
||||||
|
xrule = rvec[j];
|
||||||
|
if ((match = rule_data_write(xt, xr, xrule, access)) < 0)
|
||||||
|
goto done;
|
||||||
|
if (match) /* match */
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (match)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (match){
|
||||||
|
if ((action = xml_find_body(xrule, "action")) == NULL)
|
||||||
|
goto step9;
|
||||||
|
if (strcmp(action, "deny")==0){
|
||||||
|
if (netconf_access_denied(cbret, "application", "access denied") < 0)
|
||||||
|
goto done;
|
||||||
|
goto deny;
|
||||||
|
}
|
||||||
|
else if (strcmp(action, "permit")==0)
|
||||||
|
goto permit;
|
||||||
|
|
||||||
|
}
|
||||||
|
/* 8. At this point, no matching rule was found in any rule-list
|
||||||
|
entry. */
|
||||||
|
step9:
|
||||||
|
/* 10. For a "write" access operation, if the requested data node is
|
||||||
|
defined in a YANG module advertised in the server capabilities
|
||||||
|
and the data definition statement contains a
|
||||||
|
"nacm:default-deny-write" or a "nacm:default-deny-all"
|
||||||
|
statement, then the access request is denied for the data node
|
||||||
|
and all its descendants.
|
||||||
|
XXX
|
||||||
|
*/
|
||||||
|
/*12. For a "write" access operation, if the "write-default" leaf is
|
||||||
|
set to "permit", then permit the data node access request;
|
||||||
|
otherwise, deny the request.*/
|
||||||
|
write_default = xml_find_body(xnacm, "write-default");
|
||||||
|
if (write_default == NULL || strcmp(write_default, "permit") != 0){
|
||||||
|
if (netconf_access_denied(cbret, "application", "default deny") < 0)
|
||||||
|
goto done;
|
||||||
|
goto deny;
|
||||||
|
}
|
||||||
|
permit:
|
||||||
|
retval = 1;
|
||||||
|
done:
|
||||||
|
clicon_debug(1, "%s retval:%d (0:deny 1:permit)", __FUNCTION__, retval);
|
||||||
if (gvec)
|
if (gvec)
|
||||||
free(gvec);
|
free(gvec);
|
||||||
if (rlistvec)
|
if (rlistvec)
|
||||||
|
|
@ -603,21 +845,28 @@ nacm_datanode_read(cxobj *xt,
|
||||||
if (rvec)
|
if (rvec)
|
||||||
free(rvec);
|
free(rvec);
|
||||||
return retval;
|
return retval;
|
||||||
|
deny: /* Here, cbret must contain a netconf error msg */
|
||||||
|
assert(cbuf_len(cbret));
|
||||||
|
retval = 0;
|
||||||
|
goto done;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*---------------------------------------------------------------
|
||||||
|
* NACM pre-procesing
|
||||||
|
*/
|
||||||
|
|
||||||
/*! NACM intial pre- access control enforcements
|
/*! NACM intial pre- access control enforcements
|
||||||
* Initial NACM steps and common to all NACM access validation.
|
* Initial NACM steps and common to all NACM access validation.
|
||||||
* If retval=0 continue with next NACM step, eg rpc, module,
|
* If retval=0 continue with next NACM step, eg rpc, module,
|
||||||
* etc. If retval = 1 access is OK and skip next NACM step.
|
* etc. If retval = 1 access is OK and skip next NACM step.
|
||||||
* @param[in] h Clicon handle
|
* @param[in] h Clicon handle
|
||||||
* @param[in] username
|
* @param[in] xnacm NACM XML tree, root should be "nacm"
|
||||||
* @param[out] xncam NACM XML tree, set if retval=0. Free after use
|
* @param[in] username User name of requestor
|
||||||
* @retval -1 Error
|
* @retval -1 Error
|
||||||
* @retval 0 OK not yet permit/. continue with next NACM step using xnacm
|
* @retval 0 OK but not validated. Need to do NACM step using xnacm
|
||||||
* @retval 1 OK permitted. dont do next NACM step
|
* @retval 1 OK permitted. You do not need to do next NACM step
|
||||||
* @code
|
* @code
|
||||||
* cxobj *xnacm = NULL;
|
* if ((ret = nacm_access(mode, xnacm, username)) < 0)
|
||||||
* if ((ret = nacm_access(h, username, &xnacm)) < 0)
|
|
||||||
* err;
|
* err;
|
||||||
* if (ret == 0){
|
* if (ret == 0){
|
||||||
* // Next step NACM processing
|
* // Next step NACM processing
|
||||||
|
|
@ -627,47 +876,27 @@ nacm_datanode_read(cxobj *xt,
|
||||||
* @see RFC8341 3.4 Access Control Enforcement Procedures
|
* @see RFC8341 3.4 Access Control Enforcement Procedures
|
||||||
*/
|
*/
|
||||||
int
|
int
|
||||||
nacm_access(clicon_handle h,
|
nacm_access(char *mode,
|
||||||
char *username,
|
cxobj *xnacm,
|
||||||
cxobj **xnacmp)
|
char *username)
|
||||||
{
|
{
|
||||||
int retval = -1;
|
int retval = -1;
|
||||||
cxobj *xnacm0 = NULL;
|
cxobj *xnacm0 = NULL;
|
||||||
cxobj *xnacm;
|
|
||||||
char *mode;
|
|
||||||
char *enabled;
|
char *enabled;
|
||||||
cxobj *x;
|
cxobj *x;
|
||||||
|
|
||||||
clicon_debug(1, "%s", __FUNCTION__);
|
clicon_debug(1, "%s", __FUNCTION__);
|
||||||
mode = clicon_option_str(h, "CLICON_NACM_MODE");
|
|
||||||
if (mode == NULL || strcmp(mode, "disabled") == 0)
|
if (mode == NULL || strcmp(mode, "disabled") == 0)
|
||||||
goto permit;
|
goto permit;
|
||||||
/* 0. If nacm-mode is external, get NACM defintion from separet tree,
|
/* 0. If nacm-mode is external, get NACM defintion from separet tree,
|
||||||
otherwise get it from internal configuration */
|
otherwise get it from internal configuration */
|
||||||
if (strcmp(mode, "external")==0){
|
if (strcmp(mode, "external") && strcmp(mode, "internal")){
|
||||||
cxobj *xne;
|
clicon_err(OE_XML, 0, "Invalid NACM mode: %s", mode);
|
||||||
if ((xne = clicon_nacm_ext(h)) == NULL){
|
|
||||||
clicon_err(OE_XML, 0, "No nacm external tree");
|
|
||||||
goto done;
|
|
||||||
}
|
|
||||||
if ((xnacm0 = xml_dup(xne)) == NULL)
|
|
||||||
goto done;
|
|
||||||
}
|
|
||||||
else if (strcmp(mode, "internal")==0){
|
|
||||||
if (xmldb_get(h, "running", "nacm", 0, &xnacm0) < 0)
|
|
||||||
goto done;
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
clicon_err(OE_UNIX, 0, "Invalid NACM mode: %s", mode);
|
|
||||||
goto done;
|
goto done;
|
||||||
}
|
}
|
||||||
/* If config does not exist, then the operation is permitted. (?) */
|
/* If config does not exist, then the operation is permitted. (?) */
|
||||||
if ((xnacm = xpath_first(xnacm0, "nacm")) == NULL)
|
if (xnacm == NULL)
|
||||||
goto permit;
|
goto permit;
|
||||||
if (xml_rootchild_node(xnacm0, xnacm) < 0)
|
|
||||||
goto done;
|
|
||||||
xnacm0 = NULL;
|
|
||||||
*xnacmp = xnacm;
|
|
||||||
/* Do initial nacm processing common to all access validation in
|
/* Do initial nacm processing common to all access validation in
|
||||||
* RFC8341 3.4 */
|
* RFC8341 3.4 */
|
||||||
/* 1. If the "enable-nacm" leaf is set to "false", then the protocol
|
/* 1. If the "enable-nacm" leaf is set to "false", then the protocol
|
||||||
|
|
@ -692,3 +921,74 @@ nacm_access(clicon_handle h,
|
||||||
retval = 1;
|
retval = 1;
|
||||||
goto done;
|
goto done;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*! NACM intial pre- access control enforcements
|
||||||
|
* Initial NACM steps and common to all NACM access validation.
|
||||||
|
* If retval=0 continue with next NACM step, eg rpc, module,
|
||||||
|
* etc. If retval = 1 access is OK and skip next NACM step.
|
||||||
|
* @param[in] h Clicon handle
|
||||||
|
* @param[in] username User name of requestor
|
||||||
|
* @param[out] xncam NACM XML tree, set if retval=0. Free after use
|
||||||
|
* @retval -1 Error
|
||||||
|
* @retval 0 OK but not validated. Need to do NACM step using xnacm
|
||||||
|
* @retval 1 OK permitted. You do not need to do next NACM step
|
||||||
|
* @code
|
||||||
|
* cxobj *xnacm = NULL;
|
||||||
|
* if ((ret = nacm_access_h(h, username, &xnacm)) < 0)
|
||||||
|
* err;
|
||||||
|
* if (ret == 0){
|
||||||
|
* // Next step NACM processing
|
||||||
|
* xml_free(xnacm);
|
||||||
|
* }
|
||||||
|
* @endcode
|
||||||
|
* @see RFC8341 3.4 Access Control Enforcement Procedures
|
||||||
|
*/
|
||||||
|
|
||||||
|
int
|
||||||
|
nacm_access_h(clicon_handle h,
|
||||||
|
char *username,
|
||||||
|
cxobj **xnacmp)
|
||||||
|
{
|
||||||
|
int retval = -1;
|
||||||
|
char *mode;
|
||||||
|
cxobj *x;
|
||||||
|
cxobj *xnacm0 = NULL;
|
||||||
|
cxobj *xnacm = NULL;
|
||||||
|
|
||||||
|
if ((mode = clicon_option_str(h, "CLICON_NACM_MODE")) != NULL){
|
||||||
|
if (strcmp(mode, "external")==0){
|
||||||
|
if ((x = clicon_nacm_ext(h)))
|
||||||
|
if ((xnacm0 = xml_dup(x)) == NULL)
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
else if (strcmp(mode, "internal")==0){
|
||||||
|
if (xmldb_get(h, "running", "nacm", 0, &xnacm0) < 0)
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* If config does not exist then the operation is permitted(?) */
|
||||||
|
if (xnacm0 == NULL)
|
||||||
|
goto permit;
|
||||||
|
/* If config does not exist then the operation is permitted(?) */
|
||||||
|
if ((xnacm = xpath_first(xnacm0, "nacm")) == NULL)
|
||||||
|
goto permit;
|
||||||
|
if (xml_rootchild_node(xnacm0, xnacm) < 0)
|
||||||
|
goto done;
|
||||||
|
xnacm0 = NULL;
|
||||||
|
if ((retval = nacm_access(mode, xnacm, username)) < 0)
|
||||||
|
goto done;
|
||||||
|
if (retval == 0){ /* if retval == 0 then return an xml nacm tree */
|
||||||
|
*xnacmp = xnacm;
|
||||||
|
xnacm = NULL;
|
||||||
|
}
|
||||||
|
done:
|
||||||
|
if (xnacm0)
|
||||||
|
xml_free(xnacm0);
|
||||||
|
else if (xnacm)
|
||||||
|
xml_free(xnacm);
|
||||||
|
return retval;
|
||||||
|
permit:
|
||||||
|
retval = 1;
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1389,6 +1389,7 @@ ys_populate_identity(yang_stmt *ys,
|
||||||
/* Iterate through all base statements and check the base identity exists
|
/* Iterate through all base statements and check the base identity exists
|
||||||
* AND populate the base identity recursively
|
* AND populate the base identity recursively
|
||||||
*/
|
*/
|
||||||
|
yc = NULL;
|
||||||
while ((yc = yn_each((yang_node*)ys, yc)) != NULL) {
|
while ((yc = yn_each((yang_node*)ys, yc)) != NULL) {
|
||||||
if (yc->ys_keyword != Y_BASE)
|
if (yc->ys_keyword != Y_BASE)
|
||||||
continue;
|
continue;
|
||||||
|
|
|
||||||
|
|
@ -475,6 +475,7 @@ cv_validate1(cg_var *cv,
|
||||||
if (restype){
|
if (restype){
|
||||||
if (strcmp(restype, "enumeration") == 0){
|
if (strcmp(restype, "enumeration") == 0){
|
||||||
found = 0;
|
found = 0;
|
||||||
|
yi = NULL;
|
||||||
while ((yi = yn_each((yang_node*)yrestype, yi)) != NULL){
|
while ((yi = yn_each((yang_node*)yrestype, yi)) != NULL){
|
||||||
if (yi->ys_keyword != Y_ENUM)
|
if (yi->ys_keyword != Y_ENUM)
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -500,6 +501,7 @@ cv_validate1(cg_var *cv,
|
||||||
if ((v = vec[i]) == NULL || !strlen(v))
|
if ((v = vec[i]) == NULL || !strlen(v))
|
||||||
continue;
|
continue;
|
||||||
found = 0;
|
found = 0;
|
||||||
|
yi = NULL;
|
||||||
while ((yi = yn_each((yang_node*)yrestype, yi)) != NULL){
|
while ((yi = yn_each((yang_node*)yrestype, yi)) != NULL){
|
||||||
if (yi->ys_keyword != Y_BIT)
|
if (yi->ys_keyword != Y_BIT)
|
||||||
continue;
|
continue;
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@ RULES=$(cat <<EOF
|
||||||
<name>guest-acl</name>
|
<name>guest-acl</name>
|
||||||
<group>guest</group>
|
<group>guest</group>
|
||||||
<rule>
|
<rule>
|
||||||
<name>deny-ncm</name>
|
<name>permit-read</name>
|
||||||
<module-name>clixon-example</module-name>
|
<module-name>clixon-example</module-name>
|
||||||
<access-operations>*</access-operations>
|
<access-operations>*</access-operations>
|
||||||
<action>deny</action>
|
<action>deny</action>
|
||||||
|
|
@ -157,7 +157,7 @@ expecteq "$(curl -u andy:bar -sS -X PUT -d '{"enable-nacm": true}' http://localh
|
||||||
|
|
||||||
#--------------- nacm enabled
|
#--------------- nacm enabled
|
||||||
|
|
||||||
#----READ monitoring information from example - (ietf-netconf-monitoring)
|
#----READ access
|
||||||
#user:admin
|
#user:admin
|
||||||
new2 "admin read ok"
|
new2 "admin read ok"
|
||||||
expecteq "$(curl -u andy:bar -sS -X GET http://localhost/restconf/data/clixon-example:translate)" '{"clixon-example:translate": [{"k": "key42","value": "val42"},{ "k": "key43","value": "val43"}]}
|
expecteq "$(curl -u andy:bar -sS -X GET http://localhost/restconf/data/clixon-example:translate)" '{"clixon-example:translate": [{"k": "key42","value": "val42"},{ "k": "key43","value": "val43"}]}
|
||||||
|
|
|
||||||
224
test/test_nacm_module_write.sh
Executable file
224
test/test_nacm_module_write.sh
Executable file
|
|
@ -0,0 +1,224 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Authentication and authorization and IETF NACM
|
||||||
|
# NACM module rules
|
||||||
|
# A module rule has the "module-name" leaf set but no nodes from the
|
||||||
|
# "rule-type" choice set.
|
||||||
|
# @see test_nacm.sh is slightly modified - this follows the RFC more closely
|
||||||
|
# See RFC 8341 A.1 and A.2
|
||||||
|
# Note: use clixon-example instead of ietf-netconf-monitoring since the latter is
|
||||||
|
# Tests for
|
||||||
|
# deny-ncm: This rule prevents the "guest" group from reading any
|
||||||
|
# monitoring information in the "clixon-example" YANG
|
||||||
|
# module.
|
||||||
|
# permit-ncm: This rule allows the "limited" group to read the
|
||||||
|
# "clixon-example" YANG module.
|
||||||
|
# permit-exec: This rule allows the "limited" group to invoke any
|
||||||
|
# protocol operation supported by the server.
|
||||||
|
# permit-all: This rule allows the "admin" group complete access to
|
||||||
|
# all content in the server. No subsequent rule will match for the
|
||||||
|
# "admin" group because of this module rule
|
||||||
|
|
||||||
|
APPNAME=example
|
||||||
|
# include err() and new() functions and creates $dir
|
||||||
|
. ./lib.sh
|
||||||
|
. ./nacm.sh
|
||||||
|
|
||||||
|
cfg=$dir/conf_yang.xml
|
||||||
|
fyang=$dir/nacm-example.yang
|
||||||
|
|
||||||
|
cat <<EOF > $cfg
|
||||||
|
<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_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_CLI_GENMODEL_COMPLETION>1</CLICON_CLI_GENMODEL_COMPLETION>
|
||||||
|
<CLICON_XMLDB_DIR>/usr/local/var/$APPNAME</CLICON_XMLDB_DIR>
|
||||||
|
<CLICON_XMLDB_PLUGIN>/usr/local/lib/xmldb/text.so</CLICON_XMLDB_PLUGIN>
|
||||||
|
<CLICON_RESTCONF_PRETTY>false</CLICON_RESTCONF_PRETTY>
|
||||||
|
<CLICON_NACM_MODE>internal</CLICON_NACM_MODE>
|
||||||
|
</config>
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat <<EOF > $fyang
|
||||||
|
module nacm-example{
|
||||||
|
yang-version 1.1;
|
||||||
|
namespace "urn:example:nacm";
|
||||||
|
prefix nacm;
|
||||||
|
import clixon-example {
|
||||||
|
prefix ex;
|
||||||
|
}
|
||||||
|
import ietf-netconf-acm {
|
||||||
|
prefix nacm;
|
||||||
|
}
|
||||||
|
leaf x{
|
||||||
|
type int32;
|
||||||
|
description "something to edit";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# The groups are slightly modified from RFC8341 A.1 ($USER added in admin group)
|
||||||
|
# The rule-list is from A.2
|
||||||
|
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>deny</exec-default>
|
||||||
|
|
||||||
|
$NGROUPS
|
||||||
|
|
||||||
|
<rule-list>
|
||||||
|
<name>guest-acl</name>
|
||||||
|
<group>guest</group>
|
||||||
|
<rule>
|
||||||
|
<name>permit-get</name>
|
||||||
|
<module-name>ietf-netconf</module-name>
|
||||||
|
<rpc-name>*</rpc-name>
|
||||||
|
<access-operations>exec</access-operations>
|
||||||
|
<action>permit</action>
|
||||||
|
<comment>
|
||||||
|
Allow invocation of get rpc
|
||||||
|
</comment>
|
||||||
|
</rule>
|
||||||
|
<rule>
|
||||||
|
<name>permit-read</name>
|
||||||
|
<module-name>clixon-example</module-name>
|
||||||
|
<access-operations>read</access-operations>
|
||||||
|
<action>permit</action>
|
||||||
|
<comment>
|
||||||
|
Do not allow guests any access to the NETCONF
|
||||||
|
monitoring information.
|
||||||
|
</comment>
|
||||||
|
</rule>
|
||||||
|
<rule>
|
||||||
|
<name>deny-write</name>
|
||||||
|
<module-name>clixon-example</module-name>
|
||||||
|
<access-operations>*</access-operations>
|
||||||
|
<action>deny</action>
|
||||||
|
<comment>
|
||||||
|
Do not allow guests any access to the NETCONF
|
||||||
|
monitoring information.
|
||||||
|
</comment>
|
||||||
|
</rule>
|
||||||
|
|
||||||
|
</rule-list>
|
||||||
|
<rule-list>
|
||||||
|
<name>limited-acl</name>
|
||||||
|
<group>limited</group>
|
||||||
|
<rule>
|
||||||
|
<name>permit-ncm</name>
|
||||||
|
<module-name>clixon-example</module-name>
|
||||||
|
<access-operations>read create update delete</access-operations>
|
||||||
|
<action>permit</action>
|
||||||
|
<comment>
|
||||||
|
Allow write access to the NETCONF monitoring information.
|
||||||
|
</comment>
|
||||||
|
</rule>
|
||||||
|
<rule>
|
||||||
|
<name>permit-exec</name>
|
||||||
|
<module-name>*</module-name>
|
||||||
|
<access-operations>exec</access-operations>
|
||||||
|
<action>permit</action>
|
||||||
|
<comment>
|
||||||
|
Allow invocation of the supported server operations.
|
||||||
|
</comment>
|
||||||
|
</rule>
|
||||||
|
</rule-list>
|
||||||
|
|
||||||
|
$NADMIN
|
||||||
|
|
||||||
|
</nacm>
|
||||||
|
<x xmlns="urn:example:nacm">42</x>
|
||||||
|
<translate xmlns="urn:example:clixon"><k>key42</k><value>val42</value></translate>
|
||||||
|
<translate xmlns="urn:example:clixon"><k>key43</k><value>val43</value></translate>
|
||||||
|
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"
|
||||||
|
sudo $clixon_backend -s init -f $cfg -D $DBG
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
err
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
new "kill old restconf daemon"
|
||||||
|
sudo pkill -u www-data -f "/www-data/clixon_restconf"
|
||||||
|
|
||||||
|
sleep 1
|
||||||
|
new "start restconf daemon (-a is enable basic authentication)"
|
||||||
|
sudo su -c "$clixon_restconf -f $cfg -D $DBG -- -a" -s /bin/sh www-data &
|
||||||
|
|
||||||
|
sleep $RCWAIT
|
||||||
|
|
||||||
|
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 "commit it"
|
||||||
|
expecteof "$clixon_netconf -qf $cfg" 0 "<rpc><commit/></rpc>]]>]]>" "^<rpc-reply><ok/></rpc-reply>]]>]]>$"
|
||||||
|
|
||||||
|
new "enable nacm"
|
||||||
|
expecteq "$(curl -u andy:bar -sS -X PUT -d '{"enable-nacm": true}' http://localhost/restconf/data/ietf-netconf-acm:nacm/enable-nacm)" ""
|
||||||
|
|
||||||
|
#--------------- nacm enabled
|
||||||
|
#----WRITE access
|
||||||
|
#user:admin
|
||||||
|
new2 "admin read element ok"
|
||||||
|
expecteq "$(curl -u andy:bar -sS -X GET http://localhost/restconf/data/clixon-example:translate=key42/value)" '{"clixon-example:value": "val42"}
|
||||||
|
'
|
||||||
|
|
||||||
|
new "admin write element ok"
|
||||||
|
expecteq "$(curl -u andy:bar -sS -X PUT http://localhost/restconf/data/clixon-example:translate=key42/value -d '{"clixon-example:value": "val99"}')"
|
||||||
|
|
||||||
|
#user:limit
|
||||||
|
new2 "limit read element ok"
|
||||||
|
expecteq "$(curl -u wilma:bar -sS -X GET http://localhost/restconf/data/clixon-example:translate=key42/value)" '{"clixon-example:value": "val99"}
|
||||||
|
'
|
||||||
|
|
||||||
|
new "limit write element ok"
|
||||||
|
expecteq "$(curl -u wilma:bar -sS -X PUT http://localhost/restconf/data/clixon-example:translate=key42/value -d '{"clixon-example:value": "val55"}')"
|
||||||
|
|
||||||
|
#user:guest
|
||||||
|
new2 "guest read element ok"
|
||||||
|
expecteq "$(curl -u guest:bar -sS -X GET http://localhost/restconf/data/clixon-example:translate=key42/value)" '{"clixon-example:value": "val55"}
|
||||||
|
'
|
||||||
|
|
||||||
|
new2 "guest write element ok"
|
||||||
|
expecteq "$(curl -u guest:bar -sS -X PUT http://localhost/restconf/data/clixon-example:translate=key42/value -d '{"clixon-example:value": "val99"}')" '{"ietf-restconf:errors" : {"error": {"error-type": "application","error-tag": "access-denied","error-severity": "error","error-message": "access denied"}}}
'
|
||||||
|
|
||||||
|
new "Kill restconf daemon"
|
||||||
|
sudo pkill -u www-data -f "/www-data/clixon_restconf"
|
||||||
|
|
||||||
|
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
|
||||||
|
sudo clixon_backend -z -f $cfg
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
err "kill backend"
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -rf $dir
|
||||||
|
|
@ -75,7 +75,7 @@ RULES=$(cat <<EOF
|
||||||
<nacm xmlns="urn:ietf:params:xml:ns:yang:ietf-netconf-acm">
|
<nacm xmlns="urn:ietf:params:xml:ns:yang:ietf-netconf-acm">
|
||||||
<enable-nacm>false</enable-nacm>
|
<enable-nacm>false</enable-nacm>
|
||||||
<read-default>deny</read-default>
|
<read-default>deny</read-default>
|
||||||
<write-default>deny</write-default>
|
<write-default>permit</write-default>
|
||||||
<exec-default>deny</exec-default>
|
<exec-default>deny</exec-default>
|
||||||
|
|
||||||
$NGROUPS
|
$NGROUPS
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue