summaryrefslogtreecommitdiffstats
path: root/src/modules/rlm_sql_map/rlm_sql_map.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/modules/rlm_sql_map/rlm_sql_map.c')
-rw-r--r--src/modules/rlm_sql_map/rlm_sql_map.c426
1 files changed, 426 insertions, 0 deletions
diff --git a/src/modules/rlm_sql_map/rlm_sql_map.c b/src/modules/rlm_sql_map/rlm_sql_map.c
new file mode 100644
index 0000000..b6a27e5
--- /dev/null
+++ b/src/modules/rlm_sql_map/rlm_sql_map.c
@@ -0,0 +1,426 @@
+/*
+ * This program is is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or (at
+ * your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ */
+
+/**
+ * $Id$
+ * @file rlm_sql_map.c
+ * @brief Tracks data usage and other counters using SQL.
+ *
+ * @copyright 2021 The FreeRADIUS server project
+ * @copyright 2021 Alan DeKok <aland@ox.org>
+ */
+RCSID("$Id$")
+
+#include <freeradius-devel/radiusd.h>
+#include <freeradius-devel/modules.h>
+#include <freeradius-devel/rad_assert.h>
+
+#include <ctype.h>
+
+#include <rlm_sql.h>
+
+#define MAX_QUERY_LEN 2048
+
+typedef struct rlm_sql_map_t {
+ char const *sql_instance_name; //!< Instance of SQL module to use,
+ //!< usually just 'sql'.
+ bool multiple_rows; //!< Process all rows creating an attr[*] array
+
+ char const *query; //!< SQL query to retrieve current
+
+ rlm_sql_t *sql_inst;
+
+ CONF_SECTION *cs;
+
+ /*
+ * SQL columns to RADIUS stuff
+ */
+ vp_map_t *user_map;
+} rlm_sql_map_t;
+
+/*
+ * A mapping of configuration file names to internal variables.
+ *
+ * Note that the string is dynamically allocated, so it MUST
+ * be freed. When the configuration file parse re-reads the string,
+ * it free's the old one, and strdup's the new one, placing the pointer
+ * to the strdup'd string into 'config.string'. This gets around
+ * buffer over-flows.
+ */
+static const CONF_PARSER module_config[] = {
+ { "sql_module_instance", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_REQUIRED, rlm_sql_map_t, sql_instance_name), NULL },
+ { "multiple_rows", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, rlm_sql_map_t, multiple_rows), "no" },
+ { "query", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_XLAT | PW_TYPE_REQUIRED | PW_TYPE_NOT_EMPTY, rlm_sql_map_t, query), NULL },
+
+ CONF_PARSER_TERMINATOR
+};
+
+#define SQL_MAX_ATTRMAP (128)
+
+static int sql_map_verify(vp_map_t *map, UNUSED void *instance)
+{
+ /*
+ * Destinations where we can put the VALUE_PAIRs we
+ * create using SQL values.
+ */
+ switch (map->lhs->type) {
+ case TMPL_TYPE_ATTR:
+ break;
+
+ case TMPL_TYPE_ATTR_UNDEFINED:
+ cf_log_err(map->ci, "Unknown attribute %s", map->lhs->tmpl_unknown_name);
+ return -1;
+
+ default:
+ cf_log_err(map->ci, "Left hand side of map must be an attribute, not a %s",
+ fr_int2str(tmpl_names, map->lhs->type, "<INVALID>"));
+ return -1;
+ }
+
+ /*
+ * The RHS MUST be only a column number.
+ */
+ switch (map->rhs->type) {
+ case TMPL_TYPE_LITERAL:
+ case TMPL_TYPE_DATA:
+ if (tmpl_cast_in_place(map->rhs, PW_TYPE_INTEGER, NULL) < 0) {
+ cf_log_err(map->ci, "Failed parsing right hand side of map as an integer.");
+ return -1;
+ }
+
+ if (map->rhs->tmpl_data_value.integer > SQL_MAX_ATTRMAP) {
+ cf_log_err(map->ci, "Column number %u is larger than allowed maximum %u",
+ map->rhs->tmpl_data_value.integer, SQL_MAX_ATTRMAP);
+ return -1;
+ }
+ break;
+
+ default:
+ cf_log_err(map->ci, "Right hand side of map must be a column number, not a %s",
+ fr_int2str(tmpl_names, map->rhs->type, "<INVALID>"));
+ return -1;
+ }
+
+ /*
+ * Only =, :=, += and -= operators are supported for SQL mappings.
+ */
+ switch (map->op) {
+ case T_OP_SET:
+ case T_OP_EQ:
+ case T_OP_SUB:
+ case T_OP_ADD:
+ break;
+
+ default:
+ cf_log_err(map->ci, "Operator \"%s\" not allowed for SQL mappings",
+ fr_int2str(fr_tokens, map->op, "<INVALID>"));
+ return -1;
+ }
+
+ return 0;
+}
+
+typedef struct sql_map_row_s {
+ int num_columns;
+ char **row;
+} sql_map_row_t;
+
+
+/** Callback for map_to_request
+ *
+ * Performs exactly the same job as map_to_vp, but pulls attribute values from SQL entries
+ *
+ * @see map_to_vp
+ */
+static int sql_map_getvalue(TALLOC_CTX *ctx, VALUE_PAIR **out, REQUEST *request, vp_map_t const *map, void *uctx)
+{
+ VALUE_PAIR *head = NULL, *vp;
+ int column;
+ sql_map_row_t *data = uctx;
+ char *value;
+ vp_cursor_t cursor;
+
+ *out = NULL;
+ fr_cursor_init(&cursor, &head);
+
+ switch (map->lhs->type) {
+ /*
+ * Iterate over all the retrieved values,
+ * don't try and be clever about changing operators
+ * just use whatever was set in the attribute map.
+ */
+ case TMPL_TYPE_ATTR:
+ fr_assert(map->rhs->type == TMPL_TYPE_DATA);
+ fr_assert(map->rhs->tmpl_data_type == PW_TYPE_INTEGER);
+
+ column = map->rhs->tmpl_data_value.integer;
+ if (column >= data->num_columns) {
+ RWDEBUG("Ignoring source column number %u, as it is larger than the number of returned columns %d",
+ column, data->num_columns);
+ return 0;
+ }
+
+ if (!data->row[column]) {
+ RWDEBUG("Ignoring source column number %u - it is empty", column);
+ return 0;
+ }
+
+ value = data->row[column];
+
+ vp = fr_pair_afrom_da(ctx, map->lhs->tmpl_da);
+ rad_assert(vp);
+
+ if (fr_pair_value_from_str(vp, value, -1) < 0) {
+ char *escaped;
+
+ escaped = fr_aprints(vp, value, -1, '"');
+ RWDEBUG("Failed parsing value \"%s\" for attribute %s: %s", escaped,
+ map->lhs->tmpl_da->name, fr_strerror());
+ talloc_free(vp); /* also frees escaped */
+ break;
+ }
+
+ vp->op = map->op;
+ fr_cursor_insert(&cursor, vp);
+ break;
+
+ default:
+ rad_assert(0);
+ }
+
+ *out = head;
+
+ return 0;
+}
+
+
+/** Convert attribute map into valuepairs
+ *
+ * Use the attribute map built earlier to convert SQL values into valuepairs and insert them into whichever
+ * list they need to go into.
+ *
+ * This is *NOT* atomic, but there's no condition for which we should error out...
+ *
+ * @param[in] inst module configuration.
+ * @param[in] request Current request.
+ * @param[in] handle associated with entry.
+ * @return
+ * - Number of maps successfully applied.
+ * - -1 on failure.
+ */
+static int sql_map_do(const rlm_sql_map_t *inst, REQUEST *request, rlm_sql_handle_t **handle)
+{
+ vp_map_t const *map;
+ int applied = 0; /* How many maps have been applied to the current request */
+ sql_map_row_t ctx;
+
+ /*
+ * Cache all of the rows in a simple array.
+ */
+ while ((inst->sql_inst->module->sql_fetch_row)(*handle, inst->sql_inst->config) == RLM_SQL_OK) {
+#ifdef __clang_analyzer__
+ if (!*handle) return -1; /* only true when return code is not RLM_SQL_OK */
+#endif
+
+ ctx.row = (*handle)->row;
+ ctx.num_columns = (inst->sql_inst->module->sql_num_fields)(*handle, inst->sql_inst->config);
+
+ if (applied >= 1 && !inst->multiple_rows) {
+ RWDEBUG("Ignoring multiple rows. Enable the option 'multiple_rows' if you need multiple rows.");
+ break;
+ }
+
+ for (map = inst->user_map; map != NULL; map = map->next) {
+ /*
+ * If something bad happened, just skip, this is probably
+ * a case of the dst being incorrect for the current
+ * request context
+ */
+ if (map_to_request(request, map, sql_map_getvalue, &ctx) < 0) {
+ return -1; /* Fail */
+ }
+ }
+
+ applied++;
+ }
+
+ return applied;
+}
+
+/*
+ * Do any per-module initialization that is separate to each
+ * configured instance of the module. e.g. set up connections
+ * to external databases, read configuration files, set up
+ * dictionary entries, etc.
+ *
+ * If configuration information is given in the config section
+ * that must be referenced in later calls, store a handle to it
+ * in *instance otherwise put a null pointer there.
+ */
+static int mod_instantiate(CONF_SECTION *conf, void *instance)
+{
+ rlm_sql_map_t *inst = instance;
+ module_instance_t *sql_inst;
+ CONF_SECTION *update;
+
+ sql_inst = module_instantiate(cf_section_find("modules"),
+ inst->sql_instance_name);
+ if (!sql_inst) {
+ cf_log_err_cs(conf, "Failed to find sql instance named %s",
+ inst->sql_instance_name);
+ return -1;
+ }
+ inst->sql_inst = (rlm_sql_t *)sql_inst->insthandle;
+
+ inst->cs = conf;
+
+ /*
+ * Build the attribute map
+ */
+ update = cf_section_sub_find(inst->cs, "update");
+ if (!update) {
+ cf_log_err_cs(conf, "Failed to find 'update' section");
+ return -1;
+ }
+
+ if (map_afrom_cs(&inst->user_map, update,
+ PAIR_LIST_REPLY, PAIR_LIST_REQUEST, sql_map_verify, inst,
+ SQL_MAX_ATTRMAP) < 0) {
+ return -1;
+ }
+
+ return 0;
+}
+
+static int mod_bootstrap(CONF_SECTION *conf, void *instance)
+{
+ rlm_sql_map_t *inst = instance;
+ char const *p = inst->query;
+
+ if (!p || !*p) {
+ cf_log_err_cs(conf, "'query' cannot be empty");
+ return -1;
+ }
+
+ while (isspace((uint8_t) *p)) p++;
+
+ if ((strncasecmp(p, "insert", 6) == 0) ||
+ (strncasecmp(p, "update", 6) == 0) ||
+ (strncasecmp(p, "delete", 6) == 0)) {
+ cf_log_err_cs(conf, "'query' MUST be 'SELECT ...', not 'INSERT', 'UPDATE', or 'DELETE'");
+ return -1;
+ }
+
+ return 0;
+}
+
+
+/** Detach from the SQL server and cleanup internal state.
+ *
+ */
+static int mod_detach(void *instance)
+{
+ rlm_sql_map_t *inst = instance;
+
+ talloc_free(inst->user_map);
+
+ return 0;
+}
+
+
+/*
+ * Find the named user in this modules database. Create the set
+ * of attribute-value pairs to check and reply with for this user
+ * from the database. The authentication code only needs to check
+ * the password, the rest is done here.
+ */
+static rlm_rcode_t CC_HINT(nonnull) mod_map(void *instance, REQUEST *request)
+{
+ int res;
+ rlm_rcode_t rcode = RLM_MODULE_NOOP;
+ char *query;
+ rlm_sql_map_t *inst = instance;
+ rlm_sql_handle_t *handle;
+
+ handle = fr_connection_get(inst->sql_inst->pool);
+ if (!handle) {
+ REDEBUG("Failed reserving SQL connection");
+ return RLM_MODULE_FAIL;
+ }
+
+ if (inst->sql_inst->sql_set_user(inst->sql_inst, request, NULL) < 0) {
+ return RLM_MODULE_FAIL;
+ }
+
+ if (radius_axlat(&query, request, inst->query, inst->sql_inst->sql_escape_func, handle) < 0) {
+ return RLM_MODULE_FAIL;
+ }
+
+ res = inst->sql_inst->sql_select_query(inst->sql_inst, request, &handle, query);
+ talloc_free(query);
+ if (res != RLM_SQL_OK) {
+ if (handle) fr_connection_release(inst->sql_inst->pool, handle);
+
+ return RLM_MODULE_FAIL;
+ }
+
+ fr_assert(handle != NULL);
+
+ if (sql_map_do(inst, request, &handle) > 0) rcode = RLM_MODULE_UPDATED;
+
+ if (handle) {
+ (inst->sql_inst->module->sql_finish_query)(handle, inst->sql_inst->config);
+
+ fr_connection_release(inst->sql_inst->pool, handle);
+ }
+
+ return rcode;
+}
+
+/*
+ * The module name should be the only globally exported symbol.
+ * That is, everything else should be 'static'.
+ *
+ * If the module needs to temporarily modify it's instantiation
+ * data, the type should be changed to RLM_TYPE_THREAD_UNSAFE.
+ * The server will then take care of ensuring that the module
+ * is single-threaded.
+ */
+extern module_t rlm_sql_map;
+module_t rlm_sql_map = {
+ .magic = RLM_MODULE_INIT,
+ .name = "sqlcounter",
+ .type = RLM_TYPE_THREAD_SAFE,
+ .inst_size = sizeof(rlm_sql_map_t),
+ .config = module_config,
+ .bootstrap = mod_bootstrap,
+ .instantiate = mod_instantiate,
+ .detach = mod_detach,
+ .methods = {
+ [MOD_AUTHENTICATE] = mod_map,
+ [MOD_AUTHORIZE] = mod_map,
+ [MOD_PREACCT] = mod_map,
+ [MOD_ACCOUNTING] = mod_map,
+ [MOD_PRE_PROXY] = mod_map,
+ [MOD_POST_PROXY] = mod_map,
+ [MOD_POST_AUTH] = mod_map,
+#ifdef WITH_COA
+ [MOD_RECV_COA] = mod_map,
+ [MOD_SEND_COA] = mod_map
+#endif
+ },
+};
+