Rename mod_socket_create/delete to mod_conn_create/delete
[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.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_raw), 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: json-c version: %s", json_c_version());
63                 INFO("rlm_couchbase: libcouchbase version: %s", lcb_get_version(NULL));
64         }
65
66         {
67                 char *server, *p;
68                 size_t len, i;
69                 bool sep = false;
70
71                 len = talloc_array_length(inst->server_raw);
72                 server = p = talloc_array(inst, char, len);
73                 for (i = 0; i < len; i++) {
74                         switch (inst->server_raw[i]) {
75                         case '\t':
76                         case ' ':
77                         case ',':
78                                 /* Consume multiple separators occurring in sequence */
79                                 if (sep == true) continue;
80
81                                 sep = true;
82                                 *p++ = ';';
83                                 break;
84
85                         default:
86                                 sep = false;
87                                 *p++ = inst->server_raw[i];
88                                 break;
89                         }
90                 }
91
92                 *p = '\0';
93                 inst->server = server;
94         }
95
96         /* setup item map */
97         if (mod_build_attribute_element_map(conf, inst) != 0) {
98                 /* fail */
99                 return -1;
100         }
101
102         /* initiate connection pool */
103         inst->pool = fr_connection_pool_init(conf, inst, mod_conn_create, mod_conn_alive, mod_conn_delete, NULL);
104
105         /* check connection pool */
106         if (!inst->pool) {
107                 ERROR("rlm_couchbase: failed to initiate connection pool");
108                 /* fail */
109                 return -1;
110         }
111
112         /* return okay */
113         return 0;
114 }
115
116 /* authorize users via couchbase */
117 static rlm_rcode_t CC_HINT(nonnull) mod_authorize(void *instance, REQUEST *request) {
118         rlm_couchbase_t *inst = instance;       /* our module instance */
119         void *handle = NULL;                    /* connection pool handle */
120         char dockey[MAX_KEY_SIZE];              /* our document key */
121         lcb_error_t cb_error = LCB_SUCCESS;     /* couchbase error holder */
122
123         /* assert packet as not null */
124         rad_assert(request->packet != NULL);
125
126         /* attempt to build document key */
127         if (radius_xlat(dockey, sizeof(dockey), request, inst->user_key, NULL, NULL) < 0) {
128                 /* log error */
129                 RERROR("could not find user key attribute (%s) in packet", inst->user_key);
130                 /* return */
131                 return RLM_MODULE_FAIL;
132         }
133
134         /* get handle */
135         handle = fr_connection_get(inst->pool);
136
137         /* check handle */
138         if (!handle) return RLM_MODULE_FAIL;
139
140         /* set handle pointer */
141         rlm_couchbase_handle_t *handle_t = handle;
142
143         /* set couchbase instance */
144         lcb_t cb_inst = handle_t->handle;
145
146         /* set cookie */
147         cookie_t *cookie = handle_t->cookie;
148
149         /* check cookie */
150         if (cookie) {
151                 /* clear cookie */
152                 memset(cookie, 0, sizeof(cookie_t));
153         } else {
154                 /* log error */
155                 RERROR("cookie not usable - possibly not allocated");
156                 /* free connection */
157                 if (handle) {
158                         fr_connection_release(inst->pool, handle);
159                 }
160                 /* return */
161                 return RLM_MODULE_FAIL;
162         }
163
164         /* reset  cookie error status */
165         cookie->jerr = json_tokener_success;
166
167         /* fetch document */
168         cb_error = couchbase_get_key(cb_inst, cookie, dockey);
169
170         /* check error */
171         if (cb_error != LCB_SUCCESS || cookie->jerr != json_tokener_success || cookie->jobj == NULL) {
172                 /* log error */
173                 RERROR("failed to fetch document or parse return");
174                 /* free json object */
175                 if (cookie->jobj) {
176                         json_object_put(cookie->jobj);
177                 }
178                 /* release handle */
179                 if (handle) {
180                         fr_connection_release(inst->pool, handle);
181                 }
182                 /* return */
183                 return RLM_MODULE_FAIL;
184         }
185
186         /* debugging */
187         RDEBUG("parsed user document == %s", json_object_to_json_string(cookie->jobj));
188
189         /* inject config value pairs defined in this json oblect */
190         mod_json_object_to_value_pairs(cookie->jobj, "config", request);
191
192         /* inject reply value pairs defined in this json oblect */
193         mod_json_object_to_value_pairs(cookie->jobj, "reply", request);
194
195         /* free json object */
196         if (cookie->jobj) {
197                 json_object_put(cookie->jobj);
198         }
199
200         /* release handle */
201         if (handle) {
202                 fr_connection_release(inst->pool, handle);
203         }
204
205         /* return okay */
206         return RLM_MODULE_OK;
207 }
208
209 /* write accounting data to couchbase */
210 static rlm_rcode_t CC_HINT(nonnull) mod_accounting(void *instance, REQUEST *request) {
211         rlm_couchbase_t *inst = instance;   /* our module instance */
212         void *handle = NULL;                /* connection pool handle */
213         VALUE_PAIR *vp;                     /* radius value pair linked list */
214         char dockey[MAX_KEY_SIZE];          /* our document key */
215         char document[MAX_VALUE_SIZE];      /* our document body */
216         char element[MAX_KEY_SIZE];         /* mapped radius attribute to element name */
217         int status = 0;                     /* account status type */
218         int docfound = 0;                   /* document found toggle */
219         lcb_error_t cb_error = LCB_SUCCESS; /* couchbase error holder */
220
221         /* assert packet as not null */
222         rad_assert(request->packet != NULL);
223
224         /* sanity check */
225         if ((vp = pairfind(request->packet->vps, PW_ACCT_STATUS_TYPE, 0, TAG_ANY)) == NULL) {
226                 /* log debug */
227                 RDEBUG("could not find status type in packet");
228                 /* return */
229                 return RLM_MODULE_NOOP;
230         }
231
232         /* set status */
233         status = vp->vp_integer;
234
235         /* acknowledge the request but take no action */
236         if (status == PW_STATUS_ACCOUNTING_ON || status == PW_STATUS_ACCOUNTING_OFF) {
237                 /* log debug */
238                 RDEBUG("handling accounting on/off request without action");
239                 /* return */
240                 return RLM_MODULE_OK;
241         }
242
243         /* get handle */
244         handle = fr_connection_get(inst->pool);
245
246         /* check handle */
247         if (!handle) return RLM_MODULE_FAIL;
248
249         /* set handle pointer */
250         rlm_couchbase_handle_t *handle_t = handle;
251
252         /* set couchbase instance */
253         lcb_t cb_inst = handle_t->handle;
254
255         /* set cookie */
256         cookie_t *cookie = handle_t->cookie;
257
258         /* check cookie */
259         if (cookie) {
260                 /* clear cookie */
261                 memset(cookie, 0, sizeof(cookie_t));
262         } else {
263                 /* log error */
264                 RERROR("cookie not usable - possibly not allocated");
265                 /* free connection */
266                 if (handle) {
267                         fr_connection_release(inst->pool, handle);
268                 }
269                 /* return */
270                 return RLM_MODULE_FAIL;
271         }
272
273         /* attempt to build document key */
274         if (radius_xlat(dockey, sizeof(dockey), request, inst->acct_key, NULL, NULL) < 0) {
275                 /* log error */
276                 RERROR("could not find accounting key attribute (%s) in packet", inst->acct_key);
277                 /* release handle */
278                 if (handle) {
279                         fr_connection_release(inst->pool, handle);
280                 }
281                 /* return */
282                 return RLM_MODULE_NOOP;
283         }
284
285         /* init cookie error status */
286         cookie->jerr = json_tokener_success;
287
288         /* attempt to fetch document */
289         cb_error = couchbase_get_key(cb_inst, cookie, dockey);
290
291         /* check error */
292         if (cb_error != LCB_SUCCESS || cookie->jerr != json_tokener_success) {
293                 /* log error */
294                 RERROR("failed to execute get request or parse returned json object");
295                 /* free json object */
296                 if (cookie->jobj) {
297                         json_object_put(cookie->jobj);
298                 }
299         } else {
300                 /* check cookie json object */
301                 if (cookie->jobj != NULL) {
302                         /* set doc found */
303                         docfound = 1;
304                         /* debugging */
305                         RDEBUG("parsed json body from couchbase: %s", json_object_to_json_string(cookie->jobj));
306                 }
307         }
308
309         /* start json document if needed */
310         if (docfound != 1) {
311                 /* debugging */
312                 RDEBUG("document not found - creating new json document");
313                 /* create new json object */
314                 cookie->jobj = json_object_new_object();
315                 /* set 'docType' element for new document */
316                 json_object_object_add(cookie->jobj, "docType", json_object_new_string(inst->doctype));
317                 /* set start and stop times ... ensure we always have these elements */
318                 json_object_object_add(cookie->jobj, "startTimestamp", json_object_new_string("null"));
319                 json_object_object_add(cookie->jobj, "stopTimestamp", json_object_new_string("null"));
320         }
321
322         /* status specific replacements for start/stop time */
323         switch (status) {
324                 case PW_STATUS_START:
325                         /* add start time */
326                         if ((vp = pairfind(request->packet->vps, PW_EVENT_TIMESTAMP, 0, TAG_ANY)) != NULL) {
327                                 /* add to json object */
328                                 json_object_object_add(cookie->jobj, "startTimestamp", mod_value_pair_to_json_object(request, vp));
329                         }
330                 break;
331                 case PW_STATUS_STOP:
332                         /* add stop time */
333                         if ((vp = pairfind(request->packet->vps, PW_EVENT_TIMESTAMP, 0, TAG_ANY)) != NULL) {
334                                 /* add to json object */
335                                 json_object_object_add(cookie->jobj, "stopTimestamp", mod_value_pair_to_json_object(request, vp));
336                         }
337                         /* check start timestamp and adjust if needed */
338                         mod_ensure_start_timestamp(cookie->jobj, request->packet->vps);
339                 break;
340                 case PW_STATUS_ALIVE:
341                         /* check start timestamp and adjust if needed */
342                         mod_ensure_start_timestamp(cookie->jobj, request->packet->vps);
343                 break;
344                 default:
345                         /* we shouldn't get here - free json object */
346                         if (cookie->jobj) {
347                                 json_object_put(cookie->jobj);
348                         }
349                         /* release our connection handle */
350                         if (handle) {
351                                 fr_connection_release(inst->pool, handle);
352                         }
353                         /* return without doing anything */
354                         return RLM_MODULE_NOOP;
355         }
356
357         /* loop through pairs and add to json document */
358         for (vp = request->packet->vps; vp; vp = vp->next) {
359                 /* map attribute to element */
360                 if (mod_attribute_to_element(vp->da->name, inst->map, &element) == 0) {
361                         /* debug */
362                         RDEBUG("mapped attribute %s => %s", vp->da->name, element);
363                         /* add to json object with mapped name */
364                         json_object_object_add(cookie->jobj, element, mod_value_pair_to_json_object(request, vp));
365                 }
366         }
367
368         /* make sure we have enough room in our document buffer */
369         if ((unsigned int) json_object_get_string_len(cookie->jobj) > sizeof(document) - 1) {
370                 /* this isn't good */
371                 RERROR("could not write json document - insufficient buffer space");
372                 /* free json output */
373                 if (cookie->jobj) {
374                         json_object_put(cookie->jobj);
375                 }
376                 /* release handle */
377                 if (handle) {
378                         fr_connection_release(inst->pool, handle);
379                 }
380                 /* return */
381                 return RLM_MODULE_FAIL;
382         } else {
383                 /* copy json string to document */
384                 strlcpy(document, json_object_to_json_string(cookie->jobj), sizeof(document));
385                 /* free json output */
386                 if (cookie->jobj) {
387                         json_object_put(cookie->jobj);
388                 }
389         }
390
391         /* debugging */
392         RDEBUG("setting '%s' => '%s'", dockey, document);
393
394         /* store document/key in couchbase */
395         cb_error = couchbase_set_key(cb_inst, dockey, document, inst->expire);
396
397         /* check return */
398         if (cb_error != LCB_SUCCESS) {
399                 RERROR("failed to store document (%s): %s (0x%x)", dockey, lcb_strerror(NULL, cb_error), cb_error);
400         }
401
402         /* release handle */
403         if (handle) {
404                 fr_connection_release(inst->pool, handle);
405         }
406
407         /* return */
408         return RLM_MODULE_OK;
409 }
410
411 /* free any memory we allocated */
412 static int mod_detach(void *instance) {
413         rlm_couchbase_t *inst = instance;  /* instance struct */
414
415         /* free json object attribute map */
416         if (inst->map) {
417                 json_object_put(inst->map);
418         }
419
420         /* destroy connection pool */
421         if (inst->pool) {
422                 fr_connection_pool_delete(inst->pool);
423         }
424
425         /* return okay */
426         return 0;
427 }
428
429 /* hook the module into freeradius */
430 module_t rlm_couchbase = {
431         RLM_MODULE_INIT,
432         "rlm_couchbase",
433         RLM_TYPE_THREAD_SAFE,       /* type */
434         sizeof(rlm_couchbase_t),
435         module_config,
436         mod_instantiate,            /* instantiation */
437         mod_detach,                 /* detach */
438         {
439                 NULL,                   /* authentication */
440                 mod_authorize,          /* authorization */
441                 NULL,                   /* preaccounting */
442                 mod_accounting,         /* accounting */
443                 NULL,                   /* checksimul */
444                 NULL,                   /* pre-proxy */
445                 NULL,                   /* post-proxy */
446                 NULL                    /* post-auth */
447         },
448 };