From 50b37d4a27d3295a29afca2286f1a5a086142cec Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 28 Apr 2024 11:49:46 +0200 Subject: Adding upstream version 3.2.1+dfsg. Signed-off-by: Daniel Baumann --- src/modules/rlm_couchbase/mod.c | 767 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 767 insertions(+) create mode 100644 src/modules/rlm_couchbase/mod.c (limited to 'src/modules/rlm_couchbase/mod.c') diff --git a/src/modules/rlm_couchbase/mod.c b/src/modules/rlm_couchbase/mod.c new file mode 100644 index 0000000..1d4e024 --- /dev/null +++ b/src/modules/rlm_couchbase/mod.c @@ -0,0 +1,767 @@ +/* + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + */ + +/** + * $Id$ + * + * @brief Utillity functions used in the module. + * @file mod.c + * + * @author Aaron Hurt + * @copyright 2013-2014 The FreeRADIUS Server Project. + */ + +RCSID("$Id$") + +#include + +#include "mod.h" +#include "couchbase.h" +#include "jsonc_missing.h" + +/** Delete a conneciton pool handle and free related resources + * + * Destroys the underlying Couchbase connection handle freeing any related + * resources and closes the socket connection. + * + * @param chandle The connection handle to destroy. + * @return Always returns 0 (success) in all conditions. + */ +static int _mod_conn_free(rlm_couchbase_handle_t *chandle) +{ + lcb_t cb_inst = chandle->handle; /* couchbase instance */ + + /* destroy/free couchbase instance */ + lcb_destroy(cb_inst); + + /* return */ + return 0; +} + +/** Create a new connection pool handle + * + * Create a new connection to Couchbase within the pool and initialize + * information associated with the connection instance. + * + * @param ctx The connection parent context. + * @param instance The module instance. + * @return The new connection handle or NULL on error. + */ +void *mod_conn_create(TALLOC_CTX *ctx, void *instance) +{ + rlm_couchbase_t *inst = instance; /* module instance pointer */ + rlm_couchbase_handle_t *chandle = NULL; /* connection handle pointer */ + cookie_t *cookie = NULL; /* couchbase cookie */ + lcb_t cb_inst; /* couchbase connection instance */ + lcb_error_t cb_error; /* couchbase error status */ + + /* create instance */ + cb_error = couchbase_init_connection(&cb_inst, inst->server, inst->bucket, inst->password); + + /* check couchbase instance */ + if (cb_error != LCB_SUCCESS) { + ERROR("rlm_couchbase: failed to initiate couchbase connection: %s (0x%x)", + lcb_strerror(NULL, cb_error), cb_error); + /* destroy/free couchbase instance */ + lcb_destroy(cb_inst); + /* fail */ + return NULL; + } + + /* allocate memory for couchbase connection instance abstraction */ + chandle = talloc_zero(ctx, rlm_couchbase_handle_t); + talloc_set_destructor(chandle, _mod_conn_free); + + /* allocate cookie off handle */ + cookie = talloc_zero(chandle, cookie_t); + + /* init tokener error and json object */ + cookie->jerr = json_tokener_success; + cookie->jobj = NULL; + + /* populate handle */ + chandle->cookie = cookie; + chandle->handle = cb_inst; + + /* return handle struct */ + return chandle; +} + +/** Build a JSON object map from the configuration "update" section + * + * Parse the "map" section from the module configuration file and store this + * as a JSON object (key/value list) in the module instance. This map will be + * used to lookup and map attributes for all incoming accounting requests. + * + * @param conf Configuration section. + * @param instance The module instance. + * @return Returns 0 on success, -1 on error. + */ +int mod_build_attribute_element_map(CONF_SECTION *conf, void *instance) +{ + rlm_couchbase_t *inst = instance; /* our module instance */ + CONF_SECTION *cs; /* module config section */ + CONF_ITEM *ci; /* config item */ + CONF_PAIR *cp; /* conig pair */ + const char *attribute, *element; /* attribute and element names */ + + /* find update section */ + cs = cf_section_sub_find(conf, "update"); + + /* backwards compatibility */ + if (!cs) { + cs = cf_section_sub_find(conf, "map"); + WARN("rlm_couchbase: found deprecated 'map' section - please change to 'update'"); + } + + /* check section */ + if (!cs) { + ERROR("rlm_couchbase: failed to find 'update' section in config"); + /* fail */ + return -1; + } + + /* create attribute map object */ + inst->map = json_object_new_object(); + + /* parse update section */ + for (ci = cf_item_find_next(cs, NULL); ci != NULL; ci = cf_item_find_next(cs, ci)) { + /* validate item */ + if (!cf_item_is_pair(ci)) { + ERROR("rlm_couchbase: failed to parse invalid item in 'update' section"); + /* free map */ + if (inst->map) { + json_object_put(inst->map); + } + /* fail */ + return -1; + } + + /* get value pair from item */ + cp = cf_item_to_pair(ci); + + /* get pair name (attribute name) */ + attribute = cf_pair_attr(cp); + + if (!dict_attrbyname(attribute)) { + ERROR("Unknown RADIUS attribute '%s'", attribute); + return -1; + } + + /* get pair value (element name) */ + element = cf_pair_value(cp); + + /* add pair name and value */ + json_object_object_add(inst->map, attribute, json_object_new_string(element)); + + /* debugging */ + DEBUG3("rlm_couchbase: added attribute '%s' to element '%s' mapping", attribute, element); + } + + /* debugging */ + DEBUG3("rlm_couchbase: built attribute to element mapping %s", json_object_to_json_string(inst->map)); + + /* return */ + return 0; +} + +/** Map attributes to JSON element names + * + * Attempt to map the passed attribute name to the configured JSON element + * name using the JSON object map mod_build_attribute_element_map(). + * + * @param name The character name of the requested attribute. + * @param map The JSON object map to use for the lookup. + * @param buf The buffer where the given element will be stored if found. + * @return Returns 0 on success, -1 on error. + */ +int mod_attribute_to_element(const char *name, json_object *map, void *buf) +{ + json_object *jval; /* json object values */ + + /* clear buffer */ + memset((char *) buf, 0, MAX_KEY_SIZE); + + /* attempt to map attribute */ + if (json_object_object_get_ex(map, name, &jval)) { + /* copy and check size */ + if (strlcpy(buf, json_object_get_string(jval), MAX_KEY_SIZE) >= MAX_KEY_SIZE) { + /* oops ... this value is bigger than our buffer ... error out */ + ERROR("rlm_couchbase: json map value larger than MAX_KEY_SIZE - %d", MAX_KEY_SIZE); + /* return fail */ + return -1; + } + /* looks good */ + return 0; + } + + /* debugging */ + DEBUG("rlm_couchbase: skipping attribute with no map entry - %s", name); + + /* default return */ + return -1; +} + +/** Build value pairs from the passed JSON object and add to the request + * + * Parse the passed JSON object and create value pairs that will be injected into + * the given request for authorization. + * + * Example JSON document structure: + * @code{.json} + * { + * "docType": "raduser", + * "userName": "test", + * "config": { + * "SHA-Password": { + * "value": "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", + * "op": ":=" + * } + * }, + * "reply": { + * "Reply-Message": { + * "value": "Hidey Ho!", + * "op": "=" + * } + * } + * } + * @endcode + * + * @param json The JSON object representation of the user documnent. + * @param section The pair section ("config" or "reply"). + * @param request The request to which the generated pairs should be added. + */ +void *mod_json_object_to_value_pairs(json_object *json, const char *section, REQUEST *request) +{ + json_object *jobj, *jval, *jop; /* json object pointers */ + TALLOC_CTX *ctx; /* talloc context for fr_pair_make */ + VALUE_PAIR *vp, **ptr; /* value pair and value pair pointer for fr_pair_make */ + + /* assign ctx and vps for fr_pair_make based on section */ + if (strcmp(section, "config") == 0) { + ctx = request; + ptr = &(request->config); + } else if (strcmp(section, "reply") == 0) { + ctx = request->reply; + ptr = &(request->reply->vps); + } else { + /* log error - this shouldn't happen */ + RERROR("invalid section passed for fr_pair_make"); + /* return */ + return NULL; + } + + /* get config payload */ + if (json_object_object_get_ex(json, section, &jobj)) { + /* make sure we have the correct type */ + if ((jobj == NULL) || !json_object_is_type(jobj, json_type_object)) { + /* log error */ + RERROR("invalid json type for '%s' section - sections must be json objects", section); + /* reuturn */ + return NULL; + } + /* loop through object */ + json_object_object_foreach(jobj, attribute, json_vp) { + /* check for appropriate type in value and op */ + if ((jobj == NULL) || !json_object_is_type(json_vp, json_type_object)) { + /* log error */ + RERROR("invalid json type for '%s' attribute - attributes must be json objects", + attribute); + /* return */ + return NULL; + } + /* debugging */ + RDEBUG("parsing '%s' attribute: %s => %s", section, attribute, + json_object_to_json_string(json_vp)); + /* create pair from json object */ + if (json_object_object_get_ex(json_vp, "value", &jval) && + json_object_object_get_ex(json_vp, "op", &jop)) { + /* check for null before getting type */ + if (jval == NULL) return NULL; + /* make correct pairs based on json object type */ + switch (json_object_get_type(jval)) { + case json_type_double: + case json_type_int: + case json_type_string: + /* debugging */ + RDEBUG("adding '%s' attribute to '%s' section", attribute, section); + /* add pair */ + vp = fr_pair_make(ctx, ptr, attribute, json_object_get_string(jval), + fr_str2int(fr_tokens, json_object_get_string(jop), 0)); + /* check pair */ + if (!vp) { + RERROR("could not build value pair for '%s' attribute (%s)", + attribute, fr_strerror()); + /* return */ + return NULL; + } + break; + + case json_type_object: + case json_type_array: + /* log error - we want to handle these eventually */ + RERROR("skipping unhandled nested json object or array value pair object"); + break; + + default: + /* log error - this shouldn't ever happen */ + RERROR("skipping unhandled json type in value pair object"); + break; + } + } else { + /* log error */ + RERROR("failed to get 'value' or 'op' element for '%s' attribute", attribute); + } + } + /* return NULL */ + return NULL; + } + + /* debugging */ + RDEBUG("couldn't find '%s' section in json object - not adding value pairs for this section", section); + + /* return NULL */ + return NULL; +} + +/** Convert value pairs to json objects + * + * Take the passed value pair and convert it to a json-c JSON object. + * This code is heavily based on the vp_prints_value_json() function + * from src/lib/print.c. + * + * @param request The request object. + * @param vp The value pair to convert. + * @param raw_value Print all values as raw, even if enum values exist. + * @return Returns a JSON object. + */ +json_object *mod_value_pair_to_json_object(REQUEST *request, VALUE_PAIR *vp, bool raw_value) +{ + char value[255]; /* radius attribute value */ + + /* add this attribute/value pair to our json output */ + if (!vp->da->flags.has_tag) { + unsigned int i; + + switch (vp->da->type) { + case PW_TYPE_INTEGER: + i = vp->vp_integer; + goto print_int; + + case PW_TYPE_SHORT: + i = vp->vp_short; + goto print_int; + + case PW_TYPE_BYTE: + i = vp->vp_byte; + + print_int: + /* add a raw value to our json output - i.e. do not try resolve enum. + skip this if raw_value is false, and we have a value in the dictionary */ + if (!raw_value && !vp->da->flags.has_value) break; +#ifdef HAVE_JSON_OBJECT_NEW_INT64 + /* debug */ + RDEBUG3("creating new int64 for unsigned 32 bit int/byte/short '%s'", vp->da->name); + /* return as 64 bit int - JSON spec does not support unsigned ints */ + return json_object_new_int64(i); +#else + /* debug */ + RDEBUG3("creating new int for unsigned 32 bit int/byte/short '%s'", vp->da->name); + /* return as 64 bit int - JSON spec does not support unsigned ints */ + return json_object_new_int(i); +#endif + + case PW_TYPE_SIGNED: +#ifdef HAVE_JSON_OBJECT_NEW_INT64 + /* debug */ + RDEBUG3("creating new int64 for signed 32 bit integer '%s'", vp->da->name); + /* return as 64 bit int - json-c represents all ints as 64 bits internally */ + return json_object_new_int64(vp->vp_signed); +#else + RDEBUG3("creating new int for signed 32 bit integer '%s'", vp->da->name); + /* return as signed int */ + return json_object_new_int(vp->vp_signed); +#endif + + case PW_TYPE_INTEGER64: +#ifdef HAVE_JSON_OBJECT_NEW_INT64 + /* debug */ + RDEBUG3("creating new int64 for 64 bit integer '%s'", vp->da->name); + /* return as 64 bit int - because it is a 64 bit int */ + return json_object_new_int64(vp->vp_integer64); +#else + /* warning */ + RWARN("skipping 64 bit integer attribute '%s' - please upgrade json-c to 0.10+", vp->da->name); + break; +#endif + + default: + /* silence warnings - do nothing */ + break; + } + } + + /* keep going if not set above */ + switch (vp->da->type) { + case PW_TYPE_STRING: + /* debug */ + RDEBUG3("assigning string '%s' as string", vp->da->name); + /* return string value */ + return json_object_new_string(vp->vp_strvalue); + + default: + /* debug */ + RDEBUG3("assigning unhandled '%s' as string", vp->da->name); + /* get standard value */ + vp_prints_value(value, sizeof(value), vp, 0); + /* return string value from above */ + return json_object_new_string(value); + } +} + +/** Ensure accounting documents always contain a valid timestamp + * + * Inspect the given JSON object representation of an accounting document + * fetched from Couchbase and ensuse it contains a valid (non NULL) timestamp value. + * + * @param json JSON object representation of an accounting document. + * @param vps The value pairs associated with the current accounting request. + * @return Returns 0 on success, -1 on error. + */ +int mod_ensure_start_timestamp(json_object *json, VALUE_PAIR *vps) +{ + json_object *jval; /* json object value */ + struct tm tm; /* struct to hold event time */ + time_t ts = 0; /* values to hold time in seconds */ + VALUE_PAIR *vp; /* values to hold value pairs */ + char value[255]; /* store radius attribute values and our timestamp */ + + /* get our current start timestamp from our json body */ + if (json_object_object_get_ex(json, "startTimestamp", &jval) == 0) { + /* debugging ... this shouldn't ever happen */ + DEBUG("rlm_couchbase: failed to find 'startTimestamp' in current json body"); + /* return */ + return -1; + } + + /* check for null value */ + if (json_object_get_string(jval) != NULL) { + /* already set - nothing left to do */ + return 0; + } + + /* get current event timestamp */ + if ((vp = fr_pair_find_by_num(vps, PW_EVENT_TIMESTAMP, 0, TAG_ANY)) != NULL) { + /* get seconds value from attribute */ + ts = vp->vp_date; + } else { + /* debugging */ + DEBUG("rlm_couchbase: failed to find event timestamp in current request"); + /* return */ + return -1; + } + + /* clear value */ + memset(value, 0, sizeof(value)); + + /* get elapsed session time */ + if ((vp = fr_pair_find_by_num(vps, PW_ACCT_SESSION_TIME, 0, TAG_ANY)) != NULL) { + /* calculate diff */ + ts = (ts - vp->vp_integer); + /* calculate start time */ + size_t length = strftime(value, sizeof(value), "%b %e %Y %H:%M:%S %Z", localtime_r(&ts, &tm)); + /* check length */ + if (length > 0) { + /* debugging */ + DEBUG("rlm_couchbase: calculated start timestamp: %s", value); + /* store new value in json body */ + json_object_object_add(json, "startTimestamp", json_object_new_string(value)); + } else { + /* debugging */ + DEBUG("rlm_couchbase: failed to format calculated timestamp"); + /* return */ + return -1; + } + } + + /* default return */ + return 0; +} + +/** Handle client value processing for client_map_section() + * + * @param out Character output + * @param cp Configuration pair + * @param data The client data + * @return Returns 0 on success, -1 on error. + */ +static int _get_client_value(char **out, CONF_PAIR const *cp, void *data) +{ + json_object *jval; + + if (!json_object_object_get_ex((json_object *)data, cf_pair_value(cp), &jval)) { + *out = NULL; + return 0; + } + + if (!jval) return -1; + + *out = talloc_strdup(NULL, json_object_get_string(jval)); + if (!*out) return -1; + + return 0; +} + +/** Load client entries from Couchbase client documents on startup + * + * This function executes the view defined in the module configuration and loops + * through all returned rows. The view is called with "stale=false" to ensure the + * most accurate data available when the view is called. This will force an index + * rebuild on this design document in Couchbase. However, since this function is only + * run once at sever startup this should not be a concern. + * + * @param inst The module instance. + * @param tmpl Default values for new clients. + * @param map The client attribute configuration section. + * @return Returns 0 on success, -1 on error. + */ +int mod_load_client_documents(rlm_couchbase_t *inst, CONF_SECTION *tmpl, CONF_SECTION *map) +{ + rlm_couchbase_handle_t *handle = NULL; /* connection pool handle */ + char vpath[256], vid[MAX_KEY_SIZE], vkey[MAX_KEY_SIZE]; /* view path and fields */ + char error[512]; /* view error return */ + size_t idx = 0; /* row array index counter */ + int retval = 0; /* return value */ + lcb_error_t cb_error = LCB_SUCCESS; /* couchbase error holder */ + json_object *json, *jval; /* json object holders */ + json_object *jrows = NULL; /* json object to hold view rows */ + CONF_SECTION *client; /* freeradius config section */ + RADCLIENT *c; /* freeradius client */ + int slen; + + /* get handle */ + handle = fr_connection_get(inst->pool); + + /* check handle */ + if (!handle) return -1; + + /* set couchbase instance */ + lcb_t cb_inst = handle->handle; + + /* set cookie */ + cookie_t *cookie = handle->cookie; + + /* build view path */ + slen = snprintf(vpath, sizeof(vpath), "%s?stale=false", inst->client_view); + if (slen >= (int) sizeof(vpath) || slen < 0) { + ERROR("rlm_couchbase: view path too long"); + retval=-1; + goto free_and_return; + } + + + /* query view for document */ + cb_error = couchbase_query_view(cb_inst, cookie, vpath, NULL); + + /* check error and object */ + if (cb_error != LCB_SUCCESS || cookie->jerr != json_tokener_success || !cookie->jobj) { + /* log error */ + ERROR("rlm_couchbase: failed to execute view request or parse return"); + /* set return */ + retval = -1; + /* return */ + goto free_and_return; + } + + /* debugging */ + DEBUG3("rlm_couchbase: cookie->jobj == %s", json_object_to_json_string(cookie->jobj)); + + /* check for error in json object */ + if (json_object_object_get_ex(cookie->jobj, "error", &json)) { + /* build initial error buffer */ + strlcpy(error, json_object_get_string(json), sizeof(error)); + /* get error reason */ + if (json_object_object_get_ex(cookie->jobj, "reason", &json)) { + /* append divider */ + strlcat(error, " - ", sizeof(error)); + /* append reason */ + strlcat(error, json_object_get_string(json), sizeof(error)); + } + /* log error */ + ERROR("rlm_couchbase: view request failed with error: %s", error); + /* set return */ + retval = -1; + /* return */ + goto free_and_return; + } + + /* check for document id in return */ + if (!json_object_object_get_ex(cookie->jobj, "rows", &json)) { + /* log error */ + ERROR("rlm_couchbase: failed to fetch rows from view payload"); + /* set return */ + retval = -1; + /* return */ + goto free_and_return; + } + + /* get and hold rows */ + jrows = json_object_get(json); + + /* free cookie object */ + if (cookie->jobj) { + json_object_put(cookie->jobj); + cookie->jobj = NULL; + } + + /* debugging */ + DEBUG3("rlm_couchbase: jrows == %s", json_object_to_json_string(jrows)); + + /* check for valid row value */ + if ((jrows == NULL) || !json_object_is_type(jrows, json_type_array) || json_object_array_length(jrows) < 1) { + /* log error */ + ERROR("rlm_couchbase: no valid rows returned from view: %s", vpath); + /* set return */ + retval = -1; + /* return */ + goto free_and_return; + } + + /* loop across all row elements */ + for (idx = 0; idx < (size_t)json_object_array_length(jrows); idx++) { + /* fetch current index */ + json = json_object_array_get_idx(jrows, idx); + + /* get view id */ + if (json_object_object_get_ex(json, "id", &jval)) { + /* clear view id */ + memset(vid, 0, sizeof(vid)); + /* copy and check length */ + if (strlcpy(vid, json_object_get_string(jval), sizeof(vid)) >= sizeof(vid)) { + ERROR("rlm_couchbase: id from row longer than MAX_KEY_SIZE (%d)", + MAX_KEY_SIZE); + continue; + } + } else { + WARN("rlm_couchbase: failed to fetch id from row - skipping"); + continue; + } + + /* get view key */ + if (json_object_object_get_ex(json, "key", &jval)) { + /* clear view key */ + memset(vkey, 0, sizeof(vkey)); + /* copy and check length */ + if (strlcpy(vkey, json_object_get_string(jval), sizeof(vkey)) >= sizeof(vkey)) { + ERROR("rlm_couchbase: key from row longer than MAX_KEY_SIZE (%d)", + MAX_KEY_SIZE); + continue; + } + } else { + WARN("rlm_couchbase: failed to fetch key from row - skipping"); + continue; + } + + /* fetch document */ + cb_error = couchbase_get_key(cb_inst, cookie, vid); + + /* check error and object */ + if (cb_error != LCB_SUCCESS || cookie->jerr != json_tokener_success || !cookie->jobj) { + /* log error */ + ERROR("rlm_couchbase: failed to execute get request or parse return"); + /* set return */ + retval = -1; + /* return */ + goto free_and_return; + } + + /* debugging */ + DEBUG3("rlm_couchbase: cookie->jobj == %s", json_object_to_json_string(cookie->jobj)); + + /* allocate conf section */ + client = tmpl ? cf_section_dup(NULL, tmpl, "client", vkey, true) : + cf_section_alloc(NULL, "client", vkey); + + if (client_map_section(client, map, _get_client_value, cookie->jobj) < 0) { + /* free config setion */ + talloc_free(client); + /* set return */ + retval = -1; + /* return */ + goto free_and_return; + } + + /* + * @todo These should be parented from something. + */ + c = client_afrom_cs(NULL, client, false, false); + if (!c) { + ERROR("rlm_couchbase: failed to allocate client"); + /* free config setion */ + talloc_free(client); + /* set return */ + retval = -1; + /* return */ + goto free_and_return; + } + + /* + * Client parents the CONF_SECTION which defined it. + */ + talloc_steal(c, client); + + /* attempt to add client */ + if (!client_add(NULL, c)) { + ERROR("rlm_couchbase: failed to add client '%s' from '%s', possible duplicate?", vkey, vid); + /* free client */ + client_free(c); + /* set return */ + retval = -1; + /* return */ + goto free_and_return; + } + + /* debugging */ + DEBUG("rlm_couchbase: client '%s' added", c->longname); + + /* free json object */ + if (cookie->jobj) { + json_object_put(cookie->jobj); + cookie->jobj = NULL; + } + } + + free_and_return: + + /* free rows */ + if (jrows) { + json_object_put(jrows); + } + + /* free json object */ + if (cookie->jobj) { + json_object_put(cookie->jobj); + cookie->jobj = NULL; + } + + /* release handle */ + if (handle) { + fr_connection_release(inst->pool, handle); + } + + /* return */ + return retval; +} -- cgit v1.2.3