From e40d785d5ca7b81167f9e5bc6dc119d81db83e0d Mon Sep 17 00:00:00 2001 From: Olof hagsand Date: Thu, 8 Feb 2018 15:24:05 +0700 Subject: [PATCH] * Added a "user" parameter to plugin_credentials() restconf callback. To enable authentication and in preparation for access control a la RFC 6536. * yang string length "max" keyword set to MAXPATHLEN --- CHANGELOG.md | 4 +- apps/cli/cli_generate.c | 17 +- apps/cli/cli_show.c | 2 +- apps/restconf/restconf_lib.c | 24 +- apps/restconf/restconf_lib.h | 2 +- apps/restconf/restconf_main.c | 24 +- apps/restconf/restconf_methods.c | 18 +- apps/restconf/restconf_methods.h | 3 +- lib/clixon/clixon_plugin.h | 5 +- lib/src/clixon_xml.c | 3 + yang/Makefile.in | 1 + yang/ietf-netconf-acm@2012-02-22.yang | 445 ++++++++++++++++++++++++++ 12 files changed, 514 insertions(+), 34 deletions(-) create mode 100644 yang/ietf-netconf-acm@2012-02-22.yang diff --git a/CHANGELOG.md b/CHANGELOG.md index 44f07982..25cedf06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 3.5.0 (Upcoming) ### Major changes: +* Added a "user" parameter to plugin_credentials() restconf callback. To enable authentication and in preparation for access control a la RFC 6536. * Major Restconf feature update to compy to RFC 8040. Thanks Stephen Jones for getting right. * GET well-known, top-level resource, yang library version, * PUT whole datastore, check for different keys in put lists. @@ -10,7 +11,7 @@ ### Minor changes: - +* Added RFC 6536 ietf-netconf-acm@2012-02-22.yang access control (but not implemented). * The following backward compatible options to configure have been obsoleted. If you havent already migrated this code you must do this now. * Backend startup modes prior to 3.3.3. As enabled with `configure --with-startup-compat`. Configure option CLICON_USE_STARTUP_CONFIG is also obsoleted. * Configuration files (non-XML) prior to 3.3.3. As enabled with `configure --with-config-compat`. The template clicon.conf.cpp files are also removed. @@ -25,6 +26,7 @@ * /etc/clixon.xml ### Corrected Bugs +* yang string length "max" keyword set to MAXPATHLEN * Corrected "No yang spec" printed on tty on leafref CLI usage * xml2cvec: range error (eg 1000 for int8) is not treated as error, just log and skip. diff --git a/apps/cli/cli_generate.c b/apps/cli/cli_generate.c index 3b8f2ce4..023b531d 100644 --- a/apps/cli/cli_generate.c +++ b/apps/cli/cli_generate.c @@ -238,12 +238,21 @@ yang2cli_var_sub(clicon_handle h, goto done; } } + else{ /* Cligen does not have 'max' keyword in range so need to find actual - max value of type if yang range expression is 0..max */ - if ((r = cvtype_max2str_dup(cvtype)) == NULL){ - clicon_err(OE_UNIX, errno, "cvtype_max2str"); - goto done; + max value of type if yang range expression is 0..max + */ + if (cvtype==CGV_STRING){ + if ((r = malloc(512)) == NULL){ + clicon_err(OE_UNIX, errno, "malloc"); + goto done; + } + snprintf(r, 512, "%d", MAXPATHLEN); } + else if ((r = cvtype_max2str_dup(cvtype)) == NULL){ + clicon_err(OE_UNIX, errno, "cvtype_max2str"); + goto done; + } } cprintf(cb, "%s]", r); free(r); diff --git a/apps/cli/cli_show.c b/apps/cli/cli_show.c index 0948f46b..0c3769d7 100644 --- a/apps/cli/cli_show.c +++ b/apps/cli/cli_show.c @@ -79,7 +79,7 @@ * Returns an expand-type list of commands as used by cligen 'expand' * functionality. * - * Assume callback given in a cligen spec: a ") * @param[in] h clicon handle * @param[in] name Name of this function (eg "expand_dbvar") * @param[in] cvv The command so far. Eg: cvec [0]:"a 5 b"; [1]: x=5; diff --git a/apps/restconf/restconf_lib.c b/apps/restconf/restconf_lib.c index 819ed211..c8b49512 100644 --- a/apps/restconf/restconf_lib.c +++ b/apps/restconf/restconf_lib.c @@ -296,11 +296,10 @@ readdata(FCGX_Request *r) return cb; } -typedef int (credentials_t)(clicon_handle h, FCGX_Request *r); static int nplugins = 0; static plghndl_t *plugins = NULL; -static credentials_t *p_credentials = NULL; /* Credentials callback */ +static plgcredentials_t *_credentials_fn = NULL; /* Credentials callback */ /*! Load all plugins you can find in CLICON_RESTCONF_DIR */ @@ -330,7 +329,7 @@ restconf_plugin_load(clicon_handle h) (int)strlen(filename), filename); if ((handle = plugin_load(h, filename, RTLD_NOW)) == NULL) goto quit; - p_credentials = dlsym(handle, PLUGIN_CREDENTIALS); + _credentials_fn = dlsym(handle, PLUGIN_CREDENTIALS); if ((plugins = realloc(plugins, (nplugins+1) * sizeof (*plugins))) == NULL) { clicon_err(OE_UNIX, errno, "realloc"); goto quit; @@ -385,23 +384,24 @@ restconf_plugin_start(clicon_handle h, } int -plugin_credentials(clicon_handle h, - FCGX_Request *r, - int *auth) +restconf_credentials(clicon_handle h, + FCGX_Request *r, + char **user) { int retval = -1; clicon_debug(1, "%s", __FUNCTION__); /* If no authentication callback then allow anything. Is this OK? */ - if (p_credentials == 0){ - *auth = 1; + if (_credentials_fn == NULL){ + if ((*user = strdup("none")) == NULL){ + clicon_err(OE_XML, errno, "strdup"); + goto done; + } retval = 0; goto done; } - if (p_credentials(h, r) < 0) - *auth = 0; - else - *auth = 1; + if (_credentials_fn(h, r, user) < 0) + user = NULL; retval = 0; done: return retval; diff --git a/apps/restconf/restconf_lib.h b/apps/restconf/restconf_lib.h index 2813295f..271207ff 100644 --- a/apps/restconf/restconf_lib.h +++ b/apps/restconf/restconf_lib.h @@ -56,7 +56,7 @@ cbuf *readdata(FCGX_Request *r); int restconf_plugin_load(clicon_handle h); int restconf_plugin_start(clicon_handle h, int argc, char **argv); int restconf_plugin_unload(clicon_handle h); -int plugin_credentials(clicon_handle h, FCGX_Request *r, int *auth); +int restconf_credentials(clicon_handle h, FCGX_Request *r, char **user); int get_user_cookie(char *cookiestr, char *attribute, char **val); diff --git a/apps/restconf/restconf_main.c b/apps/restconf/restconf_main.c index b386616b..b0216204 100644 --- a/apps/restconf/restconf_main.c +++ b/apps/restconf/restconf_main.c @@ -142,7 +142,8 @@ api_operations(clicon_handle h, cvec *pcvec, int pi, cvec *qvec, - char *data) + char *data, + char *username) { int retval = -1; char *request_method; @@ -153,7 +154,7 @@ api_operations(clicon_handle h, if (strcmp(request_method, "GET")==0) retval = api_operation_get(h, r, path, pcvec, pi, qvec, data); else if (strcmp(request_method, "POST")==0) - retval = api_operation_post(h, r, path, pcvec, pi, qvec, data); + retval = api_operation_post(h, r, path, pcvec, pi, qvec, data, username); else retval = notfound(r); return retval; @@ -275,7 +276,7 @@ api_restconf(clicon_handle h, cvec *pcvec = NULL; /* for rest api */ cbuf *cb = NULL; char *data; - int auth = 0; + char *username = NULL; clicon_debug(1, "%s", __FUNCTION__); path = FCGX_GetParam("REQUEST_URI", r->envp); @@ -318,20 +319,21 @@ api_restconf(clicon_handle h, retval = 0; test(r, 1); - /* If present, check credentials */ - if (plugin_credentials(h, r, &auth) < 0) + /* If present, check credentials. See "plugin_credentials" in plugin + * See RFC 8040 section 2.5 + */ + if (restconf_credentials(h, r, &username) < 0) goto done; - clicon_debug(1, "%s credentials ok auth:%d (should be 1)", - __FUNCTION__, auth); - if (auth == 0) + clicon_debug(1, "%s credentials ok username:%s (should be non-NULL)", + __FUNCTION__, username); + if (username == NULL) goto done; - clicon_debug(1, "%s credentials ok 2", __FUNCTION__); if (strcmp(method, "yang-library-version")==0) retval = api_yang_library_version(h, r); else if (strcmp(method, "data") == 0) /* restconf, skip /api/data */ retval = api_data(h, r, path, pcvec, 2, qvec, data); else if (strcmp(method, "operations") == 0) /* rpc */ - retval = api_operations(h, r, path, pcvec, 2, qvec, data); + retval = api_operations(h, r, path, pcvec, 2, qvec, data, username); else if (strcmp(method, "test") == 0) retval = test(r, 0); else @@ -348,6 +350,8 @@ api_restconf(clicon_handle h, cvec_free(pcvec); if (cb) cbuf_free(cb); + if (username) + free(username); return retval; } diff --git a/apps/restconf/restconf_methods.c b/apps/restconf/restconf_methods.c index ecec7d17..0248ee7b 100644 --- a/apps/restconf/restconf_methods.c +++ b/apps/restconf/restconf_methods.c @@ -846,7 +846,8 @@ api_operation_post(clicon_handle h, cvec *pcvec, int pi, cvec *qvec, - char *data) + char *data, + char *username) { int retval = -1; int i; @@ -869,7 +870,8 @@ api_operation_post(clicon_handle h, char *media_accept; int use_xml = 0; /* By default return JSON */ int pretty; - + cxobj *xa; + clicon_debug(1, "%s json:\"%s\" path:\"%s\"", __FUNCTION__, data, path); pretty = clicon_option_bool(h, "CLICON_RESTCONF_PRETTY"); if ((media_accept = FCGX_GetParam("HTTP_ACCEPT", r->envp)) && @@ -939,8 +941,17 @@ api_operation_post(clicon_handle h, } } } - /* Non-standard: add cookie as attribute for backend + /* Non-standard: add username attribute for backend ACM (RFC 6536) + * */ + if (username){ + if ((xa = xml_new("username", xtop, NULL)) == NULL) + goto done; + xml_type_set(xa, CX_ATTR); + if (xml_value_set(xa, username) < 0) + goto done; + } +#ifdef obsolete { cxobj *xa; char *cookie; @@ -957,6 +968,7 @@ api_operation_post(clicon_handle h, free(cookieval); } } +#endif /* Send to backend */ if (clicon_rpc_netconf_xml(h, xtop, &xret, NULL) < 0) goto done; diff --git a/apps/restconf/restconf_methods.h b/apps/restconf/restconf_methods.h index 0aef4428..659a2c6c 100644 --- a/apps/restconf/restconf_methods.h +++ b/apps/restconf/restconf_methods.h @@ -66,6 +66,7 @@ int api_operation_get(clicon_handle h, FCGX_Request *r, int api_operation_post(clicon_handle h, FCGX_Request *r, char *path, - cvec *pcvec, int pi, cvec *qvec, char *data); + cvec *pcvec, int pi, cvec *qvec, char *data, + char *username); #endif /* _RESTCONF_METHODS_H_ */ diff --git a/lib/clixon/clixon_plugin.h b/lib/clixon/clixon_plugin.h index fbb8ffce..22ef53b5 100644 --- a/lib/clixon/clixon_plugin.h +++ b/lib/clixon/clixon_plugin.h @@ -77,7 +77,10 @@ typedef int (plgexit_t)(clicon_handle); /* Plugin exit */ * Returns 0 if credentials OK, -1 if failed */ #define PLUGIN_CREDENTIALS "plugin_credentials" -typedef int (plgcredentials_t)(clicon_handle, void *); /* Plugin credentials */ +/* Plugin credentials + * username should be freed after use + */ +typedef int (plgcredentials_t)(clicon_handle, void *, char **username); /* Find a function in global namespace or a plugin. XXX clicon internal */ void *clicon_find_func(clicon_handle h, char *plugin, char *func); diff --git a/lib/src/clixon_xml.c b/lib/src/clixon_xml.c index 635498cc..75286ed2 100644 --- a/lib/src/clixon_xml.c +++ b/lib/src/clixon_xml.c @@ -1704,6 +1704,7 @@ xml_apply_ancestor(cxobj *xn, * @param[out] cvp CLIgen variable containing the parsed value * @note free cv with cv_free after use. * @see xml_body_int32 etc, for type-specific parse functions + * @note range check failure returns 0 */ int xml_body_parse(cxobj *xb, @@ -1751,6 +1752,7 @@ xml_body_parse(cxobj *xb, * alloc error. * @note extend to all other cligen var types and generalize * @note use yang type info? + * @note range check failure returns 0 */ int xml_body_int32(cxobj *xb, @@ -1774,6 +1776,7 @@ xml_body_int32(cxobj *xb, * alloc error. * @note extend to all other cligen var types and generalize * @note use yang type info? + * @note range check failure returns 0 */ int xml_body_uint32(cxobj *xb, diff --git a/yang/Makefile.in b/yang/Makefile.in index c52badc0..134f67d2 100644 --- a/yang/Makefile.in +++ b/yang/Makefile.in @@ -40,6 +40,7 @@ datarootdir = @datarootdir@ YANGSPECS = clixon-config@2017-12-27.yang YANGSPECS += ietf-netconf@2011-06-01.yang +YANGSPECS += ietf-netconf-acm@2012-02-22.yang YANGSPECS += ietf-inet-types@2013-07-15.yang APPNAME = clixon # subdir ehere these files are installed diff --git a/yang/ietf-netconf-acm@2012-02-22.yang b/yang/ietf-netconf-acm@2012-02-22.yang new file mode 100644 index 00000000..99ad961f --- /dev/null +++ b/yang/ietf-netconf-acm@2012-02-22.yang @@ -0,0 +1,445 @@ +module ietf-netconf-acm { + namespace "urn:ietf:params:xml:ns:yang:ietf-netconf-acm"; + prefix "nacm"; + import ietf-yang-types { + prefix yang; + } + organization + "IETF NETCONF (Network Configuration) Working Group"; + + contact + "WG Web: + WG List: + + WG Chair: Mehmet Ersue + + + WG Chair: Bert Wijnen + + + Editor: Andy Bierman + + + Editor: Martin Bjorklund + "; + + description + "NETCONF Access Control Model. + + Copyright (c) 2012 IETF Trust and the persons identified as + authors of the code. All rights reserved. + + Redistribution and use in source and binary forms, with or + without modification, is permitted pursuant to, and subject + to the license terms contained in, the Simplified BSD + License set forth in Section 4.c of the IETF Trust's + Legal Provisions Relating to IETF Documents + (http://trustee.ietf.org/license-info). + + This version of this YANG module is part of RFC 6536; see + the RFC itself for full legal notices."; + + revision "2012-02-22" { + description + "Initial version"; + reference + "RFC 6536: Network Configuration Protocol (NETCONF) + Access Control Model"; + } + + /* + * Extension statements + */ + + extension default-deny-write { + description + "Used to indicate that the data model node + represents a sensitive security system parameter. + + If present, and the NACM module is enabled (i.e., + /nacm/enable-nacm object equals 'true'), the NETCONF server + will only allow the designated 'recovery session' to have + write access to the node. An explicit access control rule is + required for all other users. + + The 'default-deny-write' extension MAY appear within a data + definition statement. It is ignored otherwise."; + } + + extension default-deny-all { + description + "Used to indicate that the data model node + controls a very sensitive security system parameter. + + If present, and the NACM module is enabled (i.e., + /nacm/enable-nacm object equals 'true'), the NETCONF server + will only allow the designated 'recovery session' to have + read, write, or execute access to the node. An explicit + access control rule is required for all other users. + + The 'default-deny-all' extension MAY appear within a data + definition statement, 'rpc' statement, or 'notification' + statement. It is ignored otherwise."; + } + + /* + * Derived types + */ + + typedef user-name-type { + type string { + length "1..max"; + } + description + "General Purpose Username string."; + } + + typedef matchall-string-type { + type string { + pattern "\*"; + } + description + "The string containing a single asterisk '*' is used + to conceptually represent all possible values + for the particular leaf using this data type."; + } + + typedef access-operations-type { + type bits { + bit create { + description + "Any protocol operation that creates a + new data node."; + } + bit read { + description + "Any protocol operation or notification that + returns the value of a data node."; + } + bit update { + description + "Any protocol operation that alters an existing + data node."; + } + bit delete { + description + "Any protocol operation that removes a data node."; + } + bit exec { + description + "Execution access to the specified protocol operation."; + } + } + description + "NETCONF Access Operation."; + } + + typedef group-name-type { + type string { + length "1..max"; + pattern "[^\*].*"; + } + description + "Name of administrative group to which + users can be assigned."; + } + + typedef action-type { + type enumeration { + enum permit { + description + "Requested action is permitted."; + } + enum deny { + description + "Requested action is denied."; + } + } + description + "Action taken by the server when a particular + rule matches."; + } + + typedef node-instance-identifier { + type yang:xpath1.0; + description + "Path expression used to represent a special + data node instance identifier string. + + A node-instance-identifier value is an + unrestricted YANG instance-identifier expression. + All the same rules as an instance-identifier apply + except predicates for keys are optional. If a key + predicate is missing, then the node-instance-identifier + represents all possible server instances for that key. + + This XPath expression is evaluated in the following context: + + o The set of namespace declarations are those in scope on + the leaf element where this type is used. + + o The set of variable bindings contains one variable, + 'USER', which contains the name of the user of the current + session. + + o The function library is the core function library, but + note that due to the syntax restrictions of an + instance-identifier, no functions are allowed. + + o The context node is the root node in the data tree."; + } + + /* + * Data definition statements + */ + + container nacm { + /* nacm:default-deny-all; XXX How is this parsed ?? */ + + description + "Parameters for NETCONF Access Control Model."; + + leaf enable-nacm { + type boolean; + default true; + description + "Enables or disables all NETCONF access control + enforcement. If 'true', then enforcement + is enabled. If 'false', then enforcement + is disabled."; + } + + leaf read-default { + type action-type; + default "permit"; + description + "Controls whether read access is granted if + no appropriate rule is found for a + particular read request."; + } + + leaf write-default { + type action-type; + default "deny"; + description + "Controls whether create, update, or delete access + is granted if no appropriate rule is found for a + particular write request."; + } + + leaf exec-default { + type action-type; + default "permit"; + description + "Controls whether exec access is granted if no appropriate + rule is found for a particular protocol operation request."; + } + + leaf enable-external-groups { + type boolean; + default true; + description + "Controls whether the server uses the groups reported by the + NETCONF transport layer when it assigns the user to a set of + NACM groups. If this leaf has the value 'false', any group + names reported by the transport layer are ignored by the + server."; + } + + leaf denied-operations { + type yang:zero-based-counter32; + config false; + mandatory true; + description + "Number of times since the server last restarted that a + protocol operation request was denied."; + } + + leaf denied-data-writes { + type yang:zero-based-counter32; + config false; + mandatory true; + description + "Number of times since the server last restarted that a + protocol operation request to alter + a configuration datastore was denied."; + } + + leaf denied-notifications { + type yang:zero-based-counter32; + config false; + mandatory true; + description + "Number of times since the server last restarted that + a notification was dropped for a subscription because + access to the event type was denied."; + } + + container groups { + description + "NETCONF Access Control Groups."; + + list group { + key name; + + description + "One NACM Group Entry. This list will only contain + configured entries, not any entries learned from + any transport protocols."; + + leaf name { + type group-name-type; + description + "Group name associated with this entry."; + } + + leaf-list user-name { + type user-name-type; + description + "Each entry identifies the username of + a member of the group associated with + this entry."; + } + } + } + + list rule-list { + key "name"; + ordered-by user; + description + "An ordered collection of access control rules."; + + leaf name { + type string { + length "1..max"; + } + description + "Arbitrary name assigned to the rule-list."; + } + leaf-list group { + type union { + type matchall-string-type; + type group-name-type; + } + description + "List of administrative groups that will be + assigned the associated access rights + defined by the 'rule' list. + + The string '*' indicates that all groups apply to the + entry."; + } + + list rule { + key "name"; + ordered-by user; + description + "One access control rule. + + Rules are processed in user-defined order until a match is + found. A rule matches if 'module-name', 'rule-type', and + 'access-operations' match the request. If a rule + matches, the 'action' leaf determines if access is granted + or not."; + + leaf name { + type string { + length "1..max"; + } + description + "Arbitrary name assigned to the rule."; + } + + leaf module-name { + type union { + type matchall-string-type; + type string; + } + default "*"; + description + "Name of the module associated with this rule. + + This leaf matches if it has the value '*' or if the + object being accessed is defined in the module with the + specified module name."; + } + choice rule-type { + description + "This choice matches if all leafs present in the rule + match the request. If no leafs are present, the + choice matches all requests."; + case protocol-operation { + leaf rpc-name { + type union { + type matchall-string-type; + type string; + } + description + "This leaf matches if it has the value '*' or if + its value equals the requested protocol operation + name."; + } + } + case notification { + leaf notification-name { + type union { + type matchall-string-type; + type string; + } + description + "This leaf matches if it has the value '*' or if its + value equals the requested notification name."; + } + } + case data-node { + leaf path { + type node-instance-identifier; + mandatory true; + description + "Data Node Instance Identifier associated with the + data node controlled by this rule. + + Configuration data or state data instance + identifiers start with a top-level data node. A + complete instance identifier is required for this + type of path value. + + The special value '/' refers to all possible + datastore contents."; + } + } + } + + leaf access-operations { + type union { + type matchall-string-type; + type access-operations-type; + } + default "*"; + description + "Access operations associated with this rule. + + This leaf matches if it has the value '*' or if the + bit corresponding to the requested operation is set."; + } + + leaf action { + type action-type; + mandatory true; + description + "The access control action associated with the + rule. If a rule is determined to match a + particular request, then this object is used + to determine whether to permit or deny the + request."; + } + + leaf comment { + type string; + description + "A textual description of the access rule."; + } + } + } + } +} \ No newline at end of file