Move assertions to separate storage records, improve error handling.
[shibboleth/cpp-sp.git] / shibsp / impl / StorageServiceSessionCache.cpp
index f8aeac3..7c1421f 100644 (file)
@@ -58,8 +58,7 @@ namespace shibsp {
     class StoredSession : public virtual Session\r
     {\r
     public:\r
-        StoredSession(SSCache* cache, const Application& app, DDF& obj)\r
-                : m_appId(app.getId()), m_obj(obj), m_cache(cache) {\r
+        StoredSession(SSCache* cache, DDF& obj) : m_obj(obj), m_cache(cache) {\r
             const char* nameid = obj["nameid"].string();\r
             if (!nameid)\r
                 throw FatalProfileException("NameID missing from cached session.");\r
@@ -121,7 +120,7 @@ namespace shibsp {
             if (m_ids.empty()) {\r
                 DDF id = m_obj["assertions"].first();\r
                 while (id.isstring()) {\r
-                    m_ids.push_back(id.name());\r
+                    m_ids.push_back(id.string());\r
                     id = id.next();\r
                 }\r
             }\r
@@ -135,7 +134,6 @@ namespace shibsp {
     private:\r
         void unmarshallAttributes(DDF& in);\r
 \r
-        string m_appId;\r
         DDF m_obj;\r
         saml2::NameID* m_nameid;\r
         vector<const Attribute*> m_attributes;\r
@@ -232,7 +230,18 @@ void StoredSession::addAttributes(const vector<Attribute*>& attributes)
         str << m_obj;\r
         string record(str.str()); \r
 \r
-        ver = m_cache->m_storage->updateText(m_appId.c_str(), m_obj.name(), record.c_str(), 0, m_obj["version"].integer()-1);\r
+        try {\r
+            ver = m_cache->m_storage->updateText(m_obj.name(), "session", record.c_str(), 0, m_obj["version"].integer()-1);\r
+        }\r
+        catch (exception&) {\r
+            // Roll back modification to record.\r
+            m_obj["version"].integer(m_obj["version"].integer()-1);\r
+            vector<Attribute*>::size_type count = attributes.size();\r
+            while (count--)\r
+                attrs.last().destroy();            \r
+            throw;\r
+        }\r
+\r
         if (ver <= 0) {\r
             // Roll back modification to record.\r
             m_obj["version"].integer(m_obj["version"].integer()-1);\r
@@ -242,13 +251,12 @@ void StoredSession::addAttributes(const vector<Attribute*>& attributes)
         }\r
         if (!ver) {\r
             // Fatal problem with update.\r
-            m_cache->m_log.error("updateText failed on StorageService for session (%s)", m_obj.name());\r
             throw IOException("Unable to update stored session.");\r
         }\r
         else if (ver < 0) {\r
             // Out of sync.\r
             m_cache->m_log.warn("storage service indicates the record is out of sync, updating with a fresh copy...");\r
-            ver = m_cache->m_storage->readText(m_appId.c_str(), m_obj.name(), &record, NULL);\r
+            ver = m_cache->m_storage->readText(m_obj.name(), "session", &record, NULL);\r
             if (!ver) {\r
                 m_cache->m_log.error("readText failed on StorageService for session (%s)", m_obj.name());\r
                 throw IOException("Unable to read back stored session.");\r
@@ -281,7 +289,7 @@ void StoredSession::addAttributes(const vector<Attribute*>& attributes)
         "Added the following attributes to session (ID: " <<\r
             m_obj.name() <<\r
         ") for (applicationId: " <<\r
-            m_appId.c_str() <<\r
+            m_obj["application_id"].string() <<\r
         ") {";\r
     for (vector<Attribute*>::const_iterator a=attributes.begin(); a!=attributes.end(); ++a)\r
         xlog->log.infoStream() << "\t" << (*a)->getId() << " (" << (*a)->valueCount() << " values)";\r
@@ -294,10 +302,11 @@ const RootObject* StoredSession::getAssertion(const char* id) const
     if (i!=m_tokens.end())\r
         return i->second;\r
     \r
-    // Parse and bind the document into an XMLObject.\r
-    const char* tokenstr = m_obj["assertions"][id].string();\r
-    if (!tokenstr)\r
+    string tokenstr;\r
+    if (!m_cache->m_storage->readText(m_obj.name(), id, &tokenstr, NULL))\r
         throw FatalProfileException("Assertion not found in cache.");\r
+\r
+    // Parse and bind the document into an XMLObject.\r
     istringstream instr(tokenstr);\r
     DOMDocument* doc = XMLToolingConfig::getConfig().getParser().parse(instr); \r
     XercesJanitor<DOMDocument> janitor(doc);\r
@@ -327,15 +336,18 @@ void StoredSession::addAssertion(RootObject* assertion)
 \r
     m_cache->m_log.debug("adding assertion (%s) to session (%s)", id.get(), m_obj.name());\r
 \r
-    ostringstream os;\r
-    os << *assertion;\r
+    time_t exp;\r
+    if (!m_cache->m_storage->readText(m_obj.name(), "session", NULL, &exp))\r
+        throw IOException("Unable to load expiration time for stored session.");\r
+\r
+    ostringstream tokenstr;\r
+    tokenstr << *assertion;\r
+    m_cache->m_storage->createText(m_obj.name(), id.get(), tokenstr.str().c_str(), exp);\r
     \r
     int ver;\r
     do {\r
-        DDF token = m_obj["assertions"];\r
-        if (!token.isstruct())\r
-            token = m_obj.addmember("assertions").structure();\r
-        token = token.addmember(id.get()).string(os.str().c_str());\r
+        DDF token = DDF(NULL).string(id.get());\r
+        m_obj["assertions"].add(token);\r
 \r
         // Tentatively increment the version.\r
         m_obj["version"].integer(m_obj["version"].integer()+1);\r
@@ -344,7 +356,16 @@ void StoredSession::addAssertion(RootObject* assertion)
         str << m_obj;\r
         string record(str.str()); \r
 \r
-        ver = m_cache->m_storage->updateText(m_appId.c_str(), m_obj.name(), record.c_str(), 0, m_obj["version"].integer()-1);\r
+        try {\r
+            ver = m_cache->m_storage->updateText(m_obj.name(), "session", record.c_str(), 0, m_obj["version"].integer()-1);\r
+        }\r
+        catch (exception&) {\r
+            token.destroy();\r
+            m_obj["version"].integer(m_obj["version"].integer()-1);\r
+            m_cache->m_storage->deleteText(m_obj.name(), id.get());\r
+            throw;\r
+        }\r
+\r
         if (ver <= 0) {\r
             token.destroy();\r
             m_obj["version"].integer(m_obj["version"].integer()-1);\r
@@ -352,14 +373,16 @@ void StoredSession::addAssertion(RootObject* assertion)
         if (!ver) {\r
             // Fatal problem with update.\r
             m_cache->m_log.error("updateText failed on StorageService for session (%s)", m_obj.name());\r
+            m_cache->m_storage->deleteText(m_obj.name(), id.get());\r
             throw IOException("Unable to update stored session.");\r
         }\r
         else if (ver < 0) {\r
             // Out of sync.\r
             m_cache->m_log.warn("storage service indicates the record is out of sync, updating with a fresh copy...");\r
-            ver = m_cache->m_storage->readText(m_appId.c_str(), m_obj.name(), &record, NULL);\r
+            ver = m_cache->m_storage->readText(m_obj.name(), "session", &record, NULL);\r
             if (!ver) {\r
                 m_cache->m_log.error("readText failed on StorageService for session (%s)", m_obj.name());\r
+                m_cache->m_storage->deleteText(m_obj.name(), id.get());\r
                 throw IOException("Unable to read back stored session.");\r
             }\r
             \r
@@ -381,13 +404,14 @@ void StoredSession::addAssertion(RootObject* assertion)
         }\r
     } while (ver < 0); // negative indicates a sync issue so we retry\r
 \r
+    m_ids.clear();\r
     delete assertion;\r
 \r
     TransactionLog* xlog = SPConfig::getConfig().getServiceProvider()->getTransactionLog();\r
     Locker locker(xlog);\r
     xlog->log.info(\r
         "Added assertion (ID: %s) to session for (applicationId: %s) with (ID: %s)",\r
-        id.get(), m_appId.c_str(), m_obj.name()\r
+        id.get(), m_obj["application_id"].string(), m_obj.name()\r
         );\r
 }\r
 \r
@@ -441,6 +465,7 @@ string SSCache::insert(
     // Store session properties in DDF.\r
     DDF obj = DDF(key.get()).structure();\r
     obj.addmember("version").integer(1);\r
+    obj.addmember("application_id").string(application.getId());\r
     if (expires > 0) {\r
         // On 64-bit Windows, time_t doesn't fit in a long, so I'm using ISO timestamps.  \r
 #ifndef HAVE_GMTIME_R\r
@@ -471,11 +496,14 @@ string SSCache::insert(
     namestr << nameid;\r
     obj.addmember("nameid").string(namestr.str().c_str());\r
     \r
+    string tokenstr;\r
     if (ssoToken) {\r
-        ostringstream tokenstr;\r
-        tokenstr << *ssoToken;\r
+        ostringstream tstr;\r
+        tstr << *ssoToken;\r
+        tokenstr = tstr.str();\r
         auto_ptr_char tokenid(ssoToken->getID());\r
-        obj.addmember("assertions").structure().addmember(tokenid.get()).string(tokenstr.str().c_str());\r
+        DDF tokid = DDF(NULL).string(tokenid.get());\r
+        obj.addmember("assertions").list().add(tokid);\r
     }\r
     \r
     if (attributes) {\r
@@ -491,7 +519,17 @@ string SSCache::insert(
     record << obj;\r
     \r
     m_log.debug("storing new session...");\r
-    m_storage->createText(application.getId(), key.get(), record.str().c_str(), time(NULL) + m_cacheTimeout);\r
+    time_t now = time(NULL);\r
+    m_storage->createText(key.get(), "session", record.str().c_str(), now + m_cacheTimeout);\r
+    if (ssoToken) {\r
+        try {\r
+            m_storage->createText(key.get(), obj["assertions"].first().string(), tokenstr.c_str(), now + m_cacheTimeout);\r
+        }\r
+        catch (exception& ex) {\r
+            m_log.error("error storing assertion along with session: %s", ex.what());\r
+        }\r
+    }\r
+\r
     const char* pid = obj["entity_id"].string();\r
     m_log.debug("new session created: SessionID (%s) IdP (%s) Address (%s)", key.get(), pid ? pid : "none", client_addr);\r
 \r
@@ -525,23 +563,28 @@ Session* SSCache::find(const char* key, const Application& application, const ch
     \r
     time_t lastAccess;\r
     string record;\r
-    int ver = m_storage->readText(application.getId(), key, &record, &lastAccess);\r
+    int ver = m_storage->readText(key, "session", &record, &lastAccess);\r
     if (!ver)\r
         return NULL;\r
     \r
-    m_log.debug("reconstituting session and checking for validity");\r
+    m_log.debug("reconstituting session and checking validity");\r
     \r
     DDF obj;\r
     istringstream in(record);\r
     in >> obj;\r
     \r
-    lastAccess -= m_cacheTimeout;   // adjusts it back to the last time the record's timestamp was touched\r
\r
+    if (!XMLString::equals(obj["application_id"].string(), application.getId())) {\r
+        m_log.error("an application (%s) tried to access another application's session", application.getId());\r
+        obj.destroy();\r
+        return NULL;\r
+    }\r
+\r
     if (client_addr) {\r
         if (m_log.isDebugEnabled())\r
             m_log.debug("comparing client address %s against %s", client_addr, obj["client_addr"].string());\r
         if (strcmp(obj["client_addr"].string(),client_addr)) {\r
             m_log.warn("client address mismatch");\r
+            remove(key, application, client_addr);\r
             RetryableProfileException ex(\r
                 "Your IP address ($1) does not match the address recorded at the time the session was established.",\r
                 params(1,client_addr)\r
@@ -556,10 +599,12 @@ Session* SSCache::find(const char* key, const Application& application, const ch
         }\r
     }\r
 \r
+    lastAccess -= m_cacheTimeout;   // adjusts it back to the last time the record's timestamp was touched\r
     time_t now=time(NULL);\r
     \r
     if (timeout > 0 && now - lastAccess >= timeout) {\r
         m_log.info("session timed out (ID: %s)", key);\r
+        remove(key, application, client_addr);\r
         RetryableProfileException ex("Your session has expired, and you must re-authenticate.");\r
         string eid(obj["entity_id"].string());\r
         obj.destroy();\r
@@ -576,6 +621,7 @@ Session* SSCache::find(const char* key, const Application& application, const ch
         iso.parseDateTime();\r
         if (now > iso.getEpoch()) {\r
             m_log.info("session expired (ID: %s)", key);\r
+            remove(key, application, client_addr);\r
             RetryableProfileException ex("Your session has expired, and you must re-authenticate.");\r
             string eid(obj["entity_id"].string());\r
             obj.destroy();\r
@@ -588,12 +634,16 @@ Session* SSCache::find(const char* key, const Application& application, const ch
     }\r
     \r
     // Update storage expiration, if possible.\r
-    if (!m_storage->updateText(application.getId(), key, NULL, now + m_cacheTimeout))\r
-        m_log.error("failed to update record expiration");\r
+    try {\r
+        m_storage->updateContext(key, now + m_cacheTimeout);\r
+    }\r
+    catch (exception& ex) {\r
+        m_log.error("failed to update session expiration: %s", ex.what());\r
+    }\r
 \r
     // Finally build the Session object.\r
     try {\r
-        return new StoredSession(this, application, obj);\r
+        return new StoredSession(this, obj);\r
     }\r
     catch (exception&) {\r
         obj.destroy();\r
@@ -609,7 +659,7 @@ void SSCache::remove(const char* key, const Application& application, const char
 \r
     m_log.debug("removing session (%s)", key);\r
 \r
-    m_storage->deleteText(application.getId(), key);\r
+    m_storage->deleteContext(key);\r
 \r
     TransactionLog* xlog = SPConfig::getConfig().getServiceProvider()->getTransactionLog();\r
     Locker locker(xlog);\r
@@ -622,26 +672,26 @@ void SSCache::receive(DDF& in, ostream& out)
     xmltooling::NDC ndc("receive");\r
 #endif\r
 \r
-    // Find application.\r
-    ServiceProvider* sp = SPConfig::getConfig().getServiceProvider(); \r
-    Locker confLocker(sp);\r
-    const char* aid=in["application_id"].string();\r
-    const Application* app=aid ? sp->getApplication(aid) : NULL;\r
-    if (!app) {\r
-        // Something's horribly wrong.\r
-        m_log.error("couldn't find application (%s) for session", aid ? aid : "(missing)");\r
-        throw ConfigurationException("Unable to locate application for session, deleted?");\r
-    }\r
-\r
     if (!strcmp(in.name(),"insert::"REMOTED_SESSION_CACHE)) {\r
-        in["application_id"].destroy();\r
         auto_ptr_char key(SAMLConfig::getConfig().generateIdentifier());\r
         in.name(key.get());\r
+\r
+        DDF token = in["token"].remove();\r
+        DDFJanitor tjan(token);\r
         \r
         m_log.debug("storing new session...");\r
         ostringstream record;\r
         record << in;\r
-        m_storage->createText(app->getId(), key.get(), record.str().c_str(), time(NULL) + m_cacheTimeout);\r
+        time_t now = time(NULL);\r
+        m_storage->createText(key.get(), "session", record.str().c_str(), now + m_cacheTimeout);\r
+        if (token.isstring()) {\r
+            try {\r
+                m_storage->createText(key.get(), in["assertions"].first().string(), token.string(), now + m_cacheTimeout);\r
+            }\r
+            catch (IOException& ex) {\r
+                m_log.error("error storing assertion along with session: %s", ex.what());\r
+            }\r
+        }\r
         const char* pid = in["entity_id"].string();\r
         m_log.debug("new session created: SessionID (%s) IdP (%s) Address (%s)", key.get(), pid ? pid : "none", in["client_addr"].string());\r
     \r
@@ -658,7 +708,7 @@ void SSCache::receive(DDF& in, ostream& out)
         // Do an unversioned read.\r
         string record;\r
         time_t lastAccess;\r
-        if (!m_storage->readText(aid, key, &record, &lastAccess)) {\r
+        if (!m_storage->readText(key, "session", &record, &lastAccess)) {\r
             DDF ret(NULL);\r
             DDFJanitor jan(ret);\r
             out << ret;\r
@@ -680,12 +730,17 @@ void SSCache::receive(DDF& in, ostream& out)
                 \r
         if (timeout > 0 && now - lastAccess >= timeout) {\r
             m_log.info("session timed out (ID: %s)", key);\r
+            remove(key,*(SPConfig::getConfig().getServiceProvider()->getApplication("default")),NULL);\r
             throw RetryableProfileException("Your session has expired, and you must re-authenticate.");\r
         } \r
 \r
         // Update storage expiration, if possible.\r
-        if (!m_storage->updateText(aid, key, NULL, now + m_cacheTimeout)) \r
-            m_log.error("failed to update record expiration");\r
+        try {\r
+            m_storage->updateContext(key, now + m_cacheTimeout);\r
+        }\r
+        catch (exception& ex) {\r
+            m_log.error("failed to update session expiration: %s", ex.what());\r
+        }\r
             \r
         // Send the record back.\r
         out << record;\r
@@ -699,7 +754,7 @@ void SSCache::receive(DDF& in, ostream& out)
         string record;\r
         time_t lastAccess;\r
         int curver = in["version"].integer();\r
-        int ver = m_storage->readText(aid, key, &record, &lastAccess, curver);\r
+        int ver = m_storage->readText(key, "session", &record, &lastAccess, curver);\r
         if (ver == 0) {\r
             m_log.warn("unsuccessful versioned read of session (ID: %s), caches out of sync?", key);\r
             throw RetryableProfileException("Your session has expired, and you must re-authenticate.");\r
@@ -724,8 +779,12 @@ void SSCache::receive(DDF& in, ostream& out)
         } \r
 \r
         // Update storage expiration, if possible.\r
-        if (!m_storage->updateText(aid, key, NULL, now + m_cacheTimeout)) \r
-            m_log.error("failed to update record expiration");\r
+        try {\r
+            m_storage->updateContext(key, now + m_cacheTimeout);\r
+        }\r
+        catch (exception& ex) {\r
+            m_log.error("failed to update session expiration: %s", ex.what());\r
+        }\r
             \r
         if (ver > curver) {\r
             // Send the record back.\r
@@ -739,13 +798,22 @@ void SSCache::receive(DDF& in, ostream& out)
     }\r
     else if (!strcmp(in.name(),"remove::"REMOTED_SESSION_CACHE)) {\r
         const char* key=in["key"].string();\r
-        const char* client_addr=in["client_addr"].string();\r
-        if (!key || !client_addr)\r
-            throw ListenerException("Required parameters missing for session removal.");\r
+        if (!key)\r
+            throw ListenerException("Required parameter missing for session removal.");\r
         \r
-        remove(key,*app,client_addr);\r
+        remove(key,*(SPConfig::getConfig().getServiceProvider()->getApplication("default")),NULL);\r
         DDF ret(NULL);\r
         DDFJanitor jan(ret);\r
         out << ret;\r
     }\r
+    else if (!strcmp(in.name(),"getAssertion::"REMOTED_SESSION_CACHE)) {\r
+        const char* key=in["key"].string();\r
+        const char* id=in["id"].string();\r
+        if (!key || !id)\r
+            throw ListenerException("Required parameters missing for assertion retrieval.");\r
+        string token;\r
+        if (!m_storage->readText(key, id, &token, NULL))\r
+            throw FatalProfileException("Assertion not found in cache.");\r
+        out << token;\r
+    }\r
 }\r