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 rlm_couchbase_t *inst = instance; /* our module instance */
63 len = talloc_array_length(inst->server_raw);
64 server = p = talloc_array(inst, char, len);
65 for (i = 0; i < len; i++) {
66 switch (inst->server_raw[i]) {
70 /* Consume multiple separators occurring in sequence */
71 if (sep == true) continue;
79 *p++ = inst->server_raw[i];
85 inst->server = server;
89 if (mod_build_attribute_element_map(conf, inst) != 0) {
94 /* initiate connection pool */
95 inst->pool = fr_connection_pool_init(conf, inst, mod_conn_create, mod_conn_alive, mod_conn_delete, NULL);
97 /* check connection pool */
99 ERROR("rlm_couchbase: failed to initiate connection pool");
108 /* authorize users via couchbase */
109 static rlm_rcode_t CC_HINT(nonnull) mod_authorize(void *instance, REQUEST *request) {
110 rlm_couchbase_t *inst = instance; /* our module instance */
111 void *handle = NULL; /* connection pool handle */
112 char dockey[MAX_KEY_SIZE]; /* our document key */
113 lcb_error_t cb_error = LCB_SUCCESS; /* couchbase error holder */
115 /* assert packet as not null */
116 rad_assert(request->packet != NULL);
118 /* attempt to build document key */
119 if (radius_xlat(dockey, sizeof(dockey), request, inst->user_key, NULL, NULL) < 0) {
121 RERROR("could not find user key attribute (%s) in packet", inst->user_key);
123 return RLM_MODULE_FAIL;
127 handle = fr_connection_get(inst->pool);
130 if (!handle) return RLM_MODULE_FAIL;
132 /* set handle pointer */
133 rlm_couchbase_handle_t *handle_t = handle;
135 /* set couchbase instance */
136 lcb_t cb_inst = handle_t->handle;
139 cookie_t *cookie = handle_t->cookie;
144 memset(cookie, 0, sizeof(cookie_t));
147 RERROR("cookie not usable - possibly not allocated");
148 /* free connection */
150 fr_connection_release(inst->pool, handle);
153 return RLM_MODULE_FAIL;
156 /* reset cookie error status */
157 cookie->jerr = json_tokener_success;
160 cb_error = couchbase_get_key(cb_inst, cookie, dockey);
163 if (cb_error != LCB_SUCCESS || cookie->jerr != json_tokener_success || cookie->jobj == NULL) {
165 RERROR("failed to fetch document or parse return");
166 /* free json object */
168 json_object_put(cookie->jobj);
172 fr_connection_release(inst->pool, handle);
175 return RLM_MODULE_FAIL;
179 RDEBUG("parsed user document == %s", json_object_to_json_string(cookie->jobj));
181 /* inject config value pairs defined in this json oblect */
182 mod_json_object_to_value_pairs(cookie->jobj, "config", request);
184 /* inject reply value pairs defined in this json oblect */
185 mod_json_object_to_value_pairs(cookie->jobj, "reply", request);
187 /* free json object */
189 json_object_put(cookie->jobj);
194 fr_connection_release(inst->pool, handle);
198 return RLM_MODULE_OK;
201 /* write accounting data to couchbase */
202 static rlm_rcode_t CC_HINT(nonnull) mod_accounting(void *instance, REQUEST *request) {
203 rlm_couchbase_t *inst = instance; /* our module instance */
204 void *handle = NULL; /* connection pool handle */
205 VALUE_PAIR *vp; /* radius value pair linked list */
206 char dockey[MAX_KEY_SIZE]; /* our document key */
207 char document[MAX_VALUE_SIZE]; /* our document body */
208 char element[MAX_KEY_SIZE]; /* mapped radius attribute to element name */
209 int status = 0; /* account status type */
210 int docfound = 0; /* document found toggle */
211 lcb_error_t cb_error = LCB_SUCCESS; /* couchbase error holder */
213 /* assert packet as not null */
214 rad_assert(request->packet != NULL);
217 if ((vp = pairfind(request->packet->vps, PW_ACCT_STATUS_TYPE, 0, TAG_ANY)) == NULL) {
219 RDEBUG("could not find status type in packet");
221 return RLM_MODULE_NOOP;
225 status = vp->vp_integer;
227 /* acknowledge the request but take no action */
228 if (status == PW_STATUS_ACCOUNTING_ON || status == PW_STATUS_ACCOUNTING_OFF) {
230 RDEBUG("handling accounting on/off request without action");
232 return RLM_MODULE_OK;
236 handle = fr_connection_get(inst->pool);
239 if (!handle) return RLM_MODULE_FAIL;
241 /* set handle pointer */
242 rlm_couchbase_handle_t *handle_t = handle;
244 /* set couchbase instance */
245 lcb_t cb_inst = handle_t->handle;
248 cookie_t *cookie = handle_t->cookie;
253 memset(cookie, 0, sizeof(cookie_t));
256 RERROR("cookie not usable - possibly not allocated");
257 /* free connection */
259 fr_connection_release(inst->pool, handle);
262 return RLM_MODULE_FAIL;
265 /* attempt to build document key */
266 if (radius_xlat(dockey, sizeof(dockey), request, inst->acct_key, NULL, NULL) < 0) {
268 RERROR("could not find accounting key attribute (%s) in packet", inst->acct_key);
271 fr_connection_release(inst->pool, handle);
274 return RLM_MODULE_NOOP;
277 /* init cookie error status */
278 cookie->jerr = json_tokener_success;
280 /* attempt to fetch document */
281 cb_error = couchbase_get_key(cb_inst, cookie, dockey);
284 if (cb_error != LCB_SUCCESS || cookie->jerr != json_tokener_success) {
286 RERROR("failed to execute get request or parse returned json object");
287 /* free json object */
289 json_object_put(cookie->jobj);
292 /* check cookie json object */
293 if (cookie->jobj != NULL) {
297 RDEBUG("parsed json body from couchbase: %s", json_object_to_json_string(cookie->jobj));
301 /* start json document if needed */
304 RDEBUG("document not found - creating new json document");
305 /* create new json object */
306 cookie->jobj = json_object_new_object();
307 /* set 'docType' element for new document */
308 json_object_object_add(cookie->jobj, "docType", json_object_new_string(inst->doctype));
309 /* set start and stop times ... ensure we always have these elements */
310 json_object_object_add(cookie->jobj, "startTimestamp", json_object_new_string("null"));
311 json_object_object_add(cookie->jobj, "stopTimestamp", json_object_new_string("null"));
314 /* status specific replacements for start/stop time */
316 case PW_STATUS_START:
318 if ((vp = pairfind(request->packet->vps, PW_EVENT_TIMESTAMP, 0, TAG_ANY)) != NULL) {
319 /* add to json object */
320 json_object_object_add(cookie->jobj, "startTimestamp", mod_value_pair_to_json_object(request, vp));
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, "stopTimestamp", mod_value_pair_to_json_object(request, vp));
329 /* check start timestamp and adjust if needed */
330 mod_ensure_start_timestamp(cookie->jobj, request->packet->vps);
332 case PW_STATUS_ALIVE:
333 /* check start timestamp and adjust if needed */
334 mod_ensure_start_timestamp(cookie->jobj, request->packet->vps);
337 /* we shouldn't get here - free json object */
339 json_object_put(cookie->jobj);
341 /* release our connection handle */
343 fr_connection_release(inst->pool, handle);
345 /* return without doing anything */
346 return RLM_MODULE_NOOP;
349 /* loop through pairs and add to json document */
350 for (vp = request->packet->vps; vp; vp = vp->next) {
351 /* map attribute to element */
352 if (mod_attribute_to_element(vp->da->name, inst->map, &element) == 0) {
354 RDEBUG("mapped attribute %s => %s", vp->da->name, element);
355 /* add to json object with mapped name */
356 json_object_object_add(cookie->jobj, element, mod_value_pair_to_json_object(request, vp));
360 /* make sure we have enough room in our document buffer */
361 if ((unsigned int) json_object_get_string_len(cookie->jobj) > sizeof(document) - 1) {
362 /* this isn't good */
363 RERROR("could not write json document - insufficient buffer space");
364 /* free json output */
366 json_object_put(cookie->jobj);
370 fr_connection_release(inst->pool, handle);
373 return RLM_MODULE_FAIL;
375 /* copy json string to document */
376 strlcpy(document, json_object_to_json_string(cookie->jobj), sizeof(document));
377 /* free json output */
379 json_object_put(cookie->jobj);
384 RDEBUG("setting '%s' => '%s'", dockey, document);
386 /* store document/key in couchbase */
387 cb_error = couchbase_set_key(cb_inst, dockey, document, inst->expire);
390 if (cb_error != LCB_SUCCESS) {
391 RERROR("failed to store document (%s): %s (0x%x)", dockey, lcb_strerror(NULL, cb_error), cb_error);
396 fr_connection_release(inst->pool, handle);
400 return RLM_MODULE_OK;
403 /* free any memory we allocated */
404 static int mod_detach(void *instance) {
405 rlm_couchbase_t *inst = instance; /* instance struct */
407 /* free json object attribute map */
409 json_object_put(inst->map);
412 /* destroy connection pool */
414 fr_connection_pool_delete(inst->pool);
421 /* hook the module into freeradius */
422 module_t rlm_couchbase = {
425 RLM_TYPE_THREAD_SAFE, /* type */
426 sizeof(rlm_couchbase_t),
428 mod_instantiate, /* instantiation */
429 mod_detach, /* detach */
431 NULL, /* authentication */
432 mod_authorize, /* authorization */
433 NULL, /* preaccounting */
434 mod_accounting, /* accounting */
435 NULL, /* checksimul */
436 NULL, /* pre-proxy */
437 NULL, /* post-proxy */