2 * This program is free software; you can redistribute it and/or modify
3 * it under the terms of the GNU General Public License as published by
4 * the Free Software Foundation; either version 2 of the License, or
5 * (at your option) any later version.
7 * This program is distributed in the hope that it will be useful,
8 * but WITHOUT ANY WARRANTY; without even the implied warranty of
9 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 * GNU General Public License for more details.
12 * You should have received a copy of the GNU General Public License
13 * along with this program; if not, write to the Free Software
14 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
20 * @brief Integrate FreeRADIUS with the Couchbase document database.
21 * @file rlm_couchbase.c
23 * @copyright 2013-2014 Aaron Hurt <ahurt@anbcs.com>
28 #include <freeradius-devel/radiusd.h>
29 #include <freeradius-devel/libradius.h>
30 #include <freeradius-devel/modules.h>
31 #include <freeradius-devel/rad_assert.h>
33 #include <libcouchbase/couchbase.h>
34 #include <json/json.h>
37 #include "couchbase.h"
38 #include "jsonc_missing.h"
41 * Module Configuration
43 static const CONF_PARSER module_config[] = {
44 { "acct_key", FR_CONF_OFFSET(PW_TYPE_STRING, rlm_couchbase_t, acct_key), "radacct_%{%{Acct-Unique-Session-Id}:-%{Acct-Session-Id}}" },
45 { "doctype", FR_CONF_OFFSET(PW_TYPE_STRING, rlm_couchbase_t, doctype), "radacct" },
46 { "server", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_REQUIRED, rlm_couchbase_t, server), NULL },
47 { "bucket", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_REQUIRED, rlm_couchbase_t, bucket), NULL },
48 { "password", FR_CONF_OFFSET(PW_TYPE_STRING, rlm_couchbase_t, password), NULL },
49 { "expire", FR_CONF_OFFSET(PW_TYPE_INTEGER, rlm_couchbase_t, expire), 0 },
50 { "user_key", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_REQUIRED, rlm_couchbase_t, user_key), "raduser_%{md5:%{tolower:%{%{Stripped-User-Name}:-%{User-Name}}}}" },
51 {NULL, -1, 0, NULL, NULL} /* end the list */
54 /* initialize couchbase connection */
55 static int mod_instantiate(CONF_SECTION *conf, void *instance) {
56 static bool version_done;
58 rlm_couchbase_t *inst = instance; /* our module instance */
62 INFO("rlm_couchbase: liblcouchbase version: %s", lcb_get_version(NULL));
70 len = talloc_array_length(inst->server_raw);
71 server = p = talloc_array(inst, char, len);
72 for (i = 0; i < len; i++) {
73 switch (inst->server_raw[i]) {
77 /* Consume multiple separators occurring in sequence */
78 if (sep == true) continue;
86 *p++ = inst->server_raw[i];
92 inst->server = server;
96 if (mod_build_attribute_element_map(conf, inst) != 0) {
101 /* initiate connection pool */
102 inst->pool = fr_connection_pool_init(conf, inst, mod_conn_create, mod_conn_alive, mod_conn_delete, NULL);
104 /* check connection pool */
106 ERROR("rlm_couchbase: failed to initiate connection pool");
115 /* authorize users via couchbase */
116 static rlm_rcode_t CC_HINT(nonnull) mod_authorize(void *instance, REQUEST *request) {
117 rlm_couchbase_t *inst = instance; /* our module instance */
118 void *handle = NULL; /* connection pool handle */
119 char dockey[MAX_KEY_SIZE]; /* our document key */
120 lcb_error_t cb_error = LCB_SUCCESS; /* couchbase error holder */
122 /* assert packet as not null */
123 rad_assert(request->packet != NULL);
125 /* attempt to build document key */
126 if (radius_xlat(dockey, sizeof(dockey), request, inst->user_key, NULL, NULL) < 0) {
128 RERROR("could not find user key attribute (%s) in packet", inst->user_key);
130 return RLM_MODULE_FAIL;
134 handle = fr_connection_get(inst->pool);
137 if (!handle) return RLM_MODULE_FAIL;
139 /* set handle pointer */
140 rlm_couchbase_handle_t *handle_t = handle;
142 /* set couchbase instance */
143 lcb_t cb_inst = handle_t->handle;
146 cookie_t *cookie = handle_t->cookie;
151 memset(cookie, 0, sizeof(cookie_t));
154 RERROR("cookie not usable - possibly not allocated");
155 /* free connection */
157 fr_connection_release(inst->pool, handle);
160 return RLM_MODULE_FAIL;
163 /* reset cookie error status */
164 cookie->jerr = json_tokener_success;
167 cb_error = couchbase_get_key(cb_inst, cookie, dockey);
170 if (cb_error != LCB_SUCCESS || cookie->jerr != json_tokener_success || cookie->jobj == NULL) {
172 RERROR("failed to fetch document or parse return");
173 /* free json object */
175 json_object_put(cookie->jobj);
179 fr_connection_release(inst->pool, handle);
182 return RLM_MODULE_FAIL;
186 RDEBUG("parsed user document == %s", json_object_to_json_string(cookie->jobj));
188 /* inject config value pairs defined in this json oblect */
189 mod_json_object_to_value_pairs(cookie->jobj, "config", request);
191 /* inject reply value pairs defined in this json oblect */
192 mod_json_object_to_value_pairs(cookie->jobj, "reply", request);
194 /* free json object */
196 json_object_put(cookie->jobj);
201 fr_connection_release(inst->pool, handle);
205 return RLM_MODULE_OK;
208 /* write accounting data to couchbase */
209 static rlm_rcode_t CC_HINT(nonnull) mod_accounting(void *instance, REQUEST *request) {
210 rlm_couchbase_t *inst = instance; /* our module instance */
211 void *handle = NULL; /* connection pool handle */
212 VALUE_PAIR *vp; /* radius value pair linked list */
213 char dockey[MAX_KEY_SIZE]; /* our document key */
214 char document[MAX_VALUE_SIZE]; /* our document body */
215 char element[MAX_KEY_SIZE]; /* mapped radius attribute to element name */
216 int status = 0; /* account status type */
217 int docfound = 0; /* document found toggle */
218 lcb_error_t cb_error = LCB_SUCCESS; /* couchbase error holder */
220 /* assert packet as not null */
221 rad_assert(request->packet != NULL);
224 if ((vp = pairfind(request->packet->vps, PW_ACCT_STATUS_TYPE, 0, TAG_ANY)) == NULL) {
226 RDEBUG("could not find status type in packet");
228 return RLM_MODULE_NOOP;
232 status = vp->vp_integer;
234 /* acknowledge the request but take no action */
235 if (status == PW_STATUS_ACCOUNTING_ON || status == PW_STATUS_ACCOUNTING_OFF) {
237 RDEBUG("handling accounting on/off request without action");
239 return RLM_MODULE_OK;
243 handle = fr_connection_get(inst->pool);
246 if (!handle) return RLM_MODULE_FAIL;
248 /* set handle pointer */
249 rlm_couchbase_handle_t *handle_t = handle;
251 /* set couchbase instance */
252 lcb_t cb_inst = handle_t->handle;
255 cookie_t *cookie = handle_t->cookie;
260 memset(cookie, 0, sizeof(cookie_t));
263 RERROR("cookie not usable - possibly not allocated");
264 /* free connection */
266 fr_connection_release(inst->pool, handle);
269 return RLM_MODULE_FAIL;
272 /* attempt to build document key */
273 if (radius_xlat(dockey, sizeof(dockey), request, inst->acct_key, NULL, NULL) < 0) {
275 RERROR("could not find accounting key attribute (%s) in packet", inst->acct_key);
278 fr_connection_release(inst->pool, handle);
281 return RLM_MODULE_NOOP;
284 /* init cookie error status */
285 cookie->jerr = json_tokener_success;
287 /* attempt to fetch document */
288 cb_error = couchbase_get_key(cb_inst, cookie, dockey);
291 if (cb_error != LCB_SUCCESS || cookie->jerr != json_tokener_success) {
293 RERROR("failed to execute get request or parse returned json object");
294 /* free json object */
296 json_object_put(cookie->jobj);
299 /* check cookie json object */
300 if (cookie->jobj != NULL) {
304 RDEBUG("parsed json body from couchbase: %s", json_object_to_json_string(cookie->jobj));
308 /* start json document if needed */
311 RDEBUG("document not found - creating new json document");
312 /* create new json object */
313 cookie->jobj = json_object_new_object();
314 /* set 'docType' element for new document */
315 json_object_object_add(cookie->jobj, "docType", json_object_new_string(inst->doctype));
316 /* set start and stop times ... ensure we always have these elements */
317 json_object_object_add(cookie->jobj, "startTimestamp", json_object_new_string("null"));
318 json_object_object_add(cookie->jobj, "stopTimestamp", json_object_new_string("null"));
321 /* status specific replacements for start/stop time */
323 case PW_STATUS_START:
325 if ((vp = pairfind(request->packet->vps, PW_EVENT_TIMESTAMP, 0, TAG_ANY)) != NULL) {
326 /* add to json object */
327 json_object_object_add(cookie->jobj, "startTimestamp", mod_value_pair_to_json_object(request, vp));
332 if ((vp = pairfind(request->packet->vps, PW_EVENT_TIMESTAMP, 0, TAG_ANY)) != NULL) {
333 /* add to json object */
334 json_object_object_add(cookie->jobj, "stopTimestamp", mod_value_pair_to_json_object(request, vp));
336 /* check start timestamp and adjust if needed */
337 mod_ensure_start_timestamp(cookie->jobj, request->packet->vps);
339 case PW_STATUS_ALIVE:
340 /* check start timestamp and adjust if needed */
341 mod_ensure_start_timestamp(cookie->jobj, request->packet->vps);
344 /* we shouldn't get here - free json object */
346 json_object_put(cookie->jobj);
348 /* release our connection handle */
350 fr_connection_release(inst->pool, handle);
352 /* return without doing anything */
353 return RLM_MODULE_NOOP;
356 /* loop through pairs and add to json document */
357 for (vp = request->packet->vps; vp; vp = vp->next) {
358 /* map attribute to element */
359 if (mod_attribute_to_element(vp->da->name, inst->map, &element) == 0) {
361 RDEBUG("mapped attribute %s => %s", vp->da->name, element);
362 /* add to json object with mapped name */
363 json_object_object_add(cookie->jobj, element, mod_value_pair_to_json_object(request, vp));
367 /* make sure we have enough room in our document buffer */
368 if ((unsigned int) json_object_get_string_len(cookie->jobj) > sizeof(document) - 1) {
369 /* this isn't good */
370 RERROR("could not write json document - insufficient buffer space");
371 /* free json output */
373 json_object_put(cookie->jobj);
377 fr_connection_release(inst->pool, handle);
380 return RLM_MODULE_FAIL;
382 /* copy json string to document */
383 strlcpy(document, json_object_to_json_string(cookie->jobj), sizeof(document));
384 /* free json output */
386 json_object_put(cookie->jobj);
391 RDEBUG("setting '%s' => '%s'", dockey, document);
393 /* store document/key in couchbase */
394 cb_error = couchbase_set_key(cb_inst, dockey, document, inst->expire);
397 if (cb_error != LCB_SUCCESS) {
398 RERROR("failed to store document (%s): %s (0x%x)", dockey, lcb_strerror(NULL, cb_error), cb_error);
403 fr_connection_release(inst->pool, handle);
407 return RLM_MODULE_OK;
410 /* free any memory we allocated */
411 static int mod_detach(void *instance) {
412 rlm_couchbase_t *inst = instance; /* instance struct */
414 /* free json object attribute map */
416 json_object_put(inst->map);
419 /* destroy connection pool */
421 fr_connection_pool_delete(inst->pool);
428 /* hook the module into freeradius */
429 module_t rlm_couchbase = {
432 RLM_TYPE_THREAD_SAFE, /* type */
433 sizeof(rlm_couchbase_t),
435 mod_instantiate, /* instantiation */
436 mod_detach, /* detach */
438 NULL, /* authentication */
439 mod_authorize, /* authorization */
440 NULL, /* preaccounting */
441 mod_accounting, /* accounting */
442 NULL, /* checksimul */
443 NULL, /* pre-proxy */
444 NULL, /* post-proxy */