Nearly testable draft of storage-based cache, minus remoting.
[shibboleth/sp.git] / shibsp / impl / StorageServiceSessionCache.cpp
1 /*\r
2  *  Copyright 2001-2005 Internet2\r
3  * \r
4  * Licensed under the Apache License, Version 2.0 (the "License");\r
5  * you may not use this file except in compliance with the License.\r
6  * You may obtain a copy of the License at\r
7  *\r
8  *     http://www.apache.org/licenses/LICENSE-2.0\r
9  *\r
10  * Unless required by applicable law or agreed to in writing, software\r
11  * distributed under the License is distributed on an "AS IS" BASIS,\r
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
13  * See the License for the specific language governing permissions and\r
14  * limitations under the License.\r
15  */\r
16 \r
17 /** StorageServiceSessionCache.cpp\r
18  * \r
19  * StorageService-based SessionCache implementation.\r
20  * \r
21  * Instead of optimizing this plugin with a buffering scheme that keeps objects around\r
22  * and avoids extra parsing steps, I'm assuming that systems that require such can\r
23  * layer their own cache plugin on top of this version either by delegating to it\r
24  * or using the remoting support. So this version will load sessions directly\r
25  * from the StorageService, instantiate enough to expose the Session API,\r
26  * and then delete everything when they're unlocked. All data in memory is always\r
27  * kept in sync with the StorageService (no lazy updates).\r
28  */\r
29 \r
30 #include "internal.h"\r
31 #include "Application.h"\r
32 #include "exceptions.h"\r
33 #include "ServiceProvider.h"\r
34 #include "SessionCache.h"\r
35 #include "TransactionLog.h"\r
36 #include "attribute/Attribute.h"\r
37 #include "remoting/ListenerService.h"\r
38 #include "util/SPConstants.h"\r
39 \r
40 #include <log4cpp/Category.hh>\r
41 #include <saml/SAMLConfig.h>\r
42 #include <xmltooling/util/NDC.h>\r
43 #include <xmltooling/util/StorageService.h>\r
44 #include <xmltooling/util/XMLHelper.h>\r
45 #include <xercesc/util/XMLUniDefs.hpp>\r
46 \r
47 using namespace shibsp;\r
48 using namespace opensaml::saml2md;\r
49 using namespace opensaml;\r
50 using namespace xmltooling;\r
51 using namespace log4cpp;\r
52 using namespace std;\r
53 \r
54 namespace shibsp {\r
55 \r
56     class SSCache;\r
57     class StoredSession : public virtual Session\r
58     {\r
59     public:\r
60         StoredSession(SSCache* cache, const Application& app, DDF& obj, int version)\r
61                 : m_appId(app.getId()), m_version(version), m_obj(obj), m_cache(cache) {\r
62             const char* nameid = obj["nameid"].string();\r
63             if (!nameid)\r
64                 throw FatalProfileException("NameID missing from cached session.");\r
65             \r
66             // Parse and bind the document into an XMLObject.\r
67             istringstream instr(nameid);\r
68             DOMDocument* doc = XMLToolingConfig::getConfig().getParser().parse(instr); \r
69             XercesJanitor<DOMDocument> janitor(doc);\r
70             auto_ptr<saml2::NameID> n(saml2::NameIDBuilder::buildNameID());\r
71             n->unmarshall(doc->getDocumentElement(), true);\r
72             janitor.release();\r
73             \r
74             // TODO: Process attributes...\r
75 \r
76             m_nameid = n.release();\r
77         }\r
78         \r
79         ~StoredSession();\r
80         \r
81         Lockable* lock() {\r
82             return this;\r
83         }\r
84         void unlock() {\r
85             delete this;\r
86         }\r
87         \r
88         const char* getClientAddress() const {\r
89             return m_obj["client_address"].string();\r
90         }\r
91         const char* getEntityID() const {\r
92             return m_obj["entity_id"].string();\r
93         }\r
94         const char* getAuthnInstant() const {\r
95             return m_obj["authn_instant"].string();\r
96         }\r
97         const opensaml::saml2::NameID& getNameID() const {\r
98             return *m_nameid;\r
99         }\r
100         const char* getSessionIndex() const {\r
101             return m_obj["session_index"].string();\r
102         }\r
103         const char* getAuthnContextClassRef() const {\r
104             return m_obj["authncontext_class"].string();\r
105         }\r
106         const char* getAuthnContextDeclRef() const {\r
107             return m_obj["authncontext_decl"].string();\r
108         }\r
109         const vector<const Attribute*>& getAttributes() const {\r
110             return m_attributes;\r
111         }\r
112         const vector<const char*>& getAssertionIDs() const {\r
113             if (m_ids.empty()) {\r
114                 DDF id = m_obj["assertions"].first();\r
115                 while (id.isstring()) {\r
116                     m_ids.push_back(id.name());\r
117                     id = id.next();\r
118                 }\r
119             }\r
120             return m_ids;\r
121         }\r
122         \r
123         void addAttributes(const vector<Attribute*>& attributes);\r
124         const RootObject* getAssertion(const char* id) const;\r
125         void addAssertion(RootObject* assertion);\r
126 \r
127     private:\r
128         string m_appId;\r
129         int m_version;\r
130         DDF m_obj;\r
131         saml2::NameID* m_nameid;\r
132         vector<const Attribute*> m_attributes;\r
133         mutable vector<const char*> m_ids;\r
134         mutable map<string,RootObject*> m_tokens;\r
135         SSCache* m_cache;\r
136     };\r
137     \r
138     class SSCache : public SessionCache, public virtual Remoted\r
139     {\r
140     public:\r
141         SSCache(const DOMElement* e);\r
142         ~SSCache() {}\r
143     \r
144         void receive(const DDF& in, ostream& out);\r
145         \r
146         string insert(\r
147             time_t expires,\r
148             const Application& application,\r
149             const char* client_addr,\r
150             const saml2md::EntityDescriptor* issuer,\r
151             const saml2::NameID& nameid,\r
152             const char* authn_instant=NULL,\r
153             const char* session_index=NULL,\r
154             const char* authncontext_class=NULL,\r
155             const char* authncontext_decl=NULL,\r
156             const RootObject* ssoToken=NULL,\r
157             const vector<Attribute*>* attributes=NULL\r
158             );\r
159         Session* find(const char* key, const Application& application, const char* client_addr=NULL, time_t timeout=0);\r
160         void remove(const char* key, const Application& application, const char* client_addr);\r
161 \r
162         Category& m_log;\r
163         StorageService* m_storage;\r
164     };\r
165 \r
166     SessionCache* SHIBSP_DLLLOCAL StorageServiceCacheFactory(const DOMElement* const & e)\r
167     {\r
168         return new SSCache(e);\r
169     }\r
170 \r
171     static const XMLCh _StorageService[] =   UNICODE_LITERAL_14(S,t,o,r,a,g,e,S,e,r,v,i,c,e);\r
172 }\r
173 \r
174 StoredSession::~StoredSession()\r
175 {\r
176     m_obj.destroy();\r
177     delete m_nameid;\r
178     for_each(m_attributes.begin(), m_attributes.end(), xmltooling::cleanup<Attribute>());\r
179     for_each(m_tokens.begin(), m_tokens.end(), xmltooling::cleanup_pair<string,RootObject>());\r
180 }\r
181 \r
182 void StoredSession::addAttributes(const vector<Attribute*>& attributes)\r
183 {\r
184 #ifdef _DEBUG\r
185     xmltooling::NDC ndc("addAttributes");\r
186 #endif\r
187 \r
188     m_cache->m_log.debug("adding attributes to session (%s)", m_obj.name());\r
189     \r
190     int ver;\r
191     do {\r
192         DDF attr;\r
193         DDF attrs = m_obj["attributes"];\r
194         if (!attrs.islist())\r
195             attrs = m_obj.addmember("attributes").list();\r
196         for (vector<Attribute*>::const_iterator a=attributes.begin(); a!=attributes.end(); ++a) {\r
197             attr = (*a)->marshall();\r
198             attrs.add(attr);\r
199         }\r
200         \r
201         ostringstream str;\r
202         str << m_obj;\r
203         string record(str.str()); \r
204 \r
205         ver = m_cache->m_storage->updateText(m_appId.c_str(), m_obj.name(), record.c_str(), 0, m_version);\r
206         if (ver <= 0) {\r
207             // Roll back modification to record.\r
208             vector<Attribute*>::size_type count = attributes.size();\r
209             while (count--)\r
210                 attrs.last().destroy();            \r
211         }\r
212         if (!ver) {\r
213             // Fatal problem with update.\r
214             m_cache->m_log.error("updateText failed on StorageService for session (%s)", m_obj.name());\r
215             throw IOException("Unable to update stored session.");\r
216         }\r
217         else if (ver < 0) {\r
218             // Out of sync.\r
219             m_cache->m_log.warn("storage service indicates the record is out of sync, updating with a fresh copy...");\r
220             ver = m_cache->m_storage->readText(m_appId.c_str(), m_obj.name(), &record, NULL);\r
221             if (!ver) {\r
222                 m_cache->m_log.error("updateText failed on StorageService for session (%s)", m_obj.name());\r
223                 throw IOException("Unable to update stored session.");\r
224             }\r
225             \r
226             // Reset object.\r
227             DDF newobj;\r
228             istringstream in(record);\r
229             in >> newobj;\r
230 \r
231             m_obj.destroy();\r
232             m_ids.clear();\r
233             for_each(m_attributes.begin(), m_attributes.end(), xmltooling::cleanup<Attribute>());\r
234             m_attributes.clear();\r
235             m_version = ver;\r
236             m_obj = newobj;\r
237             // TODO: handle attributes\r
238             \r
239             ver = -1;\r
240         }\r
241         else {\r
242             // Update with new version.\r
243             m_version = ver;\r
244         }\r
245     } while (ver < 0);  // negative indicates a sync issue so we retry\r
246 \r
247     // Transfer ownership to us.\r
248     m_attributes.insert(m_attributes.end(), attributes.begin(), attributes.end());\r
249 \r
250     TransactionLog* xlog = SPConfig::getConfig().getServiceProvider()->getTransactionLog();\r
251     Locker locker(xlog);\r
252     xlog->log.infoStream() <<\r
253         "Added the following attributes to session (ID: " <<\r
254             m_obj.name() <<\r
255         ") for (applicationId: " <<\r
256             m_appId.c_str() <<\r
257         ") {";\r
258     for (vector<Attribute*>::const_iterator a=attributes.begin(); a!=attributes.end(); ++a)\r
259         xlog->log.infoStream() << "\t" << (*a)->getId() << " (" << (*a)->valueCount() << " values)";\r
260     xlog->log.info("}");\r
261 }\r
262 \r
263 const RootObject* StoredSession::getAssertion(const char* id) const\r
264 {\r
265     map<string,RootObject*>::const_iterator i = m_tokens.find(id);\r
266     if (i!=m_tokens.end())\r
267         return i->second;\r
268     \r
269     // Parse and bind the document into an XMLObject.\r
270     const char* tokenstr = m_obj["assertions"][id].string();\r
271     if (!tokenstr)\r
272         throw FatalProfileException("Assertion not found in cache.");\r
273     istringstream instr(tokenstr);\r
274     DOMDocument* doc = XMLToolingConfig::getConfig().getParser().parse(instr); \r
275     XercesJanitor<DOMDocument> janitor(doc);\r
276     auto_ptr<XMLObject> xmlObject(XMLObjectBuilder::buildOneFromElement(doc->getDocumentElement(), true));\r
277     janitor.release();\r
278     \r
279     RootObject* token = dynamic_cast<RootObject*>(xmlObject.get());\r
280     if (!token || !token->isAssertion())\r
281         throw FatalProfileException("Request for cached assertion returned an unknown object type.");\r
282 \r
283     // Transfer ownership to us.\r
284     xmlObject.release();\r
285     m_tokens[id]=token;\r
286     return token;\r
287 }\r
288 \r
289 void StoredSession::addAssertion(RootObject* assertion)\r
290 {\r
291 #ifdef _DEBUG\r
292     xmltooling::NDC ndc("addAssertion");\r
293 #endif\r
294     \r
295     if (!assertion || !assertion->isAssertion())\r
296         throw FatalProfileException("Unknown object type passed to session for storage.");\r
297 \r
298     auto_ptr_char id(assertion->getID());\r
299 \r
300     m_cache->m_log.debug("adding assertion (%s) to session (%s)", id.get(), m_obj.name());\r
301 \r
302     ostringstream os;\r
303     os << *assertion;\r
304     \r
305     int ver;\r
306     do {\r
307         DDF token = m_obj["assertions"];\r
308         if (!token.isstruct())\r
309             token = m_obj.addmember("assertions").structure();\r
310         token = token.addmember(id.get()).string(os.str().c_str());\r
311     \r
312         ostringstream str;\r
313         str << m_obj;\r
314         string record(str.str()); \r
315 \r
316         ver = m_cache->m_storage->updateText(m_appId.c_str(), m_obj.name(), record.c_str(), 0, m_version);\r
317         if (ver <= 0)\r
318             token.destroy();            \r
319         if (!ver) {\r
320             // Fatal problem with update.\r
321             m_cache->m_log.error("updateText failed on StorageService for session (%s)", m_obj.name());\r
322             throw IOException("Unable to update stored session.");\r
323         }\r
324         else if (ver < 0) {\r
325             // Out of sync.\r
326             m_cache->m_log.warn("storage service indicates the record is out of sync, updating with a fresh copy...");\r
327             ver = m_cache->m_storage->readText(m_appId.c_str(), m_obj.name(), &record, NULL);\r
328             if (!ver) {\r
329                 m_cache->m_log.error("updateText failed on StorageService for session (%s)", m_obj.name());\r
330                 throw IOException("Unable to update stored session.");\r
331             }\r
332             \r
333             // Reset object.\r
334             DDF newobj;\r
335             istringstream in(record);\r
336             in >> newobj;\r
337 \r
338             m_obj.destroy();\r
339             m_ids.clear();\r
340             for_each(m_attributes.begin(), m_attributes.end(), xmltooling::cleanup<Attribute>());\r
341             m_attributes.clear();\r
342             m_version = ver;\r
343             m_obj = newobj;\r
344             // TODO: handle attributes\r
345             \r
346             ver = -1;\r
347         }\r
348         else {\r
349             // Update with new version.\r
350             m_version = ver;\r
351         }\r
352     } while (ver < 0); // negative indicates a sync issue so we retry\r
353 \r
354     delete assertion;\r
355 \r
356     TransactionLog* xlog = SPConfig::getConfig().getServiceProvider()->getTransactionLog();\r
357     Locker locker(xlog);\r
358     xlog->log.info(\r
359         "Added assertion (ID: %s) to session for (applicationId: %s) with (ID: %s)",\r
360         id.get(), m_appId.c_str(), m_obj.name()\r
361         );\r
362 }\r
363 \r
364 SSCache::SSCache(const DOMElement* e)\r
365     : SessionCache(e), m_log(Category::getInstance(SHIBSP_LOGCAT".SessionCache")), m_storage(NULL)\r
366 {\r
367     SPConfig& conf = SPConfig::getConfig();\r
368     const XMLCh* tag = e ? e->getAttributeNS(NULL,_StorageService) : NULL;\r
369     if (tag && *tag) {\r
370         auto_ptr_char ssid(tag);\r
371         m_storage = conf.getServiceProvider()->getStorageService(ssid.get());\r
372         if (m_storage)\r
373             m_log.info("bound to StorageService (%s)", ssid.get());\r
374         else\r
375             throw ConfigurationException("SessionCache unable to locate StorageService, check configuration.");\r
376     }\r
377 \r
378     ListenerService* listener=conf.getServiceProvider()->getListenerService(false);\r
379     if (listener && conf.isEnabled(SPConfig::OutOfProcess)) {\r
380         listener->regListener("insert::"REMOTED_SESSION_CACHE,this);\r
381         listener->regListener("find::"REMOTED_SESSION_CACHE,this);\r
382         listener->regListener("remove::"REMOTED_SESSION_CACHE,this);\r
383     }\r
384     else {\r
385         m_log.info("no ListenerService available, cache remoting is disabled");\r
386     }\r
387 }\r
388 \r
389 string SSCache::insert(\r
390     time_t expires,\r
391     const Application& application,\r
392     const char* client_addr,\r
393     const saml2md::EntityDescriptor* issuer,\r
394     const saml2::NameID& nameid,\r
395     const char* authn_instant,\r
396     const char* session_index,\r
397     const char* authncontext_class,\r
398     const char* authncontext_decl,\r
399     const RootObject* ssoToken,\r
400     const vector<Attribute*>* attributes\r
401     )\r
402 {\r
403 #ifdef _DEBUG\r
404     xmltooling::NDC ndc("insert");\r
405 #endif\r
406 \r
407     m_log.debug("creating new session");\r
408 \r
409     auto_ptr_char key(SAMLConfig::getConfig().generateIdentifier());\r
410 \r
411     // Store session properties in DDF.\r
412     DDF obj = DDF(key.get()).structure();\r
413     if (expires > 0) {\r
414         // On 64-bit Windows, time_t doesn't fit in a long, so I'm using ISO timestamps.  \r
415 #ifndef HAVE_GMTIME_R\r
416         struct tm* ptime=gmtime(&expires);\r
417 #else\r
418         struct tm res;\r
419         struct tm* ptime=gmtime_r(&expires,&res);\r
420 #endif\r
421         char timebuf[32];\r
422         strftime(timebuf,32,"%Y-%m-%dT%H:%M:%SZ",ptime);\r
423         obj.addmember("expires").string(timebuf);\r
424     }\r
425     obj.addmember("client_address").string(client_addr);\r
426     if (issuer) {\r
427         auto_ptr_char entity_id(issuer->getEntityID());\r
428         obj.addmember("entity_id").string(entity_id.get());\r
429     }\r
430     if (authn_instant)\r
431         obj.addmember("authn_instant").string(authn_instant);\r
432     if (session_index)\r
433         obj.addmember("session_index").string(session_index);\r
434     if (authncontext_class)\r
435         obj.addmember("authncontext_class").string(authncontext_class);\r
436     if (authncontext_decl)\r
437         obj.addmember("authncontext_decl").string(authncontext_decl);\r
438 \r
439     ostringstream namestr;\r
440     namestr << nameid;\r
441     obj.addmember("nameid").string(namestr.str().c_str());\r
442     \r
443     if (ssoToken) {\r
444         ostringstream tokenstr;\r
445         tokenstr << *ssoToken;\r
446         auto_ptr_char tokenid(ssoToken->getID());\r
447         obj.addmember("assertions").structure().addmember(tokenid.get()).string(tokenstr.str().c_str());\r
448     }\r
449     \r
450     if (attributes) {\r
451         DDF attr;\r
452         DDF attrlist = obj.addmember("attributes").list();\r
453         for (vector<Attribute*>::const_iterator a=attributes->begin(); a!=attributes->end(); ++a) {\r
454             attr = (*a)->marshall();\r
455             attrlist.add(attr);\r
456         }\r
457     }\r
458     \r
459     ostringstream record;\r
460     record << obj;\r
461     \r
462     m_log.debug("storing new session...");\r
463     m_storage->createText(application.getId(), key.get(), record.str().c_str(), time(NULL) + m_cacheTimeout);\r
464     const char* pid = obj["entity_id"].string();\r
465     m_log.debug("new session created: SessionID (%s) IdP (%s) Address (%s)", key.get(), pid ? pid : "none", client_addr);\r
466 \r
467     // Transaction Logging\r
468     auto_ptr_char name(nameid.getName());\r
469     TransactionLog* xlog = SPConfig::getConfig().getServiceProvider()->getTransactionLog();\r
470     Locker locker(xlog);\r
471     xlog->log.infoStream() <<\r
472         "New session (ID: " <<\r
473             key.get() <<\r
474         ") with (applicationId: " <<\r
475             application.getId() <<\r
476         ") for principal from (IdP: " <<\r
477             (pid ? pid : "none") <<\r
478         ") at (ClientAddress: " <<\r
479             client_addr <<\r
480         ") with (NameIdentifier: " <<\r
481             name.get() <<\r
482         ")";\r
483     \r
484     return key.get();\r
485 }\r
486 \r
487 Session* SSCache::find(const char* key, const Application& application, const char* client_addr, time_t timeout)\r
488 {\r
489 #ifdef _DEBUG\r
490     xmltooling::NDC ndc("find");\r
491 #endif\r
492 \r
493     m_log.debug("searching for session (%s)", key);\r
494     \r
495     time_t lastAccess;\r
496     string record;\r
497     int ver = m_storage->readText(application.getId(), key, &record, &lastAccess);\r
498     if (!ver)\r
499         return NULL;\r
500     \r
501     m_log.debug("reconstituting session and checking for validity");\r
502     \r
503     DDF obj;\r
504     istringstream in(record);\r
505     in >> obj;\r
506     \r
507     lastAccess -= m_cacheTimeout;   // adjusts it back to the last time the record's timestamp was touched\r
508  \r
509     if (client_addr) {\r
510         if (m_log.isDebugEnabled())\r
511             m_log.debug("comparing client address %s against %s", client_addr, obj["client_address"].string());\r
512         if (strcmp(obj["client_address"].string(),client_addr)) {\r
513             m_log.info("client address mismatch");\r
514             RetryableProfileException ex(\r
515                 "Your IP address ($1) does not match the address recorded at the time the session was established.",\r
516                 params(1,client_addr)\r
517                 );\r
518             string eid(obj["entity_id"].string());\r
519             obj.destroy();\r
520             MetadataProvider* m=application.getMetadataProvider();\r
521             Locker locker(m);\r
522             annotateException(&ex,m->getEntityDescriptor(eid.c_str(),false)); // throws it\r
523         }\r
524     }\r
525 \r
526     time_t now=time(NULL);\r
527     \r
528     if (timeout > 0 && now - lastAccess >= timeout) {\r
529         m_log.info("session timed out (ID: %s)", key);\r
530         RetryableProfileException ex("Your session has expired, and you must re-authenticate.");\r
531         string eid(obj["entity_id"].string());\r
532         obj.destroy();\r
533         MetadataProvider* m=application.getMetadataProvider();\r
534         Locker locker(m);\r
535         annotateException(&ex,m->getEntityDescriptor(eid.c_str(),false)); // throws it\r
536     }\r
537     \r
538     auto_ptr_XMLCh exp(obj["expires"].string());\r
539     if (exp.get()) {\r
540         DateTime iso(exp.get());\r
541         iso.parseDateTime();\r
542         if (now > iso.getEpoch()) {\r
543             m_log.info("session expired (ID: %s)", key);\r
544             RetryableProfileException ex("Your session has expired, and you must re-authenticate.");\r
545             string eid(obj["entity_id"].string());\r
546             obj.destroy();\r
547             MetadataProvider* m=application.getMetadataProvider();\r
548             Locker locker(m);\r
549             annotateException(&ex,m->getEntityDescriptor(eid.c_str(),false)); // throws it\r
550         }\r
551     }\r
552     \r
553     // Update storage expiration, if possible.\r
554     ver = m_storage->updateText(application.getId(), key, NULL, now + m_cacheTimeout); \r
555     if (!ver)\r
556         m_log.error("failed to update record expiration");\r
557 \r
558     // Finally build the Session object.\r
559     try {\r
560         return new StoredSession(this, application, obj, ver);\r
561     }\r
562     catch (exception&) {\r
563         obj.destroy();\r
564         throw;\r
565     }\r
566 }\r
567 \r
568 void SSCache::remove(const char* key, const Application& application, const char* client_addr)\r
569 {\r
570 #ifdef _DEBUG\r
571     xmltooling::NDC ndc("remove");\r
572 #endif\r
573 \r
574     m_log.debug("removing session (%s)", key);\r
575 \r
576     m_storage->deleteText(application.getId(), key);\r
577 \r
578     TransactionLog* xlog = SPConfig::getConfig().getServiceProvider()->getTransactionLog();\r
579     Locker locker(xlog);\r
580     xlog->log.info("Destroyed session (applicationId: %s) (ID: %s)", application.getId(), key);\r
581 }\r
582 \r
583 void SSCache::receive(const DDF& in, ostream& out)\r
584 {\r
585 }\r