From: Derek Atkins Date: Thu, 26 Jun 2003 20:25:55 +0000 (+0000) Subject: The MySQL Credential Cache X-Git-Tag: 1.2.1~638 X-Git-Url: http://www.project-moonshot.org/gitweb/?p=shibboleth%2Fcpp-sp.git;a=commitdiff_plain;h=21b26ccdfb30120f68fa8901791ea093dee16156 The MySQL Credential Cache --- diff --git a/Makefile.am b/Makefile.am index f27229c..21c057c 100644 --- a/Makefile.am +++ b/Makefile.am @@ -42,5 +42,5 @@ WANT_SUBDIRS = @WANT_SUBDIRS@ SUBDIRS = $(WANT_SUBDIRS) DIST_SUBDIRS = doc oncrpc shib schemas configs shib-target shar test \ - mod_shire mod_shibrm siterefresh + mod_shire mod_shibrm siterefresh shib-mysql-ccache diff --git a/configure.ac b/configure.ac index bfe0e6b..7fbdc4a 100644 --- a/configure.ac +++ b/configure.ac @@ -336,6 +336,139 @@ AC_CONFIG_FILES([mod_shire/Makefile mod_shibrm/Makefile]) AM_CONDITIONAL(DO_APXS_INSTALL,test -n "$APXS_INSTALL") +# +# Implement the checks of the MySQL Credential Cache +# +# 1) Assume the user wants MySQL; if it's not found then just continue without +# 2) If the user specifically requested Mysql, look for it and ERROR if not found +# 3) If the user specifically requested no-mysql, don't build it. +# + +AC_CONFIG_FILES([shib-mysql-ccache/Makefile]) + +# determine whether we should enable the mysql ccache +enable_mysql=default +AC_ARG_ENABLE([mysql], + AC_HELP_STRING([--disable-mysql], [disable the MySQL Credential Cache])) +if test "x$enable_mysql" = "x" ; then + enable_mysql=yes +fi + +# Are we trying to build MySQL? +AC_MSG_CHECKING(whether to build the MySQL ccache) +if test "$enable_mysql" = "yes" -o "$enable_mysql" = "default" ; then + build_mysql=yes + AC_MSG_RESULT(yes) +else + build_mysql=no + AC_MSG_RESULT(no) +fi + +# If we're trying to build MySQL, try to find the mysql_config program, +# verify we've got mysql >= 4, and make sure we can build with mysqld +if test "$build_mysql" = "yes" ; then + mysql_dir="" + AC_ARG_WITH(mysql, + AC_HELP_STRING([--with-mysql=PATH], [directory where mysql is installed]), + [if test "$with_mysql" = no ; then + AC_MSG_ERROR([Try running --disable-mysql instead.]) + elif test "$with_mysql" != yes ; then + mysql_dir="$with_mysql/bin" + fi ]) + + # Try to find the mysql_config program + AC_PATH_PROG(MYSQL_CONFIG, mysql_config, no, $mysql_dir $PATH ) + + if test "$MYSQL_CONFIG" = no ; then + if test "$enable_mysql" = yes ; then + AC_MSG_ERROR(Cannot find mysql_config) + else + AC_MSG_WARN(MySQL not found. skipping.) + fi + fi +fi + +# if we found mysql_config then build_mysql is 'yes' +if test "$MYSQL_CONFIG" != no ; then + AC_MSG_CHECKING(for mysql version >= 4) + mysql_version=`$MYSQL_CONFIG --version` + mysql_major_version=`echo $mysql_version | awk -F. '{print $1}'` + mysql_version_ok=yes + if test $mysql_major_version -lt 4 ; then + mysql_version_ok=no + if test "$enable_mysql" = yes ; then + AC_MSG_ERROR(You need MySQL version >= 4, found $mysql_version) + fi + AC_MSG_RESULT(no.. skipping MySQL) + else + AC_MSG_RESULT(yes) + fi +fi + +# if mysql_version_ok is 'yes', then we've made it this far.. ;) +if test "$mysql_version_ok" = "yes" ; then + AC_MSG_CHECKING(for embedded-MySQL libraries) + MYSQL_LIBS=`$MYSQL_CONFIG --libmysqld-libs` + if test $? != 0 ; then + found_mysql=no + if test "$enable_mysql" = yes ; then + AC_MSG_ERROR(Could not find the MySQL Embedded-server libraries.) + else + AC_MSG_RESULT(no.. skipping MySQL) + fi + else + found_mysql=yes + AC_MSG_RESULT(yes) + fi +fi + +# if found_mysql=yes then test that we can actually build mysql +if test "$found_mysql" = yes ; then + MYSQL_CFLAGS=`$MYSQL_CONFIG --cflags` + MYSQL_CFLAGS=`eval echo $MYSQL_CFLAGS` + MYSQL_LIBS=`eval echo $MYSQL_LIBS` + + save_CPPFLAGS="$CPPFLAGS" + CPPFLAGS="$CPPFLAGS $MYSQL_CFLAGS" + + AC_CHECK_HEADER([mysql.h], [have_mysql_h=yes], [have_mysql_h=no]) + if test "$have_mysql_h" = no -a "$enable_mysql" = yes ; then + AC_MSG_ERROR(unable to find MySQL header files) + fi + + if test "$have_mysql_h" = yes ; then + save_LIBS="$LIBS" + LIBS="$LIBS $MYSQL_LIBS" + AC_MSG_CHECKING(if we can link againt mysql) + AC_TRY_LINK( + [#include ], + [mysql_server_init(0, NULL, NULL)], + [have_mysql_libs=yes], + [have_mysql_libs=no]) + LIBS="$save_LIBS" + + if test "$have_mysql_libs" = no ; then + if "$enable_mysql" = yes ; then + AC_MSG_ERROR([unable to link with MySQL Embedded Server Library]) + else + AC_MSG_RESULT(no. skipping MySQL) + fi + fi + fi + + CPPFLAGS="$save_CPPFLAGS" +fi + +# if have_mysql_libs=yes then go ahead with building MySQL +if test "$have_mysql_libs" = yes ; then + # this AC_MSG_RESULT is from above! + AC_MSG_RESULT(yes) + WANT_SUBDIRS="$WANT_SUBDIRS shib-mysql-ccache" + AC_SUBST(MYSQL_CFLAGS) + AC_SUBST(MYSQL_LIBS) +fi + + AC_SUBST(WANT_SUBDIRS) if test -n "$APXS_CC" && test "$APXS_CC" != "$CC" ; then diff --git a/shib-mysql-ccache/.gitignore b/shib-mysql-ccache/.gitignore new file mode 100644 index 0000000..26d1c48 --- /dev/null +++ b/shib-mysql-ccache/.gitignore @@ -0,0 +1,6 @@ +/Makefile.in +/Makefile +/.libs +/.deps +/*.lo +/*.la \ No newline at end of file diff --git a/shib-mysql-ccache/Makefile.am b/shib-mysql-ccache/Makefile.am new file mode 100644 index 0000000..5cff5b3 --- /dev/null +++ b/shib-mysql-ccache/Makefile.am @@ -0,0 +1,18 @@ +AUTOMAKE_OPTIONS = foreign + +lib_LTLIBRARIES = libshib-mysql-ccache.la + +AM_CFLAGS = $(MYSQL_CFLAGS) +AM_CXXFLAGS = $(AM_CFLAGS) + +libshib_mysql_ccache_la_LIBADD = \ + ../shib-target/libshib-target.la \ + ../shib/libshib.la \ + $(MYSQL_LIBS) + +libshib_mysql_ccache_la_SOURCES = \ + shib-mysql-ccache.cpp + +# this is different from the project version +# http://sources.redhat.com/autobook/autobook/autobook_91.html +libshib_mysql_ccache_la_LDFLAGS = -version-info 1:0:0 diff --git a/shib-mysql-ccache/shib-mysql-ccache.cpp b/shib-mysql-ccache/shib-mysql-ccache.cpp new file mode 100644 index 0000000..d5f01d4 --- /dev/null +++ b/shib-mysql-ccache/shib-mysql-ccache.cpp @@ -0,0 +1,557 @@ +/* + * shib-mysql-ccache.cpp: Shibboleth Credential Cache using MySQL. + * + * Created by: Derek Atkins + * + * $Id$ + */ + +/* This file is loosely based off the Shibboleth Credential Cache. + * This plug-in is designed as a two-layer cache. Layer 1, the + * long-term cache, stores data in a MySQL embedded database. The + * data stored in layer 1 is only the session id (cookie), the + * "posted" SAML statement (expanded into an XML string), and usage + * timestamps. + * + * Short-term data is cached in memory as SAML objects in the layer 2 + * cache. Data like Attribute Authority assertions are stored in + * the layer 2 cache. + */ + +#ifndef WIN32 +# include +#endif + +#include +#include +#include +#include + +#include +#include + +#include + +#ifdef HAVE_LIBDMALLOCXX +#include +#endif + +using namespace std; +using namespace saml; +using namespace shibboleth; +using namespace shibtarget; + +class ShibMySQLCCache; +class ShibMySQLCCacheEntry : public CCacheEntry +{ +public: + ShibMySQLCCacheEntry(const char *, CCacheEntry*, ShibMySQLCCache*); + ~ShibMySQLCCacheEntry() {} + + virtual Iterator getAssertions(Resource& resource) + { return m_cacheEntry->getAssertions(resource); } + virtual void preFetch(Resource& resource, int prefetch_window) + { return m_cacheEntry->preFetch(resource, prefetch_window); } + virtual bool isSessionValid(time_t lifetime, time_t timeout); + virtual const char* getClientAddress() + { return m_cacheEntry->getClientAddress(); } + virtual const char* getSerializedStatement() + { return m_cacheEntry->getSerializedStatement(); } + virtual const SAMLAuthenticationStatement* getStatement() + { return m_cacheEntry->getStatement(); } + virtual void release() + { m_cacheEntry->release(); delete this; } + +private: + void touch(); + + ShibMySQLCCache* m_cache; + CCacheEntry *m_cacheEntry; + string m_key; +}; + +class ShibMySQLCCache : public CCache +{ +public: + ShibMySQLCCache(); + virtual ~ShibMySQLCCache(); + + virtual SAMLBinding* getBinding(const XMLCh* bindingProt) + { return m_cache->getBinding(bindingProt); } + virtual CCacheEntry* find(const char* key); + virtual void insert(const char* key, SAMLAuthenticationStatement *s, + const char *client_addr); + virtual void remove(const char* key); + virtual void thread_init(); + + void cleanup(); + MYSQL* getMYSQL(); + + log4cpp::Category* log; + +private: + CCache* m_cache; + ThreadKey* m_mysql; + + static void* cleanup_fcn(void*); // XXX Assumed an ShibMySQLCCache + CondWait* shutdown_wait; + bool shutdown; + Thread* cleanup_thread; + + bool initialized; + + void createDatabase(MYSQL*, int major, int minor); + void getVersion(MYSQL*, int* major_p, int* minor_p); + void mysqlInit(void); +}; + +// Forward declarations +extern "C" void shib_mysql_destroy_handle(void* data); + +/************************************************************************* + * The CCache here talks to a MySQL database. The database stores + * three items: the cookie (session key index), the lastAccess time, and + * the SAMLAuthenticationStatement. All other access is performed + * through the memory cache provided by shibboleth. + */ + +MYSQL* ShibMySQLCCache::getMYSQL() +{ + void* data = m_mysql->getData(); + return (MYSQL*)data; +} + +void ShibMySQLCCache::thread_init() +{ + saml::NDC ndc("open_db"); + + // Connect to the database + MYSQL* mysql = mysql_init(NULL); + if (!mysql) { + log->error("mysql_init failed"); + mysql_close(mysql); + throw runtime_error("mysql_init()"); + } + + if (!mysql_real_connect(mysql, NULL, NULL, NULL, "shar", 0, NULL, 0)) { + if (initialized) { + log->crit("mysql_real_connect failed: %s", mysql_error(mysql)); + throw runtime_error("mysql_real_connect"); + + } else { + log->info("mysql_real_connect failed: %s. Trying to create", + mysql_error(mysql)); + + // This will throw a runtime error if it fails. + createDatabase(mysql, 0, 0); + } + } + + int major = -1, minor = -1; + getVersion (mysql, &major, &minor); + + // Make sure we've got the right version + if (major != 0 && minor != 0) { + log->crit("Invalid database version: %d.%d", major, minor); + throw runtime_error("Invalid Database version"); + } + + // We're all set.. Save off the handle for this thread. + m_mysql->setData((void*)mysql); +} + +ShibMySQLCCache::ShibMySQLCCache() +{ + saml::NDC ndc("shibmysql::ShibMySQLCCache"); + + m_mysql = ThreadKey::create(&shib_mysql_destroy_handle); + string ctx = "shibmysql::ShibMySQLCCache"; + log = &(log4cpp::Category::getInstance(ctx)); + + initialized = false; + mysqlInit(); + thread_init(); + initialized = true; + + m_cache = CCache::getInstance("memory"); + + // Initialize the cleanup thread + shutdown_wait = CondWait::create(); + shutdown = false; + cleanup_thread = Thread::create(&cleanup_fcn, (void*)this); +} + +ShibMySQLCCache::~ShibMySQLCCache() +{ + shutdown = true; + shutdown_wait->signal(); + cleanup_thread->join(NULL); + + delete m_cache; + delete m_mysql; + + // Shutdown MySQL + mysql_server_end(); +} + +CCacheEntry* ShibMySQLCCache::find(const char* key) +{ + saml::NDC ndc("mysql::find"); + CCacheEntry* res = m_cache->find(key); + if (!res) { + + log->debug("Looking in database..."); + + // nothing cached; see if this exists in the database + ostringstream q; + q << "SELECT addr,statement FROM state WHERE cookie='" << key << "' LIMIT 1"; + + MYSQL_RES* rows; + MYSQL* mysql = getMYSQL(); + if (mysql_query(mysql, q.str().c_str())) + log->error("Error searching for %s: %s", key, mysql_error(mysql)); + + rows = mysql_store_result(mysql); + + // Nope, doesn't exist. + if (!rows) + return NULL; + + // Make sure we got 1 and only 1 rows. + if (mysql_num_rows(rows) != 1) { + log->error("Select returned wrong number of rows: %d", mysql_num_rows(rows)); + mysql_free_result(rows); + return NULL; + } + + log->debug("Match found. Parsing..."); + // Pull apart the row and process the results + MYSQL_ROW row = mysql_fetch_row(rows); + istringstream str(row[1]); + SAMLAuthenticationStatement *s = NULL; + + // Try to parse the AuthStatement + try { + s = new SAMLAuthenticationStatement(str); + } catch (...) { + mysql_free_result(rows); + throw; + } + + // Insert it into the memory cache + if (s) + m_cache->insert(key, s, row[0]); + + // Free the results, and then re-run the 'find' query + mysql_free_result(rows); + res = m_cache->find(key); + if (!res) + return NULL; + } + + return new ShibMySQLCCacheEntry(key, res, this); +} + +void ShibMySQLCCache::insert(const char* key, SAMLAuthenticationStatement *s, + const char *client_addr) +{ + saml::NDC ndc("mysql::insert"); + ostringstream os; + os << *s; + + ostringstream q; + q << "INSERT INTO state VALUES('" << key << "', NOW(), '" << client_addr + << "', '" << os.str() << "')"; + + log->debug("Query: %s", q.str().c_str()); + + // Add it to the memory cache + m_cache->insert(key, s, client_addr); + + // then add it to the database + MYSQL* mysql = getMYSQL(); + if (mysql_query(mysql, q.str().c_str())) + log->error("Error inserting %s: %s", key, mysql_error(mysql)); +} + +void ShibMySQLCCache::remove(const char* key) +{ + saml::NDC ndc("mysql::remove"); + + // Remove the cached version + m_cache->remove(key); + + // Remove from the database + ostringstream q; + q << "DELETE FROM state WHERE cookie='" << key << "'"; + MYSQL* mysql = getMYSQL(); + if (mysql_query(mysql, q.str().c_str())) + log->error("Error deleting entry %s: %s", key, mysql_error(mysql)); +} + +void ShibMySQLCCache::cleanup() +{ + Mutex* mutex = Mutex::create(); + saml::NDC ndc("mysql::cleanup()"); + + thread_init(); + + ShibTargetConfig& config = ShibTargetConfig::getConfig(); + ShibINI& ini = config.getINI(); + + int rerun_timer = 0; + int timeout_life = 0; + + string tag; + if (ini.get_tag (SHIBTARGET_SHAR, SHIBTARGET_TAG_CACHECLEAN, true, &tag)) + rerun_timer = atoi(tag.c_str()); + + // search for 'mysql-cache-timeout' and then the regular cache timeout + if (ini.get_tag (SHIBTARGET_SHAR, "mysql-cache-timeout", true, &tag)) + timeout_life = atoi(tag.c_str()); + else if (ini.get_tag (SHIBTARGET_SHAR, SHIBTARGET_TAG_CACHETIMEOUT, true, &tag)) + timeout_life = atoi(tag.c_str()); + + if (rerun_timer <= 0) + rerun_timer = 300; // rerun every 5 minutes + + if (timeout_life <= 0) + timeout_life = 28800; // timeout after 8 hours + + mutex->lock(); + + MYSQL* mysql = getMYSQL(); + + while (shutdown == false) { + shutdown_wait->timedwait(mutex, rerun_timer); + + if (shutdown == true) + break; + + // Find all the entries in the database that haven't been used + // recently In particular, find all entries that have not been + // accessed in 'timeout_life' seconds. + ostringstream q; + q << "SELECT cookie FROM state WHERE " << + "UNIX_TIMESTAMP(NOW()) - UNIX_TIMESTAMP(atime) >= " << timeout_life; + + MYSQL_RES *rows; + if (mysql_query(mysql, q.str().c_str())) + log->error("Error searching for old items: %s", mysql_error(mysql)); + + rows = mysql_store_result(mysql); + if (!rows) + continue; + + if (mysql_num_fields(rows) != 1) { + log->error("Wrong number of rows, 1 != %d", mysql_num_fields(rows)); + mysql_free_result(rows); + continue; + } + + // For each row, remove the entry from the database. + MYSQL_ROW row; + while ((row = mysql_fetch_row(rows)) != NULL) + remove(row[0]); + + mysql_free_result(rows); + } + + mutex->unlock(); + delete mutex; + thread_end(); + Thread::exit(NULL); +} + +void* ShibMySQLCCache::cleanup_fcn(void* cache_p) +{ + ShibMySQLCCache* cache = (ShibMySQLCCache*)cache_p; + + // First, let's block all signals + Thread::mask_all_signals(); + + // Now run the cleanup process. + cache->cleanup(); + return NULL; +} + +void ShibMySQLCCache::createDatabase(MYSQL* mysql, int major, int minor) +{ + log->info("Creating database."); + + MYSQL* ms = NULL; + try { + ms = mysql_init(NULL); + if (!ms) { + log->crit("mysql_init failed"); + throw new ShibTargetException(); + } + + if (!mysql_real_connect(ms, NULL, NULL, NULL, NULL, 0, NULL, 0)) { + log->crit("cannot open DB file to create DB: %s", mysql_error(ms)); + throw new ShibTargetException(); + } + + if (mysql_query(ms, "CREATE DATABASE shar")) { + log->crit("cannot create shar database: %s", mysql_error(ms)); + throw new ShibTargetException(); + } + + if (!mysql_real_connect(mysql, NULL, NULL, NULL, "shar", 0, NULL, 0)) { + log->crit("cannot open SHAR database"); + throw new ShibTargetException(); + } + + mysql_close(ms); + + } catch (ShibTargetException &e) { + if (ms) + mysql_close(ms); + mysql_close(mysql); + throw runtime_error("mysql_real_connect"); + } + + // Now create the tables if they don't exist + log->info("Creating database tables."); + + if (mysql_query(mysql, "CREATE TABLE version (major INT, minor INT)")) + log->error ("Error creating version: %s", mysql_error(mysql)); + + if (mysql_query(mysql, + "CREATE TABLE state (cookie VARCHAR(64) PRIMARY KEY, " + "atime DATETIME, addr VARCHAR(128), statement TEXT)")) + log->error ("Error creating state: %s", mysql_error(mysql)); + + ostringstream q; + q << "INSERT INTO version VALUES(" << major << "," << minor << ")"; + if (mysql_query(mysql, q.str().c_str())) + log->error ("Error setting version: %s", mysql_error(mysql)); +} + +void ShibMySQLCCache::getVersion(MYSQL* mysql, int* major_p, int* minor_p) +{ + // grab the version number from the database + if (mysql_query(mysql, "SELECT * FROM version")) + log->error ("Error reading version: %s", mysql_error(mysql)); + + MYSQL_RES* rows = mysql_store_result(mysql); + if (rows) { + if (mysql_num_rows(rows) == 1 && mysql_num_fields(rows) == 2) { + MYSQL_ROW row = mysql_fetch_row(rows); + + int major = row[0] ? atoi(row[0]) : -1; + int minor = row[1] ? atoi(row[1]) : -1; + log->debug("opening database version %d.%d", major, minor); + + mysql_free_result (rows); + + *major_p = major; + *minor_p = minor; + return; + + } else { + // Wrong number of rows or wrong number of fields... + + log->crit("Houston, we've got a problem with the database.."); + mysql_free_result (rows); + throw runtime_error("Database version verification failed"); + } + } + log->crit("MySQL Read Failed in version verificatoin"); + throw runtime_error("MySQL Read Failed"); +} + +void ShibMySQLCCache::mysqlInit(void) +{ + log->info ("Opening MySQL Database"); + + // Setup the argument array + vector arg_array; + string tag; + + tag = SHIBTARGET_SHAR; + arg_array.push_back(tag); + + // grab any MySQL parameters from the config file + ShibTargetConfig& config = ShibTargetConfig::getConfig(); + ShibINI& ini = config.getINI(); + + if (ini.exists("mysql")) { + ShibINI::Iterator* iter = ini.tag_iterator("mysql"); + + for (const string* str = iter->begin(); str; str = iter->next()) { + string arg = ini.get("mysql", *str); + arg_array.push_back(arg); + } + delete iter; + } + + // Compute the argument array + int arg_count = arg_array.size(); + const char* args[arg_count]; + for (int i = 0; i < arg_count; i++) + args[i] = arg_array[i].c_str(); + + // Initialize MySQL with the arguments + mysql_server_init(arg_count, (char **)args, NULL); +} + +/************************************************************************* + * The CCacheEntry here is mostly a wrapper around the "memory" + * cacheentry provided by shibboleth. The only difference is that we + * intercept the isSessionValid() so that we can "touch()" the + * database if the session is still valid. + */ + +ShibMySQLCCacheEntry::ShibMySQLCCacheEntry(const char* key, CCacheEntry *entry, + ShibMySQLCCache* cache) +{ + m_cacheEntry = entry; + m_key = key; + m_cache = cache; +} + +bool ShibMySQLCCacheEntry::isSessionValid(time_t lifetime, time_t timeout) +{ + bool res = m_cacheEntry->isSessionValid(lifetime, timeout); + if (res == true) + touch(); + return res; +} + +void ShibMySQLCCacheEntry::touch() +{ + ostringstream q; + q << "UPDATE state SET atime=NOW() WHERE cookie='" << m_key << "'"; + + MYSQL* mysql = m_cache->getMYSQL(); + if (mysql_query(mysql, q.str().c_str())) + m_cache->log->error("Error updating timestamp on %s: %s", + m_key.c_str(), mysql_error(mysql)); +} + +/************************************************************************* + * The registration functions here... + */ + +extern "C" CCache* new_mysql_ccache(void) +{ + return new ShibMySQLCCache(); +} + +extern "C" int saml_extension_init(void* context) +{ + // register this ccache type + CCache::registerFactory("mysql", &new_mysql_ccache); + return 0; +} + +/************************************************************************* + * Local Functions + */ + +extern "C" void shib_mysql_destroy_handle(void* data) +{ + MYSQL* mysql = (MYSQL*) data; + mysql_close(mysql); +}