NACM cleanup, uniform rule function, change of function names, etc.
This commit is contained in:
parent
8bf5cb0de5
commit
1e4022e73c
13 changed files with 180 additions and 247 deletions
15
CHANGELOG.md
15
CHANGELOG.md
|
|
@ -76,13 +76,16 @@
|
|||
* Note CLIXON_DATADIR (=/usr/local/share/clixon) need to be in the list
|
||||
* 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.
|
||||
* NACM extension (RFC8341)
|
||||
* NACM Data node READ and WRITE access module support (RFC8341 3.4.5)
|
||||
* Access control points added for `get`, `get-config`, `edit-config` in addition to incoming rpc.
|
||||
* RFC 8341 Example A.2 implemented, see: [test/test_nacm_module.sh]
|
||||
* Remaining work: data-node PATH
|
||||
* NACM (RFC8341) experimental
|
||||
* Incoming RPC Message validation is supported (3.4.4)
|
||||
* Data Node Access validation is supported (3.4.5), except:
|
||||
* rule-type data-node path is not supported
|
||||
* Outgoing noitification aithorization is _not_ supported (3.4.6)
|
||||
* RPC:s are supported _except_:
|
||||
* `copy-config`for other src/target combinations than running/startup (3.2.6)
|
||||
* `commit` - NACM is applied to candidate and running operations only (3.2.8)
|
||||
* Client-side RPC:s are _not_ supported.
|
||||
* 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.
|
||||
|
||||
### API changes on existing features (you may need to change your code)
|
||||
* Added `username` argument on `xmldb_put()` datastore function for NACM data-node write checks
|
||||
|
|
|
|||
39
README.md
39
README.md
|
|
@ -220,35 +220,28 @@ so the clients can in principle fake a username.
|
|||
|
||||
NACM
|
||||
====
|
||||
Clixon includes an experimental Network Configuration Access Control Model (NACM) according to [RFC8341(NACM)](https://tools.ietf.org/html/rfc8341). It has limited functionality.
|
||||
Clixon includes an experimental Network Configuration Access Control Model (NACM) according to [RFC8341(NACM)](https://tools.ietf.org/html/rfc8341).
|
||||
|
||||
The support is as follows:
|
||||
To enable NACM:
|
||||
|
||||
* There is a yang config variable `CLICON_NACM_MODE` to set whether NACM is disabled, uses internal(embedded) NACM configuration, or external configuration. (See yang/clixon-config.yang)
|
||||
* If the mode is internal, NACM configurations is expected to be in the regular configuration, managed by regular candidate/runing/commit procedures. This mode may have some problems with bootstrapping.
|
||||
* The `CLICON_NACM_MODE` config variable is by default `disabled`.
|
||||
* If the mode is internal`, NACM configurations are expected to be in the regular configuration, managed by regular candidate/runing/commit procedures. This mode may have some problems with bootstrapping.
|
||||
* If the mode is `external`, the `CLICON_NACM_FILE` yang config variable contains the name of a separate configuration file containing the NACM configurations. After changes in this file, the backend needs to be restarted.
|
||||
* The [example](example/README.md) contains a http basic auth and a NACM backend callback for mandatory state variables.
|
||||
* There are two [tests](test/README.md) using internal and external NACM config
|
||||
* The backend provides a limited NACM support (when enabled) described below
|
||||
|
||||
NACM is implemented in the backend and a single access check is made
|
||||
in `from_client_msg()` when an internal netconf RPC has
|
||||
just been received and decoded. The code is in `nacm_access()`.
|
||||
The [example](example/README.md) contains a http basic auth and a NACM backend callback for mandatory state variables.
|
||||
|
||||
The functionality is as follows:
|
||||
* Notification is not supported
|
||||
* Groups are supported
|
||||
* Rule-lists are supported
|
||||
* Rules are supported as follows
|
||||
* module-name: fully supported
|
||||
* access-operations: only '*' and 'exec' supported
|
||||
* rpc-name: fully supported (eg edit-config/get-config, etc)
|
||||
* action: fully supported (permit/deny)
|
||||
NACM is implemented in the backend with incoming RPC and data node access control points.
|
||||
|
||||
The tests outlines an example of three groups (taken from the RFC): admin, limited and guest:
|
||||
* admin: Full access
|
||||
* limited: Read access (get and get-config)
|
||||
* guest: No access
|
||||
The functionality is as follows (references to sections in [RFC8341](https://tools.ietf.org/html/rfc8341)):
|
||||
* Access control point support:
|
||||
* Incoming RPC Message validation is supported (3.4.4)
|
||||
* Data Node Access validation is supported (3.4.5), except:
|
||||
* rule-type data-node path is not supported
|
||||
* Outgoing noitification aithorization is _not_ supported (3.4.6)
|
||||
* RPC:s are supported _except_:
|
||||
* `copy-config`for other src/target combinations than running/startup (3.2.6)
|
||||
* `commit` - NACM is applied to candidate and running operations only (3.2.8)
|
||||
* Client-side RPC:s are _not_ supported.
|
||||
|
||||
Runtime
|
||||
=======
|
||||
|
|
|
|||
|
|
@ -214,7 +214,7 @@ from_client_get_config(clicon_handle h,
|
|||
goto ok;
|
||||
}
|
||||
/* Pre-NACM access step */
|
||||
if ((ret = nacm_access_h(h, username, &xnacm)) < 0)
|
||||
if ((ret = nacm_access_pre(h, username, &xnacm)) < 0)
|
||||
goto done;
|
||||
if (ret == 0){ /* Do NACM validation */
|
||||
if (xpath_vec(xret, "%s", &xvec, &xlen, xpath?xpath:"/") < 0)
|
||||
|
|
@ -407,7 +407,7 @@ from_client_get(clicon_handle h,
|
|||
goto ok;
|
||||
}
|
||||
/* Pre-NACM access step */
|
||||
if ((ret = nacm_access_h(h, username, &xnacm)) < 0)
|
||||
if ((ret = nacm_access_pre(h, username, &xnacm)) < 0)
|
||||
goto done;
|
||||
if (ret == 0){ /* Do NACM validation */
|
||||
if (xpath_vec(xret, "%s", &xvec, &xlen, xpath?xpath:"/") < 0)
|
||||
|
|
@ -728,6 +728,10 @@ from_client_kill_session(clicon_handle h,
|
|||
* @param[out] cbret Return xml value cligen buffer
|
||||
* @retval 0 OK
|
||||
* @retval -1 Error. Send error message back to client.
|
||||
* NACM: If source running and target startup --> only exec permission
|
||||
* else:
|
||||
* - omit data nodes to which the client does not have read access
|
||||
* - access denied if user lacks create/delete/update
|
||||
*/
|
||||
static int
|
||||
from_client_copy_config(clicon_handle h,
|
||||
|
|
@ -1052,7 +1056,7 @@ from_client_msg(clicon_handle h,
|
|||
clicon_debug(1, "%s module:%s rpc:%s", __FUNCTION__, module, rpc);
|
||||
/* Pre-NACM access step */
|
||||
xnacm = NULL;
|
||||
if ((ret = nacm_access_h(h, username, &xnacm)) < 0)
|
||||
if ((ret = nacm_access_pre(h, username, &xnacm)) < 0)
|
||||
goto done;
|
||||
if (ret == 0){ /* Do NACM validation */
|
||||
/* NACM rpc operation exec validation */
|
||||
|
|
|
|||
|
|
@ -323,6 +323,10 @@ candidate_commit(clicon_handle h,
|
|||
* @param[out] cbret Return xml value cligen buffer
|
||||
* @retval 0 OK. This may indicate both ok and err msg back to client
|
||||
* @retval -1 (Local) Error
|
||||
* NACM: The server MUST determine the exact nodes in the running
|
||||
* configuration datastore that are actually different and only check
|
||||
* "create", "update", and "delete" access permissions for this set of
|
||||
* nodes, which could be empty.
|
||||
*/
|
||||
int
|
||||
from_client_commit(clicon_handle h,
|
||||
|
|
@ -368,6 +372,7 @@ from_client_commit(clicon_handle h,
|
|||
* @param[out] cbret Return xml value cligen buffer
|
||||
* @retval 0 OK. This may indicate both ok and err msg back to client
|
||||
* @retval -1 (Local) Error
|
||||
* NACM: No datastore permissions are needed.
|
||||
*/
|
||||
int
|
||||
from_client_discard_changes(clicon_handle h,
|
||||
|
|
|
|||
|
|
@ -306,6 +306,7 @@ You may also add a default method in the configuration file:
|
|||
Yes. Systemd example files are provide for the backend and the
|
||||
restconf daemon as part of the [example](../example/systemd).
|
||||
|
||||
|
||||
## How can I add extra XML?
|
||||
|
||||
There are two ways to add extra XML to running database after start. Note that this XML is not "committed" into running.
|
||||
|
|
@ -474,6 +475,14 @@ To authenticate, the callback needs to return the value 1 and supply a username.
|
|||
See [../apps/example/example_restconf.c] example_restconf_credentials() for
|
||||
an example of HTTP basic auth.
|
||||
|
||||
## What about access control?
|
||||
|
||||
Clixon has experimental support of the Network Configuration Access
|
||||
Control Model defined in [RFC8341](https://tools.ietf.org/html/rfc8341)
|
||||
|
||||
Incoming RPC and data node access points are supported with some
|
||||
limitations. See the (README)(../README.md) for more information.
|
||||
|
||||
## How do I write a CLI translator function?
|
||||
|
||||
The CLI can perform variable translation. This is useful if you want to
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ 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_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_pre(clicon_handle h, char *username, cxobj **xnacmp);
|
||||
int nacm_access(char *mode, cxobj *xnacmin, char *username);
|
||||
|
||||
#endif /* _CLIXON_NACM_H */
|
||||
|
|
|
|||
|
|
@ -101,23 +101,9 @@ match_access(char *access_operations,
|
|||
* @param[out] cbret Cligen buffer result. Set to an error msg if retval=0.
|
||||
* @retval -1 Error
|
||||
* @retval 0 Matching rule AND Not access and cbret set
|
||||
* @retval 1 Matchung rule AND Access
|
||||
* @retval 1 Matching rule AND Access
|
||||
* @retval 2 No matching rule Goto step 10
|
||||
* From RFC8341 3.4.4. Incoming RPC Message Validation
|
||||
+---------+-----------------+---------------------+-----------------+
|
||||
| Method | Resource class | NETCONF operation | Access |
|
||||
| | | | operation |
|
||||
+---------+-----------------+---------------------+-----------------+
|
||||
| OPTIONS | all | none | none |
|
||||
| HEAD | all | <get>, <get-config> | read |
|
||||
| GET | all | <get>, <get-config> | read |
|
||||
| POST | datastore, data | <edit-config> | create |
|
||||
| POST | operation | specified operation | execute |
|
||||
| PUT | data | <edit-config> | create, update |
|
||||
| PUT | datastore | <copy-config> | update |
|
||||
| PATCH | data, datastore | <edit-config> | update |
|
||||
| DELETE | data | <edit-config> | delete |
|
||||
|
||||
* @see RFC8341 3.4.4. Incoming RPC Message Validation
|
||||
7.(cont) A rule matches if all of the following criteria are met:
|
||||
* The rule's "module-name" leaf is "*" or equals the name of
|
||||
the YANG module where the protocol operation is defined.
|
||||
|
|
@ -133,44 +119,40 @@ match_access(char *access_operations,
|
|||
static int
|
||||
nacm_rule_rpc(char *rpc,
|
||||
char *module,
|
||||
cxobj *xrule,
|
||||
cbuf *cbret)
|
||||
cxobj *xrule)
|
||||
{
|
||||
int retval = -1;
|
||||
char *module_rule; /* rule module name */
|
||||
char *rpc_rule;
|
||||
char *access_operations;
|
||||
char *action;
|
||||
|
||||
module_rule = xml_find_body(xrule, "module-name");
|
||||
rpc_rule = xml_find_body(xrule, "rpc-name");
|
||||
/* XXX access_operations can be a set of bits */
|
||||
access_operations = xml_find_body(xrule, "access-operations");
|
||||
action = xml_find_body(xrule, "action");
|
||||
clicon_debug(1, "%s: %s %s %s %s", __FUNCTION__,
|
||||
module_rule, rpc_rule, access_operations, action);
|
||||
if (module_rule &&
|
||||
(strcmp(module_rule,"*")==0 || strcmp(module_rule,module)==0)){
|
||||
if (match_access(access_operations, "exec", NULL)){
|
||||
if (rpc_rule==NULL ||
|
||||
strcmp(rpc_rule, "*")==0 || strcmp(rpc_rule, rpc)==0){
|
||||
/* Here is a matching rule */
|
||||
if (action && strcmp(action, "permit")==0){
|
||||
retval = 1;
|
||||
goto done;
|
||||
}
|
||||
else{
|
||||
if (netconf_access_denied(cbret, "protocol", "access denied") < 0)
|
||||
goto done;
|
||||
retval = 0;
|
||||
goto done;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* 7a) The rule's "module-name" leaf is "*" or equals the name of
|
||||
the YANG module where the protocol operation is defined. */
|
||||
if ((module_rule = xml_find_body(xrule, "module-name")) == NULL)
|
||||
goto nomatch;
|
||||
if (strcmp(module_rule,"*") && strcmp(module_rule,module))
|
||||
goto nomatch;
|
||||
/* 7b) Either (1) the rule does not have a "rule-type" defined or
|
||||
(2) the "rule-type" is "protocol-operation" and the
|
||||
"rpc-name" is "*" or equals the name of the requested
|
||||
protocol operation. */
|
||||
if ((rpc_rule = xml_find_body(xrule, "rpc-name")) == NULL){
|
||||
if (xml_find_body(xrule, "path") || xml_find_body(xrule, "notification-name"))
|
||||
goto nomatch;
|
||||
}
|
||||
retval = 2; /* no matching rule */
|
||||
if (rpc_rule && (strcmp(rpc_rule, "*") && strcmp(rpc_rule, rpc)))
|
||||
goto nomatch;
|
||||
/* 7c) The rule's "access-operations" leaf has the "exec" bit set or
|
||||
has the special value "*". */
|
||||
access_operations = xml_find_body(xrule, "access-operations");
|
||||
if (!match_access(access_operations, "exec", NULL))
|
||||
goto nomatch;
|
||||
retval = 1;
|
||||
done:
|
||||
return retval;
|
||||
nomatch:
|
||||
retval = 0;
|
||||
goto done;
|
||||
}
|
||||
|
||||
/*! Process nacm incoming RPC message validation steps
|
||||
|
|
@ -202,10 +184,11 @@ nacm_rpc(char *rpc,
|
|||
size_t rlistlen;
|
||||
cxobj **rvec = NULL; /* rules */
|
||||
size_t rlen;
|
||||
int ret;
|
||||
int i, j;
|
||||
char *exec_default = NULL;
|
||||
char *gname;
|
||||
char *action;
|
||||
int match= 0;
|
||||
|
||||
/* 3. If the requested operation is the NETCONF <close-session>
|
||||
protocol operation, then the protocol operation is permitted.
|
||||
|
|
@ -249,25 +232,30 @@ nacm_rpc(char *rpc,
|
|||
goto done;
|
||||
for (j=0; j<rlen; j++){
|
||||
xrule = rvec[j];
|
||||
/* -1 error, 0 deny, 1 permit, 2 continue */
|
||||
if ((ret = nacm_rule_rpc(rpc, module, xrule, cbret)) < 0)
|
||||
if ((match = nacm_rule_rpc(rpc, module, xrule)) < 0)
|
||||
goto done;
|
||||
switch(ret){
|
||||
case 0: /* deny */
|
||||
goto deny;
|
||||
if (match)
|
||||
break;
|
||||
case 1: /* permit */
|
||||
goto permit;
|
||||
break;
|
||||
case 2: /* no match, continue */
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (match)
|
||||
break;
|
||||
if (rvec){
|
||||
free(rvec);
|
||||
rvec=NULL;
|
||||
}
|
||||
}
|
||||
if (match){
|
||||
if ((action = xml_find_body(xrule, "action")) == NULL)
|
||||
goto step10;
|
||||
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;
|
||||
|
||||
}
|
||||
step10:
|
||||
/* 10. If the requested protocol operation is defined in a YANG module
|
||||
advertised in the server capabilities and the "rpc" statement
|
||||
|
|
@ -277,7 +265,7 @@ nacm_rpc(char *rpc,
|
|||
<kill-session> or <delete-config>, then the protocol operation
|
||||
is denied. */
|
||||
if (strcmp(rpc, "kill-session")==0 || strcmp(rpc, "delete-config")==0){
|
||||
if (netconf_access_denied(cbret, "protocol", "default deny") < 0)
|
||||
if (netconf_access_denied(cbret, "application", "default deny") < 0)
|
||||
goto done;
|
||||
goto deny;
|
||||
}
|
||||
|
|
@ -286,7 +274,7 @@ nacm_rpc(char *rpc,
|
|||
exec_default = xml_find_body(xnacm, "exec-default");
|
||||
if (exec_default ==NULL || strcmp(exec_default, "permit")==0)
|
||||
goto permit;
|
||||
if (netconf_access_denied(cbret, "protocol", "default deny") < 0)
|
||||
if (netconf_access_denied(cbret, "application", "default deny") < 0)
|
||||
goto done;
|
||||
goto deny;
|
||||
permit:
|
||||
|
|
@ -307,45 +295,31 @@ nacm_rpc(char *rpc,
|
|||
}
|
||||
|
||||
/*---------------------------------------------------------------
|
||||
* Datanode/module read
|
||||
* Datanode/module read and write
|
||||
*/
|
||||
|
||||
/*! 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)
|
||||
/*! 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 rule_data_write
|
||||
* @see RFC8341 3.4.5. Data Node Access Validation point (6)
|
||||
*/
|
||||
static int
|
||||
rule_data_read(cxobj *xrule,
|
||||
cxobj *xr,
|
||||
cxobj *xt)
|
||||
nacm_rule_datanode(cxobj *xt,
|
||||
cxobj *xr,
|
||||
cxobj *xrule,
|
||||
enum nacm_access access)
|
||||
{
|
||||
int retval = -1;
|
||||
cxobj *xp; /* parent */
|
||||
char *path;
|
||||
char *access_operations;
|
||||
char *module_rule; /* rule module name */
|
||||
yang_stmt *ys;
|
||||
yang_stmt *ymod;
|
||||
char *module;
|
||||
char *path;
|
||||
cxobj *xpath; /* xpath match */
|
||||
|
||||
/* 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 "*" */
|
||||
access_operations = xml_find_body(xrule, "access-operations");
|
||||
if (!match_access(access_operations, "read", NULL))
|
||||
goto nomatch;
|
||||
cxobj *xp; /* parent */
|
||||
|
||||
/* 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)
|
||||
|
|
@ -358,7 +332,46 @@ rule_data_read(cxobj *xrule,
|
|||
if (strcmp(module, module_rule) != 0)
|
||||
goto nomatch;
|
||||
}
|
||||
/* Here module is matched, now check for path if any */
|
||||
|
||||
/* 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;
|
||||
}
|
||||
access_operations = xml_find_body(xrule, "access-operations");
|
||||
switch (access){
|
||||
case NACM_READ:
|
||||
/* 6c) For a "read" access operation, the rule's "access-operations"
|
||||
leaf has the "read" bit set or has the special value "*" */
|
||||
if (!match_access(access_operations, "read", NULL))
|
||||
goto nomatch;
|
||||
break;
|
||||
case NACM_CREATE:
|
||||
/* 6d) For a "create" access operation, the rule's "access-operations"
|
||||
leaf has the "create" bit set or has the special value "*". */
|
||||
if (!match_access(access_operations, "create", "write"))
|
||||
goto nomatch;
|
||||
break;
|
||||
case NACM_DELETE:
|
||||
/* 6e) For a "delete" access operation, the rule's "access-operations"
|
||||
leaf has the "delete" bit set or has the special value "*". */
|
||||
if (!match_access(access_operations, "delete", "write"))
|
||||
goto nomatch;
|
||||
break;
|
||||
case NACM_UPDATE:
|
||||
/* 6f) For an "update" access operation, the rule's "access-operations"
|
||||
leaf has the "update" bit set or has the special value "*". */
|
||||
if (!match_access(access_operations, "update", "write"))
|
||||
goto nomatch;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
/* Here module is matched, now check for path if any NYI */
|
||||
if (path){
|
||||
if ((xpath = xpath_first(xt, "%s", path)) == NULL)
|
||||
goto nomatch;
|
||||
|
|
@ -420,13 +433,13 @@ nacm_data_read_xr(cxobj *xt,
|
|||
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 rule_data_read)
|
||||
found. (see 6 sub rules in nacm_rule_datanode
|
||||
*/
|
||||
if (xpath_vec(rlist, "rule", &rvec, &rlen) < 0)
|
||||
goto done;
|
||||
for (j=0; j<rlen; j++){ /* Loop through rules */
|
||||
xrule = rvec[j];
|
||||
if ((match = rule_data_read(xrule, xr, xt)) < 0)
|
||||
if ((match = nacm_rule_datanode(xt, xr, xrule, NACM_READ)) < 0)
|
||||
goto done;
|
||||
if (match) /* xrule match */
|
||||
break;
|
||||
|
|
@ -615,106 +628,6 @@ nacm_datanode_read(cxobj *xt,
|
|||
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
|
||||
|
|
@ -790,17 +703,21 @@ nacm_datanode_write(cxobj *xt,
|
|||
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)
|
||||
found. (see 6 sub rules in nacm_rule_data_write)
|
||||
*/
|
||||
for (j=0; j<rlen; j++){ /* Loop through rules */
|
||||
xrule = rvec[j];
|
||||
if ((match = rule_data_write(xt, xr, xrule, access)) < 0)
|
||||
if ((match = nacm_rule_datanode(xt, xr, xrule, access)) < 0)
|
||||
goto done;
|
||||
if (match) /* match */
|
||||
break;
|
||||
}
|
||||
if (match)
|
||||
break;
|
||||
if (rvec){
|
||||
free(rvec);
|
||||
rvec = NULL;
|
||||
}
|
||||
}
|
||||
if (match){
|
||||
if ((action = xml_find_body(xrule, "action")) == NULL)
|
||||
|
|
@ -934,7 +851,7 @@ nacm_access(char *mode,
|
|||
* @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)
|
||||
* if ((ret = nacm_access_pre(h, username, &xnacm)) < 0)
|
||||
* err;
|
||||
* if (ret == 0){
|
||||
* // Next step NACM processing
|
||||
|
|
@ -943,11 +860,10 @@ nacm_access(char *mode,
|
|||
* @endcode
|
||||
* @see RFC8341 3.4 Access Control Enforcement Procedures
|
||||
*/
|
||||
|
||||
int
|
||||
nacm_access_h(clicon_handle h,
|
||||
char *username,
|
||||
cxobj **xnacmp)
|
||||
nacm_access_pre(clicon_handle h,
|
||||
char *username,
|
||||
cxobj **xnacmp)
|
||||
{
|
||||
int retval = -1;
|
||||
char *mode;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@ application. Assumes setup of http daemon as describe under apps/restonf
|
|||
- site.sh Add your site-specific modifications here
|
||||
- test_nacm.sh Auth tests using internal NACM
|
||||
- test_nacm_ext.sh Auth tests using external NACM (separate file)
|
||||
- test_nacm_protocol.sh Auth tests for incoming RPC:s
|
||||
- test_nacm_module_read.sh Auth tests for data node read operations
|
||||
- test_nacm_module_write.sh Auth tests for data node write operations
|
||||
- test_cli.sh CLI tests
|
||||
- test_netconf.sh Netconf tests
|
||||
- test_restconf.sh Restconf tests
|
||||
|
|
|
|||
|
|
@ -165,16 +165,16 @@ expecteq "$(curl -u wilma:bar -sS -X GET http://localhost/restconf/data/nacm-exa
|
|||
'
|
||||
|
||||
new2 "guest get nacm"
|
||||
expecteq "$(curl -u guest:bar -sS -X GET http://localhost/restconf/data/nacm-example:x)" '{"ietf-restconf:errors" : {"error": {"error-type": "protocol","error-tag": "access-denied","error-severity": "error","error-message": "access denied"}}}
'
|
||||
expecteq "$(curl -u guest:bar -sS -X GET http://localhost/restconf/data/nacm-example:x)" '{"ietf-restconf:errors" : {"error": {"error-type": "application","error-tag": "access-denied","error-severity": "error","error-message": "access denied"}}}
'
|
||||
|
||||
new "admin edit nacm"
|
||||
expecteq "$(curl -u andy:bar -sS -X PUT -d '{"nacm-example:x": 1}' http://localhost/restconf/data/nacm-example:x)" ""
|
||||
|
||||
new2 "limited edit nacm"
|
||||
expecteq "$(curl -u wilma:bar -sS -X PUT -d '{"nacm-example:x": 2}' http://localhost/restconf/data/nacm-example:x)" '{"ietf-restconf:errors" : {"error": {"error-type": "protocol","error-tag": "access-denied","error-severity": "error","error-message": "default deny"}}}
'
|
||||
expecteq "$(curl -u wilma:bar -sS -X PUT -d '{"nacm-example:x": 2}' http://localhost/restconf/data/nacm-example:x)" '{"ietf-restconf:errors" : {"error": {"error-type": "application","error-tag": "access-denied","error-severity": "error","error-message": "default deny"}}}
'
|
||||
|
||||
new2 "guest edit nacm"
|
||||
expecteq "$(curl -u guest:bar -sS -X PUT -d '{"nacm-example:x": 3}' http://localhost/restconf/data/nacm-example:x)" '{"ietf-restconf:errors" : {"error": {"error-type": "protocol","error-tag": "access-denied","error-severity": "error","error-message": "access denied"}}}
'
|
||||
expecteq "$(curl -u guest:bar -sS -X PUT -d '{"nacm-example:x": 3}' http://localhost/restconf/data/nacm-example:x)" '{"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"
|
||||
|
|
|
|||
|
|
@ -177,16 +177,16 @@ expecteq "$(curl -u wilma:bar -sS -X GET http://localhost/restconf/data/nacm-exa
|
|||
'
|
||||
|
||||
new2 "guest get nacm"
|
||||
expecteq "$(curl -u guest:bar -sS -X GET http://localhost/restconf/data/nacm-example:x)" '{"ietf-restconf:errors" : {"error": {"error-type": "protocol","error-tag": "access-denied","error-severity": "error","error-message": "access denied"}}}
'
|
||||
expecteq "$(curl -u guest:bar -sS -X GET http://localhost/restconf/data/nacm-example:x)" '{"ietf-restconf:errors" : {"error": {"error-type": "application","error-tag": "access-denied","error-severity": "error","error-message": "access denied"}}}
'
|
||||
|
||||
new "admin edit nacm"
|
||||
expecteq "$(curl -u andy:bar -sS -X PUT -d '{"nacm-example:x": 1}' http://localhost/restconf/data/nacm-example:x)" ""
|
||||
|
||||
new2 "limited edit nacm"
|
||||
expecteq "$(curl -u wilma:bar -sS -X PUT -d '{"x": 2}' http://localhost/restconf/data/nacm-example:x)" '{"ietf-restconf:errors" : {"error": {"error-type": "protocol","error-tag": "access-denied","error-severity": "error","error-message": "default deny"}}}
'
|
||||
expecteq "$(curl -u wilma:bar -sS -X PUT -d '{"x": 2}' http://localhost/restconf/data/nacm-example:x)" '{"ietf-restconf:errors" : {"error": {"error-type": "application","error-tag": "access-denied","error-severity": "error","error-message": "default deny"}}}
'
|
||||
|
||||
new2 "guest edit nacm"
|
||||
expecteq "$(curl -u guest:bar -sS -X PUT -d '{"x": 3}' http://localhost/restconf/data/nacm-example:x)" '{"ietf-restconf:errors" : {"error": {"error-type": "protocol","error-tag": "access-denied","error-severity": "error","error-message": "access denied"}}}
'
|
||||
expecteq "$(curl -u guest:bar -sS -X PUT -d '{"x": 3}' http://localhost/restconf/data/nacm-example:x)" '{"ietf-restconf:errors" : {"error": {"error-type": "application","error-tag": "access-denied","error-severity": "error","error-message": "access denied"}}}
'
|
||||
|
||||
new "cli show conf as admin"
|
||||
expectfn "$clixon_cli -1 -U andy -l o -f $cfg show conf" 0 "^x 1;$"
|
||||
|
|
|
|||
|
|
@ -214,22 +214,22 @@ expecteq "$(curl -u wilma:bar -sS -X GET http://localhost/restconf/data)" '{"dat
|
|||
#user:guest
|
||||
|
||||
new2 "guest read fail"
|
||||
expecteq "$(curl -u guest:bar -sS -X GET http://localhost/restconf/data/clixon-example:translate)" '{"ietf-restconf:errors" : {"error": {"error-type": "protocol","error-tag": "access-denied","error-severity": "error","error-message": "default deny"}}}
'
|
||||
expecteq "$(curl -u guest:bar -sS -X GET http://localhost/restconf/data/clixon-example:translate)" '{"ietf-restconf:errors" : {"error": {"error-type": "application","error-tag": "access-denied","error-severity": "error","error-message": "default deny"}}}
'
|
||||
|
||||
new2 "guest read netconf fail"
|
||||
expecteof "$clixon_netconf -U guest -qf $cfg" 0 '<rpc><get-config><source><candidate/></source><filter type="xpath" select="/translate"/></get-config></rpc>]]>]]>' '^<rpc-reply><rpc-error><error-type>protocol</error-type><error-tag>access-denied</error-tag><error-severity>error</error-severity><error-message>default deny</error-message></rpc-error></rpc-reply>]]>]]>$'
|
||||
expecteof "$clixon_netconf -U guest -qf $cfg" 0 '<rpc><get-config><source><candidate/></source><filter type="xpath" select="/translate"/></get-config></rpc>]]>]]>' '^<rpc-reply><rpc-error><error-type>application</error-type><error-tag>access-denied</error-tag><error-severity>error</error-severity><error-message>default deny</error-message></rpc-error></rpc-reply>]]>]]>$'
|
||||
|
||||
new2 "guest read element fail"
|
||||
expecteq "$(curl -u guest:bar -sS -X GET http://localhost/restconf/data/clixon-example:translate=key42/value)" '{"ietf-restconf:errors" : {"error": {"error-type": "protocol","error-tag": "access-denied","error-severity": "error","error-message": "default deny"}}}
'
|
||||
expecteq "$(curl -u guest:bar -sS -X GET http://localhost/restconf/data/clixon-example:translate=key42/value)" '{"ietf-restconf:errors" : {"error": {"error-type": "application","error-tag": "access-denied","error-severity": "error","error-message": "default deny"}}}
'
|
||||
|
||||
new2 "guest read other module fail"
|
||||
expecteq "$(curl -u guest:bar -sS -X GET http://localhost/restconf/data/nacm-example:x)" '{"ietf-restconf:errors" : {"error": {"error-type": "protocol","error-tag": "access-denied","error-severity": "error","error-message": "default deny"}}}
'
|
||||
expecteq "$(curl -u guest:bar -sS -X GET http://localhost/restconf/data/nacm-example:x)" '{"ietf-restconf:errors" : {"error": {"error-type": "application","error-tag": "access-denied","error-severity": "error","error-message": "default deny"}}}
'
|
||||
|
||||
new2 "guest read state fail"
|
||||
expecteq "$(curl -u guest:bar -sS -X GET http://localhost/restconf/data/clixon-example:state)" '{"ietf-restconf:errors" : {"error": {"error-type": "protocol","error-tag": "access-denied","error-severity": "error","error-message": "default deny"}}}
'
|
||||
expecteq "$(curl -u guest:bar -sS -X GET http://localhost/restconf/data/clixon-example:state)" '{"ietf-restconf:errors" : {"error": {"error-type": "application","error-tag": "access-denied","error-severity": "error","error-message": "default deny"}}}
'
|
||||
|
||||
new2 "guest read top ok (part)"
|
||||
expecteq "$(curl -u guest:bar -sS -X GET http://localhost/restconf/data)" '{"ietf-restconf:errors" : {"error": {"error-type": "protocol","error-tag": "access-denied","error-severity": "error","error-message": "default deny"}}}
'
|
||||
expecteq "$(curl -u guest:bar -sS -X GET http://localhost/restconf/data)" '{"ietf-restconf:errors" : {"error": {"error-type": "application","error-tag": "access-denied","error-severity": "error","error-message": "default deny"}}}
'
|
||||
|
||||
#------- RPC operation
|
||||
|
||||
|
|
@ -248,10 +248,10 @@ new "limit rpc netconf ok"
|
|||
expecteof "$clixon_netconf -U wilma -qf $cfg" 0 '<rpc xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"><example xmlns="urn:example:clixon"><x>0</x></example></rpc>]]>]]>' '^<rpc-reply xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"><x xmlns="urn:example:clixon">0</x><y xmlns="urn:example:clixon">42</y></rpc-reply>]]>]]>$'
|
||||
|
||||
new2 "guest rpc fail"
|
||||
expecteq "$(curl -u guest:bar -s -X POST http://localhost/restconf/operations/clixon-example:example -d '{"clixon-example:input":{"x":42}}' )" '{"ietf-restconf:errors" : {"error": {"error-type": "protocol","error-tag": "access-denied","error-severity": "error","error-message": "access denied"}}}
'
|
||||
expecteq "$(curl -u guest:bar -s -X POST http://localhost/restconf/operations/clixon-example:example -d '{"clixon-example:input":{"x":42}}' )" '{"ietf-restconf:errors" : {"error": {"error-type": "application","error-tag": "access-denied","error-severity": "error","error-message": "access denied"}}}
'
|
||||
|
||||
new "guest rpc netconf fail"
|
||||
expecteof "$clixon_netconf -U guest -qf $cfg" 0 '<rpc xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"><example xmlns="urn:example:clixon"><x>0</x></example></rpc>]]>]]>' '^<rpc-reply xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"><rpc-error><error-type>protocol</error-type><error-tag>access-denied</error-tag><error-severity>error</error-severity><error-message>access denied</error-message></rpc-error></rpc-reply>]]>]]>$'
|
||||
expecteof "$clixon_netconf -U guest -qf $cfg" 0 '<rpc xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"><example xmlns="urn:example:clixon"><x>0</x></example></rpc>]]>]]>' '^<rpc-reply xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"><rpc-error><error-type>application</error-type><error-tag>access-denied</error-tag><error-severity>error</error-severity><error-message>access denied</error-message></rpc-error></rpc-reply>]]>]]>$'
|
||||
|
||||
#------------------ Set read-default permit
|
||||
|
||||
|
|
@ -267,7 +267,7 @@ expecteq "$(curl -u wilma:bar -sS -X GET http://localhost/restconf/data/nacm-exa
|
|||
'
|
||||
|
||||
new2 "guest read state fail"
|
||||
expecteq "$(curl -u guest:bar -sS -X GET http://localhost/restconf/data/clixon-example:state)" '{"ietf-restconf:errors" : {"error": {"error-type": "protocol","error-tag": "access-denied","error-severity": "error","error-message": "default deny"}}}
'
|
||||
expecteq "$(curl -u guest:bar -sS -X GET http://localhost/restconf/data/clixon-example:state)" '{"ietf-restconf:errors" : {"error": {"error-type": "application","error-tag": "access-denied","error-severity": "error","error-message": "default deny"}}}
'
|
||||
|
||||
|
||||
new "Kill restconf daemon"
|
||||
|
|
|
|||
|
|
@ -184,7 +184,7 @@ MSG="<data>$RULES</data>"
|
|||
new "update root list permit"
|
||||
expecteq "$(curl -u andy:bar -sS -H 'Content-Type: application/yang-data+xml' -X PUT http://localhost/restconf/data -d "$MSG")" ''
|
||||
|
||||
new "delete root list deny"
|
||||
new2 "delete root list deny"
|
||||
expecteq "$(curl -u wilma:bar -sS -X DELETE http://localhost/restconf/data)" '{"ietf-restconf:errors" : {"error": {"error-type": "application","error-tag": "access-denied","error-severity": "error","error-message": "default deny"}}}
'
|
||||
|
||||
new "delete root permit"
|
||||
|
|
|
|||
|
|
@ -171,20 +171,20 @@ expecteq "$(curl -u andy:bar -sS -X GET http://localhost/restconf/data/nacm-exam
|
|||
|
||||
# Rule 1: deny-kill-session
|
||||
new "deny-kill-session: limited fail (netconf)"
|
||||
expecteof "$clixon_netconf -qf $cfg -U wilma" 0 "<rpc><kill-session><session-id>44</session-id></kill-session></rpc>]]>]]>" "^<rpc-reply><rpc-error><error-type>protocol</error-type><error-tag>access-denied</error-tag><error-severity>error</error-severity><error-message>access denied</error-message></rpc-error></rpc-reply>]]>]]>$"
|
||||
expecteof "$clixon_netconf -qf $cfg -U wilma" 0 "<rpc><kill-session><session-id>44</session-id></kill-session></rpc>]]>]]>" "^<rpc-reply><rpc-error><error-type>application</error-type><error-tag>access-denied</error-tag><error-severity>error</error-severity><error-message>access denied</error-message></rpc-error></rpc-reply>]]>]]>$"
|
||||
|
||||
new "deny-kill-session: guest fail (netconf)"
|
||||
expecteof "$clixon_netconf -qf $cfg -U guest" 0 "<rpc><kill-session><session-id>44</session-id></kill-session></rpc>]]>]]>" "^<rpc-reply><rpc-error><error-type>protocol</error-type><error-tag>access-denied</error-tag><error-severity>error</error-severity><error-message>access denied</error-message></rpc-error></rpc-reply>]]>]]>$"
|
||||
expecteof "$clixon_netconf -qf $cfg -U guest" 0 "<rpc><kill-session><session-id>44</session-id></kill-session></rpc>]]>]]>" "^<rpc-reply><rpc-error><error-type>application</error-type><error-tag>access-denied</error-tag><error-severity>error</error-severity><error-message>access denied</error-message></rpc-error></rpc-reply>]]>]]>$"
|
||||
|
||||
new "deny-kill-session: admin ok (netconf)"
|
||||
expecteof "$clixon_netconf -qf $cfg -U andy" 0 "<rpc><kill-session><session-id>44</session-id></kill-session></rpc>]]>]]>" "^<rpc-reply><ok/></rpc-reply>]]>]]>$"
|
||||
|
||||
# Rule 2: deny-delete-config
|
||||
new "deny-delete-config: limited fail (netconf)"
|
||||
expecteof "$clixon_netconf -qf $cfg -U wilma" 0 "<rpc><delete-config><target><startup/></target></delete-config></rpc>]]>]]>" "^<rpc-reply><rpc-error><error-type>protocol</error-type><error-tag>access-denied</error-tag><error-severity>error</error-severity><error-message>access denied</error-message></rpc-error></rpc-reply>]]>]]>$"
|
||||
expecteof "$clixon_netconf -qf $cfg -U wilma" 0 "<rpc><delete-config><target><startup/></target></delete-config></rpc>]]>]]>" "^<rpc-reply><rpc-error><error-type>application</error-type><error-tag>access-denied</error-tag><error-severity>error</error-severity><error-message>access denied</error-message></rpc-error></rpc-reply>]]>]]>$"
|
||||
|
||||
new2 "deny-delete-config: guest fail (restconf)"
|
||||
expecteq "$(curl -u guest:bar -sS -X DELETE http://localhost/restconf/data)" '{"ietf-restconf:errors" : {"error": {"error-type": "protocol","error-tag": "access-denied","error-severity": "error","error-message": "default deny"}}}
'
|
||||
expecteq "$(curl -u guest:bar -sS -X DELETE http://localhost/restconf/data)" '{"ietf-restconf:errors" : {"error": {"error-type": "application","error-tag": "access-denied","error-severity": "error","error-message": "default deny"}}}
'
|
||||
|
||||
# In restconf delete-config is translated to edit-config which is permitted
|
||||
new "deny-delete-config: limited fail (restconf) ok"
|
||||
|
|
@ -212,7 +212,7 @@ new "permit-edit-config: limited ok restconf"
|
|||
expecteq "$(curl -u wilma:bar -sS -X PUT -d '{"nacm-example:x": 2}' http://localhost/restconf/data/nacm-example:x)" ''
|
||||
|
||||
new2 "permit-edit-config: guest fail restconf"
|
||||
expecteq "$(curl -u guest:bar -sS -X PUT -d '{"nacm-example:x": 2}' http://localhost/restconf/data/nacm-example:x)" '{"ietf-restconf:errors" : {"error": {"error-type": "protocol","error-tag": "access-denied","error-severity": "error","error-message": "default deny"}}}
'
|
||||
expecteq "$(curl -u guest:bar -sS -X PUT -d '{"nacm-example:x": 2}' http://localhost/restconf/data/nacm-example:x)" '{"ietf-restconf:errors" : {"error": {"error-type": "application","error-tag": "access-denied","error-severity": "error","error-message": "default deny"}}}
'
|
||||
|
||||
new "Kill restconf daemon"
|
||||
sudo pkill -u www-data -f "/www-data/clixon_restconf"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue