8ebcd9c3ecffd35251b9e4b34b226b4fec1eace1
[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         rlm_couchbase_t *inst = instance;   /* our module instance */
57
58         {
59                 char *server, *p;
60                 size_t len, i;
61                 bool sep = false;
62
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]) {
67                         case '\t':
68                         case ' ':
69                         case ',':
70                                 /* Consume multiple separators occurring in sequence */
71                                 if (sep == true) continue;
72
73                                 sep = true;
74                                 *p++ = ';';
75                                 break;
76
77                         default:
78                                 sep = false;
79                                 *p++ = inst->server_raw[i];
80                                 break;
81                         }
82                 }
83
84                 *p = '\0';
85                 inst->server = server;
86         }
87
88         /* setup item map */
89         if (mod_build_attribute_element_map(conf, inst) != 0) {
90                 /* fail */
91                 return -1;
92         }
93
94         /* initiate connection pool */
95         inst->pool = fr_connection_pool_init(conf, inst, mod_conn_create, mod_conn_alive, mod_conn_delete, NULL);
96
97         /* check connection pool */
98         if (!inst->pool) {
99                 ERROR("rlm_couchbase: failed to initiate connection pool");
100                 /* fail */
101                 return -1;
102         }
103
104         /* return okay */
105         return 0;
106 }
107
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 */
114
115         /* assert packet as not null */
116         rad_assert(request->packet != NULL);
117
118         /* attempt to build document key */
119         if (radius_xlat(dockey, sizeof(dockey), request, inst->user_key, NULL, NULL) < 0) {
120                 /* log error */
121                 RERROR("could not find user key attribute (%s) in packet", inst->user_key);
122                 /* return */
123                 return RLM_MODULE_FAIL;
124         }
125
126         /* get handle */
127         handle = fr_connection_get(inst->pool);
128
129         /* check handle */
130         if (!handle) return RLM_MODULE_FAIL;
131
132         /* set handle pointer */
133         rlm_couchbase_handle_t *handle_t = handle;
134
135         /* set couchbase instance */
136         lcb_t cb_inst = handle_t->handle;
137
138         /* set cookie */
139         cookie_t *cookie = handle_t->cookie;
140
141         /* check cookie */
142         if (cookie) {
143                 /* clear cookie */
144                 memset(cookie, 0, sizeof(cookie_t));
145         } else {
146                 /* log error */
147                 RERROR("cookie not usable - possibly not allocated");
148                 /* free connection */
149                 if (handle) {
150                         fr_connection_release(inst->pool, handle);
151                 }
152                 /* return */
153                 return RLM_MODULE_FAIL;
154         }
155
156         /* reset  cookie error status */
157         cookie->jerr = json_tokener_success;
158
159         /* fetch document */
160         cb_error = couchbase_get_key(cb_inst, cookie, dockey);
161
162         /* check error */
163         if (cb_error != LCB_SUCCESS || cookie->jerr != json_tokener_success || cookie->jobj == NULL) {
164                 /* log error */
165                 RERROR("failed to fetch document or parse return");
166                 /* free json object */
167                 if (cookie->jobj) {
168                         json_object_put(cookie->jobj);
169                 }
170                 /* release handle */
171                 if (handle) {
172                         fr_connection_release(inst->pool, handle);
173                 }
174                 /* return */
175                 return RLM_MODULE_FAIL;
176         }
177
178         /* debugging */
179         RDEBUG("parsed user document == %s", json_object_to_json_string(cookie->jobj));
180
181         /* inject config value pairs defined in this json oblect */
182         mod_json_object_to_value_pairs(cookie->jobj, "config", request);
183
184         /* inject reply value pairs defined in this json oblect */
185         mod_json_object_to_value_pairs(cookie->jobj, "reply", request);
186
187         /* free json object */
188         if (cookie->jobj) {
189                 json_object_put(cookie->jobj);
190         }
191
192         /* release handle */
193         if (handle) {
194                 fr_connection_release(inst->pool, handle);
195         }
196
197         /* return okay */
198         return RLM_MODULE_OK;
199 }
200
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 */
212
213         /* assert packet as not null */
214         rad_assert(request->packet != NULL);
215
216         /* sanity check */
217         if ((vp = pairfind(request->packet->vps, PW_ACCT_STATUS_TYPE, 0, TAG_ANY)) == NULL) {
218                 /* log debug */
219                 RDEBUG("could not find status type in packet");
220                 /* return */
221                 return RLM_MODULE_NOOP;
222         }
223
224         /* set status */
225         status = vp->vp_integer;
226
227         /* acknowledge the request but take no action */
228         if (status == PW_STATUS_ACCOUNTING_ON || status == PW_STATUS_ACCOUNTING_OFF) {
229                 /* log debug */
230                 RDEBUG("handling accounting on/off request without action");
231                 /* return */
232                 return RLM_MODULE_OK;
233         }
234
235         /* get handle */
236         handle = fr_connection_get(inst->pool);
237
238         /* check handle */
239         if (!handle) return RLM_MODULE_FAIL;
240
241         /* set handle pointer */
242         rlm_couchbase_handle_t *handle_t = handle;
243
244         /* set couchbase instance */
245         lcb_t cb_inst = handle_t->handle;
246
247         /* set cookie */
248         cookie_t *cookie = handle_t->cookie;
249
250         /* check cookie */
251         if (cookie) {
252                 /* clear cookie */
253                 memset(cookie, 0, sizeof(cookie_t));
254         } else {
255                 /* log error */
256                 RERROR("cookie not usable - possibly not allocated");
257                 /* free connection */
258                 if (handle) {
259                         fr_connection_release(inst->pool, handle);
260                 }
261                 /* return */
262                 return RLM_MODULE_FAIL;
263         }
264
265         /* attempt to build document key */
266         if (radius_xlat(dockey, sizeof(dockey), request, inst->acct_key, NULL, NULL) < 0) {
267                 /* log error */
268                 RERROR("could not find accounting key attribute (%s) in packet", inst->acct_key);
269                 /* release handle */
270                 if (handle) {
271                         fr_connection_release(inst->pool, handle);
272                 }
273                 /* return */
274                 return RLM_MODULE_NOOP;
275         }
276
277         /* init cookie error status */
278         cookie->jerr = json_tokener_success;
279
280         /* attempt to fetch document */
281         cb_error = couchbase_get_key(cb_inst, cookie, dockey);
282
283         /* check error */
284         if (cb_error != LCB_SUCCESS || cookie->jerr != json_tokener_success) {
285                 /* log error */
286                 RERROR("failed to execute get request or parse returned json object");
287                 /* free json object */
288                 if (cookie->jobj) {
289                         json_object_put(cookie->jobj);
290                 }
291         } else {
292                 /* check cookie json object */
293                 if (cookie->jobj != NULL) {
294                         /* set doc found */
295                         docfound = 1;
296                         /* debugging */
297                         RDEBUG("parsed json body from couchbase: %s", json_object_to_json_string(cookie->jobj));
298                 }
299         }
300
301         /* start json document if needed */
302         if (docfound != 1) {
303                 /* debugging */
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"));
312         }
313
314         /* status specific replacements for start/stop time */
315         switch (status) {
316                 case PW_STATUS_START:
317                         /* add start time */
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));
321                         }
322                 break;
323                 case PW_STATUS_STOP:
324                         /* add stop 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, "stopTimestamp", mod_value_pair_to_json_object(request, vp));
328                         }
329                         /* check start timestamp and adjust if needed */
330                         mod_ensure_start_timestamp(cookie->jobj, request->packet->vps);
331                 break;
332                 case PW_STATUS_ALIVE:
333                         /* check start timestamp and adjust if needed */
334                         mod_ensure_start_timestamp(cookie->jobj, request->packet->vps);
335                 break;
336                 default:
337                         /* we shouldn't get here - free json object */
338                         if (cookie->jobj) {
339                                 json_object_put(cookie->jobj);
340                         }
341                         /* release our connection handle */
342                         if (handle) {
343                                 fr_connection_release(inst->pool, handle);
344                         }
345                         /* return without doing anything */
346                         return RLM_MODULE_NOOP;
347         }
348
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) {
353                         /* debug */
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));
357                 }
358         }
359
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 */
365                 if (cookie->jobj) {
366                         json_object_put(cookie->jobj);
367                 }
368                 /* release handle */
369                 if (handle) {
370                         fr_connection_release(inst->pool, handle);
371                 }
372                 /* return */
373                 return RLM_MODULE_FAIL;
374         } else {
375                 /* copy json string to document */
376                 strlcpy(document, json_object_to_json_string(cookie->jobj), sizeof(document));
377                 /* free json output */
378                 if (cookie->jobj) {
379                         json_object_put(cookie->jobj);
380                 }
381         }
382
383         /* debugging */
384         RDEBUG("setting '%s' => '%s'", dockey, document);
385
386         /* store document/key in couchbase */
387         cb_error = couchbase_set_key(cb_inst, dockey, document, inst->expire);
388
389         /* check return */
390         if (cb_error != LCB_SUCCESS) {
391                 RERROR("failed to store document (%s): %s (0x%x)", dockey, lcb_strerror(NULL, cb_error), cb_error);
392         }
393
394         /* release handle */
395         if (handle) {
396                 fr_connection_release(inst->pool, handle);
397         }
398
399         /* return */
400         return RLM_MODULE_OK;
401 }
402
403 /* free any memory we allocated */
404 static int mod_detach(void *instance) {
405         rlm_couchbase_t *inst = instance;  /* instance struct */
406
407         /* free json object attribute map */
408         if (inst->map) {
409                 json_object_put(inst->map);
410         }
411
412         /* destroy connection pool */
413         if (inst->pool) {
414                 fr_connection_pool_delete(inst->pool);
415         }
416
417         /* return okay */
418         return 0;
419 }
420
421 /* hook the module into freeradius */
422 module_t rlm_couchbase = {
423         RLM_MODULE_INIT,
424         "rlm_couchbase",
425         RLM_TYPE_THREAD_SAFE,       /* type */
426         sizeof(rlm_couchbase_t),
427         module_config,
428         mod_instantiate,            /* instantiation */
429         mod_detach,                 /* detach */
430         {
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 */
438                 NULL                    /* post-auth */
439         },
440 };