diff --git a/CHANGELOG.md b/CHANGELOG.md index 49759dea..9e50f925 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,6 +62,9 @@ Users may have to change how they access the system * New clixon-lib@2020-12-30.yang revision * Changed: RPC process-control output parameter status to pid * New clixon-config@2020-12-30.yang revision + * Added CLICON_ANONYMOUS_USER + * Only applies to restconf + * used to be hardcoded as "none", now default value is "anonymous" * Removed obsolete RESTCONF and SSL options (CLICON_SSL_* and CLICON_RESTCONF_IP*/HTTP*) * Removed obsolete: CLICON_TRANSACTION_MOD option * Marked as obsolete: CLICON_RESTCONF_PATH CLICON_RESTCONF_PRETTY diff --git a/apps/restconf/restconf_lib.c b/apps/restconf/restconf_lib.c index da756d42..4be5e0a3 100644 --- a/apps/restconf/restconf_lib.c +++ b/apps/restconf/restconf_lib.c @@ -457,7 +457,7 @@ restconf_drop_privileges(clicon_handle h, return -1; } if (group_name2gid(group, &gid) < 0){ - clicon_log(LOG_ERR, "'%s' does not seem to be a valid user group.\n" /* \n required here due to multi-line log */ + clicon_log(LOG_ERR, "'%s' does not seem to be a valid user group." /* \n required here due to multi-line log */ "The config demon requires a valid group to create a server UNIX socket\n" "Define a valid CLICON_SOCK_GROUP in %s or via the -g option\n" "or create the group and add the user to it. Check documentation for how to do this on your platform", @@ -516,24 +516,30 @@ restconf_authentication_cb(clicon_handle h, char *username = NULL; cxobj *xret = NULL; cxobj *xerr; + char *anonymous = NULL; auth_type = restconf_auth_type_get(h); clicon_debug(1, "%s auth-type:%s", __FUNCTION__, clixon_auth_type_int2str(auth_type)); ret = 0; authenticated = 0; - if (auth_type != CLIXON_AUTH_NONE) - if ((ret = clixon_plugin_auth_all(h, req, - auth_type, - &authenticated, - &username)) < 0) - goto done; + /* ret: -1 Error, 0: Ignore/not handled, 1: OK see authenticated parameter */ + if ((ret = clixon_plugin_auth_all(h, req, + auth_type, + &authenticated, + &username)) < 0) + goto done; if (ret == 1){ /* OK, tag username to handle */ - clicon_username_set(h, username); + if (authenticated == 1) + clicon_username_set(h, username); } else { /* Default behaviour */ switch (auth_type){ case CLIXON_AUTH_NONE: - clicon_username_set(h, "none"); + /* if not handled by callback, use anonymous user */ + if ((anonymous = clicon_option_str(h, "CLICON_ANONYMOUS_USER")) == NULL){ + break; /* not authenticated */ + } + clicon_username_set(h, anonymous); authenticated = 1; break; case CLIXON_AUTH_CLIENT_CERTIFICATE: { diff --git a/apps/restconf/restconf_main_evhtp.c b/apps/restconf/restconf_main_evhtp.c index 541ba13c..5aab0b47 100644 --- a/apps/restconf/restconf_main_evhtp.c +++ b/apps/restconf/restconf_main_evhtp.c @@ -451,6 +451,7 @@ static void cx_path_wellknown(evhtp_request_t *req, void *arg) { + int retval = -1; cx_evhtp_handle *eh = (cx_evhtp_handle*)arg; clicon_handle h = eh->eh_h; int ret; @@ -472,17 +473,32 @@ cx_path_wellknown(evhtp_request_t *req, /* Clear (fcgi) paramaters from this request */ if (restconf_param_del_all(h) < 0) goto done; + retval = 0; done: + /* Catch all on fatal error. This does not terminate the process but closes request stream */ + if (retval < 0) + evhtp_send_reply(req, EVHTP_RES_ERROR); return; /* void */ } -/*! /restconf callback +/*! Callback for each incoming http request for path / + * + * This are all messages except /.well-known, Registered with evhtp_set_cb + * + * @param[in] req evhtp request structure defining the incoming message + * @param[in] arg cx_evhtp handle clixon specific fields + * @retval void + * Discussion: problematic if fatal error -1 is returneod from clixon routines + * without actually terminating. Consider: + * 1) sending some error? and/or + * 2) terminating the process? * @see cx_genb */ static void cx_path_restconf(evhtp_request_t *req, void *arg) { + int retval = -1; cx_evhtp_handle *eh = (cx_evhtp_handle*)arg; clicon_handle h = eh->eh_h; int ret; @@ -505,13 +521,17 @@ cx_path_restconf(evhtp_request_t *req, if (ret == 1){ /* call generic function */ if (api_root_restconf(h, req, qvec) < 0) - goto done; + goto done; } /* Clear (fcgi) paramaters from this request */ if (restconf_param_del_all(h) < 0) goto done; + retval = 0; done: + /* Catch all on fatal error. This does not terminate the process but closes request stream */ + if (retval < 0) + evhtp_send_reply(req, EVHTP_RES_ERROR); if (qvec) cvec_free(qvec); return; /* void */ diff --git a/example/main/example_restconf.c b/example/main/example_restconf.c index 39240228..2f5908fe 100644 --- a/example/main/example_restconf.c +++ b/example/main/example_restconf.c @@ -52,8 +52,6 @@ #include /* minor use */ /* Command line options to be passed to getopt(3) - * -a basic authentication - * -s ssl client certificates */ #define RESTCONF_EXAMPLE_OPTS "" @@ -194,16 +192,16 @@ b64_decode(const char *src, * @param[out] authp 0: Credentials failed, no user set (401 returned). 1: Credentials OK and user set * @param[out] userp If retval is OK and auth=1, the associated user, malloced by plugin * @retval -1 Fatal error - * @retval 0 OK, see auth parameter on result. + * @retval 0 Ignore, undecided, not handled, same as no callback + * @retval 1 OK, see auth parameter on result. * @note user should be malloced * @note: Three hardwired users: andy, wilma, guest w password "bar". - * Enabled by passing -- -a to the main function */ static int -example_basic_auth(clicon_handle h, - void *req, - int *authp, - char **userp) +example_basic_auth(clicon_handle h, + void *req, + int *authp, + char **userp) { int retval = -1; cxobj *xt = NULL; @@ -242,7 +240,7 @@ example_basic_auth(clicon_handle h, *passwd = '\0'; passwd++; clicon_debug(1, "%s http user:%s passwd:%s", __FUNCTION__, user, passwd); - /* Here get auth sub-tree whjere all the users are */ + /* Here get auth sub-tree where all the users are */ if ((cb = cbuf_new()) == NULL) goto done; /* XXX Three hardcoded user/passwd (from RFC8341 A.1)*/ @@ -252,11 +250,10 @@ example_basic_auth(clicon_handle h, } if (strcmp(passwd, passwd2)) goto fail; - /* authenticated */ - *userp = user; + *userp = user; /* authenticated */ user=NULL; /* to avoid free below */ *authp = 1; - retval = 0; + retval = 1; done: /* error */ clicon_debug(1, "%s retval:%d authp:%d userp:%s", __FUNCTION__, retval, *authp, *userp); if (user) @@ -268,7 +265,78 @@ example_basic_auth(clicon_handle h, return retval; fail: /* unauthenticated */ *authp = 0; - retval = 0; + retval = 1; + goto done; +} + +/*! HTTP "no auth" but uses basic authentication to get a user + * @param[in] h Clicon handle + * @param[in] req Per-message request www handle to use with restconf_api.h + * @param[out] authp 0: Credentials failed, no user set (401 returned). 1: Credentials OK and user set + * @param[out] userp If retval is OK and auth=1, the associated user, malloced by plugin + * @retval -1 Fatal error + * @retval 0 Ignore, undecided, not handled, same as no callback + * @retval 1 OK, see auth parameter on result. + * @note user should be malloced + */ +static int +example_no_auth(clicon_handle h, + void *req, + int *authp, + char **userp) +{ + int retval = -1; + cxobj *xt = NULL; + char *user = NULL; + cbuf *cb = NULL; + char *auth; + char *passwd; + size_t authlen; + int ret; + + clicon_debug(1, "%s", __FUNCTION__); + if (authp == NULL || userp == NULL){ + clicon_err(OE_PLUGIN, EINVAL, "Output parameter is NULL"); + goto done; + } + /* At this point in the code we must use HTTP basic authentication */ + if ((auth = restconf_param_get(h, "HTTP_AUTHORIZATION")) == NULL) + goto fail; + if (strlen(auth) < strlen("Basic ")) + goto fail; + if (strncmp("Basic ", auth, strlen("Basic "))) + goto fail; + auth += strlen("Basic "); + authlen = strlen(auth)*2; + if ((user = malloc(authlen)) == NULL){ + clicon_err(OE_UNIX, errno, "malloc"); + goto done; + } + memset(user, 0, authlen); + if ((ret = b64_decode(auth, user, authlen)) < 0) + goto done; + /* auth string is on the format user:passwd */ + if ((passwd = index(user,':')) == NULL) + goto fail; + *passwd = '\0'; + passwd++; + clicon_debug(1, "%s http user:%s passwd:%s", __FUNCTION__, user, passwd); + *userp = user; /* authenticated */ + user=NULL; /* to avoid free below */ + *authp = 1; + retval = 1; + done: /* error */ + clicon_debug(1, "%s retval:%d authp:%d userp:%s", __FUNCTION__, retval, *authp, *userp); + if (user) + free(user); + if (cb) + cbuf_free(cb); + if (xt) + xml_free(xt); + return retval; + fail: /* unauthenticated */ + *authp = 0; + retval = 0; /* Ignore use anonymous */ goto done; } @@ -295,16 +363,15 @@ example_restconf_credentials(clicon_handle h, clicon_debug(1, "%s auth:%s", __FUNCTION__, clixon_auth_type_int2str(auth_type)); switch (auth_type){ case CLIXON_AUTH_NONE: - /* Shouldnt happen */ - retval = 0; /* Ignore, shouldnt happen */ + if ((retval = example_no_auth(h, req, authp, userp)) < 0) + goto done; break; case CLIXON_AUTH_CLIENT_CERTIFICATE: retval = 0; /* Ignore, use default */ break; case CLIXON_AUTH_USER: - if (example_basic_auth(h, req, authp, userp) < 0) + if ((retval = example_basic_auth(h, req, authp, userp)) < 0) goto done; - retval = 1; break; } done: @@ -369,8 +436,6 @@ static clixon_plugin_api api = { * @retval NULL Error with clicon_err set * @retval api Pointer to API struct * Arguments are argc/argv after -- - * Currently defined: -a enable http basic authentication - * @note There are three hardwired users andy, wilma and guest from RFC8341 A.1 */ clixon_plugin_api * clixon_plugin_init(clicon_handle h) diff --git a/test/test_restconf.sh b/test/test_restconf.sh index adb46c36..e3819946 100755 --- a/test/test_restconf.sh +++ b/test/test_restconf.sh @@ -89,7 +89,7 @@ EOF ) fi -# Start with common config, then append fcgi/evhtp specific config +# Clixon config cat < $cfg $cfg @@ -149,7 +149,6 @@ function testrun() stop_restconf_pre new "start restconf daemon" - echo "cfg:$cfg" start_restconf -f $cfg fi diff --git a/test/test_restconf_basic_auth.sh b/test/test_restconf_basic_auth.sh new file mode 100755 index 00000000..1f14120c --- /dev/null +++ b/test/test_restconf_basic_auth.sh @@ -0,0 +1,318 @@ +#!/usr/bin/env bash +# Restconf basic authentication tests as implemented by main example +# Note this is not supported by core clixon: you need ca-auth callback implemented a la the example +# For auth-type=none and auth-type=user, +# For auth-type=ssl-certs, See test_restconf.sh test_restconf_ssl_certs.sh +# evhtp? and http only +# Use the following user settings: +# 1. none (eg no -u to curl) +# 2. anonymous - the registered anonymous user +# 3. andy - a well-known user +# 3. unknown - unknown user +# Use NACM to return XML for different returns for anonymous and andy + +# Magic line must be first in script (see README.md) +s="$_" ; . ./lib.sh || if [ "$s" = $0 ]; then exit 0; else return 0; fi + +APPNAME=example + +# Common NACM scripts +. ./nacm.sh + +cfg=$dir/conf.xml + +# The anonymous user +anonymous=myanonymous + +fyang=$dir/myexample.yang + +# No ssl +RCPROTO=http + +# Start with common config, then append fcgi/evhtp specific config +cat < $cfg + + $cfg + ietf-netconf:startup + /usr/local/share/clixon + $IETFRFC + $fyang + /usr/local/lib/$APPNAME/clispec + /usr/local/lib/$APPNAME/backend + example_backend.so$ + /usr/local/lib/$APPNAME/restconf + /usr/local/lib/$APPNAME/cli + $APPNAME + /usr/local/var/$APPNAME/$APPNAME.sock + /usr/local/var/$APPNAME/$APPNAME.pidfile + $dir + true + internal + $anonymous + +EOF + +# Start with common config, then append fcgi/evhtp specific config +cat < $cfg + + $cfg + ietf-netconf:startup + /usr/local/share/clixon + $IETFRFC + $fyang + /usr/local/lib/$APPNAME/clispec + /usr/local/lib/$APPNAME/backend + example_backend.so$ + /usr/local/lib/$APPNAME/restconf + /usr/local/lib/$APPNAME/cli + $APPNAME + /usr/local/var/$APPNAME/$APPNAME.sock + /usr/local/var/$APPNAME/$APPNAME.pidfile + $dir + true + internal + $anonymous + +EOF + +# There are two implicit modules defined by RFC 8341 +# This is a try to define them +cat < $fyang +module myexample{ + yang-version 1.1; + namespace "urn:example:auth"; + import ietf-netconf-acm { + prefix nacm; + } + prefix ex; + container top { + leaf anonymous{ + type string; + } + leaf wilma { + type string; + } + } +} +EOF + +# NACM rules and top/ config +cat < $dir/startup_db + + + true + deny + deny + deny + + + anonymous + $anonymous + + + limited + wilma + + + admin + root + $USER + + + + data-anon + anonymous + + allow-get + ietf-netconf + get + exec + permit + + + allow-anon + myexample + * + /ex:top/ex:anonymous + permit + + + + data-limited + limited + + allow-get + ietf-netconf + get + exec + permit + + + allow-wilma + myexample + * + /ex:top/ex:wilma + permit + + + + $NADMIN + + + + 42 + 71 + + +EOF + +# Restconf auth test with arguments: +# 1. auth-type +# 2: -u user:passwd or "" +# 3: expectcode expected HTTP return code +# 4: expectmsg top return JSON message +# The return cases are: authentication permit/deny, authorization permit/deny +# We use authorization returns here only to verify we got the right user in authentication. +# Authentication ok/nok +# permit: 200 or 403 +# deny: 401 +# The user replies are: +# $anonymous: {"myexample:top":{"anonymous":"42"}} +# wilma: {"myexample:top":{"wilma":"71"}} +# unknown: retval 403 +function testrun() +{ + auth=$1 + user=$2 + expectcode=$3 + expectmsg=$4 + +# echo "auth:$auth" +# echo "user:$user" +# echo "expectcode:$expectcode" +# echo "expectmsg:$expectmsg" + + # Change restconf configuration before start restconf daemon + restconf_config $auth false + + # Start with common config, then append fcgi/evhtp specific config + cat < $cfg + + $cfg + ietf-netconf:startup + /usr/local/share/clixon + $IETFRFC + $fyang + /usr/local/lib/$APPNAME/clispec + /usr/local/lib/$APPNAME/backend + example_backend.so$ + /usr/local/lib/$APPNAME/restconf + /usr/local/lib/$APPNAME/cli + $APPNAME + /usr/local/var/$APPNAME/$APPNAME.sock + /usr/local/var/$APPNAME/$APPNAME.pidfile + $dir + true + internal + $anonymous + $RESTCONFIG + +EOF + + if [ $RC -ne 0 ]; then + new "kill old restconf daemon" + stop_restconf_pre + + new "start restconf daemon" + start_restconf -f $cfg + + new "wait restconf" + wait_restconf + fi + + new "curl $CURLOPTS $user -X GET $RCPROTO://localhost/restconf/data/myexample:top" + expectpart "$(curl $CURLOPTS $user -X GET $RCPROTO://localhost/restconf/data/myexample:top)" 0 $expectcode "$expectmsg" + + if [ $RC -ne 0 ]; then + new "Kill restconf daemon" + stop_restconf + fi +} + +if [ $BE -ne 0 ]; then + new "kill old backend" + sudo clixon_backend -zf $cfg + if [ $? -ne 0 ]; then + err + fi + sudo pkill -f clixon_backend # to be sure + + new "start backend -s startup -f $cfg" + start_backend -s startup -f $cfg + + new "wait backend" + wait_backend +fi + +MSGANON='{"myexample:top":{"anonymous":"42"}}' +MSGWILMA='{"myexample:top":{"wilma":"71"}}' +# Authentication failed: +MSGERR1='{"ietf-restconf:errors":{"error":{"error-type":"protocol","error-tag":"access-denied","error-severity":"error","error-message":"The requested URL was unauthorized"}}}' +# Authentication OK Authorization failed: +MSGERR2='{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"access-denied","error-severity":"error","error-message":"default deny"}}}' + +AUTH=none + +new "auth-type=$AUTH no user" +testrun $AUTH "" "HTTP/1.1 200 OK" "$MSGANON" # OK - anonymous + +new "auth-type=$AUTH anonymous" +testrun $AUTH "-u ${anonymous}:foo" "HTTP/1.1 200 OK" "$MSGANON" # OK - anonymous + +new "auth-type=$AUTH wilma" +testrun $AUTH "-u wilma:bar" "HTTP/1.1 200 OK" "$MSGWILMA" # OK - wilma + +new "auth-type=$AUTH wilma wrong passwd" +testrun $AUTH "-u wilma:wrong" "HTTP/1.1 200 OK" "$MSGWILMA" # OK - wilma + +new "auth-type=$AUTH unknown" +testrun $AUTH "-u unknown:any" "HTTP/1.1 403 Forbidden" "$MSGERR2" # OK, but nacm authorization fail + + +AUTH=user + +new "auth-type=$AUTH no user" +testrun $AUTH "" "HTTP/1.1 401 Unauthorized" "$MSGERR1" # denied + +new "auth-type=$AUTH anonymous" +testrun $AUTH "-u ${anonymous}:foo" "HTTP/1.1 401 Unauthorized" "$MSGERR1" # denied + +new "auth-type=$AUTH wilma" +testrun $AUTH "-u wilma:bar" "HTTP/1.1 200 OK" "$MSGWILMA" # OK - wilma + +new "auth-type=$AUTH wilma wrong passwd" +testrun $AUTH "-u wilma:wrong" "HTTP/1.1 401 Unauthorized" "$MSGERR1" # denied + +new "auth-type=$AUTH unknown" +testrun $AUTH "-u unknown:any" "HTTP/1.1 401 Unauthorized" "$MSGERR1" # denied + +if [ $BE -ne 0 ]; then + new "Kill backend" + # Check if premature kill + pid=$(pgrep -u root -f clixon_backend) + if [ -z "$pid" ]; then + err "backend already dead" + fi + # kill backend + stop_backend -f $cfg +fi + +# unset conditional parameters +unset RCPROTO +unset RESTCONFIG1 +unset MSGANON +unset MSGWILMA +unset MSGERR1 +unset MSGERR2 + +rm -rf $dir diff --git a/yang/clixon/clixon-config@2020-12-30.yang b/yang/clixon/clixon-config@2020-12-30.yang index e80dc036..6e5abb96 100644 --- a/yang/clixon/clixon-config@2020-12-30.yang +++ b/yang/clixon/clixon-config@2020-12-30.yang @@ -45,7 +45,9 @@ module clixon-config { revision 2020-12-30 { description - "Removed obsolete options: + "Added option: + CLICON_ANONYMOUS_USER + Removed obsolete options: CLICON_RESTCONF_IPV4_ADDR CLICON_RESTCONF_IPV6_ADDR CLICON_RESTCONF_HTTP_PORT @@ -787,6 +789,14 @@ module clixon-config { type startup_mode; description "Which method to boot/start clicon backend"; } + leaf CLICON_ANONYMOUS_USER { + type string; + default "anonymous"; + description + "Name of anonymous user. + The current only case where such a user is used is in RESTCONF authentication when + auth-type=none and no known user is known."; + } leaf CLICON_NACM_MODE { type nacm_mode; default disabled;