From 379ed2b800e4ae29efa0af242a61ed1b81b18cb5 Mon Sep 17 00:00:00 2001
From: Robert Anderson <randerson@lbsd.net>
Date: Wed, 23 Mar 2011 06:56:53 +0000
Subject: [PATCH] Provide user data storage with cache support

Added new data_set() and data_get() userdb functions for setting and fetching user data. New table users_data stores user data as Name => Value. Updated UPGRADING file with upgrading instructions.
---
 UPGRADING                                 |  18 ++
 database/core.tsql                        |  17 ++
 smradius/modules/userdb/mod_userdb_sql.pm | 272 +++++++++++++++++++++-
 smradiusd.conf                            |  57 +++++
 4 files changed, 357 insertions(+), 7 deletions(-)

diff --git a/UPGRADING b/UPGRADING
index 79eefe1b..ddd463f1 100644
--- a/UPGRADING
+++ b/UPGRADING
@@ -1,4 +1,22 @@
 
+2011-03-22:
+	Add users_data table for various functions
+
+	CREATE TABLE @PREFIX@users_data (
+		ID 		@SERIAL_TYPE@,
+
+		UserID 		@INT_UNSIGNED@,
+
+		LastUpdated 	DATETIME,
+
+		Name 		VARCHAR(255),
+
+		Value 		VARCHAR(255),
+
+		UNIQUE 		(UserID,Name)
+	);
+
+
 2011-01-11:
 	Move configuration file item "use_packet_timestamp=" to "[radius]" section
 
diff --git a/database/core.tsql b/database/core.tsql
index a179dca9..922c4bc3 100644
--- a/database/core.tsql
+++ b/database/core.tsql
@@ -264,3 +264,20 @@ CREATE TABLE @PREFIX@accounting_summary (
 
 	TotalOutput		@INT_UNSIGNED@
 ) @CREATE_TABLE_SUFFIX@;
+
+
+
+/* Users data */
+CREATE TABLE @PREFIX@users_data (
+	ID 			@SERIAL_TYPE@,
+
+	UserID			@INT_UNSIGNED@,
+
+	LastUpdated		DATETIME,
+
+	Name			VARCHAR(255),
+
+	Value			VARCHAR(255),
+
+	UNIQUE (UserID,Name)
+) @CREATE_TABLE_SUFFIX@; 
diff --git a/smradius/modules/userdb/mod_userdb_sql.pm b/smradius/modules/userdb/mod_userdb_sql.pm
index 6f104f63..21af1c13 100644
--- a/smradius/modules/userdb/mod_userdb_sql.pm
+++ b/smradius/modules/userdb/mod_userdb_sql.pm
@@ -22,6 +22,7 @@ use warnings;
 
 # Modules we need
 use smradius::constants;
+use awitpt::cache;
 use smradius::logging;
 use awitpt::db::dblayer;
 use smradius::util;
@@ -46,6 +47,10 @@ our $pluginInfo = {
 	# User database
 	User_find => \&find,
 	User_get => \&get,
+
+	# Users data
+	Users_data_set => \&data_set,
+	Users_data_get => \&data_get
 };
 
 # Module config
@@ -92,10 +97,62 @@ sub init
 		FROM
 			@TP@user_attributes
 		WHERE
-			UserID = %{userdb.ID}
+			UserID = %{userdb.id}
 			AND Disabled = 0
 	';
 	
+	$config->{'users_data_set_query'} = '
+		INSERT INTO
+			@TP@users_data (UserID, LastUpdated, Name, Value)
+		VALUES
+			(
+				%{userdb.id},
+				%{query.LastUpdated},
+				%{query.Name},
+				%{query.Value}
+			)
+	';
+	
+	$config->{'users_data_update_query'} = '
+		UPDATE
+			@TP@users_data
+		SET
+			LastUpdated = %{query.LastUpdated},
+			Value = %{query.Value}
+		WHERE
+			UserID = %{userdb.id}
+			AND Name = %{query.Name}
+	';
+	
+	$config->{'users_data_get_query'} = '
+		SELECT
+			LastUpdated, Name, Value
+		FROM
+			@TP@users_data
+		WHERE
+			UserID = %{userdb.id}
+			AND Name > %{query.Name}
+	';
+	
+	$config->{'users_data_delete_query'} = '
+		DELETE FROM
+			@TP@users_data
+		WHERE
+			UserID = %{userdb.id}
+			AND Name = %{query.Name}
+	';
+
+	$config->{'users_data_cleanup_query'} = '
+		DELETE FROM
+			@TP@users_data
+		WHERE UserID NOT IN
+			(
+				SELECT ID FROM users
+			)
+	';
+
+	# Default cache time for user data
+	$config->{'userdb_data_cache_time'} = 300;
 
 	# Setup SQL queries
 	if (defined($scfg->{'mod_userdb_sql'})) {
@@ -130,6 +187,76 @@ sub init
 						$scfg->{'mod_userdb_sql'}->{'userdb_get_user_attributes_query'};
 			}
 		}
+
+		if (defined($scfg->{'mod_userdb_sql'}->{'users_data_set_query'}) &&
+				$scfg->{'mod_userdb_sql'}->{'users_data_set_query'} ne "") {
+			if (ref($scfg->{'mod_userdb_sql'}->{'users_data_set_query'}) eq "ARRAY") {
+				$config->{'users_data_set_query'} = join(' ',
+						@{$scfg->{'mod_userdb_sql'}->{'users_data_set_query'}});
+			} else {
+					$config->{'users_data_set_query'} = $scfg->{'mod_userdb_sql'}->{'users_data_set_query'};
+			}
+		}
+
+		if (defined($scfg->{'mod_userdb_sql'}->{'users_data_update_query'}) &&
+				$scfg->{'mod_userdb_sql'}->{'users_data_update_query'} ne "") {
+			if (ref($scfg->{'mod_userdb_sql'}->{'users_data_update_query'}) eq "ARRAY") {
+				$config->{'users_data_update_query'} = join(' ',
+						@{$scfg->{'mod_userdb_sql'}->{'users_data_update_query'}});
+			} else {
+					$config->{'users_data_update_query'} = $scfg->{'mod_userdb_sql'}->{'users_data_update_query'};
+			}
+		}
+
+		if (defined($scfg->{'mod_userdb_sql'}->{'users_data_get_query'}) &&
+				$scfg->{'mod_userdb_sql'}->{'users_data_get_query'} ne "") {
+			if (ref($scfg->{'mod_userdb_sql'}->{'users_data_get_query'}) eq "ARRAY") {
+				$config->{'users_data_get_query'} = join(' ',
+						@{$scfg->{'mod_userdb_sql'}->{'users_data_get_query'}});
+			} else {
+					$config->{'users_data_get_query'} = $scfg->{'mod_userdb_sql'}->{'users_data_get_query'};
+			}
+		}
+
+		if (defined($scfg->{'mod_userdb_sql'}->{'users_data_delete_query'}) &&
+				$scfg->{'mod_userdb_sql'}->{'users_data_delete_query'} ne "") {
+			if (ref($scfg->{'mod_userdb_sql'}->{'users_data_delete_query'}) eq "ARRAY") {
+				$config->{'users_data_delete_query'} = join(' ',
+						@{$scfg->{'mod_userdb_sql'}->{'users_data_delete_query'}});
+			} else {
+					$config->{'users_data_delete_query'} = $scfg->{'mod_userdb_sql'}->{'users_data_delete_query'};
+			}
+		}
+
+		if (defined($scfg->{'mod_userdb_sql'}->{'users_data_cleanup_query'}) &&
+				$scfg->{'mod_userdb_sql'}->{'users_data_cleanup_query'} ne "") {
+			if (ref($scfg->{'mod_userdb_sql'}->{'users_data_cleanup_query'}) eq "ARRAY") {
+				$config->{'users_data_cleanup_query'} = join(' ',
+						@{$scfg->{'mod_userdb_sql'}->{'users_data_cleanup_query'}});
+			} else {
+					$config->{'users_data_cleanup_query'} = $scfg->{'mod_userdb_sql'}->{'users_data_cleanup_query'};
+			}
+		}
+
+		if (defined($scfg->{'mod_userdb_sql'}->{'userdb_data_cache_time'})) {
+			if ($scfg->{'mod_userdb_sql'}{'userdb_data_cache_time'} =~ /^\s*(yes|true|1)\s*$/i) {
+				# Default?
+			} elsif ($scfg->{'mod_userdb_sql'}{'userdb_data_cache_time'} =~ /^\s*(no|false|0)\s*$/i) {
+				$config->{'mod_userdb_sql'}{'userdb_data_cache_time'} = undef;
+			} elsif ($scfg->{'mod_userdb_sql'}{'userdb_data_cache_time'} =~ /^[0-9]+$/) {
+				$config->{'mod_userdb_sql'}{'userdb_data_cache_time'} = $scfg->{'mod_userdb_sql'}{'userdb_data_cache_time'};
+			} else {
+				$server->log(LOG_NOTICE,"[MOD_USERDB_SQL] Value for 'userdb_data_cache_time' is invalid");
+			}
+		}
+	}
+
+	# Log this for info sake
+	if (defined($config->{'userdb_data_cache_time'})) {
+		$server->log(LOG_NOTICE,"[MOD_USERDB_SQL] Users data caching ENABLED, cache time is %ds.",
+				$config->{'userdb_data_cache_time'});
+	} else {
+		$server->log(LOG_NOTICE,"[MOD_USERDB_SQL] Users caching DISABLED");
 	}
 }
 
@@ -137,10 +264,11 @@ sub init
 # Try find a user
 #
 # @param server Server object
-# @param user User
+# @param user SMRadius user hash
+# @li Username Username of the user we want
 # @param packet Radius packet
 #
-# @return Result
+# @return UserDB hash of db query
 sub find
 {
 	my ($server,$user,$packet) = @_;
@@ -191,10 +319,12 @@ sub find
 # Try to get a user
 #
 # @param server Server object
-# @param user User
+# @param user UserDB hash we got from find()
 # @param packet Radius packet
 #
-# @return Result
+# @return User attributes hash
+# @li Attributes Radius attribute hash
+# @li VAttributes Radius vendor attribute hash
 sub get
 {
 	my ($server,$user,$packet) = @_;
@@ -220,7 +350,7 @@ sub get
 	my $sth = DBSelect(@dbDoParams);
 	if (!$sth) {
 		$server->log(LOG_ERR,"Failed to get group attributes: ".awitpt::db::dblayer::Error());
-		return -1;
+		return RES_ERROR;
 	}
 
 	# Loop with group attributes
@@ -238,7 +368,7 @@ sub get
 	$sth = DBSelect(@dbDoParams);
 	if (!$sth) {
 		$server->log(LOG_ERR,"Failed to get user attributes: ".awitpt::db::dblayer::Error());
-		return -1;
+		return RES_ERROR;
 	}
 
 	# Loop with user attributes
@@ -256,5 +386,133 @@ sub get
 }
 
 
+## @data_set
+# Set user data
+#
+# @param server Server object
+# @param user UserDB hash we got from find()
+# @param name Variable name
+# @param value Variable value
+#
+# @return RES_OK on success, RES_ERROR on error
+sub data_set
+{
+	my ($server, $user, $name, $value) = @_;
+
+
+	# Build template
+	my $template;
+	# Last updated time would be now
+	$template->{'query'}->{'LastUpdated'} = $user->{'_Internal'}->{'Timestamp-Unix'};
+	$template->{'query'}->{'Name'} = $name;
+	$template->{'query'}->{'Value'} = $value;
+
+	# Add in userdb data
+	foreach my $item (keys %{$user->{'_UserDB_Data'}}) {
+		$template->{'userdb'}->{$item} =  $user->{'_UserDB_Data'}->{$item};
+	}
+
+	# Replace template entries
+	my @dbDoParams = templateReplace($config->{'users_data_update_query'},$template);
+
+	# Query database
+	my $sth = DBDo(@dbDoParams);
+	if (!$sth) {
+		$server->log(LOG_ERR,"Failed to update users data: ".awitpt::db::dblayer::Error());
+		return RES_ERROR;
+	}
+
+	# If we updated *something* ...
+	if ($sth eq "0E0") {
+		@dbDoParams = templateReplace($config->{'users_data_set_query'},$template);
+
+		# Insert
+		$sth = DBDo(@dbDoParams);
+		if (!$sth) {
+			$server->log(LOG_ERR,"Failed to set users data: ".awitpt::db::dblayer::Error());
+			return RES_ERROR;
+		}
+	}
+
+	# If we using caching, cache the result of this set
+	if (defined($config->{'userdb_data_cache_time'})) {
+		# Build hash to store
+		my %data;
+		$data{'CachedUntil'} = $user->{'_Internal'}->{'Timestamp-Unix'} + $config->{'accounting_usage_cache_time'};
+		$data{'LastUpdated'} = $user->{'_Internal'}->{'Timestamp-Unix'};
+		$data{'Name'} = $name;
+		$data{'Value'} = $value;
+		
+		# Cache the result
+		cacheStoreComplexKeyPair('mod_userdb_sql(data_get)',$user->{'UserID'}."/".$template->{'query'}->{'Name'},\%data);
+	}
+
+	return RES_OK;
+}
+
+
+## @data_get
+# Get user data
+#
+# @param server Server object
+# @param user UserDB hash we got from find()
+# @param name Variable name
+#
+# @return Users data hash
+# @li LastUpdated Time of last update
+# @li Name Variable Name
+# @li Value Variable Value
+sub data_get
+{
+	my ($server, $user, $name) = @_;
+
+
+	# Build template
+	my $template;
+	$template->{'query'}->{'Name'} = $name;
+
+	# Add in userdb data
+	foreach my $item (keys %{$user->{'_UserDB_Data'}}) {
+		$template->{'userdb'}->{$item} =  $user->{'_UserDB_Data'}->{$item};
+	}
+
+	# If we using caching, check how old the result is
+	if (defined($config->{'userdb_data_cache_time'})) {
+		my ($res,$val) = cacheGetComplexKeyPair('mod_userdb_sql(data_get)',$user->{'UserID'}."/".$template->{'query'}->{'Name'});
+		if (defined($val) && $val->{'CachedUntil'} > $user->{'_Internal'}->{'Timestamp-Unix'}) {
+			return $val;
+		}
+	}
+
+	# Replace template entries
+	my @dbDoParams = templateReplace($config->{'user_data_get_query'},$template);
+
+	# Query database
+	my $sth = DBSelect(@dbDoParams);
+	if (!$sth) {
+		$server->log(LOG_ERR,"Failed to get users data: ".awitpt::db::dblayer::Error());
+		return RES_ERROR;
+	}
+
+	# Fetch user data
+	my $row = $sth->fetchrow_hashref();
+
+	my %data;
+	$data{'LastUpdated'} = $row->{'LastUpdated'};
+	$data{'Name'} = $row->{'Name'};
+	$data{'Value'} = $row->{'Value'};
+
+	# If we using caching and got here, it means that we must cache the result
+	if (defined($config->{'userdb_data_cache_time'})) {
+		$data{'CachedUntil'} = $user->{'_Internal'}->{'Timestamp-Unix'} + $config->{'userdb_data_cache_time'};
+		
+		# Cache the result
+		cacheStoreComplexKeyPair('mod_userdb_sql(data_get)',$user->{'UserID'}."/".$template->{'query'}->{'Name'},\%data);
+	}
+
+	return \%data;
+}
+
+
 1;
 # vim: ts=4
diff --git a/smradiusd.conf b/smradiusd.conf
index 97c32b8f..0d2ef8e1 100644
--- a/smradiusd.conf
+++ b/smradiusd.conf
@@ -447,6 +447,63 @@ userdb_get_user_attributes_query=<<EOT
 		AND Disabled = 0
 EOT
 
+users_data_set_query=<<EOT
+	INSERT INTO
+		@TP@users_data (UserID, LastUpdated, Name, Value)
+	VALUES
+		(
+			%{userdb.id},
+			%{query.LastUpdated},
+			%{query.Name},
+			%{query.Value}
+		)
+EOT
+
+users_data_update_query=<<EOT
+	UPDATE
+		@TP@users_data
+	SET
+		LastUpdated = %{query.LastUpdated},
+		Value = %{query.Value}
+	WHERE
+		UserID = %{userdb.id}
+		AND Name = %{query.Name}
+EOT
+
+users_data_get_query=<<EOT
+	SELECT
+		LastUpdated, Name, Value
+	FROM
+		@TP@users_data
+	WHERE
+		UserID = %{userdb.id}
+		AND Name > %{query.Name}
+EOT
+
+users_data_delete_query=<<EOT
+	DELETE FROM
+		@TP@users_data
+	WHERE
+		UserID = %{userdb.id}
+		AND Name = %{query.Name}
+EOT
+
+users_data_cleanup_query=<<EOT
+	DELETE FROM
+		@TP@users_data
+	WHERE UserID NOT IN
+		(
+			SELECT ID FROM users
+		)
+EOT
+
+# This is how long we going to cache the data query for
+# Default: 300 (seconds)
+#
+# You can use  "no", "0", "false"  to disable, specify a number > 1, or use 
+# "yes", "1", "true" to enable with the default value
+userdb_data_cache_time=300
+
 
 # MOD_FEATURE_UPDATE_USER_STATS_SQL
 [mod_feature_update_user_stats_sql]
-- 
GitLab