summaryrefslogtreecommitdiffstats
path: root/src/modules/rlm_sql/rlm_sql.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/modules/rlm_sql/rlm_sql.c')
-rw-r--r--src/modules/rlm_sql/rlm_sql.c1847
1 files changed, 1847 insertions, 0 deletions
diff --git a/src/modules/rlm_sql/rlm_sql.c b/src/modules/rlm_sql/rlm_sql.c
new file mode 100644
index 0000000..4989dd4
--- /dev/null
+++ b/src/modules/rlm_sql/rlm_sql.c
@@ -0,0 +1,1847 @@
+/*
+ * 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.c
+ * @brief Implements SQL 'users' file, and SQL accounting.
+ *
+ * @copyright 2012-2014 Arran Cudbard-Bell <a.cudbardb@freeradius.org>
+ * @copyright 2000,2006 The FreeRADIUS server project
+ * @copyright 2000 Mike Machado <mike@innercite.com>
+ * @copyright 2000 Alan DeKok <aland@ox.org>
+ */
+RCSID("$Id$")
+
+#include <ctype.h>
+
+#include <freeradius-devel/radiusd.h>
+#include <freeradius-devel/modules.h>
+#include <freeradius-devel/token.h>
+#include <freeradius-devel/rad_assert.h>
+#include <freeradius-devel/exfile.h>
+
+#include <sys/stat.h>
+
+#include "rlm_sql.h"
+
+/*
+ * So we can do pass2 xlat checks on the queries.
+ */
+static const CONF_PARSER query_config[] = {
+ { "query", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_XLAT | PW_TYPE_MULTI, rlm_sql_config_t, accounting.query), NULL },
+
+ CONF_PARSER_TERMINATOR
+};
+
+/*
+ * For now hard-code the subsections. This isn't perfect, but it
+ * helps the average case.
+ */
+static const CONF_PARSER type_config[] = {
+ { "accounting-on", FR_CONF_POINTER(PW_TYPE_SUBSECTION, NULL), (void const *) query_config },
+ { "accounting-off", FR_CONF_POINTER(PW_TYPE_SUBSECTION, NULL), (void const *) query_config },
+ { "start", FR_CONF_POINTER(PW_TYPE_SUBSECTION, NULL), (void const *) query_config },
+ { "interim-update", FR_CONF_POINTER(PW_TYPE_SUBSECTION, NULL), (void const *) query_config },
+ { "stop", FR_CONF_POINTER(PW_TYPE_SUBSECTION, NULL), (void const *) query_config },
+
+ CONF_PARSER_TERMINATOR
+};
+
+static const CONF_PARSER acct_config[] = {
+ { "reference", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_XLAT, rlm_sql_config_t, accounting.reference), ".query" },
+ { "logfile", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_XLAT, rlm_sql_config_t, accounting.logfile), NULL },
+
+ { "type", FR_CONF_POINTER(PW_TYPE_SUBSECTION, NULL), (void const *) type_config },
+
+ CONF_PARSER_TERMINATOR
+};
+
+static const CONF_PARSER postauth_config[] = {
+ { "reference", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_XLAT, rlm_sql_config_t, postauth.reference), ".query" },
+ { "logfile", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_XLAT, rlm_sql_config_t, postauth.logfile), NULL },
+
+ { "query", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_XLAT | PW_TYPE_MULTI, rlm_sql_config_t, postauth.query), NULL },
+ CONF_PARSER_TERMINATOR
+};
+
+static const CONF_PARSER module_config[] = {
+ { "driver", FR_CONF_OFFSET(PW_TYPE_STRING, rlm_sql_config_t, sql_driver_name), "rlm_sql_null" },
+ { "server", FR_CONF_OFFSET(PW_TYPE_STRING, rlm_sql_config_t, sql_server), "" }, /* Must be zero length so drivers can determine if it was set */
+ { "port", FR_CONF_OFFSET(PW_TYPE_INTEGER, rlm_sql_config_t, sql_port), "0" },
+ { "login", FR_CONF_OFFSET(PW_TYPE_STRING, rlm_sql_config_t, sql_login), "" },
+ { "password", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_SECRET, rlm_sql_config_t, sql_password), "" },
+ { "radius_db", FR_CONF_OFFSET(PW_TYPE_STRING, rlm_sql_config_t, sql_db), "radius" },
+ { "read_groups", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, rlm_sql_config_t, read_groups), "yes" },
+ { "read_profiles", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, rlm_sql_config_t, read_profiles), "yes" },
+ { "readclients", FR_CONF_OFFSET(PW_TYPE_BOOLEAN | PW_TYPE_DEPRECATED, rlm_sql_config_t, do_clients), NULL },
+ { "read_clients", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, rlm_sql_config_t, do_clients), "no" },
+ { "deletestalesessions", FR_CONF_OFFSET(PW_TYPE_BOOLEAN | PW_TYPE_DEPRECATED, rlm_sql_config_t, delete_stale_sessions), NULL },
+ { "delete_stale_sessions", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, rlm_sql_config_t, delete_stale_sessions), "yes" },
+ { "sql_user_name", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_XLAT, rlm_sql_config_t, query_user), "" },
+ { "logfile", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_XLAT, rlm_sql_config_t, logfile), NULL },
+ { "default_user_profile", FR_CONF_OFFSET(PW_TYPE_STRING, rlm_sql_config_t, default_profile), "" },
+ { "nas_query", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_DEPRECATED, rlm_sql_config_t, client_query), NULL },
+ { "client_query", FR_CONF_OFFSET(PW_TYPE_STRING, rlm_sql_config_t, client_query), "SELECT id,nasname,shortname,type,secret FROM nas" },
+ { "open_query", FR_CONF_OFFSET(PW_TYPE_STRING, rlm_sql_config_t, connect_query), NULL },
+
+ { "authorize_check_query", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_XLAT, rlm_sql_config_t, authorize_check_query), NULL },
+ { "authorize_reply_query", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_XLAT, rlm_sql_config_t, authorize_reply_query), NULL },
+
+ { "authorize_group_check_query", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_XLAT, rlm_sql_config_t, authorize_group_check_query), NULL },
+ { "authorize_group_reply_query", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_XLAT, rlm_sql_config_t, authorize_group_reply_query), NULL },
+ { "group_membership_query", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_XLAT, rlm_sql_config_t, groupmemb_query), NULL },
+#ifdef WITH_SESSION_MGMT
+ { "simul_count_query", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_XLAT, rlm_sql_config_t, simul_count_query), NULL },
+ { "simul_verify_query", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_XLAT, rlm_sql_config_t, simul_verify_query), NULL },
+#endif
+ { "safe-characters", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_DEPRECATED, rlm_sql_config_t, allowed_chars), NULL },
+ { "safe_characters", FR_CONF_OFFSET(PW_TYPE_STRING, rlm_sql_config_t, allowed_chars), "@abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-_: /" },
+ { "auto_escape", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, rlm_sql_config_t, driver_specific_escape), "no" },
+
+ /*
+ * This only works for a few drivers.
+ */
+ { "query_timeout", FR_CONF_OFFSET(PW_TYPE_INTEGER, rlm_sql_config_t, query_timeout), NULL },
+
+ { "accounting", FR_CONF_POINTER(PW_TYPE_SUBSECTION, NULL), (void const *) acct_config },
+
+ { "post-auth", FR_CONF_POINTER(PW_TYPE_SUBSECTION, NULL), (void const *) postauth_config },
+ CONF_PARSER_TERMINATOR
+};
+
+static size_t sql_escape_for_xlat_func(REQUEST *request, char *out, size_t outlen, char const *in, void *arg);
+
+/*
+ * Fall-Through checking function from rlm_files.c
+ */
+static sql_fall_through_t fall_through(VALUE_PAIR *vp)
+{
+ VALUE_PAIR *tmp;
+ tmp = fr_pair_find_by_num(vp, PW_FALL_THROUGH, 0, TAG_ANY);
+
+ return tmp ? tmp->vp_integer : FALL_THROUGH_DEFAULT;
+}
+
+/*
+ * Yucky prototype.
+ */
+static int generate_sql_clients(rlm_sql_t *inst);
+static size_t sql_escape_func(REQUEST *, char *out, size_t outlen, char const *in, void *arg);
+
+/*
+ * SQL xlat function
+ *
+ * For selects the first value of the first column will be returned,
+ * for inserts, updates and deletes the number of rows affected will be
+ * returned instead.
+ */
+static ssize_t sql_xlat(void *instance, REQUEST *request, char const *query, char *out, size_t freespace)
+{
+ rlm_sql_handle_t *handle = NULL;
+ rlm_sql_row_t row;
+ rlm_sql_t *inst = instance;
+ sql_rcode_t rcode;
+ ssize_t ret = 0;
+ size_t len = 0;
+ char const *p;
+
+ /*
+ * Add SQL-User-Name attribute just in case it is needed
+ * We could search the string fmt for SQL-User-Name to see if this is
+ * needed or not
+ */
+ sql_set_user(inst, request, NULL);
+
+ handle = fr_connection_get(inst->pool); /* connection pool should produce error */
+ if (!handle) return 0;
+
+ rlm_sql_query_log(inst, request, NULL, query);
+
+ /*
+ * Trim whitespace for the prefix check
+ */
+ for (p = query; is_whitespace(p); p++);
+
+ /*
+ * If the query starts with any of the following prefixes,
+ * then return the number of rows affected
+ */
+ if ((strncasecmp(p, "insert", 6) == 0) ||
+ (strncasecmp(p, "update", 6) == 0) ||
+ (strncasecmp(p, "delete", 6) == 0)) {
+ int numaffected;
+ char buffer[21]; /* 64bit max is 20 decimal chars + null byte */
+
+ rcode = rlm_sql_query(inst, request, &handle, query);
+ if (rcode != RLM_SQL_OK) {
+ query_error:
+ RERROR("SQL query failed: %s", fr_int2str(sql_rcode_table, rcode, "<INVALID>"));
+
+ ret = -1;
+ goto finish;
+ }
+
+ numaffected = (inst->module->sql_affected_rows)(handle, inst->config);
+ if (numaffected < 1) {
+ RDEBUG("SQL query affected no rows");
+ (inst->module->sql_finish_query)(handle, inst->config);
+
+ goto finish;
+ }
+
+ /*
+ * Don't chop the returned number if freespace is
+ * too small. This hack is necessary because
+ * some implementations of snprintf return the
+ * size of the written data, and others return
+ * the size of the data they *would* have written
+ * if the output buffer was large enough.
+ */
+ snprintf(buffer, sizeof(buffer), "%d", numaffected);
+
+ len = strlen(buffer);
+ if (len >= freespace){
+ RDEBUG("rlm_sql (%s): Can't write result, insufficient string space", inst->name);
+
+ (inst->module->sql_finish_query)(handle, inst->config);
+
+ ret = -1;
+ goto finish;
+ }
+
+ memcpy(out, buffer, len + 1); /* we did bounds checking above */
+ ret = len;
+
+ (inst->module->sql_finish_query)(handle, inst->config);
+
+ goto finish;
+ } /* else it's a SELECT statement */
+
+ rcode = rlm_sql_select_query(inst, request, &handle, query);
+ if (rcode != RLM_SQL_OK) goto query_error;
+
+ rcode = rlm_sql_fetch_row(inst, request, &handle);
+ if (rcode < 0) {
+ (inst->module->sql_finish_select_query)(handle, inst->config);
+ goto query_error;
+ }
+
+ row = handle->row;
+ if (!row) {
+ RDEBUG("SQL query returned no results");
+ (inst->module->sql_finish_select_query)(handle, inst->config);
+ ret = -1;
+
+ goto finish;
+ }
+
+ if (!row[0]){
+ RDEBUG("NULL value in first column of result");
+ (inst->module->sql_finish_select_query)(handle, inst->config);
+ ret = -1;
+
+ goto finish;
+ }
+
+ len = strlen(row[0]);
+ if (len >= freespace){
+ RDEBUG("Insufficient string space");
+ (inst->module->sql_finish_select_query)(handle, inst->config);
+
+ ret = -1;
+ goto finish;
+ }
+
+ strlcpy(out, row[0], freespace);
+ ret = len;
+
+ (inst->module->sql_finish_select_query)(handle, inst->config);
+
+finish:
+ fr_connection_release(inst->pool, handle);
+
+ return ret;
+}
+
+static int generate_sql_clients(rlm_sql_t *inst)
+{
+ rlm_sql_handle_t *handle;
+ rlm_sql_row_t row;
+ unsigned int i = 0;
+ RADCLIENT *c;
+
+ DEBUG("rlm_sql (%s): Processing generate_sql_clients",
+ inst->name);
+
+ DEBUG("rlm_sql (%s) in generate_sql_clients: query is %s",
+ inst->name, inst->config->client_query);
+
+ handle = fr_connection_get(inst->pool);
+ if (!handle) return -1;
+
+ if (rlm_sql_select_query(inst, NULL, &handle, inst->config->client_query) != RLM_SQL_OK) return -1;
+
+ while ((rlm_sql_fetch_row(inst, NULL, &handle) == RLM_SQL_OK) && (row = handle->row)) {
+ int num_rows;
+ char *server = NULL;
+
+ i++;
+
+ num_rows = (inst->module->sql_num_fields)(handle, inst->config);
+ if (num_rows < 5) {
+ WARN("SELECT returned too few fields. Please do not edit 'client_query'");
+ continue;
+ }
+
+ /*
+ * The return data for each row MUST be in the following order:
+ *
+ * 0. Row ID (currently unused)
+ * 1. Name (or IP address)
+ * 2. Shortname
+ * 3. Type
+ * 4. Secret
+ * 5. Virtual Server (optional)
+ */
+ if (!row[0]){
+ ERROR("rlm_sql (%s): No row id found on pass %d",inst->name,i);
+ continue;
+ }
+ if (!row[1]){
+ ERROR("rlm_sql (%s): No nasname found for row %s",inst->name,row[0]);
+ continue;
+ }
+ if (!row[2]){
+ ERROR("rlm_sql (%s): No short name found for row %s",inst->name,row[0]);
+ continue;
+ }
+ if (!row[4]){
+ ERROR("rlm_sql (%s): No secret found for row %s",inst->name,row[0]);
+ continue;
+ }
+
+ if ((num_rows > 5) && (row[5] != NULL) && *row[5]) {
+ server = row[5];
+ }
+
+ DEBUG("rlm_sql (%s): Adding client %s (%s) to %s clients list",
+ inst->name,
+ row[1], row[2], server ? server : "global");
+
+ /* FIXME: We should really pass a proper ctx */
+ c = client_afrom_query(NULL,
+ row[1], /* identifier */
+ row[4], /* secret */
+ row[2], /* shortname */
+ row[3], /* type */
+ server, /* server */
+ false); /* require message authenticator */
+ if (!c) {
+ continue;
+ }
+
+ if (!client_add(NULL, c)) {
+ WARN("Failed to add client, possible duplicate?");
+
+ client_free(c);
+ continue;
+ }
+
+ DEBUG("rlm_sql (%s): Client \"%s\" (%s) added", c->longname, c->shortname,
+ inst->name);
+ }
+
+ (inst->module->sql_finish_select_query)(handle, inst->config);
+ fr_connection_release(inst->pool, handle);
+
+ return 0;
+}
+
+
+/*
+ * Translate the SQL queries.
+ */
+static size_t sql_escape_func(UNUSED REQUEST *request, char *out, size_t outlen,
+ char const *in, void *arg)
+{
+ rlm_sql_handle_t *handle = talloc_get_type_abort(arg, rlm_sql_handle_t);
+ rlm_sql_t *inst = handle->inst;
+ size_t len = 0;
+
+ while (in[0]) {
+ size_t utf8_len;
+
+ /*
+ * Allow all multi-byte UTF8 characters.
+ */
+ utf8_len = fr_utf8_char((uint8_t const *) in, -1);
+ if (utf8_len > 1) {
+ if (outlen <= utf8_len) break;
+
+ memcpy(out, in, utf8_len);
+ in += utf8_len;
+ out += utf8_len;
+
+ outlen -= utf8_len;
+ len += utf8_len;
+ continue;
+ }
+
+ /*
+ * Because we register our own escape function
+ * we're now responsible for escaping all special
+ * chars in an xlat expansion or attribute value.
+ */
+ switch (in[0]) {
+ case '\n':
+ if (outlen <= 2) break;
+ out[0] = '\\';
+ out[1] = 'n';
+
+ in++;
+ out += 2;
+ outlen -= 2;
+ len += 2;
+ break;
+
+ case '\r':
+ if (outlen <= 2) break;
+ out[0] = '\\';
+ out[1] = 'r';
+
+ in++;
+ out += 2;
+ outlen -= 2;
+ len += 2;
+ break;
+
+ case '\t':
+ if (outlen <= 2) break;
+ out[0] = '\\';
+ out[1] = 't';
+
+ in++;
+ out += 2;
+ outlen -= 2;
+ len += 2;
+ break;
+ }
+
+ /*
+ * Non-printable characters get replaced with their
+ * mime-encoded equivalents.
+ */
+ if ((in[0] < 32) ||
+ strchr(inst->config->allowed_chars, *in) == NULL) {
+ /*
+ * Only 3 or less bytes available.
+ */
+ if (outlen <= 3) {
+ break;
+ }
+
+ snprintf(out, outlen, "=%02X", (unsigned char) in[0]);
+ in++;
+ out += 3;
+ outlen -= 3;
+ len += 3;
+ continue;
+ }
+
+ /*
+ * Only one byte left.
+ */
+ if (outlen <= 1) {
+ break;
+ }
+
+ /*
+ * Allowed character.
+ */
+ *out = *in;
+ out++;
+ in++;
+ outlen--;
+ len++;
+ }
+ *out = '\0';
+ return len;
+}
+
+/** Passed as the escape function to map_proc and sql xlat methods
+ *
+ * The variant reserves a connection for the escape functions to use, and releases it after
+ * escaping is complete.
+ */
+static size_t sql_escape_for_xlat_func(REQUEST *request, char *out, size_t outlen, char const *in, void *arg)
+{
+ size_t ret;
+ rlm_sql_t *inst = talloc_get_type_abort(arg, rlm_sql_t);
+ rlm_sql_handle_t *handle;
+
+ handle = fr_connection_get(inst->pool);
+ if (!handle) {
+ out[0] = '\0';
+ return 0;
+ }
+ ret = inst->sql_escape_func(request, out, outlen, in, handle);
+ fr_connection_release(inst->pool, handle);
+
+ return ret;
+}
+
+/*
+ * Set the SQL user name.
+ *
+ * We don't call the escape function here. The resulting string
+ * will be escaped later in the queries xlat so we don't need to
+ * escape it twice. (it will make things wrong if we have an
+ * escape candidate character in the username)
+ */
+int sql_set_user(rlm_sql_t *inst, REQUEST *request, char const *username)
+{
+ char *expanded = NULL;
+ VALUE_PAIR *vp = NULL;
+ char const *sqluser;
+ ssize_t len;
+
+ rad_assert(request->packet != NULL);
+
+ if (username != NULL) {
+ sqluser = username;
+ } else if (inst->config->query_user[0] != '\0') {
+ sqluser = inst->config->query_user;
+ } else {
+ return 0;
+ }
+
+ len = radius_axlat(&expanded, request, sqluser, NULL, NULL);
+ if (len < 0) {
+ return -1;
+ }
+
+ vp = fr_pair_afrom_da(request->packet, inst->sql_user);
+ if (!vp) {
+ talloc_free(expanded);
+ return -1;
+ }
+
+ fr_pair_value_strsteal(vp, expanded);
+ RDEBUG2("SQL-User-Name set to '%s'", vp->vp_strvalue);
+ vp->op = T_OP_SET;
+
+ /*
+ * Delete any existing SQL-User-Name, and replace it with ours.
+ */
+ fr_pair_delete_by_num(&request->packet->vps, vp->da->attr, vp->da->vendor, TAG_ANY);
+ fr_pair_add(&request->packet->vps, vp);
+
+ return 0;
+}
+
+/*
+ * Do a set/unset user, so it's a bit clearer what's going on.
+ */
+#define sql_unset_user(_i, _r) fr_pair_delete_by_num(&_r->packet->vps, _i->sql_user->attr, _i->sql_user->vendor, TAG_ANY)
+
+static int sql_get_grouplist(rlm_sql_t *inst, rlm_sql_handle_t **handle, REQUEST *request,
+ rlm_sql_grouplist_t **phead)
+{
+ char *expanded = NULL;
+ int num_groups = 0;
+ rlm_sql_row_t row;
+ rlm_sql_grouplist_t *entry;
+ int ret;
+
+ /* NOTE: sql_set_user should have been run before calling this function */
+
+ entry = *phead = NULL;
+
+ if (!inst->config->groupmemb_query) return 0;
+
+ if (radius_axlat(&expanded, request, inst->config->groupmemb_query, sql_escape_func, *handle) < 0) return -1;
+
+ ret = rlm_sql_select_query(inst, request, handle, expanded);
+ talloc_free(expanded);
+ if (ret != RLM_SQL_OK) return -1;
+
+ while (rlm_sql_fetch_row(inst, request, handle) == RLM_SQL_OK) {
+ row = (*handle)->row;
+ if (!row)
+ break;
+
+ if (!row[0]){
+ RDEBUG("row[0] returned NULL");
+ (inst->module->sql_finish_select_query)(*handle, inst->config);
+ talloc_free(entry);
+ return -1;
+ }
+
+ if (!*phead) {
+ *phead = talloc_zero(*handle, rlm_sql_grouplist_t);
+ entry = *phead;
+ } else {
+ entry->next = talloc_zero(*phead, rlm_sql_grouplist_t);
+ entry = entry->next;
+ }
+ entry->next = NULL;
+ entry->name = talloc_typed_strdup(entry, row[0]);
+
+ num_groups++;
+ }
+
+ (inst->module->sql_finish_select_query)(*handle, inst->config);
+
+ return num_groups;
+}
+
+
+/*
+ * sql groupcmp function. That way we can do group comparisons (in the users file for example)
+ * with the group memberships residing in sql
+ * The group membership query should only return one element which is the username. The returned
+ * username will then be checked with the passed check string.
+ */
+static int sql_groupcmp(void *instance, REQUEST *request, UNUSED VALUE_PAIR *request_vp,
+ VALUE_PAIR *check, UNUSED VALUE_PAIR *check_pairs,
+ UNUSED VALUE_PAIR **reply_pairs) CC_HINT(nonnull (1, 2, 4));
+
+static int sql_groupcmp(void *instance, REQUEST *request, UNUSED VALUE_PAIR *request_vp,
+ VALUE_PAIR *check, UNUSED VALUE_PAIR *check_pairs,
+ UNUSED VALUE_PAIR **reply_pairs)
+{
+ rlm_sql_handle_t *handle;
+ rlm_sql_t *inst = instance;
+ rlm_sql_grouplist_t *head, *entry;
+
+ /*
+ * No group queries, don't do group comparisons.
+ */
+ if (!inst->config->groupmemb_query) {
+ RWARN("Cannot do group comparison when group_membership_query is not set");
+ return 1;
+ }
+
+ RDEBUG("sql_groupcmp");
+
+ if (check->vp_length == 0){
+ RDEBUG("sql_groupcmp: Illegal group name");
+ return 1;
+ }
+
+ /*
+ * Set, escape, and check the user attr here
+ */
+ if (sql_set_user(inst, request, NULL) < 0)
+ return 1;
+
+ /*
+ * Get a socket for this lookup
+ */
+ handle = fr_connection_get(inst->pool);
+ if (!handle) {
+ return 1;
+ }
+
+ /*
+ * Get the list of groups this user is a member of
+ */
+ if (sql_get_grouplist(inst, &handle, request, &head) < 0) {
+ REDEBUG("Error getting group membership");
+ fr_connection_release(inst->pool, handle);
+ return 1;
+ }
+
+ for (entry = head; entry != NULL; entry = entry->next) {
+ if (strcmp(entry->name, check->vp_strvalue) == 0){
+ RDEBUG("sql_groupcmp finished: User is a member of group %s",
+ check->vp_strvalue);
+ talloc_free(head);
+ fr_connection_release(inst->pool, handle);
+ return 0;
+ }
+ }
+
+ /* Free the grouplist */
+ talloc_free(head);
+ fr_connection_release(inst->pool, handle);
+
+ RDEBUG("sql_groupcmp finished: User is NOT a member of group %s", check->vp_strvalue);
+
+ return 1;
+}
+
+static rlm_rcode_t rlm_sql_process_groups(rlm_sql_t *inst, REQUEST *request, rlm_sql_handle_t **handle,
+ sql_fall_through_t *do_fall_through)
+{
+ rlm_rcode_t rcode = RLM_MODULE_NOOP;
+ VALUE_PAIR *check_tmp = NULL, *reply_tmp = NULL, *sql_group = NULL;
+ rlm_sql_grouplist_t *head = NULL, *entry = NULL;
+
+ char *expanded = NULL;
+ int rows;
+
+ rad_assert(request->packet != NULL);
+
+ if (!inst->config->groupmemb_query) {
+ RWARN("Cannot do check groups when group_membership_query is not set");
+
+ do_nothing:
+ *do_fall_through = FALL_THROUGH_DEFAULT;
+
+ /*
+ * Didn't add group attributes or allocate
+ * memory, so don't do anything else.
+ */
+ return RLM_MODULE_NOTFOUND;
+ }
+
+ /*
+ * Get the list of groups this user is a member of
+ */
+ rows = sql_get_grouplist(inst, handle, request, &head);
+ if (rows < 0) {
+ REDEBUG("Error retrieving group list");
+
+ return RLM_MODULE_FAIL;
+ }
+ if (rows == 0) {
+ RDEBUG2("User not found in any groups");
+ goto do_nothing;
+ }
+ rad_assert(head);
+
+ RDEBUG2("User found in the group table");
+
+ /*
+ * Add the Sql-Group attribute to the request list so we know
+ * which group we're retrieving attributes for
+ */
+ sql_group = pair_make_request(inst->group_da->name, NULL, T_OP_EQ);
+ if (!sql_group) {
+ REDEBUG("Error creating %s attribute", inst->group_da->name);
+ rcode = RLM_MODULE_FAIL;
+ goto finish;
+ }
+
+ entry = head;
+ do {
+ next:
+ rad_assert(entry != NULL);
+ fr_pair_value_strcpy(sql_group, entry->name);
+
+ if (inst->config->authorize_group_check_query) {
+ vp_cursor_t cursor;
+ VALUE_PAIR *vp;
+
+ /*
+ * Expand the group query
+ */
+ if (radius_axlat(&expanded, request, inst->config->authorize_group_check_query,
+ inst->sql_escape_func, *handle) < 0) {
+ REDEBUG("Error generating query");
+ rcode = RLM_MODULE_FAIL;
+ goto finish;
+ }
+
+ rows = sql_getvpdata(request, inst, request, handle, &check_tmp, expanded);
+ TALLOC_FREE(expanded);
+ if (rows < 0) {
+ REDEBUG("Error retrieving check pairs for group %s", entry->name);
+ rcode = RLM_MODULE_FAIL;
+ goto finish;
+ }
+
+ /*
+ * If we got check rows we need to process them before we decide to
+ * process the reply rows
+ */
+ if ((rows > 0) &&
+ (paircompare(request, request->packet->vps, check_tmp, &request->reply->vps) != 0)) {
+ fr_pair_list_free(&check_tmp);
+ entry = entry->next;
+
+ if (!entry) break;
+
+ goto next; /* != continue */
+ }
+
+ RDEBUG2("Group \"%s\": Conditional check items matched", entry->name);
+ rcode = RLM_MODULE_OK;
+
+ RDEBUG2("Group \"%s\": Merging assignment check items", entry->name);
+ RINDENT();
+ for (vp = fr_cursor_init(&cursor, &check_tmp);
+ vp;
+ vp = fr_cursor_next(&cursor)) {
+ if (!fr_assignment_op[vp->op]) continue;
+
+ rdebug_pair(L_DBG_LVL_2, request, vp, NULL);
+ }
+ REXDENT();
+ radius_pairmove(request, &request->config, check_tmp, true);
+ check_tmp = NULL;
+ }
+
+ if (inst->config->authorize_group_reply_query) {
+ /*
+ * Now get the reply pairs since the paircompare matched
+ */
+ if (radius_axlat(&expanded, request, inst->config->authorize_group_reply_query,
+ inst->sql_escape_func, *handle) < 0) {
+ REDEBUG("Error generating query");
+ rcode = RLM_MODULE_FAIL;
+ goto finish;
+ }
+
+ rows = sql_getvpdata(request->reply, inst, request, handle, &reply_tmp, expanded);
+ TALLOC_FREE(expanded);
+ if (rows < 0) {
+ REDEBUG("Error retrieving reply pairs for group %s", entry->name);
+ rcode = RLM_MODULE_FAIL;
+ goto finish;
+ }
+ *do_fall_through = fall_through(reply_tmp);
+
+ RDEBUG2("Group \"%s\": Merging reply items", entry->name);
+ rcode = RLM_MODULE_OK;
+
+ rdebug_pair_list(L_DBG_LVL_2, request, reply_tmp, NULL);
+
+ radius_pairmove(request, &request->reply->vps, reply_tmp, true);
+ reply_tmp = NULL;
+ /*
+ * If there's no reply query configured, then we assume
+ * FALL_THROUGH_NO, which is the same as the users file if you
+ * had no reply attributes.
+ */
+ } else {
+ *do_fall_through = FALL_THROUGH_DEFAULT;
+ }
+
+ entry = entry->next;
+ } while (entry != NULL && (*do_fall_through == FALL_THROUGH_YES));
+
+finish:
+ talloc_free(head);
+ fr_pair_delete_by_num(&request->packet->vps, inst->group_da->attr, 0, TAG_ANY);
+
+ return rcode;
+}
+
+
+static int mod_detach(void *instance)
+{
+ rlm_sql_t *inst = instance;
+
+ if (inst->pool) fr_connection_pool_free(inst->pool);
+
+ /*
+ * We need to explicitly free all children, so if the driver
+ * parented any memory off the instance, their destructors
+ * run before we unload the bytecode for them.
+ *
+ * If we don't do this, we get a SEGV deep inside the talloc code
+ * when it tries to call a destructor that no longer exists.
+ */
+ talloc_free_children(inst);
+
+ /*
+ * Decrements the reference count. The driver object won't be unloaded
+ * until all instances of rlm_sql that use it have been destroyed.
+ */
+ if (inst->handle) dlclose(inst->handle);
+
+ return 0;
+}
+
+static int mod_bootstrap(CONF_SECTION *conf, void *instance)
+{
+ rlm_sql_t *inst = instance;
+
+ /*
+ * Hack...
+ */
+ inst->config = &inst->myconfig;
+ inst->cs = conf;
+
+ inst->name = cf_section_name2(conf);
+ if (!inst->name) inst->name = cf_section_name1(conf);
+
+ /*
+ * Load the appropriate driver for our database.
+ */
+ inst->handle = fr_dlopenext(inst->config->sql_driver_name);
+ if (!inst->handle) {
+ ERROR("Could not link driver %s: %s", inst->config->sql_driver_name, fr_strerror());
+ ERROR("Make sure it (and all its dependent libraries!) are in the search path of your system's ld");
+ return -1;
+ }
+
+ inst->module = (rlm_sql_module_t *) dlsym(inst->handle, inst->config->sql_driver_name);
+ if (!inst->module) {
+ ERROR("Could not link symbol %s: %s", inst->config->sql_driver_name, dlerror());
+ return -1;
+ }
+
+ INFO("rlm_sql (%s): Driver %s (module %s) loaded and linked", inst->name,
+ inst->config->sql_driver_name, inst->module->name);
+
+ if (inst->config->groupmemb_query) {
+ if (cf_section_name2(conf)) {
+ char buffer[256];
+
+ snprintf(buffer, sizeof(buffer), "%s-SQL-Group", inst->name);
+
+ if (paircompare_register_byname(buffer, dict_attrbyvalue(PW_USER_NAME, 0),
+ false, sql_groupcmp, inst) < 0) {
+ ERROR("Error registering group comparison: %s", fr_strerror());
+ return -1;
+ }
+
+ inst->group_da = dict_attrbyname(buffer);
+
+ /*
+ * We're the default instance
+ */
+ } else {
+ if (paircompare_register_byname("SQL-Group", dict_attrbyvalue(PW_USER_NAME, 0),
+ false, sql_groupcmp, inst) < 0) {
+ ERROR("Error registering group comparison: %s", fr_strerror());
+ return -1;
+ }
+
+ inst->group_da = dict_attrbyname("SQL-Group");
+ }
+
+ if (!inst->group_da) {
+ ERROR("Failed resolving group attribute");
+ return -1;
+ }
+ }
+
+ /*
+ * Register the SQL xlat function
+ */
+ xlat_register(inst->name, sql_xlat, sql_escape_for_xlat_func, inst);
+
+ return 0;
+}
+
+
+static void *mod_conn_create(TALLOC_CTX *ctx, void *instance)
+{
+ int rcode;
+ rlm_sql_t *inst = instance;
+ rlm_sql_handle_t *handle;
+
+ /*
+ * Connections cannot be alloced from the inst or
+ * pool contexts due to threading issues.
+ */
+ handle = talloc_zero(ctx, rlm_sql_handle_t);
+ if (!handle) return NULL;
+
+ handle->log_ctx = talloc_pool(handle, 2048);
+ if (!handle->log_ctx) {
+ talloc_free(handle);
+ return NULL;
+ }
+
+ /*
+ * Handle requires a pointer to the SQL inst so the
+ * destructor has access to the module configuration.
+ */
+ handle->inst = inst;
+
+ rcode = (inst->module->sql_socket_init)(handle, inst->config);
+ if (rcode != 0) {
+ fail:
+ exec_trigger(NULL, inst->cs, "modules.sql.fail", true);
+
+ /*
+ * Destroy any half opened connections.
+ */
+ talloc_free(handle);
+ return NULL;
+ }
+
+ if (inst->config->connect_query) {
+ if (rlm_sql_select_query(inst, NULL, &handle, inst->config->connect_query) != RLM_SQL_OK) goto fail;
+ (inst->module->sql_finish_select_query)(handle, inst->config);
+ }
+
+ return handle;
+}
+
+
+static int mod_instantiate(CONF_SECTION *conf, void *instance)
+{
+ rlm_sql_t *inst = instance;
+
+ /*
+ * Complain if the strings exist, but are empty.
+ */
+#define CHECK_STRING(_x) if (inst->config->_x && !inst->config->_x[0]) \
+do { \
+ WARN("rlm_sql (%s): " STRINGIFY(_x) " is empty. Please delete it from the configuration", inst->name);\
+ inst->config->_x = NULL;\
+} while (0)
+
+ CHECK_STRING(groupmemb_query);
+ CHECK_STRING(authorize_check_query);
+ CHECK_STRING(authorize_reply_query);
+ CHECK_STRING(authorize_group_check_query);
+ CHECK_STRING(authorize_group_reply_query);
+ CHECK_STRING(simul_count_query);
+ CHECK_STRING(simul_verify_query);
+ CHECK_STRING(connect_query);
+ CHECK_STRING(client_query);
+ if (strncmp(inst->config->sql_driver_name, "rlm_sql_", 8) != 0) {
+ ERROR("rlm_sql (%s): \"%s\" is NOT an SQL driver!", inst->name, inst->config->sql_driver_name);
+ return -1;
+ }
+
+ /*
+ * We need authorize_group_check_query or authorize_group_reply_query
+ * if group_membership_query is set.
+ *
+ * Or we need group_membership_query if authorize_group_check_query or
+ * authorize_group_reply_query is set.
+ */
+ if (!inst->config->groupmemb_query) {
+ if (inst->config->authorize_group_check_query) {
+ WARN("rlm_sql (%s): Ignoring authorize_group_reply_query as group_membership_query "
+ "is not configured", inst->name);
+ }
+
+ if (inst->config->authorize_group_reply_query) {
+ WARN("rlm_sql (%s): Ignoring authorize_group_check_query as group_membership_query "
+ "is not configured", inst->name);
+ }
+
+ if (!inst->config->read_groups) {
+ WARN("rlm_sql (%s): Ignoring read_groups as group_membership_query "
+ "is not configured", inst->name);
+ inst->config->read_groups = false;
+ }
+ } /* allow the group check / reply queries to be NULL */
+
+ /*
+ * This will always exist, as cf_section_parse_init()
+ * will create it if it doesn't exist. However, the
+ * "reference" config item won't exist in an auto-created
+ * configuration. So if that doesn't exist, we ignore
+ * the whole subsection.
+ */
+ inst->config->accounting.cs = cf_section_sub_find(conf, "accounting");
+ inst->config->accounting.reference_cp = (cf_pair_find(inst->config->accounting.cs, "reference") != NULL);
+
+ inst->config->postauth.cs = cf_section_sub_find(conf, "post-auth");
+ inst->config->postauth.reference_cp = (cf_pair_find(inst->config->postauth.cs, "reference") != NULL);
+
+ /*
+ * Cache the SQL-User-Name DICT_ATTR, so we can be slightly
+ * more efficient about creating SQL-User-Name attributes.
+ */
+ inst->sql_user = dict_attrbyname("SQL-User-Name");
+ if (!inst->sql_user) {
+ return -1;
+ }
+
+ /*
+ * Export these methods, too. This avoids RTDL_GLOBAL.
+ */
+ inst->sql_set_user = sql_set_user;
+ inst->sql_query = rlm_sql_query;
+ inst->sql_select_query = rlm_sql_select_query;
+ inst->sql_fetch_row = rlm_sql_fetch_row;
+
+ /*
+ * Either use the module specific escape function
+ * or our default one.
+ */
+ inst->sql_escape_func = inst->module->sql_escape_func && inst->config->driver_specific_escape ?
+ inst->module->sql_escape_func :
+ sql_escape_func;
+
+ if (inst->module->mod_instantiate) {
+ CONF_SECTION *cs;
+ char const *name;
+
+ name = strrchr(inst->config->sql_driver_name, '_');
+ if (!name) {
+ name = inst->config->sql_driver_name;
+ } else {
+ name++;
+ }
+
+ cs = cf_section_sub_find(conf, name);
+ if (!cs) {
+ cs = cf_section_alloc(conf, name, NULL);
+ if (!cs) {
+ return -1;
+ }
+ }
+
+ /*
+ * It's up to the driver to register a destructor
+ */
+ if (inst->module->mod_instantiate(cs, inst->config) < 0) {
+ return -1;
+ }
+ }
+
+ inst->ef = exfile_init(inst, 256, 30, true);
+ if (!inst->ef) {
+ cf_log_err_cs(conf, "Failed creating log file context");
+ return -1;
+ }
+
+ /*
+ * Initialise the connection pool for this instance
+ */
+ INFO("rlm_sql (%s): Attempting to connect to database \"%s\"", inst->name, inst->config->sql_db);
+
+ inst->pool = fr_connection_pool_module_init(inst->cs, inst, mod_conn_create, NULL, NULL);
+ if (!inst->pool) return -1;
+
+ if (inst->config->do_clients) {
+ if (generate_sql_clients(inst) == -1){
+ ERROR("Failed to load clients from SQL");
+ return -1;
+ }
+ }
+
+ return RLM_MODULE_OK;
+}
+
+static rlm_rcode_t mod_authorize(void *instance, REQUEST *request) CC_HINT(nonnull);
+static rlm_rcode_t mod_authorize(void *instance, REQUEST *request)
+{
+ rlm_rcode_t rcode = RLM_MODULE_NOOP;
+
+ rlm_sql_t *inst = instance;
+ rlm_sql_handle_t *handle;
+
+ VALUE_PAIR *check_tmp = NULL;
+ VALUE_PAIR *reply_tmp = NULL;
+ VALUE_PAIR *user_profile = NULL;
+
+ bool user_found = false;
+
+ sql_fall_through_t do_fall_through = FALL_THROUGH_DEFAULT;
+
+ int rows;
+
+ char *expanded = NULL;
+
+ rad_assert(request->packet != NULL);
+ rad_assert(request->reply != NULL);
+
+ if (!inst->config->authorize_check_query && !inst->config->authorize_reply_query &&
+ !inst->config->read_groups && !inst->config->read_profiles) {
+ RWDEBUG("No authorization checks configured, returning noop");
+
+ return RLM_MODULE_NOOP;
+ }
+
+ /*
+ * Set, escape, and check the user attr here
+ */
+ if (sql_set_user(inst, request, NULL) < 0) {
+ return RLM_MODULE_FAIL;
+ }
+
+ /*
+ * Reserve a socket
+ *
+ * After this point use goto error or goto release to cleanup socket temporary pairlists and
+ * temporary attributes.
+ */
+ handle = fr_connection_get(inst->pool);
+ if (!handle) {
+ rcode = RLM_MODULE_FAIL;
+ goto error;
+ }
+
+ /*
+ * Query the check table to find any conditions associated with this user/realm/whatever...
+ */
+ if (inst->config->authorize_check_query) {
+ vp_cursor_t cursor;
+ VALUE_PAIR *vp;
+
+ if (radius_axlat(&expanded, request, inst->config->authorize_check_query,
+ inst->sql_escape_func, handle) < 0) {
+ REDEBUG("Error generating query");
+ rcode = RLM_MODULE_FAIL;
+ goto error;
+ }
+
+ rows = sql_getvpdata(request, inst, request, &handle, &check_tmp, expanded);
+ TALLOC_FREE(expanded);
+ if (rows < 0) {
+ REDEBUG("Error getting check attributes");
+ rcode = RLM_MODULE_FAIL;
+ goto error;
+ }
+
+ if (rows == 0) {
+ RWDEBUG2("User not found in radcheck table.");
+ goto skipreply; /* Don't need to free VPs we don't have */
+ }
+
+ /*
+ * Only do this if *some* check pairs were returned
+ */
+ RDEBUG2("User found in radcheck table");
+ user_found = true;
+ if (paircompare(request, request->packet->vps, check_tmp, &request->reply->vps) != 0) {
+ RWDEBUG2("check items do not match.");
+ fr_pair_list_free(&check_tmp);
+ check_tmp = NULL;
+ goto skipreply;
+ }
+
+ RDEBUG2("Conditional check items matched, merging assignment check items");
+ RINDENT();
+ for (vp = fr_cursor_init(&cursor, &check_tmp);
+ vp;
+ vp = fr_cursor_next(&cursor)) {
+ if (!fr_assignment_op[vp->op]) continue;
+
+ rdebug_pair(2, request, vp, NULL);
+ }
+ REXDENT();
+ radius_pairmove(request, &request->config, check_tmp, true);
+
+ rcode = RLM_MODULE_OK;
+ check_tmp = NULL;
+ }
+
+ if (inst->config->authorize_reply_query) {
+ /*
+ * Now get the reply pairs since the paircompare matched
+ */
+ if (radius_axlat(&expanded, request, inst->config->authorize_reply_query,
+ inst->sql_escape_func, handle) < 0) {
+ REDEBUG("Error generating query");
+ rcode = RLM_MODULE_FAIL;
+ goto error;
+ }
+
+ rows = sql_getvpdata(request->reply, inst, request, &handle, &reply_tmp, expanded);
+ TALLOC_FREE(expanded);
+ if (rows < 0) {
+ REDEBUG("SQL query error getting reply attributes");
+ rcode = RLM_MODULE_FAIL;
+ goto error;
+ }
+
+ if (rows == 0) goto skipreply;
+
+ do_fall_through = fall_through(reply_tmp);
+
+ RDEBUG2("User found in radreply table, merging reply items");
+ user_found = true;
+
+ rdebug_pair_list(L_DBG_LVL_2, request, reply_tmp, NULL);
+
+ radius_pairmove(request, &request->reply->vps, reply_tmp, true);
+
+ rcode = RLM_MODULE_OK;
+ reply_tmp = NULL;
+ }
+
+skipreply:
+ if ((do_fall_through == FALL_THROUGH_YES) ||
+ (inst->config->read_groups && (do_fall_through == FALL_THROUGH_DEFAULT))) {
+ rlm_rcode_t ret;
+
+ RDEBUG3("... falling-through to group processing");
+ ret = rlm_sql_process_groups(inst, request, &handle, &do_fall_through);
+ switch (ret) {
+ /*
+ * Nothing bad happened, continue...
+ */
+ case RLM_MODULE_UPDATED:
+ rcode = RLM_MODULE_UPDATED;
+ /* FALL-THROUGH */
+ case RLM_MODULE_OK:
+ if (rcode != RLM_MODULE_UPDATED) {
+ rcode = RLM_MODULE_OK;
+ }
+ /* FALL-THROUGH */
+ case RLM_MODULE_NOOP:
+ user_found = true;
+ break;
+
+ case RLM_MODULE_NOTFOUND:
+ break;
+
+ default:
+ rcode = ret;
+ goto release;
+ }
+ }
+
+ /*
+ * Repeat the above process with the default profile or User-Profile
+ */
+ if ((do_fall_through == FALL_THROUGH_YES) ||
+ (inst->config->read_profiles && (do_fall_through == FALL_THROUGH_DEFAULT))) {
+ rlm_rcode_t ret;
+
+ /*
+ * Check for a default_profile or for a User-Profile.
+ */
+ RDEBUG3("... falling-through to profile processing");
+ user_profile = fr_pair_find_by_num(request->config, PW_USER_PROFILE, 0, TAG_ANY);
+
+ char const *profile = user_profile ?
+ user_profile->vp_strvalue :
+ inst->config->default_profile;
+
+ if (!profile || !*profile) {
+ goto release;
+ }
+
+ RDEBUG2("Checking profile %s", profile);
+
+ if (sql_set_user(inst, request, profile) < 0) {
+ REDEBUG("Error setting profile");
+ rcode = RLM_MODULE_FAIL;
+ goto error;
+ }
+
+ ret = rlm_sql_process_groups(inst, request, &handle, &do_fall_through);
+ switch (ret) {
+ /*
+ * Nothing bad happened, continue...
+ */
+ case RLM_MODULE_UPDATED:
+ rcode = RLM_MODULE_UPDATED;
+ /* FALL-THROUGH */
+ case RLM_MODULE_OK:
+ if (rcode != RLM_MODULE_UPDATED) {
+ rcode = RLM_MODULE_OK;
+ }
+ /* FALL-THROUGH */
+ case RLM_MODULE_NOOP:
+ user_found = true;
+ break;
+
+ case RLM_MODULE_NOTFOUND:
+ break;
+
+ default:
+ rcode = ret;
+ goto release;
+ }
+ }
+
+ /*
+ * At this point the key (user) hasn't been found in the check table, the reply table
+ * or the group mapping table, and there was no matching profile.
+ */
+release:
+ if (!user_found) {
+ rcode = RLM_MODULE_NOTFOUND;
+ }
+
+ fr_connection_release(inst->pool, handle);
+ sql_unset_user(inst, request);
+
+ return rcode;
+
+error:
+ fr_pair_list_free(&check_tmp);
+ fr_pair_list_free(&reply_tmp);
+ sql_unset_user(inst, request);
+
+ fr_connection_release(inst->pool, handle);
+
+ return rcode;
+}
+
+/*
+ * Generic function for failing between a bunch of queries.
+ *
+ * Uses the same principle as rlm_linelog, expanding the 'reference' config
+ * item using xlat to figure out what query it should execute.
+ *
+ * If the reference matches multiple config items, and a query fails or
+ * doesn't update any rows, the next matching config item is used.
+ *
+ */
+static int acct_redundant(rlm_sql_t *inst, REQUEST *request, sql_acct_section_t *section)
+{
+ rlm_rcode_t rcode = RLM_MODULE_OK;
+
+ rlm_sql_handle_t *handle = NULL;
+ int sql_ret;
+ int numaffected = 0;
+
+ CONF_ITEM *item;
+ CONF_PAIR *pair;
+ char const *attr = NULL;
+ char const *value;
+
+ char path[MAX_STRING_LEN];
+ char *p = path;
+ char *expanded = NULL;
+
+ rad_assert(section);
+
+ if (section->reference[0] != '.') {
+ *p++ = '.';
+ }
+
+ if (radius_xlat(p, sizeof(path) - (p - path), request, section->reference, NULL, NULL) < 0) {
+ rcode = RLM_MODULE_FAIL;
+
+ goto finish;
+ }
+
+ /*
+ * If we can't find a matching config item we do
+ * nothing so return RLM_MODULE_NOOP.
+ */
+ item = cf_reference_item(NULL, section->cs, path);
+ if (!item) {
+ RWDEBUG("No such configuration item %s", path);
+ rcode = RLM_MODULE_NOOP;
+
+ goto finish;
+ }
+ if (cf_item_is_section(item)){
+ RWDEBUG("Sections are not supported as references");
+ rcode = RLM_MODULE_NOOP;
+
+ goto finish;
+ }
+
+ pair = cf_item_to_pair(item);
+ attr = cf_pair_attr(pair);
+
+ RDEBUG2("Using query template '%s'", attr);
+
+ handle = fr_connection_get(inst->pool);
+ if (!handle) {
+ rcode = RLM_MODULE_FAIL;
+
+ goto finish;
+ }
+
+ sql_set_user(inst, request, NULL);
+
+ while (true) {
+ value = cf_pair_value(pair);
+ if (!value) {
+ RDEBUG("Ignoring null query");
+ rcode = RLM_MODULE_NOOP;
+
+ goto finish;
+ }
+
+ if (radius_axlat(&expanded, request, value, inst->sql_escape_func, handle) < 0) {
+ rcode = RLM_MODULE_FAIL;
+
+ goto finish;
+ }
+
+ if (!*expanded) {
+ RDEBUG("Ignoring null query");
+ rcode = RLM_MODULE_NOOP;
+
+ goto finish;
+ }
+
+ rlm_sql_query_log(inst, request, section, expanded);
+
+ sql_ret = rlm_sql_query(inst, request, &handle, expanded);
+ TALLOC_FREE(expanded);
+ RDEBUG("SQL query returned: %s", fr_int2str(sql_rcode_table, sql_ret, "<INVALID>"));
+
+ switch (sql_ret) {
+ /*
+ * Query was a success! Now we just need to check if it did anything.
+ */
+ case RLM_SQL_OK:
+ break;
+
+ /*
+ * A general, unrecoverable server fault.
+ */
+ case RLM_SQL_ERROR:
+ /*
+ * If we get RLM_SQL_RECONNECT it means all connections in the pool
+ * were exhausted and we couldn't create a new connection,
+ * so we do not need to call fr_connection_release.
+ */
+ case RLM_SQL_RECONNECT:
+ rcode = RLM_MODULE_FAIL;
+ goto finish;
+
+ /*
+ * Query was invalid, this is a terminal error, but we still need
+ * to do cleanup, as the connection handle is still valid.
+ */
+ case RLM_SQL_QUERY_INVALID:
+ rcode = RLM_MODULE_INVALID;
+ goto finish;
+
+ /*
+ * Driver found an error (like a unique key constraint violation)
+ * that hinted it might be a good idea to try an alternative query.
+ */
+ case RLM_SQL_ALT_QUERY:
+ goto next;
+ }
+ rad_assert(handle);
+
+ /*
+ * We need to have updated something for the query to have been
+ * counted as successful.
+ */
+ numaffected = (inst->module->sql_affected_rows)(handle, inst->config);
+ (inst->module->sql_finish_query)(handle, inst->config);
+ RDEBUG("%i record(s) updated", numaffected);
+
+ if (numaffected > 0) break; /* A query succeeded, we're done! */
+ next:
+ /*
+ * We assume all entries with the same name form a redundant
+ * set of queries.
+ */
+ pair = cf_pair_find_next(section->cs, pair, attr);
+
+ if (!pair) {
+ char const *name;
+
+ /*
+ * Hack for RADIUS!
+ */
+ name = cf_section_name1(section->cs);
+ if ((strcmp(name, "accounting-on") == 0) ||
+ (strcmp(name, "accounting-off") == 0)) {
+ RDEBUG("Accounting on/off had no updates. Returning 'ok'");
+ rcode = RLM_MODULE_OK;
+ goto finish;
+ }
+
+
+ RDEBUG("No additional queries configured");
+ rcode = RLM_MODULE_NOOP;
+
+ goto finish;
+ }
+
+ RDEBUG("Trying next query...");
+ }
+
+
+finish:
+ talloc_free(expanded);
+ fr_connection_release(inst->pool, handle);
+ sql_unset_user(inst, request);
+
+ return rcode;
+}
+
+#ifdef WITH_ACCOUNTING
+
+/*
+ * Accounting: Insert or update session data in our sql table
+ */
+static rlm_rcode_t mod_accounting(void *instance, REQUEST *request) CC_HINT(nonnull);
+static rlm_rcode_t mod_accounting(void *instance, REQUEST *request)
+{
+ rlm_sql_t *inst = instance;
+
+ if (inst->config->accounting.reference_cp) {
+ return acct_redundant(inst, request, &inst->config->accounting);
+ }
+
+ return RLM_MODULE_NOOP;
+}
+
+#endif
+
+#ifdef WITH_SESSION_MGMT
+/*
+ * See if a user is already logged in. Sets request->simul_count to the
+ * current session count for this user.
+ *
+ * Check twice. If on the first pass the user exceeds his
+ * max. number of logins, do a second pass and validate all
+ * logins by querying the terminal server (using eg. SNMP).
+ */
+static rlm_rcode_t mod_checksimul(void *instance, REQUEST *request) CC_HINT(nonnull);
+static rlm_rcode_t mod_checksimul(void *instance, REQUEST * request)
+{
+ rlm_rcode_t rcode = RLM_MODULE_OK;
+ rlm_sql_handle_t *handle = NULL;
+ rlm_sql_t *inst = instance;
+ rlm_sql_row_t row;
+ int check = 0;
+ uint32_t ipno = 0;
+ char const *call_num = NULL;
+ VALUE_PAIR *vp;
+ int ret;
+ fr_ipaddr_t nas_addr;
+ char const *nas_port_id = NULL;
+
+ char *expanded = NULL;
+
+ /* If simul_count_query is not defined, we don't do any checking */
+ if (!inst->config->simul_count_query) {
+ RWDEBUG("Simultaneous-Use checking requires 'simul_count_query' to be configured");
+ return RLM_MODULE_NOOP;
+ }
+
+ if ((!request->username) || (request->username->vp_length == 0)) {
+ REDEBUG("Zero Length username not permitted");
+
+ return RLM_MODULE_INVALID;
+ }
+
+ if (sql_set_user(inst, request, NULL) < 0) {
+ return RLM_MODULE_FAIL;
+ }
+
+ /* initialize the sql socket */
+ handle = fr_connection_get(inst->pool);
+ if (!handle) {
+ talloc_free(expanded);
+ sql_unset_user(inst, request);
+ return RLM_MODULE_FAIL;
+ }
+
+ if (radius_axlat(&expanded, request, inst->config->simul_count_query, inst->sql_escape_func, handle) < 0) {
+ sql_unset_user(inst, request);
+ return RLM_MODULE_FAIL;
+ }
+
+ if (rlm_sql_select_query(inst, request, &handle, expanded) != RLM_SQL_OK) {
+ rcode = RLM_MODULE_FAIL;
+ goto release; /* handle may no longer be valid */
+ }
+
+ ret = rlm_sql_fetch_row(inst, request, &handle);
+ if (ret != 0) {
+ rcode = RLM_MODULE_FAIL;
+ goto finish;
+ }
+
+ row = handle->row;
+ if (!row) {
+ rcode = RLM_MODULE_FAIL;
+ goto finish;
+ }
+
+ request->simul_count = atoi(row[0]);
+
+ (inst->module->sql_finish_select_query)(handle, inst->config);
+ TALLOC_FREE(expanded);
+
+ if (request->simul_count < request->simul_max) {
+ rcode = RLM_MODULE_OK;
+ goto finish;
+ }
+
+ /*
+ * Looks like too many sessions, so let's start verifying
+ * them, unless told to rely on count query only.
+ */
+ if (!inst->config->simul_verify_query) {
+ rcode = RLM_MODULE_OK;
+
+ goto finish;
+ }
+
+ if (radius_axlat(&expanded, request, inst->config->simul_verify_query, inst->sql_escape_func, handle) < 0) {
+ rcode = RLM_MODULE_FAIL;
+
+ goto finish;
+ }
+
+ if (rlm_sql_select_query(inst, request, &handle, expanded) != RLM_SQL_OK) goto release;
+
+ /*
+ * Setup some stuff, like for MPP detection.
+ */
+ request->simul_count = 0;
+
+ if ((vp = fr_pair_find_by_num(request->packet->vps, PW_FRAMED_IP_ADDRESS, 0, TAG_ANY)) != NULL) {
+ ipno = vp->vp_ipaddr;
+ }
+
+ if ((vp = fr_pair_find_by_num(request->packet->vps, PW_CALLING_STATION_ID, 0, TAG_ANY)) != NULL) {
+ call_num = vp->vp_strvalue;
+ }
+
+ while (rlm_sql_fetch_row(inst, request, &handle) == RLM_SQL_OK) {
+ int num_rows;
+
+ row = handle->row;
+ if (!row) {
+ break;
+ }
+
+ num_rows = (inst->module->sql_num_fields)(handle, inst->config);
+ if (num_rows < 8) {
+ RDEBUG("Too few rows returned. Please do not edit 'simul_verify_query'");
+ rcode = RLM_MODULE_FAIL;
+
+ goto finish;
+ }
+
+ if (!row[2]){
+ RDEBUG("Cannot zap stale entry. No username present in entry");
+ rcode = RLM_MODULE_FAIL;
+
+ goto finish;
+ }
+
+ if (!row[1]){
+ RDEBUG("Cannot zap stale entry. No session id in entry");
+ rcode = RLM_MODULE_FAIL;
+
+ goto finish;
+ }
+
+ if (row[3]) {
+ if (fr_pton(&nas_addr, row[3], -1, AF_UNSPEC, false) < 0) {
+ RDEBUG("Cannot parse '%s' as an IPv4 or an IPv6 address - %s", row[3], fr_strerror());
+ rcode = RLM_MODULE_FAIL;
+ goto finish;
+ }
+ }
+
+ nas_port_id = row[4];
+
+ check = rad_check_ts(&nas_addr, 0, nas_port_id, row[2], row[1]);
+ if (check == 0) {
+ /*
+ * Stale record - zap it.
+ */
+ if (inst->config->delete_stale_sessions == true) {
+ uint32_t framed_addr = 0;
+ char proto = 0;
+ int sess_time = 0;
+
+ if (row[5])
+ framed_addr = inet_addr(row[5]);
+ if (row[7]){
+ if (strcmp(row[7], "PPP") == 0)
+ proto = 'P';
+ else if (strcmp(row[7], "SLIP") == 0)
+ proto = 'S';
+ }
+ if ((num_rows > 8) && row[8])
+ sess_time = atoi(row[8]);
+ session_zap(request, &nas_addr, 0, nas_port_id,
+ row[2], row[1], framed_addr,
+ proto, sess_time);
+ }
+ }
+ else if (check == 1) {
+ /*
+ * User is still logged in.
+ */
+ ++request->simul_count;
+
+ /*
+ * Does it look like a MPP attempt?
+ */
+ if (row[5] && ipno && inet_addr(row[5]) == ipno) {
+ request->simul_mpp = 2;
+ } else if (row[6] && call_num && !strncmp(row[6],call_num,16)) {
+ request->simul_mpp = 2;
+ }
+ } else {
+ /*
+ * Failed to check the terminal server for
+ * duplicate logins: return an error.
+ */
+ REDEBUG("Failed to check the terminal server for user '%s'.", row[2]);
+
+ rcode = RLM_MODULE_FAIL;
+ goto finish;
+ }
+ }
+
+finish:
+ (inst->module->sql_finish_select_query)(handle, inst->config);
+release:
+ fr_connection_release(inst->pool, handle);
+ talloc_free(expanded);
+ sql_unset_user(inst, request);
+
+ /*
+ * The Auth module apparently looks at request->simul_count,
+ * not the return value of this module when deciding to deny
+ * a call for too many sessions.
+ */
+ return rcode;
+}
+#endif
+
+/*
+ * Postauth: Write a record of the authentication attempt
+ */
+static rlm_rcode_t mod_post_auth(void *instance, REQUEST *request) CC_HINT(nonnull);
+static rlm_rcode_t mod_post_auth(void *instance, REQUEST *request)
+{
+ rlm_sql_t *inst = instance;
+
+ if (inst->config->postauth.reference_cp) {
+ return acct_redundant(inst, request, &inst->config->postauth);
+ }
+
+ return RLM_MODULE_NOOP;
+}
+
+/*
+ * Execute postauth_query after authentication
+ */
+
+
+/* globally exported name */
+extern module_t rlm_sql;
+module_t rlm_sql = {
+ .magic = RLM_MODULE_INIT,
+ .name = "sql",
+ .type = RLM_TYPE_THREAD_SAFE,
+ .inst_size = sizeof(rlm_sql_t),
+ .config = module_config,
+ .bootstrap = mod_bootstrap,
+ .instantiate = mod_instantiate,
+ .detach = mod_detach,
+ .methods = {
+ [MOD_AUTHORIZE] = mod_authorize,
+#ifdef WITH_ACCOUNTING
+ [MOD_ACCOUNTING] = mod_accounting,
+#endif
+#ifdef WITH_SESSION_MGMT
+ [MOD_SESSION] = mod_checksimul,
+#endif
+ [MOD_POST_AUTH] = mod_post_auth
+ },
+};