95ca1fafea2fce3832a7d99b4c52047980c8d8c7
[freeradius.git] / src / modules / rlm_couchbase / rlm_couchbase.c
1 /*
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.
6  *
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.
11  *
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
15  */
16
17 /*
18  * $Id$
19  *
20  * @brief Integrate FreeRADIUS with the Couchbase document database.
21  * @file rlm_couchbase.c
22  *
23  * @copyright 2013-2014 Aaron Hurt <ahurt@anbcs.com>
24  */
25
26 RCSID("$Id$");
27
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>
32
33 #include <libcouchbase/couchbase.h>
34 #include <json/json.h>
35
36 #include "mod.h"
37 #include "couchbase.h"
38 #include "jsonc_missing.h"
39
40 /**
41  * Module Configuration
42  */
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 */
52 };
53
54 /* initialize couchbase connection */
55 static int mod_instantiate(CONF_SECTION *conf, void *instance) {
56         static bool version_done;
57
58         rlm_couchbase_t *inst = instance;   /* our module instance */
59
60         if (!version_done) {
61                 version_done = true;
62                 INFO("rlm_couchbase: liblcouchbase version: %s", lcb_get_version(NULL));
63         }
64
65         {
66                 char *server, *p;
67                 size_t len, i;
68                 bool sep = false;
69
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]) {
74                         case '\t':
75                         case ' ':
76                         case ',':
77                                 /* Consume multiple separators occurring in sequence */
78                                 if (sep == true) continue;
79
80                                 sep = true;
81                                 *p++ = ';';
82                                 break;
83
84                         default:
85                                 sep = false;
86                                 *p++ = inst->server_raw[i];
87                                 break;
88                         }
89                 }
90
91                 *p = '\0';
92                 inst->server = server;
93         }
94
95         /* setup item map */
96         if (mod_build_attribute_element_map(conf, inst) != 0) {
97                 /* fail */
98                 return -1;
99         }
100
101         /* initiate connection pool */
102         inst->pool = fr_connection_pool_init(conf, inst, mod_conn_create, mod_conn_alive, mod_conn_delete, NULL);
103
104         /* check connection pool */
105         if (!inst->pool) {
106                 ERROR("rlm_couchbase: failed to initiate connection pool");
107                 /* fail */
108                 return -1;
109         }
110
111         /* return okay */
112         return 0;
113 }
114
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 */
121
122         /* assert packet as not null */
123         rad_assert(request->packet != NULL);
124
125         /* attempt to build document key */
126         if (radius_xlat(dockey, sizeof(dockey), request, inst->user_key, NULL, NULL) < 0) {
127                 /* log error */
128                 RERROR("could not find user key attribute (%s) in packet", inst->user_key);
129                 /* return */
130                 return RLM_MODULE_FAIL;
131         }
132
133         /* get handle */
134         handle = fr_connection_get(inst->pool);
135
136         /* check handle */
137         if (!handle) return RLM_MODULE_FAIL;
138
139         /* set handle pointer */
140         rlm_couchbase_handle_t *handle_t = handle;
141
142         /* set couchbase instance */
143         lcb_t cb_inst = handle_t->handle;
144
145         /* set cookie */
146         cookie_t *cookie = handle_t->cookie;
147
148         /* check cookie */
149         if (cookie) {
150                 /* clear cookie */
151                 memset(cookie, 0, sizeof(cookie_t));
152         } else {
153                 /* log error */
154                 RERROR("cookie not usable - possibly not allocated");
155                 /* free connection */
156                 if (handle) {
157                         fr_connection_release(inst->pool, handle);
158                 }
159                 /* return */
160                 return RLM_MODULE_FAIL;
161         }
162
163         /* reset  cookie error status */
164         cookie->jerr = json_tokener_success;
165
166         /* fetch document */
167         cb_error = couchbase_get_key(cb_inst, cookie, dockey);
168
169         /* check error */
170         if (cb_error != LCB_SUCCESS || cookie->jerr != json_tokener_success || cookie->jobj == NULL) {
171                 /* log error */
172                 RERROR("failed to fetch document or parse return");
173                 /* free json object */
174                 if (cookie->jobj) {
175                         json_object_put(cookie->jobj);
176                 }
177                 /* release handle */
178                 if (handle) {
179                         fr_connection_release(inst->pool, handle);
180                 }
181                 /* return */
182                 return RLM_MODULE_FAIL;
183         }
184
185         /* debugging */
186         RDEBUG("parsed user document == %s", json_object_to_json_string(cookie->jobj));
187
188         /* inject config value pairs defined in this json oblect */
189         mod_json_object_to_value_pairs(cookie->jobj, "config", request);
190
191         /* inject reply value pairs defined in this json oblect */
192         mod_json_object_to_value_pairs(cookie->jobj, "reply", request);
193
194         /* free json object */
195         if (cookie->jobj) {
196                 json_object_put(cookie->jobj);
197         }
198
199         /* release handle */
200         if (handle) {
201                 fr_connection_release(inst->pool, handle);
202         }
203
204         /* return okay */
205         return RLM_MODULE_OK;
206 }
207
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 */
219
220         /* assert packet as not null */
221         rad_assert(request->packet != NULL);
222
223         /* sanity check */
224         if ((vp = pairfind(request->packet->vps, PW_ACCT_STATUS_TYPE, 0, TAG_ANY)) == NULL) {
225                 /* log debug */
226                 RDEBUG("could not find status type in packet");
227                 /* return */
228                 return RLM_MODULE_NOOP;
229         }
230
231         /* set status */
232         status = vp->vp_integer;
233
234         /* acknowledge the request but take no action */
235         if (status == PW_STATUS_ACCOUNTING_ON || status == PW_STATUS_ACCOUNTING_OFF) {
236                 /* log debug */
237                 RDEBUG("handling accounting on/off request without action");
238                 /* return */
239                 return RLM_MODULE_OK;
240         }
241
242         /* get handle */
243         handle = fr_connection_get(inst->pool);
244
245         /* check handle */
246         if (!handle) return RLM_MODULE_FAIL;
247
248         /* set handle pointer */
249         rlm_couchbase_handle_t *handle_t = handle;
250
251         /* set couchbase instance */
252         lcb_t cb_inst = handle_t->handle;
253
254         /* set cookie */
255         cookie_t *cookie = handle_t->cookie;
256
257         /* check cookie */
258         if (cookie) {
259                 /* clear cookie */
260                 memset(cookie, 0, sizeof(cookie_t));
261         } else {
262                 /* log error */
263                 RERROR("cookie not usable - possibly not allocated");
264                 /* free connection */
265                 if (handle) {
266                         fr_connection_release(inst->pool, handle);
267                 }
268                 /* return */
269                 return RLM_MODULE_FAIL;
270         }
271
272         /* attempt to build document key */
273         if (radius_xlat(dockey, sizeof(dockey), request, inst->acct_key, NULL, NULL) < 0) {
274                 /* log error */
275                 RERROR("could not find accounting key attribute (%s) in packet", inst->acct_key);
276                 /* release handle */
277                 if (handle) {
278                         fr_connection_release(inst->pool, handle);
279                 }
280                 /* return */
281                 return RLM_MODULE_NOOP;
282         }
283
284         /* init cookie error status */
285         cookie->jerr = json_tokener_success;
286
287         /* attempt to fetch document */
288         cb_error = couchbase_get_key(cb_inst, cookie, dockey);
289
290         /* check error */
291         if (cb_error != LCB_SUCCESS || cookie->jerr != json_tokener_success) {
292                 /* log error */
293                 RERROR("failed to execute get request or parse returned json object");
294                 /* free json object */
295                 if (cookie->jobj) {
296                         json_object_put(cookie->jobj);
297                 }
298         } else {
299                 /* check cookie json object */
300                 if (cookie->jobj != NULL) {
301                         /* set doc found */
302                         docfound = 1;
303                         /* debugging */
304                         RDEBUG("parsed json body from couchbase: %s", json_object_to_json_string(cookie->jobj));
305                 }
306         }
307
308         /* start json document if needed */
309         if (docfound != 1) {
310                 /* debugging */
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"));
319         }
320
321         /* status specific replacements for start/stop time */
322         switch (status) {
323                 case PW_STATUS_START:
324                         /* add start time */
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));
328                         }
329                 break;
330                 case PW_STATUS_STOP:
331                         /* add stop time */
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));
335                         }
336                         /* check start timestamp and adjust if needed */
337                         mod_ensure_start_timestamp(cookie->jobj, request->packet->vps);
338                 break;
339                 case PW_STATUS_ALIVE:
340                         /* check start timestamp and adjust if needed */
341                         mod_ensure_start_timestamp(cookie->jobj, request->packet->vps);
342                 break;
343                 default:
344                         /* we shouldn't get here - free json object */
345                         if (cookie->jobj) {
346                                 json_object_put(cookie->jobj);
347                         }
348                         /* release our connection handle */
349                         if (handle) {
350                                 fr_connection_release(inst->pool, handle);
351                         }
352                         /* return without doing anything */
353                         return RLM_MODULE_NOOP;
354         }
355
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) {
360                         /* debug */
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));
364                 }
365         }
366
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 */
372                 if (cookie->jobj) {
373                         json_object_put(cookie->jobj);
374                 }
375                 /* release handle */
376                 if (handle) {
377                         fr_connection_release(inst->pool, handle);
378                 }
379                 /* return */
380                 return RLM_MODULE_FAIL;
381         } else {
382                 /* copy json string to document */
383                 strlcpy(document, json_object_to_json_string(cookie->jobj), sizeof(document));
384                 /* free json output */
385                 if (cookie->jobj) {
386                         json_object_put(cookie->jobj);
387                 }
388         }
389
390         /* debugging */
391         RDEBUG("setting '%s' => '%s'", dockey, document);
392
393         /* store document/key in couchbase */
394         cb_error = couchbase_set_key(cb_inst, dockey, document, inst->expire);
395
396         /* check return */
397         if (cb_error != LCB_SUCCESS) {
398                 RERROR("failed to store document (%s): %s (0x%x)", dockey, lcb_strerror(NULL, cb_error), cb_error);
399         }
400
401         /* release handle */
402         if (handle) {
403                 fr_connection_release(inst->pool, handle);
404         }
405
406         /* return */
407         return RLM_MODULE_OK;
408 }
409
410 /* free any memory we allocated */
411 static int mod_detach(void *instance) {
412         rlm_couchbase_t *inst = instance;  /* instance struct */
413
414         /* free json object attribute map */
415         if (inst->map) {
416                 json_object_put(inst->map);
417         }
418
419         /* destroy connection pool */
420         if (inst->pool) {
421                 fr_connection_pool_delete(inst->pool);
422         }
423
424         /* return okay */
425         return 0;
426 }
427
428 /* hook the module into freeradius */
429 module_t rlm_couchbase = {
430         RLM_MODULE_INIT,
431         "rlm_couchbase",
432         RLM_TYPE_THREAD_SAFE,       /* type */
433         sizeof(rlm_couchbase_t),
434         module_config,
435         mod_instantiate,            /* instantiation */
436         mod_detach,                 /* detach */
437         {
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 */
445                 NULL                    /* post-auth */
446         },
447 };