[cpp-sp] branch main updated: Raft of changes removing old Attribute APIs and adding new.

Scott Cantor cantor.2 at osu.edu
Tue Apr 22 15:25:57 UTC 2025


This is an automated email from the git hooks/post-receive script.

scantor pushed a commit to branch main
in repository cpp-sp.

View the commit online:
http://git.shibboleth.net/view/?p=cpp-sp.git;a=commit;h=98c8feae16e283afc40a1657ac1094dc4accc75a

The following commit(s) were added to refs/heads/main by this push:
     new 98c8feae Raft of changes removing old Attribute APIs and adding new.
98c8feae is described below

commit 98c8feae16e283afc40a1657ac1094dc4accc75a
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Tue Apr 22 11:25:53 2025 -0400

    Raft of changes removing old Attribute APIs and adding new.
---
 Projects/vc22/shibsp.vcxproj                       |   8 +-
 Projects/vc22/shibsp.vcxproj.filters               |  16 +-
 apache/mod_shib_24.cpp                             |  87 +++--
 configs/Makefile.am                                |   1 +
 configs/attributes.ini                             |  13 +
 shibsp/Agent.cpp                                   | 144 +------
 shibsp/Agent.h                                     |  17 +-
 shibsp/Makefile.am                                 |   8 +-
 shibsp/attribute/Attribute.cpp                     | 175 ---------
 shibsp/attribute/Attribute.h                       | 236 -----------
 shibsp/attribute/AttributeConfiguration.h          | 168 ++++++++
 shibsp/attribute/ScopedAttribute.cpp               | 130 -------
 shibsp/attribute/ScopedAttribute.h                 | 107 -----
 shibsp/attribute/SimpleAttribute.cpp               |  73 ----
 shibsp/attribute/SimpleAttribute.h                 |  73 ----
 .../impl/DefaultAttributeConfiguration.cpp         | 431 +++++++++++++++++++++
 shibsp/handler/HandlerConfiguration.h              |   2 +-
 shibsp/handler/impl/AdminLogoutInitiator.cpp       |   8 +-
 shibsp/handler/impl/AttributeCheckerHandler.cpp    |   6 +-
 shibsp/handler/impl/LocalLogoutInitiator.cpp       |   2 +-
 shibsp/handler/impl/SessionHandler.cpp             |  75 ++--
 shibsp/impl/DefaultAgent.cpp                       |  43 +-
 shibsp/impl/XMLAccessControl.cpp                   |  61 ++-
 shibsp/session/AbstractSessionCache.h              |  63 ++-
 shibsp/session/SessionCache.h                      |  26 +-
 shibsp/session/impl/AbstractSessionCache.cpp       | 295 +++++++++++++-
 tests/data/console-shibboleth.ini                  |   1 +
 tests/data/fatal-exts-shibboleth.ini               |   1 +
 tests/data/impl/console-shibboleth.ini             |   1 +
 tests/data/nonfatal-exts-shibboleth.ini            |   1 +
 tests/data/platform/iis/console-shibboleth.ini     |   1 +
 tests/data/remoting/impl/shibboleth.ini            |   1 +
 tests/data/syslog-shibboleth.ini                   |   1 +
 .../util/reloadablefile/console-shibboleth.ini     |   1 +
 34 files changed, 1178 insertions(+), 1098 deletions(-)

diff --git a/Projects/vc22/shibsp.vcxproj b/Projects/vc22/shibsp.vcxproj
index 4d286c22..fce607e8 100644
--- a/Projects/vc22/shibsp.vcxproj
+++ b/Projects/vc22/shibsp.vcxproj
@@ -32,9 +32,7 @@
     <ClInclude Include="..\..\shibsp\AccessControl.h" />
     <ClInclude Include="..\..\shibsp\Agent.h" />
     <ClInclude Include="..\..\shibsp\AgentConfig.h" />
-    <ClInclude Include="..\..\shibsp\attribute\Attribute.h" />
-    <ClInclude Include="..\..\shibsp\attribute\ScopedAttribute.h" />
-    <ClInclude Include="..\..\shibsp\attribute\SimpleAttribute.h" />
+    <ClInclude Include="..\..\shibsp\attribute\AttributeConfiguration.h" />
     <ClInclude Include="..\..\shibsp\base.h" />
     <ClInclude Include="..\..\shibsp\config_pub_win32.h" />
     <ClInclude Include="..\..\shibsp\exceptions.h" />
@@ -86,9 +84,7 @@
   <ItemGroup>
     <ClCompile Include="..\..\shibsp\AbstractSPRequest.cpp" />
     <ClCompile Include="..\..\shibsp\Agent.cpp" />
-    <ClCompile Include="..\..\shibsp\attribute\Attribute.cpp" />
-    <ClCompile Include="..\..\shibsp\attribute\ScopedAttribute.cpp" />
-    <ClCompile Include="..\..\shibsp\attribute\SimpleAttribute.cpp" />
+    <ClCompile Include="..\..\shibsp\attribute\impl\DefaultAttributeConfiguration.cpp" />
     <ClCompile Include="..\..\shibsp\exceptions.cpp" />
     <ClCompile Include="..\..\shibsp\handler\impl\AbstractHandler.cpp">
       <DisableSpecificWarnings Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">4296;%(DisableSpecificWarnings)</DisableSpecificWarnings>
diff --git a/Projects/vc22/shibsp.vcxproj.filters b/Projects/vc22/shibsp.vcxproj.filters
index 42830301..2b8edf71 100644
--- a/Projects/vc22/shibsp.vcxproj.filters
+++ b/Projects/vc22/shibsp.vcxproj.filters
@@ -78,13 +78,7 @@
     </Filter>
   </ItemGroup>
   <ItemGroup>
-    <ClInclude Include="..\..\shibsp\attribute\Attribute.h">
-      <Filter>Header Files\Attribute</Filter>
-    </ClInclude>
-    <ClInclude Include="..\..\shibsp\attribute\ScopedAttribute.h">
-      <Filter>Header Files\Attribute</Filter>
-    </ClInclude>
-    <ClInclude Include="..\..\shibsp\attribute\SimpleAttribute.h">
+    <ClInclude Include="..\..\shibsp\attribute\AttributeConfiguration.h">
       <Filter>Header Files\Attribute</Filter>
     </ClInclude>
     <ClInclude Include="..\..\shibsp\config_pub_win32.h">
@@ -245,13 +239,7 @@
     </ClInclude>
   </ItemGroup>
   <ItemGroup>
-    <ClCompile Include="..\..\shibsp\attribute\Attribute.cpp">
-      <Filter>Source Files\Attribute</Filter>
-    </ClCompile>
-    <ClCompile Include="..\..\shibsp\attribute\ScopedAttribute.cpp">
-      <Filter>Source Files\Attribute</Filter>
-    </ClCompile>
-    <ClCompile Include="..\..\shibsp\attribute\SimpleAttribute.cpp">
+    <ClCompile Include="..\..\shibsp\attribute\impl\DefaultAttributeConfiguraton.cpp">
       <Filter>Source Files\Attribute</Filter>
     </ClCompile>
     <ClCompile Include="..\..\shibsp\handler\impl\AbstractHandler.cpp">
diff --git a/apache/mod_shib_24.cpp b/apache/mod_shib_24.cpp
index 08b7bd8b..8e192407 100644
--- a/apache/mod_shib_24.cpp
+++ b/apache/mod_shib_24.cpp
@@ -42,7 +42,7 @@
 #include <shibsp/Agent.h>
 #include <shibsp/AgentConfig.h>
 #include <shibsp/RequestMapper.h>
-#include <shibsp/attribute/Attribute.h>
+#include <shibsp/attribute/AttributeConfiguration.h>
 #include <shibsp/session/SessionCache.h>
 #include <shibsp/util/Lockable.h>
 
@@ -819,7 +819,14 @@ public:
     aclresult_t doShibAttr(const ShibTargetApache& sta, const Session* session, const char* rule, const char* params) const;
 
 private:
-    bool checkAttribute(const SPRequest& request, const Attribute* attr, const char* toMatch, bool isRegex=false) const;
+    bool checkAttribute(
+        const SPRequest& request,
+        const Session& session,
+        const AttributeConfiguration& attrConfig,
+        const char* attributeID,
+        const char* toMatch,
+        bool isRegex=false
+    ) const;
 };
 
 AccessControl* htAccessFactory(const ptree&, bool)
@@ -946,36 +953,42 @@ AccessControl::aclresult_t htAccessControl::doAuthnContext(const ShibTargetApach
     return shib_acl_false;
 }
 
-bool htAccessControl::checkAttribute(const SPRequest& request, const Attribute* attr, const char* toMatch, bool isRegex) const
+bool htAccessControl::checkAttribute(
+    const SPRequest& request,
+    const Session& session,
+    const AttributeConfiguration& attrConfig,
+    const char* attributeID,
+    const char* toMatch,
+    bool isRegex
+    ) const
 {
-    bool caseSensitive = attr->isCaseSensitive();
-    const vector<string>& vals = attr->getSerializedValues();
-    for (vector<string>::const_iterator v = vals.begin(); v != vals.end(); ++v) {
-        if (isRegex) {
-            regexp::regex_constants::syntax_option_type flags = regexp::regex_constants::extended;
-            if (!caseSensitive) {
-                flags |= regexp::regex_constants::icase;
-            }
-            try {
-                regexp::regex exp(toMatch, flags);
-                if (regexp::regex_match(*v, exp, regexp::regex_constants::match_any | regexp::regex_constants::match_not_null)) {
-                    if (request.isPriorityEnabled(Priority::SHIB_DEBUG))
-                        request.log(Priority::SHIB_DEBUG, string("htaccess: expecting regexp ") + toMatch + ", got " + *v + ": accepted");
-                    return true;
+    bool caseSensitive = attrConfig.isCaseSensitive(attributeID);
+    if (isRegex) {
+        regexp::regex_constants::syntax_option_type flags = regexp::regex_constants::extended;
+        if (!caseSensitive) {
+            flags |= regexp::regex_constants::icase;
+        }
+        try {
+            regexp::regex exp(toMatch, flags);
+            if (attrConfig.hasMatchingValue(session, attributeID, exp)) {
+                if (request.isPriorityEnabled(Priority::SHIB_DEBUG)) {
+                    request.log(Priority::SHIB_DEBUG, string("htaccess: attribute (") + attributeID + ") matched regexp: " + toMatch);
                 }
-            } catch (const regexp::regex_error& e) {
-                request.log(Priority::SHIB_ERROR,
-                    string("htaccess plugin caught exception while parsing regular expression (") + toMatch + "): " + e.what());
+                return true;
             }
+        } catch (const regexp::regex_error& e) {
+            request.log(Priority::SHIB_ERROR,
+                string("htaccess plugin caught exception while parsing regular expression (") + toMatch + "): " + e.what());
         }
-        else if ((caseSensitive && *v == toMatch) || (!caseSensitive && !strcasecmp(v->c_str(), toMatch))) {
-            if (request.isPriorityEnabled(Priority::SHIB_DEBUG))
-                request.log(Priority::SHIB_DEBUG, string("htaccess: expecting ") + toMatch + ", got " + *v + ": accepted");
-            return true;
-        }
-        else if (request.isPriorityEnabled(Priority::SHIB_DEBUG)) {
-            request.log(Priority::SHIB_DEBUG, string("htaccess: expecting ") + toMatch + ", got " + *v + ": rejected");
+    }
+    else if (attrConfig.hasMatchingValue(session, attributeID, toMatch)) {
+        if (request.isPriorityEnabled(Priority::SHIB_DEBUG)) {
+            request.log(Priority::SHIB_DEBUG, string("htaccess: attribute (") + attributeID + ") matched " + toMatch);
         }
+        return true;
+    }
+    else if (request.isPriorityEnabled(Priority::SHIB_DEBUG)) {
+        request.log(Priority::SHIB_DEBUG, string("htaccess: attribute (") + attributeID + ") did not match " + toMatch);
     }
     return false;
 }
@@ -984,25 +997,27 @@ AccessControl::aclresult_t htAccessControl::doShibAttr(
     const ShibTargetApache& sta, const Session* session, const char* rule, const char* params
     ) const
 {
-    // Find the attribute(s) matching the require rule.
-    pair<multimap<string,const Attribute*>::const_iterator,multimap<string,const Attribute*>::const_iterator> attrs =
-        session->getIndexedAttributes().equal_range(rule ? rule : "");
+    // "rule" is the attribute ID being eval'd
+    if (!rule || !session) {
+        return shib_acl_false;
+    }
+
+    const AttributeConfiguration& attrConfig =
+        sta.getAgent().getAttributeConfiguration(sta.getRequestSettings().first->getString("attributeConfigID"));
 
     bool regexp = false;
-    while (attrs.first != attrs.second && *params) {
+    while (*params) {
         const char* w = ap_getword_conf(sta.m_req->pool, &params);
         if (*w == '~') {
             regexp = true;
             continue;
         }
 
-        pair<multimap<string,const Attribute*>::const_iterator,multimap<string,const Attribute*>::const_iterator> attrs2(attrs);
-        for (; attrs2.first != attrs2.second; ++attrs2.first) {
-            if (checkAttribute(sta, attrs2.first->second, w, regexp)) {
-                return shib_acl_true;
-            }
+        if (checkAttribute(sta, *session, attrConfig, rule, w, regexp)) {
+            return shib_acl_true;
         }
     }
+
     return shib_acl_false;
 }
 
diff --git a/configs/Makefile.am b/configs/Makefile.am
index c2a5f03d..74e10f46 100644
--- a/configs/Makefile.am
+++ b/configs/Makefile.am
@@ -19,6 +19,7 @@ CONFIGFILES = \
 	globalLogout.html \
 	partialLogout.html \
 	shibboleth.ini \
+	attributes.ini \
 	handlers.ini \
 	request-map.xml
 
diff --git a/configs/attributes.ini b/configs/attributes.ini
new file mode 100644
index 00000000..4e8c179b
--- /dev/null
+++ b/configs/attributes.ini
@@ -0,0 +1,13 @@
+[settings]
+#attributeValueDelimiter = ;
+#scopedDelimiter = @
+#exportDuplicateValues = true
+caseSensitiveAttributes = foo, bar
+legacyClassRefAttribute = Shib-AuthnContext-Class
+legacyAuthnTimeAttribute = Shib-Authentication-Instant
+# Set to URL to apply URL encoding
+#encoding =
+
+[mappings]
+foo = foo
+
diff --git a/shibsp/Agent.cpp b/shibsp/Agent.cpp
index 23a788b2..f7234e4e 100644
--- a/shibsp/Agent.cpp
+++ b/shibsp/Agent.cpp
@@ -24,7 +24,7 @@
 #include "AgentConfig.h"
 #include "AccessControl.h"
 #include "SPRequest.h"
-#include "attribute/Attribute.h"
+#include "attribute/AttributeConfiguration.h"
 #include "handler/Handler.h"
 #include "handler/HandlerConfiguration.h"
 #include "logging/Category.h"
@@ -60,7 +60,7 @@ bool Agent::CATCH_ALL_PROP_DEFAULT = false;
 
 Agent::Agent()
 {
-    m_authTypes.insert("shibboleth");
+    m_authTypes = {"shibboleth"};
 }
 
 Agent::~Agent()
@@ -120,133 +120,6 @@ long Agent::handleError(Category& log, SPRequest& request, const Session* sessio
     return request.sendResponse(msg, richEx ? richEx->getStatusCode() : HTTPResponse::SHIBSP_HTTP_STATUS_ERROR);
 }
 
-void Agent::clearHeaders(SPRequest& request) const {
-    request.clearHeader("Shib-Cookie-Name", "HTTP_SHIB_COOKIE_NAME");
-    request.clearHeader("Shib-Session-ID", "HTTP_SHIB_SESSION_ID");
-    request.clearHeader("Shib-Session-Index", "HTTP_SHIB_SESSION_INDEX");
-    request.clearHeader("Shib-Session-Expires", "HTTP_SHIB_SESSION_EXPIRES");
-    request.clearHeader("Shib-Session-Inactivity", "HTTP_SHIB_SESSION_INACTIVITY");
-    request.clearHeader("Shib-Identity-Provider", "HTTP_SHIB_IDENTITY_PROVIDER");
-    request.clearHeader("Shib-Authentication-Method", "HTTP_SHIB_AUTHENTICATION_METHOD");
-    request.clearHeader("Shib-Authentication-Instant", "HTTP_SHIB_AUTHENTICATION_INSTANT");
-    request.clearHeader("Shib-AuthnContext-Class", "HTTP_SHIB_AUTHNCONTEXT_CLASS");
-    request.clearHeader("Shib-AuthnContext-Decl", "HTTP_SHIB_AUTHNCONTEXT_DECL");
-    request.clearHeader("Shib-Assertion-Count", "HTTP_SHIB_ASSERTION_COUNT");
-    request.clearHeader("Shib-Handler", "HTTP_SHIB_HANDLER");
-    request.clearHeader("REMOTE_USER", "HTTP_REMOTE_USER");
-    // TODO: Redo the handling of attribute headers in the code, likely supplanting all of the above...
-    //request.clearAttributeHeaders();
-}
-
-void Agent::exportAttributes(SPRequest& request, const Session* session) const {
-
-    RequestMapper::Settings settings = request.getRequestSettings();
-
-    const char* enc = settings.first->getString("encoding");
-    if (enc && strcmp(enc, "URL"))
-        throw ConfigurationException(string("Unsupported value for 'encoding' content setting: ") + enc);
-
-    const URLEncoder& encoder = AgentConfig::getConfig().getURLEncoder();
-
-    // Default delimiter is semicolon but is now configurable.
-    const char* delim = settings.first->getString("attributeValueDelimiter", ";");
-    size_t delim_len = strlen(delim);
-
-    bool exportDups = settings.first->getBool("exportDuplicateValues", true);
-    const multimap<string,const Attribute*>& attributes = session->getIndexedAttributes();
-
-    // Default export strategy will include duplicates.
-    if (exportDups) {
-        for (multimap<string,const Attribute*>::const_iterator a = attributes.begin(); a != attributes.end(); ++a) {
-            if (a->second->isInternal())
-                continue;
-            string header(request.getSecureHeader(a->first.c_str()));
-            const vector<string>& vals = a->second->getSerializedValues();
-            for (vector<string>::const_iterator v = vals.begin(); v != vals.end(); ++v) {
-                if (!header.empty())
-                    header += delim;
-                if (enc) {
-                    // If URL-encoding, any semicolons will get escaped anyway.
-                    header += encoder.encode(v->c_str());
-                }
-                else {
-                    string::size_type pos = v->find(delim, string::size_type(0));
-                    if (pos != string::npos) {
-                        string value(*v);
-                        for (; pos != string::npos; pos = value.find(delim, pos)) {
-                            value.insert(pos, "\\");
-                            pos += delim_len + 1;
-                        }
-                        header += value;
-                    }
-                    else {
-                        header += (*v);
-                    }
-                }
-            }
-            request.setHeader(a->first.c_str(), header.c_str());
-        }
-    }
-    else {
-        // Capture values in a map of sets to check for duplicates on the fly.
-        map< string,set<string> > valueMap;
-        for (multimap<string,const Attribute*>::const_iterator a = attributes.begin(); a != attributes.end(); ++a) {
-            if (a->second->isInternal())
-                continue;
-            const vector<string>& vals = a->second->getSerializedValues();
-            valueMap[a->first].insert(vals.begin(), vals.end());
-        }
-
-        // Export the mapped sets to the headers.
-        for (map< string,set<string> >::const_iterator deduped = valueMap.begin(); deduped != valueMap.end(); ++deduped) {
-            string header;
-            for (set<string>::const_iterator v = deduped->second.begin(); v != deduped->second.end(); ++v) {
-                if (!header.empty())
-                    header += delim;
-                if (enc) {
-                    // If URL-encoding, any semicolons will get escaped anyway.
-                    header += encoder.encode(v->c_str());
-                }
-                else {
-                    string::size_type pos = v->find(delim, string::size_type(0));
-                    if (pos != string::npos) {
-                        string value(*v);
-                        for (; pos != string::npos; pos = value.find(delim, pos)) {
-                            value.insert(pos, "\\");
-                            pos += delim_len + 1;
-                        }
-                        header += value;
-                    }
-                    else {
-                        header += (*v);
-                    }
-                }
-            }
-            request.setHeader(deduped->first.c_str(), header.c_str());
-        }
-    }
-
-    // Check for REMOTE_USER.
-    bool remoteUserSet = false;
-    vector<string> dummy;
-    const vector<string>& rmids = dummy; // app.getRemoteUserAttributeIds(); TODO: re implement this elsewhere
-    for (vector<string>::const_iterator rmid = rmids.begin(); !remoteUserSet && rmid != rmids.end(); ++rmid) {
-        pair<multimap<string,const Attribute*>::const_iterator,multimap<string,const Attribute*>::const_iterator> matches =
-            attributes.equal_range(*rmid);
-        for (; matches.first != matches.second; ++matches.first) {
-            const vector<string>& vals = matches.first->second->getSerializedValues();
-            if (!vals.empty()) {
-                if (enc)
-                    request.setRemoteUser(encoder.encode(vals.front().c_str()).c_str());
-                else
-                    request.setRemoteUser(vals.front().c_str());
-                remoteUserSet = true;
-                break;
-            }
-        }
-    }
-}
-
 pair<bool,long> Agent::doAuthentication(SPRequest& request, bool handler) const
 {
     Category& log = Category::getInstance(SHIBSP_LOGCAT ".Agent");
@@ -305,8 +178,9 @@ pair<bool,long> Agent::doAuthentication(SPRequest& request, bool handler) const
             return make_pair(true, request.returnDecline());
         }
 
-        // Fix for secadv 20050901
-        clearHeaders(request);
+        request.getAgent().getAttributeConfiguration(
+            request.getRequestSettings().first->getString("attributeConfigID")
+            ).clearHeaders(request);
 
         bool sessionExists = false;
         try {
@@ -486,8 +360,8 @@ pair<bool,long> Agent::doExport(SPRequest& request, bool requireSession) const
         request.setHeader("Shib-Session-ID", session->getID());
         request.setHeader("Shib-Application-ID", session->getApplicationID());
 
-        // Check for export of "standard" variables.
-        request.setHeader( "Shib-Session-Expires", boost::lexical_cast<string>(session->getExpiration()).c_str());
+        unsigned int lifetime = settings.first->getUnsignedInt("lifetime", 28800);
+        request.setHeader( "Shib-Session-Expires", boost::lexical_cast<string>(session->getCreation() + lifetime).c_str());
         unsigned int timeout = settings.first->getUnsignedInt("timeout", 3600);
         if (timeout > 0) {
             request.setHeader( "Shib-Session-Inactivity", boost::lexical_cast<string>(session->getLastAccess() + timeout).c_str());
@@ -501,7 +375,9 @@ pair<bool,long> Agent::doExport(SPRequest& request, bool requireSession) const
         }
 
         // Export the attributes.
-        exportAttributes(request, session);
+        request.getAgent().getAttributeConfiguration(
+            request.getRequestSettings().first->getString("attributeConfigID")
+            ).exportAttributes(request, *session);
 
         return make_pair(false,0L);
     }
diff --git a/shibsp/Agent.h b/shibsp/Agent.h
index d0294d37..6328f9ba 100644
--- a/shibsp/Agent.h
+++ b/shibsp/Agent.h
@@ -28,6 +28,7 @@
 
 namespace shibsp {
 
+    class SHIBSP_API AttributeConfiguration;
     class SHIBSP_API Category;
     class SHIBSP_API Handler;
     class SHIBSP_API HandlerConfiguration;
@@ -101,7 +102,18 @@ namespace shibsp {
          * 
          * @return the matching configuration
          */
-        virtual HandlerConfiguration& getHandlerConfiguration(const char* id=nullptr) const=0;
+        virtual const HandlerConfiguration& getHandlerConfiguration(const char* id=nullptr) const=0;
+
+        /**
+         * Gets the identified AttributeConfiguration.
+         * 
+         * <p>If no matching configurationn is found, an exception is raised.</p>
+         * 
+         * @param id identifier for configuration (null is assumed to be the default)
+         * 
+         * @return the matching configuration
+         */
+        virtual const AttributeConfiguration& getAttributeConfiguration(const char* id=nullptr) const=0;
 
         /**
          * Enforces requirements for an authenticated session.
@@ -175,9 +187,6 @@ namespace shibsp {
             std::exception* ex=nullptr,
             bool mayRedirect=true
         ) const;
-        void clearHeaders(SPRequest& request) const;
-        void exportAttributes(SPRequest& request, const Session* session) const;
-
     };
 
 #if defined (_MSC_VER)
diff --git a/shibsp/Makefile.am b/shibsp/Makefile.am
index 1bb36701..e9cb3969 100644
--- a/shibsp/Makefile.am
+++ b/shibsp/Makefile.am
@@ -36,9 +36,7 @@ libshibspinclude_HEADERS = \
 	version.h
 
 attrinclude_HEADERS = \
-	attribute/Attribute.h \
-	attribute/ScopedAttribute.h \
-	attribute/SimpleAttribute.h
+	attribute/AttributeConfiguration.h
 
 handinclude_HEADERS = \
 	handler/AbstractHandler.h \
@@ -100,9 +98,7 @@ libshibsp_la_SOURCES = \
 	Agent.cpp \
 	exceptions.cpp \
 	version.cpp \
-	attribute/Attribute.cpp \
-	attribute/SimpleAttribute.cpp \
-	attribute/ScopedAttribute.cpp \
+	attribute/impl/DefaultAttributeConfiguration.cpp \
 	handler/impl/AbstractHandler.cpp \
 	handler/impl/AdminLogoutInitiator.cpp \
 	handler/impl/AttributeCheckerHandler.cpp \
diff --git a/shibsp/attribute/Attribute.cpp b/shibsp/attribute/Attribute.cpp
deleted file mode 100644
index b5b07c4b..00000000
--- a/shibsp/attribute/Attribute.cpp
+++ /dev/null
@@ -1,175 +0,0 @@
-/**
- * Licensed to the University Corporation for Advanced Internet
- * Development, Inc. (UCAID) under one or more contributor license
- * agreements. See the NOTICE file distributed with this work for
- * additional information regarding copyright ownership.
- *
- * UCAID licenses this file to you under the Apache License,
- * Version 2.0 (the "License"); you may not use this file except
- * in compliance with the License. You may obtain a copy of the
- * License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
- * either express or implied. See the License for the specific
- * language governing permissions and limitations under the License.
- */
-
-/**
- * shibsp/attribute/Attribute.cpp
- *
- * A resolved attribute.
- */
-
-#include "internal.h"
-#include "exceptions.h"
-#include "attribute/SimpleAttribute.h"
-#include "util/SPConstants.h"
-
-using namespace shibsp;
-using namespace std;
-
-namespace shibsp {
-    SHIBSP_DLLLOCAL Attribute* SimpleAttributeFactory(DDF& in);
-    SHIBSP_DLLLOCAL Attribute* ScopedAttributeFactory(DDF& in);
-};
-
-void shibsp::registerAttributeFactories()
-{
-    Attribute::registerFactory("", SimpleAttributeFactory);
-    Attribute::registerFactory("Simple", SimpleAttributeFactory);
-    Attribute::registerFactory("Scoped", ScopedAttributeFactory);
-}
-
-map<string,Attribute::AttributeFactory*> Attribute::m_factoryMap;
-
-void Attribute::registerFactory(const char* type, AttributeFactory* factory)
-{
-    m_factoryMap[type] = factory;
-}
-
-void Attribute::deregisterFactory(const char* type)
-{
-    m_factoryMap.erase(type);
-}
-
-void Attribute::deregisterFactories()
-{
-    m_factoryMap.clear();
-}
-
-Attribute::Attribute(const vector<string>& ids) : m_id(ids), m_caseSensitive(true), m_internal(false)
-{
-}
-
-Attribute::Attribute(DDF& in) : m_caseSensitive(in["case_insensitive"].isnull()), m_internal(!in["internal"].isnull())
-{
-    const char* id = in.first().name();
-    if (id && *id)
-        m_id.push_back(id);
-    else
-        throw AttributeException("No id found in marshalled attribute content.");
-    DDF aliases = in["aliases"];
-    if (aliases.islist()) {
-        DDF alias = aliases.first();
-        while (alias.isstring()) {
-            m_id.push_back(alias.string());
-            alias = aliases.next();
-        }
-    }
-}
-
-Attribute::~Attribute()
-{
-}
-
-const char* Attribute::getId() const
-{
-    return m_id.front().c_str();
-}
-
-const vector<string>& Attribute::getAliases() const
-{
-    return m_id;
-}
-
-vector<string>& Attribute::getAliases()
-{
-    return m_id;
-}
-
-void Attribute::setCaseSensitive(bool caseSensitive)
-{
-    m_caseSensitive = caseSensitive;
-}
-
-void Attribute::setInternal(bool internal)
-{
-    m_internal = internal;
-}
-
-bool Attribute::isCaseSensitive() const
-{
-    return m_caseSensitive;
-}
-
-bool Attribute::isInternal() const
-{
-    return m_internal;
-}
-
-size_t Attribute::valueCount() const
-{
-    return m_serialized.size();
-}
-
-const vector<string>& Attribute::getSerializedValues() const
-{
-    return m_serialized;
-}
-
-const char* Attribute::getString(size_t index) const
-{
-    return m_serialized[index].c_str();
-}
-
-const char* Attribute::getScope(size_t index) const
-{
-    return nullptr;
-}
-
-void Attribute::removeValue(size_t index)
-{
-    if (index < m_serialized.size())
-        m_serialized.erase(m_serialized.begin() + index);
-}
-
-DDF Attribute::marshall() const
-{
-    DDF ddf(nullptr);
-    ddf.structure().addmember(m_id.front().c_str()).list();
-    if (!m_caseSensitive)
-        ddf.addmember("case_insensitive");
-    if (m_internal)
-        ddf.addmember("internal");
-    if (m_id.size() > 1) {
-        DDF alias;
-        DDF aliases = ddf.addmember("aliases").list();
-        for (std::vector<std::string>::const_iterator a = m_id.begin() + 1; a != m_id.end(); ++a) {
-            alias = DDF(nullptr).string(a->c_str());
-            aliases.add(alias);
-        }
-    }
-    return ddf;
-}
-
-Attribute* Attribute::unmarshall(DDF& in)
-{
-    map<string,AttributeFactory*>::const_iterator i = m_factoryMap.find(in.name() ? in.name() : "");
-    if (i == m_factoryMap.end())
-        throw AttributeException(string("No registered factory for Attribute of type ") + in.name());
-    return (i->second)(in);
-}
diff --git a/shibsp/attribute/Attribute.h b/shibsp/attribute/Attribute.h
deleted file mode 100644
index 346c6462..00000000
--- a/shibsp/attribute/Attribute.h
+++ /dev/null
@@ -1,236 +0,0 @@
-/**
- * Licensed to the University Corporation for Advanced Internet
- * Development, Inc. (UCAID) under one or more contributor license
- * agreements. See the NOTICE file distributed with this work for
- * additional information regarding copyright ownership.
- *
- * UCAID licenses this file to you under the Apache License,
- * Version 2.0 (the "License"); you may not use this file except
- * in compliance with the License. You may obtain a copy of the
- * License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
- * either express or implied. See the License for the specific
- * language governing permissions and limitations under the License.
- */
-
-/**
- * @file shibsp/attribute/Attribute.h
- *
- * A resolved attribute.
- */
-
-#ifndef __shibsp_attribute_h__
-#define __shibsp_attribute_h__
-
-#include <shibsp/remoting/ddf.h>
-
-#include <map>
-#include <string>
-#include <vector>
-
-namespace shibsp {
-
-#if defined (_MSC_VER)
-    #pragma warning( push )
-    #pragma warning( disable : 4251 )
-#endif
-
-    /**
-     * A resolved attribute.
-     *
-     * <p>Resolved attributes are a neutral construct that represent both simple and
-     * complex attribute data structures that might be found in SAML assertions
-     * or obtained from other sources.
-     *
-     * <p>Attributes consist of an id/name that is locally unique (that is, unique to a
-     * configuration at any given point in time) and zero or more values. Values can
-     * be of any type or structure, but will generally be made available to applications
-     * only if a serialized string form exists. More complex values can be used with
-     * access control plugins and other components that understand them, however.
-     */
-    class SHIBSP_API Attribute
-    {
-        MAKE_NONCOPYABLE(Attribute);
-    protected:
-        /**
-         * Constructor
-         *
-         * @param ids   array with primary identifier in first position, followed by any aliases
-         */
-        Attribute(const std::vector<std::string>& ids);
-
-        /**
-         * Constructs based on a remoted Attribute.
-         *
-         * <p>This allows Attribute objects to be recreated after marshalling.
-         * The DDF supplied must be a struct containing a single list member named
-         * with the Attribute's "id" and containing the values.
-         *
-         * @param in    input object containing marshalled Attribute
-         */
-        Attribute(DDF& in);
-
-        /**
-         * Maintains a copy of serialized attribute values, when possible.
-         *
-         * <p>Implementations should maintain the array when values are added or removed.
-         */
-        mutable std::vector<std::string> m_serialized;
-
-    public:
-        virtual ~Attribute();
-
-        /**
-         * Returns the Attribute identifier.
-         *
-         * @return the Attribute identifier
-         */
-        const char* getId() const;
-
-        /**
-         * Returns all of the effective names for the Attribute.
-         *
-         * @return immutable array of identifiers, with the primary ID in the first position
-         */
-        const std::vector<std::string>& getAliases() const;
-
-        /**
-         * Returns all of the effective names for the Attribute.
-         *
-         * @return mutable array of identifiers, with the primary ID in the first position
-         */
-        std::vector<std::string>& getAliases();
-
-        /**
-         * Sets whether case sensitivity should apply to basic value comparisons.
-         *
-         * @param caseSensitive  true iff value comparisons should be case sensitive
-         */
-        void setCaseSensitive(bool caseSensitive);
-
-        /**
-         * Sets whether the attribute should be exported for CGI use.
-         *
-         * @param internal  true iff the attribute should <strong>NOT</strong> be exported
-         */
-        void setInternal(bool internal);
-
-        /**
-         * Indicates whether case sensitivity should apply to basic value comparisons.
-         *
-         * @return  true iff value comparisons should be case sensitive
-         */
-        bool isCaseSensitive() const;
-
-        /**
-         * Indicates whether the attribute should be exported for CGI use.
-         *
-         * @return  true iff the attribute should <strong>NOT</strong> be exported
-         */
-        bool isInternal() const;
-
-        /**
-         * Returns the number of values.
-         *
-         * @return  number of values
-         */
-        virtual size_t valueCount() const;
-
-        /**
-         * Returns serialized Attribute values encoded as UTF-8 strings.
-         *
-         * @return  an immutable vector of values
-         */
-        virtual const std::vector<std::string>& getSerializedValues() const;
-
-        /**
-         * Informs the Attribute that values have changed and any serializations
-         * must be cleared.
-         */
-        virtual void clearSerializedValues()=0;
-
-        /**
-         * Gets the string equivalent of the value at the specified position (starting from zero).
-         *
-         * @param index position of value
-         * @return the specified value in its "string" form, or nullptr if undefined
-         */
-        virtual const char* getString(size_t index) const;
-
-        /**
-         * Gets the "scope" of the value at the specified position (starting from zero).
-         *
-         * @param index position of value
-         * @return the specified value's "scope", or nullptr if attribute is unscoped
-         */
-        virtual const char* getScope(size_t index) const;
-
-        /**
-         * Removes the value at the specified position (starting from zero).
-         *
-         * @param index position of value to remove
-         */
-        virtual void removeValue(size_t index);
-
-        /**
-         * Marshalls an Attribute for remoting.
-         *
-         * <p>This allows Attribute objects to be communicated across process boundaries
-         * without excess XML parsing. The DDF returned must be a struct containing
-         * a single list member named with the Attribute's "id". The name of the struct
-         * should contain the registered name of the Attribute implementation.
-         */
-        virtual DDF marshall() const;
-
-        /**
-         * Unmarshalls a remoted Attribute.
-         *
-         * @param in    remoted Attribute data
-         * @return  a resolved Attribute of the proper subclass
-         */
-        static Attribute* unmarshall(DDF& in);
-
-        /** A function that unmarshalls remoted data into the proper Attribute subclass. */
-        typedef Attribute* AttributeFactory(DDF& in);
-
-        /**
-         * Registers an AttributeFactory function for a given attribute "type".
-         *
-         * @param type      string used at the root of remoted Attribute structures
-         * @param factory   factory function
-         */
-        static void registerFactory(const char* type, AttributeFactory* factory);
-
-        /**
-         * Deregisters an AttributeFactory function for a given attribute "type".
-         *
-         * @param type      string used at the root of remoted Attribute structures
-         */
-        static void deregisterFactory(const char* type);
-
-        /**
-         * Clears the map of factories.
-         */
-        static void deregisterFactories();
-
-    private:
-        static std::map<std::string,AttributeFactory*> m_factoryMap;
-        std::vector<std::string> m_id;
-        bool m_caseSensitive,m_internal;
-    };
-
-#if defined (_MSC_VER)
-    #pragma warning( pop )
-#endif
-
-    /** Registers built-in Attribute types into the runtime. */
-    void registerAttributeFactories();
-
-};
-
-#endif /* __shibsp_attribute_h__ */
diff --git a/shibsp/attribute/AttributeConfiguration.h b/shibsp/attribute/AttributeConfiguration.h
new file mode 100644
index 00000000..219b34de
--- /dev/null
+++ b/shibsp/attribute/AttributeConfiguration.h
@@ -0,0 +1,168 @@
+/**
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * @file shibsp/attribute/AttributeConfiguration.h
+ * 
+ * Interface to settings and functionality for attribute data manipulation.
+ */
+
+#ifndef __shibsp_attrconfig_h__
+#define __shibsp_attrconfig_h__
+
+#include <shibsp/util/PropertySet.h>
+
+#include <memory>
+#include <set>
+
+#ifdef SHIBSP_USE_BOOST_REGEX
+# include <boost/regex_fwd.hpp>
+namespace regexp = boost;
+#else
+# include <regex>
+namespace regexp = std;
+#endif
+
+namespace shibsp {
+
+    class SHIBSP_API DDF;
+    class SHIBSP_API Session;
+    class SHIBSP_API SPRequest;
+
+    /**
+     * Interface to settings and functionality for attribute data manipulation.
+     * 
+     * <p>This isn't an especially coherent interface but it centralizes config
+     * settings for a lot of what used to be scattered around the code base.</p>
+     */
+    class SHIBSP_API AttributeConfiguration : public virtual PropertySet
+    {
+        MAKE_NONCOPYABLE(AttributeConfiguration);
+    protected:
+    AttributeConfiguration();
+
+    public:
+        virtual ~AttributeConfiguration();
+
+        /** Used in ACL implementations to enforce legacy authnContextClassRef rule. */
+        static const char LEGACY_CLASSREF_ATTRIBUTE_PROP_PATH[];
+        static const char LEGACY_CLASSREF_ATTRIBUTE_PROP_DEFAULT[];
+
+        /** Used in ACL implementation to enforce legacy time-since-authn rule. */
+        static const char LEGACY_AUTHTIME_ATTRIBUTE_PROP_PATH[];
+        static const char LEGACY_AUTHTIME_ATTRIBUTE_PROP_DEFAULT[];
+
+        /**
+         * Post-process a collection of attributes and values from the hub for use by agent code.
+         * 
+         * <p>This handles serialization of data not already in simple "string" form so that
+         * subsequent usage is simpler. In particular it allows scoped data to be recombined with
+         * an agent-controlled delimiter.</p>
+         * 
+         * <p>If true is returned, the resulting object must be a list. Each element must
+         * also be a list with a non-empty name and at least one non-empty string value.
+         * Any non-conforming data must be removed from the object.</p>
+         * 
+         * @param attributes a list object containing the attributes in hub-supplied format
+         * 
+         * @return true iff at least one attribute exists after processing and the data is in
+         *  the expected form
+         */
+        virtual bool processAttributes(DDF& attributes) const=0;
+
+        /**
+         * Gets whether an attribute is configured to be "case sensitive" for value comparison
+         * purposes.
+         * 
+         * @param attributeID the ID of the attribute to check
+         * 
+         * @return true iff the attribute's values should be handled with case sensitivity
+         */
+        virtual bool isCaseSensitive(const char* attributeID) const=0;
+
+        /**
+         * "Clears" any headers controlled by the agent to reserve them for trusted attribute export.
+         * 
+         * <p>This method typically operates only when it detects the request will include header
+         * support, which is strongly discouraged.</p>
+         * 
+         * @param request request to "clear"
+         */
+        virtual void clearHeaders(SPRequest& request) const=0;
+
+        /**
+         * Exports data from the session into variables/headers in the supplied request.
+         * 
+         * @param request request to export into
+         * @param locked session to pull data from
+         */
+        virtual void exportAttributes(SPRequest& request, const Session& session) const=0;
+
+        /**
+         * Tests whether a given attribute in a session contains a matching value.
+         * 
+         * <p>It is an internal implementation detail as to the relevance of case to this
+         * comparison.</p>
+         * 
+         * @param session locked session to pull from
+         * @param attributeId the attribute to check
+         * @param value the value to check for
+         * 
+         * @return true iff the value "matched"
+         */
+        virtual bool hasMatchingValue(const Session& session, const char* attributeId, const char* value) const=0;
+
+        /**
+         * Tests whether a given attribute in a session contains a matching value from a set.
+         * 
+         * <p>It is an internal implementation detail as to the relevance of case to this
+         * comparison.</p>
+         * 
+         * @param session locked session to pull from
+         * @param attributeId the attribute to check
+         * @param values the set of values to check for
+         * 
+         * @return true iff at least one value "matched"
+         */
+        virtual bool hasMatchingValue(
+            const Session& session, const char* attributeId, const std::set<std::string>& values
+            ) const=0;
+
+        /**
+         * Tests whether a given attribute in a session contains a value matching a supplied
+         * regular expression.
+         * 
+         * @param session locked session to pull from
+         * @param attributeId the attribute to check
+         * @param values the set of values to check for
+         * 
+         * @return true iff at least one value "matched"
+         */
+        virtual bool hasMatchingValue(
+            const Session& session, const char* attributeId, const regexp::regex& expression
+            ) const=0;
+
+        /**
+         * Create a new AttributeConfiguration based on the supplied configuration file.
+         * 
+         * @param pathname  configuration file
+         * 
+         * @return the corresponding AttributeConfiguration
+         */
+        static std::unique_ptr<AttributeConfiguration> newAttributeConfiguration(const char* pathname);
+    };
+
+};
+
+#endif /* __shibsp_attrconfig_h__ */
diff --git a/shibsp/attribute/ScopedAttribute.cpp b/shibsp/attribute/ScopedAttribute.cpp
deleted file mode 100644
index 5632139e..00000000
--- a/shibsp/attribute/ScopedAttribute.cpp
+++ /dev/null
@@ -1,130 +0,0 @@
-/**
- * Licensed to the University Corporation for Advanced Internet
- * Development, Inc. (UCAID) under one or more contributor license
- * agreements. See the NOTICE file distributed with this work for
- * additional information regarding copyright ownership.
- *
- * UCAID licenses this file to you under the Apache License,
- * Version 2.0 (the "License"); you may not use this file except
- * in compliance with the License. You may obtain a copy of the
- * License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
- * either express or implied. See the License for the specific
- * language governing permissions and limitations under the License.
- */
-
-/**
- * ScopedAttribute.cpp
- *
- * An Attribute whose values are relations of a value and a scope.
- */
-
-#include "internal.h"
-#include "attribute/ScopedAttribute.h"
-
-using namespace shibsp;
-using namespace std;
-
-namespace shibsp {
-    SHIBSP_DLLLOCAL Attribute* ScopedAttributeFactory(DDF& in) {
-        return new ScopedAttribute(in);
-    }
-};
-
-ScopedAttribute::ScopedAttribute(const vector<string>& ids, char delimeter) : Attribute(ids), m_delimeter(delimeter)
-{
-}
-
-ScopedAttribute::ScopedAttribute(DDF& in) : Attribute(in), m_delimeter('@')
-{
-    DDF val = in["_delimeter"];
-    if (val.isint())
-        m_delimeter = static_cast<char>(val.integer());
-    val = in.first().first();
-    while (!val.isnull()) {
-        // There are two serializations supported. The new one is in 2.5.1 and fixes SPPCPP-504.
-        // The original is the second branch and was vulnerable to non-ASCII characters in the value.
-        // Supporting both means at least minimal support for rolling upgrades if a shibd instance is
-        // shared.
-        if (val.islist() && val.integer() == 2) {
-            m_values.push_back(make_pair(string(val.first().string()), string(val.last().string())));
-        }
-        else if (val.name() && val.string()) {
-            m_values.push_back(make_pair(string(val.name()), string(val.string())));
-        }
-        val = in.first().next();
-    }
-}
-
-ScopedAttribute::~ScopedAttribute()
-{
-}
-
-vector< pair<string,string> >& ScopedAttribute::getValues()
-{
-    return m_values;
-}
-
-const vector< pair<string,string> >& ScopedAttribute::getValues() const
-{
-    return m_values;
-}
-
-size_t ScopedAttribute::valueCount() const
-{
-    return m_values.size();
-}
-
-void ScopedAttribute::clearSerializedValues()
-{
-    m_serialized.clear();
-}
-
-const char* ScopedAttribute::getString(size_t index) const
-{
-    return m_values[index].first.c_str();
-}
-
-const char* ScopedAttribute::getScope(size_t index) const
-{
-    return m_values[index].second.c_str();
-}
-
-void ScopedAttribute::removeValue(size_t index)
-{
-    Attribute::removeValue(index);
-    if (index < m_values.size())
-        m_values.erase(m_values.begin() + index);
-}
-
-const vector<string>& ScopedAttribute::getSerializedValues() const
-{
-    if (m_serialized.empty()) {
-        for (vector< pair<string,string> >::const_iterator i=m_values.begin(); i!=m_values.end(); ++i)
-            m_serialized.push_back(i->first + m_delimeter + i->second);
-    }
-    return Attribute::getSerializedValues();
-}
-
-DDF ScopedAttribute::marshall() const
-{
-    DDF ddf = Attribute::marshall();
-    ddf.name("Scoped");
-    if (m_delimeter != '@')
-        ddf.addmember("_delimeter").integer(m_delimeter);
-    DDF vlist = ddf.first();
-    for (vector< pair<string,string> >::const_iterator i=m_values.begin(); i!=m_values.end(); ++i) {
-        DDF one = DDF(nullptr).string(i->first.c_str());
-        DDF two = DDF(nullptr).string(i->second.c_str());
-        DDF val = DDF(nullptr).list();
-        val.add(one);
-        val.add(two);
-        vlist.add(val);
-    }
-    return ddf;
-}
diff --git a/shibsp/attribute/ScopedAttribute.h b/shibsp/attribute/ScopedAttribute.h
deleted file mode 100644
index 8e2e853d..00000000
--- a/shibsp/attribute/ScopedAttribute.h
+++ /dev/null
@@ -1,107 +0,0 @@
-/**
- * Licensed to the University Corporation for Advanced Internet
- * Development, Inc. (UCAID) under one or more contributor license
- * agreements. See the NOTICE file distributed with this work for
- * additional information regarding copyright ownership.
- *
- * UCAID licenses this file to you under the Apache License,
- * Version 2.0 (the "License"); you may not use this file except
- * in compliance with the License. You may obtain a copy of the
- * License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
- * either express or implied. See the License for the specific
- * language governing permissions and limitations under the License.
- */
-
-/**
- * @file shibsp/attribute/ScopedAttribute.h
- * 
- * An Attribute whose values are relations of a value and a scope.
- */
-
-#ifndef __shibsp_scopedattr_h__
-#define __shibsp_scopedattr_h__
-
-#include <shibsp/attribute/Attribute.h>
-
-namespace shibsp {
-
-#if defined (_MSC_VER)
-    #pragma warning( push )
-    #pragma warning( disable : 4251 )
-#endif
-
-    /**
-     * An Attribute whose values are relations of a value and a scope.
-     * 
-     * <p>In practice, scoped attributes are simple pairs of strings instead
-     * of a single string. They can be expressed as a string easily using a delimeter,
-     * typically an '@' symbol. The scope concept allows certain kinds of filtering to
-     * be performed more intelligently and efficiently, although not all scoped
-     * attributes can be effectively filtered (e.g. if the set of scope values is
-     * unconstrained).
-     */
-    class SHIBSP_API ScopedAttribute : public Attribute
-    {
-    public:
-        /**
-         * Constructor.
-         * 
-         * @param ids   array with primary identifier in first position, followed by any aliases
-         * @param delimeter value/scope delimeter when serializing
-         */
-        ScopedAttribute(const std::vector<std::string>& ids, char delimeter='@');
-
-        /**
-         * Constructs based on a remoted ScopedAttribute.
-         * 
-         * @param in    input object containing marshalled ScopedAttribute
-         */
-        ScopedAttribute(DDF& in);
-        
-        virtual ~ScopedAttribute();
-
-        /**
-         * Returns the set of values encoded as UTF-8 strings.
-         * 
-         * <p>Each compound value is a pair containing the simple value and the scope. 
-         * 
-         * @return  a mutable vector of the values
-         */
-        std::vector< std::pair<std::string,std::string> >& getValues();
-
-        /**
-         * Returns the set of values encoded as UTF-8 strings.
-         * 
-         * <p>Each compound value is a pair containing the simple value and the scope. 
-         * 
-         * @return  an immutable vector of the values
-         */
-        const std::vector< std::pair<std::string,std::string> >& getValues() const;
-
-        // Virtual function overrides.
-        size_t valueCount() const;
-        void clearSerializedValues();
-        const char* getString(size_t index) const;
-        const char* getScope(size_t index) const;
-        void removeValue(size_t index);
-        const std::vector<std::string>& getSerializedValues() const;
-        DDF marshall() const;
-    
-    private:
-        char m_delimeter;
-        std::vector< std::pair<std::string,std::string> > m_values;
-    };
-
-#if defined (_MSC_VER)
-    #pragma warning( pop )
-#endif
-
-};
-
-#endif /* __shibsp_scopedattr_h__ */
diff --git a/shibsp/attribute/SimpleAttribute.cpp b/shibsp/attribute/SimpleAttribute.cpp
deleted file mode 100644
index ac29fb1f..00000000
--- a/shibsp/attribute/SimpleAttribute.cpp
+++ /dev/null
@@ -1,73 +0,0 @@
-/**
- * Licensed to the University Corporation for Advanced Internet
- * Development, Inc. (UCAID) under one or more contributor license
- * agreements. See the NOTICE file distributed with this work for
- * additional information regarding copyright ownership.
- *
- * UCAID licenses this file to you under the Apache License,
- * Version 2.0 (the "License"); you may not use this file except
- * in compliance with the License. You may obtain a copy of the
- * License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
- * either express or implied. See the License for the specific
- * language governing permissions and limitations under the License.
- */
-
-/**
- * SimpleAttribute.cpp
- *
- * An Attribute whose values are simple strings.
- */
-
-#include "internal.h"
-#include "attribute/SimpleAttribute.h"
-
-using namespace shibsp;
-using namespace std;
-
-namespace shibsp {
-    SHIBSP_DLLLOCAL Attribute* SimpleAttributeFactory(DDF& in) {
-        return new SimpleAttribute(in);
-    }
-};
-
-SimpleAttribute::SimpleAttribute(const vector<string>& ids) : Attribute(ids)
-{
-}
-
-SimpleAttribute::SimpleAttribute(DDF& in) : Attribute(in)
-{
-    DDF val = in.first().first();
-    while (val.string()) {
-        m_serialized.push_back(val.string());
-        val = in.first().next();
-    }
-}
-
-SimpleAttribute::~SimpleAttribute()
-{
-}
-
-vector<string>& SimpleAttribute::getValues()
-{
-    return m_serialized;
-}
-
-void SimpleAttribute::clearSerializedValues()
-{
-    // Do nothing, since our values are already serialized.
-}
-
-DDF SimpleAttribute::marshall() const
-{
-    DDF ddf = Attribute::marshall();
-    DDF vlist = ddf.first();
-    for (vector<string>::const_iterator i=m_serialized.begin(); i!=m_serialized.end(); ++i)
-        vlist.add(DDF(nullptr).string(i->c_str()));
-    return ddf;
-}
diff --git a/shibsp/attribute/SimpleAttribute.h b/shibsp/attribute/SimpleAttribute.h
deleted file mode 100644
index 55770c0c..00000000
--- a/shibsp/attribute/SimpleAttribute.h
+++ /dev/null
@@ -1,73 +0,0 @@
-/**
- * Licensed to the University Corporation for Advanced Internet
- * Development, Inc. (UCAID) under one or more contributor license
- * agreements. See the NOTICE file distributed with this work for
- * additional information regarding copyright ownership.
- *
- * UCAID licenses this file to you under the Apache License,
- * Version 2.0 (the "License"); you may not use this file except
- * in compliance with the License. You may obtain a copy of the
- * License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
- * either express or implied. See the License for the specific
- * language governing permissions and limitations under the License.
- */
-
-/**
- * @file shibsp/attribute/SimpleAttribute.h
- * 
- * An Attribute whose values are simple strings.
- */
-
-#ifndef __shibsp_simpattr_h__
-#define __shibsp_simpattr_h__
-
-#include <shibsp/attribute/Attribute.h>
-
-namespace shibsp {
-
-    /**
-     * An Attribute whose values are simple strings.
-     */
-    class SHIBSP_API SimpleAttribute : public Attribute
-    {
-    public:
-        /**
-         * Constructor.
-         * 
-         * @param ids   array with primary identifier in first position, followed by any aliases
-         */
-        SimpleAttribute(const std::vector<std::string>& ids);
-
-        /**
-         * Constructs based on a remoted SimpleAttribute.
-         * 
-         * @param in    input object containing marshalled SimpleAttribute
-         */
-        SimpleAttribute(DDF& in);
-        
-        virtual ~SimpleAttribute();
-
-        /**
-         * Returns the set of values encoded as UTF-8 strings.
-         * 
-         * <p>For simple values, the serialized form is just the actual string,
-         * so the value array can be directly manipulated. 
-         * 
-         * @return  a mutable vector of the values
-         */
-        std::vector<std::string>& getValues();
-        
-        // Virtual function overrides.
-        void clearSerializedValues();
-        DDF marshall() const;
-    };
-
-};
-
-#endif /* __shibsp_simpattr_h__ */
diff --git a/shibsp/attribute/impl/DefaultAttributeConfiguration.cpp b/shibsp/attribute/impl/DefaultAttributeConfiguration.cpp
new file mode 100644
index 00000000..0802284d
--- /dev/null
+++ b/shibsp/attribute/impl/DefaultAttributeConfiguration.cpp
@@ -0,0 +1,431 @@
+/**
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * attribute/impl/DefaultAttributeConfiguration.cpp
+ * 
+ * Default ptree-based AttributeConfiguration implementation.
+ */
+
+#include "internal.h"
+
+#include "exceptions.h"
+#include "AgentConfig.h"
+#include "RequestMapper.h"
+#include "SPRequest.h"
+#include "attribute/Attribute.h"
+#include "attribute/AttributeConfiguration.h"
+#include "logging/Category.h"
+#include "remoting/ddf.h"
+#include "session/SessionCache.h"
+#include "util/BoostPropertySet.h"
+#include "util/Misc.h"
+#include "util/URLEncoder.h"
+
+#include <boost/lexical_cast.hpp>
+#include <boost/property_tree/ptree.hpp>
+#include <boost/property_tree/ini_parser.hpp>
+
+#include <map>
+#include <set>
+#include <string>
+#include <stdexcept>
+
+#ifdef SHIBSP_USE_BOOST_REGEX
+# include <boost/regex.hpp>
+#endif
+
+using namespace shibsp;
+using namespace boost::property_tree;
+using namespace std;
+
+#ifndef HAVE_STRCASECMP
+# define strcasecmp _stricmp
+#endif
+
+namespace {
+
+    class DefaultAttributeConfiguration : public virtual AttributeConfiguration, public virtual BoostPropertySet {
+    public:
+        DefaultAttributeConfiguration(const char* pathname);
+        ~DefaultAttributeConfiguration() {}
+
+        bool processAttributes(DDF& attributes) const;
+        bool isCaseSensitive(const char* attributeID) const;
+        void clearHeaders(SPRequest& request) const;
+        void exportAttributes(SPRequest& request, const Session& session) const;
+        bool hasMatchingValue(const Session& session, const char* attributeId, const char* value) const;
+        bool hasMatchingValue(const Session& session, const char* attributeId, const set<string>& values) const;
+        bool hasMatchingValue(const Session& session, const char* attributeId, const regexp::regex& expression) const;
+
+    private:
+        const char* getFirstValue(const Session& session, const char* attributeId) const;
+
+        Category& m_log;
+        ptree m_pt;
+        bool m_urlEncoding,m_exportDuplicates;
+        // headers are tracked as Raw name, CGI name
+        map<string,pair<string,string>> m_mappings;
+        set<string> m_caseSensitiveIds;
+    };
+
+};
+
+const char AttributeConfiguration::LEGACY_CLASSREF_ATTRIBUTE_PROP_PATH[] = "legacyClassRefAttribute";
+const char AttributeConfiguration::LEGACY_CLASSREF_ATTRIBUTE_PROP_DEFAULT[] = "Shib-AuthnContext-Class";
+
+const char AttributeConfiguration::LEGACY_AUTHTIME_ATTRIBUTE_PROP_PATH[] = "legacyAuthnTimeAttribute";
+const char AttributeConfiguration::LEGACY_AUTHTIME_ATTRIBUTE_PROP_DEFAULT[] = "Shib-Authentication-Instant";
+
+
+AttributeConfiguration::AttributeConfiguration() {}
+
+AttributeConfiguration::~AttributeConfiguration() {}
+
+DefaultAttributeConfiguration::DefaultAttributeConfiguration(const char* pathname)
+    : m_log(Category::getInstance(SHIBSP_LOGCAT ".AttributeConfiguration")), m_urlEncoding(false)
+{
+    ini_parser::read_ini(pathname, m_pt);
+    load(m_pt);
+
+    split_to_container(m_caseSensitiveIds, getString("caseSensitiveAttributes", ""));
+
+    m_urlEncoding = !strcmp(getString("encoding", ""), "URL");
+    m_exportDuplicates = getBool("exportDuplicateValues", true);
+    
+    boost::optional<ptree&> mappings = m_pt.get_child_optional("mappings");
+    if (!mappings) {
+        return;
+    }
+
+    for (auto& child : *mappings) {
+        if (!child.first.empty()) {
+            string alias(child.second.get_value(""));
+            if (alias.empty()) {
+                continue;
+            }
+
+            string transformed("HTTP_");
+            const char* pch = alias.c_str();
+            while (*pch) {
+                transformed += (isalnum(*pch) ? toupper(*pch) : '_');
+                pch++;
+            }
+
+            m_mappings[child.first] = make_pair(alias, transformed);
+        }
+    }
+
+    m_mappings["Shib-Application-ID"] = pair<string,string>("Shib-Application-ID", "HTTP_SHIB_APPLICATION_ID");
+    m_mappings["Shib-Session-ID"] = pair<string,string>("Shib-Session-ID", "HTTP_SHIB_SESSION_ID");
+    m_mappings["Shib-Session-Expires"] = pair<string,string>("Shib-Session-Expires", "HTTP_SHIB_SESSION_EXPIRES");
+    m_mappings["Shib-Session-Inactivity"] = pair<string,string>("Shib-Session-Inactivity", "HTTP_SHIB_SESSION_INACTIVITY");
+    m_mappings["Shib-Cookie-Name"] = pair<string,string>("Shib-Cookie-Name", "HTTP_SHIB_COOKIE_NAME");
+    m_mappings["REMOTE_USER"] = pair<string,string>("REMOTE_USER", "HTTP_REMOTE_USER");
+}
+
+unique_ptr<AttributeConfiguration> AttributeConfiguration::newAttributeConfiguration(const char* pathname)
+{
+    return unique_ptr<AttributeConfiguration>(new DefaultAttributeConfiguration(pathname));
+}
+
+bool DefaultAttributeConfiguration::processAttributes(DDF& attributes) const
+{
+    if (!attributes.islist()) {
+        m_log.warn("invalid data supplied for session attributes");
+        return false;
+    }
+
+    DDF attr = attributes.first();
+    while (!attr.isnull()) {
+        if (!attr.name() || !isalnum(*(attr.name()))) {
+            m_log.warn("invalid unnamed attribute in session data");
+            attr.destroy();
+        }
+        else if (!attr.islist()) {
+            m_log.warn("invalid attribute in session data, '%s' was not a list", attr.name());
+            attr.destroy();
+        }
+        else {
+            DDF value = attr.first();
+            while (!value.isnull()) {
+                if (value.isstring()) {
+                    if (!value.string() || !(*(value.string()))) {
+                        m_log.warn("removing null or empty string value from attribute '%s'", attr.name());
+                        value.destroy();
+                    }
+                }
+                else if (value.isstruct()) {
+                    const char* lhs = value.getmember("value").string();
+                    const char* scope = value.getmember("scope").string();
+                    if (lhs && scope && *lhs && *scope) {
+                        string s = string(lhs) + getString("scopeDelimiter", "@") + scope;
+                        value.string(s.c_str());
+                    } else {
+                        value.destroy();
+                        m_log.warn("attribute '%s' scoped value had a null or empty value or scope", attr.name());
+                    }
+                }
+                else if (value.isint()) {
+                    try {
+                        string s(boost::lexical_cast<string>(attr.integer()));
+                        value.string(s.c_str());
+                    } catch (const boost::bad_lexical_cast&) {
+                        value.destroy();
+                        m_log.warn("attribute '%s' value could not be converted from int to string", attr.name());
+                    }
+                }
+                else if (value.islong()) {
+                    try {
+                        string s(boost::lexical_cast<string>(attr.longinteger()));
+                        value.string(s.c_str());
+                    } catch (const boost::bad_lexical_cast&) {
+                        value.destroy();
+                        m_log.warn("attribute '%s' value could not be converted from int to string", attr.name());
+                    }
+                }
+                else {
+                    value.destroy();
+                    m_log.warn("attribute '%s' value was not a supported type", attr.name());
+                }
+            }
+                
+            value = attr.next();
+        }
+
+        attr = attributes.next();
+    }
+
+    if (attributes.first().isnull()) {
+        m_log.warn("no valid attributes remain in session after processing");
+        return false;
+    }
+    return true;
+}
+
+bool DefaultAttributeConfiguration::isCaseSensitive(const char* attributeID) const
+{
+    if (!attributeID) {
+        return false;
+    }
+    return m_caseSensitiveIds.count(attributeID) > 0;
+}
+
+void DefaultAttributeConfiguration::clearHeaders(SPRequest& request) const
+{
+    for (const auto& names : m_mappings) {
+        request.clearHeader(names.second.first.c_str(), names.second.second.c_str());
+    }
+}
+
+void DefaultAttributeConfiguration::exportAttributes(SPRequest& request, const Session& session) const
+{
+    RequestMapper::Settings settings = request.getRequestSettings();
+    const URLEncoder& encoder = AgentConfig::getConfig().getURLEncoder();
+
+    const char* delim = getString("attributeValueDelimiter", ";");
+    size_t delim_len = strlen(delim);
+
+    // Default export strategy will include duplicates.
+    if (m_exportDuplicates) {
+        for (const auto& a : session.getAttributes()) {
+
+            const auto& headerMapping = m_mappings.find(a.first);
+            if (headerMapping == m_mappings.end()) {
+                // TODO: Check for use of headers, then log this...no mapping for attribute...
+                // If headers not used, we'd export it as is.
+                continue;
+            }
+
+            string header(request.getSecureHeader(headerMapping->second.first.c_str()));
+
+            DDF vals = a.second; // cheap copy drops const qualifier
+            DDF v = vals.first();
+            while (!v.isnull()) {
+                if (!header.empty()) {
+                    header += delim;
+                }
+
+                if (m_urlEncoding) {
+                    // If URL-encoding, any semicolons will get escaped anyway.
+                    header += encoder.encode(v.string());
+                }
+                else {
+                    string serialized(v.string());
+                    string::size_type pos = serialized.find(delim, string::size_type(0));
+                    if (pos != string::npos) {
+                        for (; pos != string::npos; pos = serialized.find(delim, pos)) {
+                            serialized.insert(pos, "\\");
+                            pos += delim_len + 1;
+                        }
+                    }
+                    header += serialized;
+                }
+
+                v = vals.next();
+            }
+            request.setHeader(headerMapping->second.first.c_str(), header.c_str());
+        }
+    }
+    else {
+        // Capture values in a map of sets to check for duplicates on the fly.
+        map<string,set<string>> valueMap;
+        for (const auto& a : session.getAttributes()) {
+
+            const auto& headerMapping = m_mappings.find(a.first);
+            if (headerMapping == m_mappings.end()) {
+                // TODO: Check for use of headers, then log this...no mapping for attribute...
+                // If headers not used, we'd export it as is.
+                continue;
+            }
+
+            DDF vals = a.second; // cheap copy drops const qualifier
+            DDF v = vals.first();
+            set<string>& targetSet = valueMap[headerMapping->first];
+            while (vals.isnull()) {
+                targetSet.insert(v.string());
+                v = vals.next();
+            }
+        }
+
+        // Export the mapped sets to the headers.
+        for (const auto& deduped : valueMap) {
+            string header;
+            for (const string& v : deduped.second) {
+                if (!header.empty())
+                    header += delim;
+                if (m_urlEncoding) {
+                    // If URL-encoding, any semicolons will get escaped anyway.
+                    header += encoder.encode(v.c_str());
+                }
+                else {
+                    string::size_type pos = v.find(delim, string::size_type(0));
+                    if (pos != string::npos) {
+                        string value(v);
+                        for (; pos != string::npos; pos = value.find(delim, pos)) {
+                            value.insert(pos, "\\");
+                            pos += delim_len + 1;
+                        }
+                        header += value;
+                    }
+                    else {
+                        header += v;
+                    }
+                }
+            }
+            request.setHeader(deduped.first.c_str(), header.c_str());
+        }
+    }
+
+    // Check for REMOTE_USER.
+    vector<string> rmids;
+    split_to_container(rmids, settings.first->getString("REMOTE_USER", ""));
+    for (const string& rmid : rmids) {
+        const char* firstVal = getFirstValue(session, rmid.c_str());
+        if (firstVal && *firstVal) {
+            if (m_urlEncoding)
+                request.setRemoteUser(encoder.encode(firstVal).c_str());
+            else
+                request.setRemoteUser(firstVal);
+            break;
+        }
+    }
+}
+
+bool DefaultAttributeConfiguration::hasMatchingValue(
+    const Session& session, const char* attributeId, const set<string>& values
+    ) const
+{
+    const auto& attr = session.getAttributes().find(attributeId);
+    if (attr == session.getAttributes().end()) {
+        return false;
+    }
+
+    bool caseSensitive = m_caseSensitiveIds.count(attributeId) > 0;
+
+    DDF val = const_cast<DDF&>(attr->second).first();
+    while (!val.isnull()) {
+        if (caseSensitive) {
+            // Can just search the set.
+            if (values.find(val.string()) != values.end()) {
+                return true;
+            }
+        } else {
+            // Have to loop over each set member for a case-insensitive comparison.
+            for (const string& candidate : values) {
+                if (!strcasecmp(val.string(), candidate.c_str())) {
+                    return true;
+                }
+            }
+        }
+
+        val = const_cast<DDF&>(attr->second).next();
+    }
+
+    return false;
+}
+
+bool DefaultAttributeConfiguration::hasMatchingValue(const Session& session, const char* attributeId, const char* value) const
+{
+    const auto& attr = session.getAttributes().find(attributeId);
+    if (attr == session.getAttributes().end()) {
+        return false;
+    }
+
+    bool caseSensitive = m_caseSensitiveIds.count(attributeId) > 0;
+
+    DDF val = const_cast<DDF&>(attr->second).first();
+    while (!val.isnull()) {
+        int result = caseSensitive ? strcmp(val.string(), value) : strcasecmp(val.string(), value);
+        if (result == 0) {
+            return true;
+        }
+        val = const_cast<DDF&>(attr->second).next();
+    }
+
+    return false;
+}
+
+bool DefaultAttributeConfiguration::hasMatchingValue(
+    const Session& session, const char* attributeId, const regexp::regex& expression
+    ) const
+{
+    static regexp::regex_constants::match_flag_type match_flags =
+        regexp::regex_constants::match_any | regexp::regex_constants::match_not_null;
+
+    const auto& attr = session.getAttributes().find(attributeId);
+    if (attr == session.getAttributes().end()) {
+        return false;
+    }
+
+    DDF val = const_cast<DDF&>(attr->second).first();
+    while (!val.isnull()) {
+        if (regexp::regex_match(val.string(), expression, match_flags)) {
+            return true;
+        }
+        val = const_cast<DDF&>(attr->second).next();
+    }
+
+    return false;
+}
+
+const char* DefaultAttributeConfiguration::getFirstValue(const Session& session, const char* attributeId) const
+{
+    const auto& attr = session.getAttributes().find(attributeId);
+    if (attr == session.getAttributes().end()) {
+        return nullptr;
+    }
+
+    return const_cast<DDF&>(attr->second).first().string();
+}
diff --git a/shibsp/handler/HandlerConfiguration.h b/shibsp/handler/HandlerConfiguration.h
index f6bc32a4..906c1c4d 100644
--- a/shibsp/handler/HandlerConfiguration.h
+++ b/shibsp/handler/HandlerConfiguration.h
@@ -31,7 +31,7 @@ namespace shibsp {
     class SHIBSP_API DDF;
 
     /**
-     * Pluggable runtime functionality that implement protocols and services
+     * Interface to the set of handlers that are exposed by an agent at specific path(s).
      */
     class SHIBSP_API HandlerConfiguration
     {
diff --git a/shibsp/handler/impl/AdminLogoutInitiator.cpp b/shibsp/handler/impl/AdminLogoutInitiator.cpp
index 06916ac4..abe588d5 100644
--- a/shibsp/handler/impl/AdminLogoutInitiator.cpp
+++ b/shibsp/handler/impl/AdminLogoutInitiator.cpp
@@ -95,12 +95,12 @@ pair<bool,long> AdminLogoutInitiator::run(SPRequest& request, bool isHandler) co
     // With no session, we return a 404 after "revoking" the session just to be safe.
     if (!session) {
         AgentConfig::getConfig().getAgent().getSessionCache()->remove(
-            request.getRequestSettings().first->getString("sessionBucket", "default"), sessionId);
+            request.getRequestSettings().first->getString("applicationId", "default"), sessionId);
         istringstream msg("NOT FOUND");
         return make_pair(true, request.sendResponse(msg, HTTPResponse::SHIBSP_HTTP_STATUS_NOTFOUND));
     }
 
-    time_t revocationExp = session->getExpiration();
+    time_t revocationExp = session->getCreation() + request.getRequestSettings().first->getUnsignedInt("lifetime", 28800);
 
     unique_lock<Session> sessionLocker(*session, adopt_lock);
 
@@ -112,7 +112,7 @@ pair<bool,long> AdminLogoutInitiator::run(SPRequest& request, bool isHandler) co
         sessionLocker.unlock();
         session = nullptr;
         AgentConfig::getConfig().getAgent().getSessionCache()->remove(
-            request.getRequestSettings().first->getString("sessionBucket", "default"), sessionId, revocationExp);
+            request.getRequestSettings().first->getString("applicationId", "default"), sessionId, revocationExp);
         
         istringstream msg("PARTIAL");
         return make_pair(true, request.sendResponse(msg, 206)); // misuse of an HTTP code, but whatever
@@ -122,7 +122,7 @@ pair<bool,long> AdminLogoutInitiator::run(SPRequest& request, bool isHandler) co
         sessionLocker.unlock();
         session = nullptr;
         AgentConfig::getConfig().getAgent().getSessionCache()->remove(
-            request.getRequestSettings().first->getString("sessionBucket", "default"), sessionId, revocationExp);
+            request.getRequestSettings().first->getString("applicationId", "default"), sessionId, revocationExp);
 
         istringstream msg("OK");
         return make_pair(true, request.sendResponse(msg, HTTPResponse::SHIBSP_HTTP_STATUS_OK));
diff --git a/shibsp/handler/impl/AttributeCheckerHandler.cpp b/shibsp/handler/impl/AttributeCheckerHandler.cpp
index b62b2cd0..097762af 100644
--- a/shibsp/handler/impl/AttributeCheckerHandler.cpp
+++ b/shibsp/handler/impl/AttributeCheckerHandler.cpp
@@ -131,13 +131,13 @@ pair<bool,long> AttributeCheckerHandler::run(SPRequest& request, bool isHandler)
     bool checked = false;
     if (session) {
         if (!m_attributes.empty()) {
-            const auto& indexed = session->getIndexedAttributes();
+            const auto& indexed = session->getAttributes();
             // Lambda returns true if the candidate attribute ID is NOT in the session.
             auto absent = [&indexed](const string& id) {
                 return indexed.find(id) == indexed.end();
             };
 
-            // Look for an attribute in the list that is not in the session multimap.
+            // Look for an attribute in the list that is not in the session map.
             // If that fails, the check succeeds.
             checked = find_if(m_attributes.begin(), m_attributes.end(), absent) == m_attributes.end();
         }
@@ -153,7 +153,7 @@ pair<bool,long> AttributeCheckerHandler::run(SPRequest& request, bool isHandler)
     }
 
     if (m_flushSession && session) {
-        time_t revocationExp = session->getExpiration();
+        time_t revocationExp = session->getCreation() + request.getRequestSettings().first->getUnsignedInt("lifetime", 28800);
         sessionLocker.unlock(); // unlock the session
         flushSession(request, revocationExp);
     }
diff --git a/shibsp/handler/impl/LocalLogoutInitiator.cpp b/shibsp/handler/impl/LocalLogoutInitiator.cpp
index 8dfbd257..c73d1395 100644
--- a/shibsp/handler/impl/LocalLogoutInitiator.cpp
+++ b/shibsp/handler/impl/LocalLogoutInitiator.cpp
@@ -88,7 +88,7 @@ pair<bool,long> LocalLogoutInitiator::run(SPRequest& request, bool isHandler) co
         bool result;
         vector<string> sessions(1, session->getID());
         result = notifyBackChannel(request, sessions, true);
-        time_t revocationExp = session->getExpiration();
+        time_t revocationExp = session->getCreation() + request.getRequestSettings().first->getUnsignedInt("lifetime", 28800);
         locker.unlock();    // unlock the session
         request.getAgent().getSessionCache()->remove(request, revocationExp);
         if (!result) {
diff --git a/shibsp/handler/impl/SessionHandler.cpp b/shibsp/handler/impl/SessionHandler.cpp
index 57feedb2..30819c6e 100644
--- a/shibsp/handler/impl/SessionHandler.cpp
+++ b/shibsp/handler/impl/SessionHandler.cpp
@@ -20,8 +20,9 @@
 
 #include "internal.h"
 #include "exceptions.h"
+#include "Agent.h"
 #include "SPRequest.h"
-#include "attribute/Attribute.h"
+#include "attribute/AttributeConfiguration.h"
 #include "handler/SecuredHandler.h"
 #include "logging/Category.h"
 #include "session/SessionCache.h"
@@ -151,10 +152,7 @@ pair<bool,long> SessionHandler::doJSON(SPRequest& request) const
 
     s << "{ ";
     s << "\"expiration\": ";
-    if (session->getExpiration())
-        s << ((session->getExpiration() - time(nullptr)) / 60);
-    else
-        s << 0;
+    s << ((session->getCreation() + request.getRequestSettings().first->getUnsignedInt("lifetime", 28800) - time(nullptr)) / 60);
 
     /*
         attributes: [ { "name": "foo", "values" : count } ]
@@ -164,12 +162,12 @@ pair<bool,long> SessionHandler::doJSON(SPRequest& request) const
         ]
     */
 
-    const multimap<string,const Attribute*>& attributes = session->getIndexedAttributes();
+    const map<string,DDF>& attributes = session->getAttributes();
     if (!attributes.empty()) {
         s << ", \"attributes\": [ ";
         string key;
-        vector<string>::size_type count=0;
-        for (multimap<string,const Attribute*>::const_iterator a = attributes.begin(); a != attributes.end(); ++a) {
+        int count=0;
+        for (map<string,DDF>::const_iterator a = attributes.begin(); a != attributes.end(); ++a) {
             if (a->first != key) {
                 // We're starting a new attribute.
                 if (a != attributes.begin()) {
@@ -187,19 +185,23 @@ pair<bool,long> SessionHandler::doJSON(SPRequest& request) const
             }
 
             if (m_values) {
-                const vector<string>& vals = a->second->getSerializedValues();
-                for (vector<string>::const_iterator v = vals.begin(); v!=vals.end(); ++v) {
-                    if (v != vals.begin() || a->first == key) {
+                bool first = true;
+                DDF val = const_cast<DDF&>(a->second).first();
+                while (!val.isnull()) {
+                    if (!first || a->first == key) {
                         s << ", ";
                     }
                     else {
                         s << ", \"values\": [ ";
                     }
-                    json_safe(s, v->c_str());
+                    json_safe(s, val.string());
+
+                    val = const_cast<DDF&>(a->second).next();
+                    first = false;
                 }
             }
             else {
-                count += a->second->getSerializedValues().size();
+                count += a->second.integer();
             }
             key = a->first;
         }
@@ -217,6 +219,12 @@ pair<bool,long> SessionHandler::doJSON(SPRequest& request) const
 
 pair<bool,long> SessionHandler::doHTML(SPRequest& request) const
 {
+    // Default delimiter is semicolon but is configurable.
+    const char* delim = request.getAgent().getAttributeConfiguration(
+        request.getRequestSettings().first->getString("attributeConfigID")
+        ).getString("attributeValueDelimiter", ";");
+    size_t delim_len = strlen(delim);
+
     stringstream s;
     s << "<html><head><title>Session Summary</title></head><body><pre>" << endl;
 
@@ -237,17 +245,13 @@ pair<bool,long> SessionHandler::doHTML(SPRequest& request) const
     s << "<u>Miscellaneous</u>" << endl;
 
     s << "<strong>Session Expiration (barring inactivity):</strong> ";
-    if (session->getExpiration())
-        s << ((session->getExpiration() - time(nullptr)) / 60) << " minute(s)" << endl;
-    else
-        s << "Infinite" << endl;
-
+    s << ((session->getCreation() + request.getRequestSettings().first->getUnsignedInt("lifetime", 28800) - time(nullptr)) / 60) << " minute(s)" << endl;
     s << endl << "<u>Attributes</u>" << endl;
 
     string key;
-    vector<string>::size_type count=0;
-    const multimap<string,const Attribute*>& attributes = session->getIndexedAttributes();
-    for (multimap<string,const Attribute*>::const_iterator a = attributes.begin(); a != attributes.end(); ++a) {
+    int count=0;
+    const map<string,DDF>& attributes = session->getAttributes();
+    for (map<string,DDF>::const_iterator a = attributes.begin(); a != attributes.end(); ++a) {
         if (a->first != key) {
             if (a != attributes.begin()) {
                 if (m_values)
@@ -261,30 +265,31 @@ pair<bool,long> SessionHandler::doHTML(SPRequest& request) const
         }
 
         if (m_values) {
-            // Default delimiter is semicolon but is now configurable.
-            const char* delim = request.getRequestSettings().first->getString("attributeValueDelimiter", ";");
-            size_t delim_len = strlen(delim);
-
-            const vector<string>& vals = a->second->getSerializedValues();
-            for (vector<string>::const_iterator v = vals.begin(); v!=vals.end(); ++v) {
-                if (v != vals.begin() || a->first == key)
+            bool first = true;
+            DDF val = const_cast<DDF&>(a->second).first();
+            while (!val.isnull()) {
+                if (!first || a->first == key) {
                     s << delim;
-                string::size_type pos = v->find(delim, string::size_type(0));
+                }
+                string serialized(val.string());
+                string::size_type pos = serialized.find(delim, string::size_type(0));
                 if (pos != string::npos) {
-                    string value(*v);
-                    for (; pos != string::npos; pos = value.find(delim, pos)) {
-                        value.insert(pos, "\\");
+                    for (; pos != string::npos; pos = serialized.find(delim, pos)) {
+                        serialized.insert(pos, "\\");
                         pos += delim_len + 1;
                     }
-                    s << value;
+                    s << serialized;
                 }
                 else {
-                    s << *v;
+                    s << val.string();
                 }
+
+                val = const_cast<DDF&>(a->second).next();
+                first = false;
             }
         }
         else {
-            count += a->second->getSerializedValues().size();
+            count += a->second.integer();
         }
         key = a->first;
     }
diff --git a/shibsp/impl/DefaultAgent.cpp b/shibsp/impl/DefaultAgent.cpp
index ac559c49..c717a029 100644
--- a/shibsp/impl/DefaultAgent.cpp
+++ b/shibsp/impl/DefaultAgent.cpp
@@ -25,6 +25,7 @@
 #include "Agent.h"
 #include "AgentConfig.h"
 #include "RequestMapper.h"
+#include "attribute/AttributeConfiguration.h"
 #include "handler/HandlerConfiguration.h"
 #include "io/HTTPResponse.h"
 #include "logging/Category.h"
@@ -77,7 +78,7 @@ namespace {
             return m_requestMapper.get();
         }
 
-        HandlerConfiguration& getHandlerConfiguration(const char* id=nullptr) const {
+        const HandlerConfiguration& getHandlerConfiguration(const char* id=nullptr) const {
             if (!id) {
                 id = "default";
             }
@@ -88,11 +89,23 @@ namespace {
             throw ConfigurationException(string("No HandlerConfiguration with ID of ") + id);
         }
 
+        const AttributeConfiguration& getAttributeConfiguration(const char* id=nullptr) const {
+            if (!id) {
+                id = "default";
+            }
+            const auto& config = m_attributeConfigurations.find(id);
+            if (config != m_attributeConfigurations.end()) {
+                return *(config->second);
+            }
+            throw ConfigurationException(string("No AttributeConfiguration with ID of ") + id);
+        }
+
     private:
         void doRemotingService();
         void doSessionCache();
         void doRequestMapper();
         void doHandlerConfigurations();
+        void doAttributeConfigurations();
 
         ptree& m_pt;
         Category& m_log;
@@ -104,6 +117,7 @@ namespace {
         unique_ptr<SessionCache> m_sessionCache;
         unique_ptr<RequestMapper> m_requestMapper;
         map<string,unique_ptr<HandlerConfiguration>> m_handlerConfigurations;
+        map<string,unique_ptr<AttributeConfiguration>> m_attributeConfigurations;
     };
 
 #if defined (_MSC_VER)
@@ -218,3 +232,30 @@ void DefaultAgent::doHandlerConfigurations()
         m_log.info("installed 'default' HandlerConfiguration from %s", path.c_str());
     }
 }
+
+void DefaultAgent::doAttributeConfigurations()
+{
+    // Check for testing boolean to disable attribute config.
+    if (getBool("skipAttributes", false)) {
+        return;
+    }
+
+    boost::optional<ptree&> child = m_pt.get_child_optional("attributes");
+    if (child) {
+        for (const auto& keys : *child) {
+            boost::optional<string> path = keys.second.get_value_optional<string>();
+            if (!path) {
+                m_log.warn("skipping property key with no value in [attributes] section");
+                continue;
+            }
+            AgentConfig::getConfig().getPathResolver().resolve(*path, PathResolver::SHIBSP_CFG_FILE);
+            m_attributeConfigurations[keys.first] = AttributeConfiguration::newAttributeConfiguration(path->c_str());
+            m_log.info("installed '%s' AttributeConfiguration from %s", keys.first.c_str(), path->c_str());
+        }
+    } else {
+        string path("attributes.ini");
+        AgentConfig::getConfig().getPathResolver().resolve(path, PathResolver::SHIBSP_CFG_FILE);
+        m_attributeConfigurations["default"] = AttributeConfiguration::newAttributeConfiguration(path.c_str());
+        m_log.info("installed 'default' AttributeConfiguration from %s", path.c_str());
+    }
+}
diff --git a/shibsp/impl/XMLAccessControl.cpp b/shibsp/impl/XMLAccessControl.cpp
index bdf78628..5f7c7cd3 100644
--- a/shibsp/impl/XMLAccessControl.cpp
+++ b/shibsp/impl/XMLAccessControl.cpp
@@ -24,8 +24,9 @@
 #include "AccessControl.h"
 #include "Agent.h"
 #include "SPRequest.h"
-#include "attribute/Attribute.h"
+#include "attribute/AttributeConfiguration.h"
 #include "logging/Category.h"
+#include "remoting/ddf.h"
 #include "session/SessionCache.h"
 #include "util/Lockable.h"
 #include "util/Misc.h"
@@ -193,31 +194,22 @@ AccessControl::aclresult_t Rule::authorized(const SPRequest& request, const Sess
         actual_alias = request.getAgent().getString("legacy-classref-attribute", "Shib-AuthnContext-Class");
     }
 
-    // Find the attribute(s) matching the require rule.
-    pair<multimap<string,const Attribute*>::const_iterator, multimap<string,const Attribute*>::const_iterator> attrs =
-        session->getIndexedAttributes().equal_range(actual_alias);
-    if (attrs.first == attrs.second) {
-        request.log(Priority::SHIB_WARN, string("AccessControl rule requires attribute (") + actual_alias + "), not found in session");
+    // Empty case is historical and not terribly smart, but we'll brute force it.
+    if (m_vals.empty()) {
+        if (session->getAttributes().find(actual_alias.c_str()) != session->getAttributes().end()) {
+            request.log(Priority::SHIB_DEBUG, string("AccessControl rule requires presence of attribute (") + actual_alias + "), authz granted");
+            return shib_acl_true;
+        }
         return shib_acl_false;
     }
-    else if (m_vals.empty()) {
-        request.log(Priority::SHIB_DEBUG, string("AccessControl rule requires presence of attribute (") + actual_alias + "), authz granted");
-        return shib_acl_true;
-    }
 
-    for (; attrs.first != attrs.second; ++attrs.first) {
-        bool caseSensitive = attrs.first->second->isCaseSensitive();
-
-        // Now we have to intersect the attribute's values against the rule's list.
-        const vector<string>& vals = attrs.first->second->getSerializedValues();
-        for (set<string>::const_iterator i = m_vals.begin(); i != m_vals.end(); ++i) {
-            for (vector<string>::const_iterator j = vals.begin(); j != vals.end(); ++j) {
-                if ((caseSensitive && *i == *j) || (!caseSensitive && !strcasecmp(i->c_str(),j->c_str()))) {
-                    request.log(Priority::SHIB_DEBUG, string("AccessControl rule expecting (") + *j + "), authz granted");
-                    return shib_acl_true;
-                }
-            }
-        }
+    // Otherwise call into the helper logic to handle matching process..
+    const AttributeConfiguration& attributeConfig = request.getAgent().getAttributeConfiguration(
+        request.getRequestSettings().first->getString("attributeConfigID")
+        );
+    if (attributeConfig.hasMatchingValue(*session, actual_alias.c_str(), m_vals)) {
+        request.log(Priority::SHIB_DEBUG, string("AccessControl rule satisfied for attribute (") + actual_alias + "), authz granted");
+        return shib_acl_true;
     }
 
     return shib_acl_false;
@@ -275,21 +267,14 @@ AccessControl::aclresult_t RuleRegex::authorized(const SPRequest& request, const
         actual_alias = request.getAgent().getString("legacy-classref-attribute", "Shib-AuthnContext-Class");
     }
 
-    // Find the attribute(s) matching the require rule.
-    auto attrs = session->getIndexedAttributes().equal_range(actual_alias);
-    if (attrs.first == attrs.second) {
-        request.log(Priority::SHIB_WARN, string("AccessControl rule requires attribute (") + actual_alias + "), not found in session");
-        return shib_acl_false;
-    }
-
-    for (; attrs.first != attrs.second; ++attrs.first) {
-        // Now we have to intersect the attribute's values against the regular expression.
-        for (const string& v : attrs.first->second->getSerializedValues()) {
-            if (regexp::regex_match(v, m_re, match_flags)) {
-                request.log(Priority::SHIB_DEBUG, string("AccessControl rule expecting regex (") + m_exp + "), authz granted");
-                return shib_acl_true;
-            }
-        }
+    // Call into the helper logic to handle matching process..
+    const AttributeConfiguration& attributeConfig = request.getAgent().getAttributeConfiguration(
+        request.getRequestSettings().first->getString("attributeConfigID")
+        );
+    if (attributeConfig.hasMatchingValue(*session, actual_alias.c_str(), m_re)) {
+        request.log(Priority::SHIB_DEBUG,
+            string("AccessControl rule for attribute (") + actual_alias + ") expecting regex (" + m_exp  + ", authz granted");
+        return shib_acl_true;
     }
 
     return shib_acl_false;
diff --git a/shibsp/session/AbstractSessionCache.h b/shibsp/session/AbstractSessionCache.h
index 3e2d0f5f..e5968c10 100644
--- a/shibsp/session/AbstractSessionCache.h
+++ b/shibsp/session/AbstractSessionCache.h
@@ -18,18 +18,69 @@
  * Base class for SessionCache implementations.
  */
 
+#ifndef __shibsp_abssessioncache_h__
+#define __shibsp_abssessioncache_h__
+
+#include <logging/Category.h>
+#include <remoting/ddf.h>
 #include <session/SessionCache.h>
 
+#include <mutex>
+
 namespace shibsp {
 
-    class SHIBSP_API AbstractSessionCache : public virtual SessionCache {
-    protected:
-        /** Constructor. */
-        AbstractSessionCache();
-        virtual ~AbstractSessionCache();
+    class SHIBSP_API AbstractSessionCache;
+    class SHIBSP_API Attribute;
 
+    class SHIBSP_API BasicSession : public virtual Session
+    {
     public:
-        bool start();
+        BasicSession(AbstractSessionCache& cache, DDF& obj);
+        virtual ~BasicSession();
+
+        void lock();
+        bool try_lock();
+        void unlock();
+
+        const char* getID() const;
+        const char* getApplicationID() const;
+        const char* getClientAddress(const char* family) const;
+        void setClientAddress(const char* client_addr);
+        const std::map<std::string,DDF>& getAttributes() const;
+        time_t getCreation() const;
+        time_t getLastAccess() const;
+
+        void validate(const char* applicationId, const char* client_addr, time_t* timeout);
+
+        // Allows the cache to bind sessions to multiple client address
+        // families based on whatever this function returns.
+        static const char* getAddressFamily(const char* addr);
+
+    private:
+        DDF m_obj;
+        std::map<std::string,DDF> m_attributes;
+
+        AbstractSessionCache& m_cache;
+        time_t m_creation,m_lastAccess;
+        // TODO: possibly convert to a shared lock where possible?
+        // I used exclusive because it avoided lock "upgrades"
+        // when mutating or deleting sessions.
+        std::mutex m_lock;
     };
 
+    class SHIBSP_API AbstractSessionCache : public virtual SessionCache {
+        protected:
+            /** Constructor. */
+            AbstractSessionCache();
+            virtual ~AbstractSessionCache();
+    
+            Category& m_log;
+    
+        public:
+            bool start();
+
+        friend class BasicSession;
+        };    
 };
+
+#endif /** __shibsp_abssessioncache_h__ */
\ No newline at end of file
diff --git a/shibsp/session/SessionCache.h b/shibsp/session/SessionCache.h
index 769402f8..b6f01d83 100644
--- a/shibsp/session/SessionCache.h
+++ b/shibsp/session/SessionCache.h
@@ -31,11 +31,14 @@
 
 namespace shibsp {
 
-    class SHIBSP_API Attribute;
+    class SHIBSP_API DDF;
     class SHIBSP_API SPRequest;
 
     /**
-     * Encapsulates access to a user's security session.
+     * Encapsulates access to a session.
+     * 
+     * <p>Sessions are returned from APIs and generally will be returned in a
+     * locked state and should be unlocked by the caller when done using them.</p>
      */
     class SHIBSP_API Session : public virtual BasicLockable
     {
@@ -60,11 +63,11 @@ namespace shibsp {
         virtual const char* getApplicationID() const=0;
 
         /**
-         * Returns the session expiration.
+         * Returns the session creation time.
          *
-         * @return  the session's expiration time or 0 for none
+         * @return  the session's creation time
          */
-        virtual time_t getExpiration() const=0;
+        virtual time_t getCreation() const=0;
 
         /**
          * Returns the last access time of the session.
@@ -73,19 +76,14 @@ namespace shibsp {
          */
         virtual time_t getLastAccess() const=0;
 
-        /**
-         * Returns the resolved attributes associated with the session.
-         *
-         * @return an immutable array of attributes
-         */
-        virtual const std::vector<std::unique_ptr<Attribute>>& getAttributes() const=0;
-
         /**
          * Returns the resolved attributes associated with the session, indexed by ID.
+         * 
+         * <p>Each "attribute" is a list containing the values (of various types).</p>
          *
-         * @return an immutable map of attributes keyed by attribute ID
+         * @return an immutable map of attribute data keyed by attribute ID
          */
-        virtual const std::multimap<std::string,const Attribute*>& getIndexedAttributes() const=0;
+        virtual const std::map<std::string,DDF>& getAttributes() const=0;
     };
 
     /**
diff --git a/shibsp/session/impl/AbstractSessionCache.cpp b/shibsp/session/impl/AbstractSessionCache.cpp
index ee9ca01d..502ecc5b 100644
--- a/shibsp/session/impl/AbstractSessionCache.cpp
+++ b/shibsp/session/impl/AbstractSessionCache.cpp
@@ -21,6 +21,7 @@
 #include "internal.h"
 #include "exceptions.h"
 #include "AgentConfig.h"
+#include "attribute/Attribute.h"
 #include "session/AbstractSessionCache.h"
 #include "logging/Category.h"
 
@@ -57,7 +58,7 @@ SessionCache::~SessionCache()
 {
 }
 
-AbstractSessionCache::AbstractSessionCache()
+AbstractSessionCache::AbstractSessionCache() : m_log(Category::getInstance(SHIBSP_LOGCAT ".SessionCache"))
 {
 }
 
@@ -68,3 +69,295 @@ AbstractSessionCache::~AbstractSessionCache()
 bool AbstractSessionCache::start()
 {
 }
+
+BasicSession::BasicSession(AbstractSessionCache& cache, DDF& obj)
+    : m_obj(obj), m_cache(cache), m_creation(0), m_lastAccess(time(nullptr))
+{
+    m_creation = m_obj["creation"].longinteger();
+
+    // We have to index and vector-up the values of the attributes.
+    // (Among other reasons, the DDF iteration API isn't threadsafe.)
+    DDF attrs = m_obj["attributes"];
+    DDF attr = attrs.first();
+    while (!attr.isnull()) {
+        m_attributes[attr.name()] = attr;
+        attr = attrs.next();
+    }
+}
+
+BasicSession::~BasicSession()
+{
+    m_obj.destroy();
+}
+
+const char* BasicSession::getID() const
+{
+    return m_obj.name();
+}
+
+const char* BasicSession::getApplicationID() const
+{
+    return m_obj["applicaton_id"].string();
+}
+
+const char* BasicSession::getClientAddress(const char* family) const
+{
+    return m_obj["client_addr"][family].string();
+}
+
+void BasicSession::setClientAddress(const char* client_addr)
+{
+    DDF obj = m_obj["client_addr"];
+    if (!obj.isstruct()) {
+        obj = m_obj.addmember("client_addr").structure();
+    }
+    obj.addmember(getAddressFamily(client_addr)).string(client_addr);
+}
+
+const std::map<std::string,DDF>& BasicSession::getAttributes() const
+{
+    return m_attributes;
+}
+
+void BasicSession::validate(const char* applicationId, const char* client_addr, time_t* timeout)
+{
+}
+
+time_t BasicSession::getCreation() const
+{
+    return m_creation;
+}
+
+time_t BasicSession::getLastAccess() const
+{
+    return m_lastAccess;
+}
+
+void BasicSession::lock()
+{
+    m_lock.lock();
+}
+
+bool BasicSession::try_lock()
+{
+    return m_lock.try_lock();
+}
+
+void BasicSession::unlock()
+{
+    m_lock.unlock();
+}
+
+// Allows the cache to bind sessions to multiple client address
+// families based on whatever this function returns.
+const char* BasicSession::getAddressFamily(const char* addr)
+{
+    if (strchr(addr, ':'))
+        return "6";
+    else
+        return "4";
+}
+
+/*
+
+void BasicSession::validate(const char* applicationId, const char* client_addr, time_t* timeout)
+{
+    time_t now = time(nullptr);
+
+    // Basic expiration?
+    if (m_expires > 0) {
+        if (now > m_expires) {
+            m_cache.m_log.info("session expired (ID: %s)", getID());
+            throw SessionException("Your session has expired, and you must re-authenticate.");
+        }
+    }
+
+    // Address check?
+    if (client_addr) {
+        const char* saddr = getClientAddress(getAddressFamily(client_addr));
+        if (saddr && *saddr) {
+            if (!m_cache.compareAddresses(client_addr, saddr)) {
+                m_cache.m_log.warn("client address mismatch, client (%s), session (%s)", client_addr, saddr);
+                throw SessionException(
+                    string("Your IP address (") + client_addr + ") does not match the address recorded at the time the session was established."
+                    );
+            }
+            client_addr = nullptr;  // clear out parameter as signal that session need not be updated below
+        }
+        else {
+            m_cache.m_log.info("session (%s) not yet bound to client address type, binding it to (%s)", getID(), client_addr);
+        }
+    }
+
+    if (!timeout && !client_addr)
+        return;
+
+    if (true) {
+        DDF in("touch::" STORAGESERVICE_SESSION_CACHE "::SessionCache"), out;
+        DDFJanitor jin(in);
+        in.structure();
+        in.addmember("key").string(getID());
+        in.addmember("version").integer(m_obj["version"].integer());
+        in.addmember("bucket_id").string(bucketID);
+        if (client_addr)    // signals we need to bind an additional address to the session
+            in.addmember("client_addr").string(client_addr);
+        if (timeout && *timeout) {
+            // On 64-bit Windows, time_t doesn't fit in a long, so I'm using ISO timestamps.
+#ifndef HAVE_GMTIME_R
+            struct tm* ptime = gmtime(timeout);
+#else
+            struct tm res;
+            struct tm* ptime = gmtime_r(timeout,&res);
+#endif
+            char timebuf[32];
+            strftime(timebuf,32,"%Y-%m-%dT%H:%M:%SZ",ptime);
+            in.addmember("timeout").string(timebuf);
+        }
+
+        //out = app.getServiceProvider().getListenerService()->send(in);
+        if (out.isstruct()) {
+            // We got an updated record back.
+            m_cache.m_log.debug("session updated, reconstituting it");
+            m_attributes.clear();
+            m_attributeIndex.clear();
+            m_obj.destroy();
+            m_obj = out;
+        }
+        else {
+            out.destroy();
+        }
+    }
+    else {
+#ifndef SHIBSP_LITE
+        if (!m_cache.m_storage)
+            throw ConfigurationException("Session touch requires a StorageService.");
+
+        // Versioned read, since we already have the data in hand if it's current.
+        string record;
+        time_t lastAccess = 0;
+        int curver = m_obj["version"].integer();
+        int ver = m_cache.m_storage->readText(getID(), "session", &record, &lastAccess, curver);
+        if (ver == 0) {
+            m_cache.m_log.info("session (ID: %s) no longer in storage", getID());
+            throw RetryableProfileException("Your session is not available in the session store, and you must re-authenticate.");
+        }
+
+        if (timeout) {
+            if (lastAccess == 0) {
+                m_cache.m_log.error("session (ID: %s) did not report time of last access", getID());
+                throw RetryableProfileException("Your session's last access time was missing, and you must re-authenticate.");
+            }
+            // Adjust for expiration to recover last access time and check timeout.
+            unsigned long cacheTimeout = m_cache.getCacheTimeout(app);
+            lastAccess -= cacheTimeout;
+            if (*timeout > 0 && now - lastAccess >= *timeout) {
+                m_cache.m_log.info("session timed out (ID: %s)", getID());
+                throw RetryableProfileException("Your session has timed out due to inactivity, and you must re-authenticate.");
+            }
+
+            // Update storage expiration, if possible.
+            try {
+                m_cache.m_storage->updateContext(getID(), now + cacheTimeout);
+            }
+            catch (std::exception& ex) {
+                m_cache.m_log.error("failed to update session expiration: %s", ex.what());
+            }
+        }
+
+        if (ver > curver) {
+            // We got an updated record back.
+            DDF newobj;
+            istringstream in(record);
+            in >> newobj;
+            m_ids.clear();
+            for_each(m_attributes.begin(), m_attributes.end(), xmltooling::cleanup<Attribute>());
+            m_attributes.clear();
+            m_attributeIndex.clear();
+            m_obj.destroy();
+            m_obj = newobj;
+        }
+
+        // We may need to write back a new address into the session using a versioned update loop.
+        if (client_addr) {
+            short attempts = 0;
+            do {
+                const char* saddr = getClientAddress(getAddressFamily(client_addr));
+                if (saddr) {
+                    // Something snuck in and bound the session to this address type, so it better match what we have.
+                    if (!m_cache.compareAddresses(client_addr, saddr)) {
+                        m_cache.m_log.warn("client address mismatch, client (%s), session (%s)", client_addr, saddr);
+                        throw RetryableProfileException(
+                            "Your IP address ($1) does not match the address recorded at the time the session was established.",
+                            params(1, client_addr)
+                            );
+                    }
+                    break;  // No need to update.
+                }
+                else {
+                    // Bind it into the session.
+                    setClientAddress(client_addr);
+                }
+
+                // Tentatively increment the version.
+                m_obj["version"].integer(m_obj["version"].integer() + 1);
+
+                ostringstream str;
+                str << m_obj;
+                record = str.str();
+
+                try {
+                    ver = m_cache.m_storage->updateText(getID(), "session", record.c_str(), 0, m_obj["version"].integer() - 1);
+                }
+                catch (std::exception&) {
+                    m_obj["version"].integer(m_obj["version"].integer() - 1);
+                    throw;
+                }
+
+                if (ver <= 0) {
+                    m_obj["version"].integer(m_obj["version"].integer() - 1);
+                }
+
+                if (!ver) {
+                    // Fatal problem with update.
+                    m_cache.m_log.error("updateText failed on StorageService for session (%s)", getID());
+                    throw IOException("Unable to update stored session.");
+                }
+                else if (ver < 0) {
+                    // Out of sync.
+                    if (++attempts > 10) {
+                        m_cache.m_log.error("failed to bind client address, update attempts exceeded limit");
+                        throw IOException("Unable to update stored session, exceeded retry limit.");
+                    }
+                    m_cache.m_log.warn("storage service indicates the record is out of sync, updating with a fresh copy...");
+                    ver = m_cache.m_storage->readText(getID(), "session", &record);
+                    if (!ver) {
+                        m_cache.m_log.error("readText failed on StorageService for session (%s)", getID());
+                        throw IOException("Unable to read back stored session.");
+                    }
+
+                    // Reset object.
+                    DDF newobj;
+                    istringstream in(record);
+                    in >> newobj;
+
+                    m_ids.clear();
+                    for_each(m_attributes.begin(), m_attributes.end(), xmltooling::cleanup<Attribute>());
+                    m_attributes.clear();
+                    m_attributeIndex.clear();
+                    newobj["version"].integer(ver);
+                    m_obj.destroy();
+                    m_obj = newobj;
+
+                    ver = -1;
+                }
+            } while (ver < 0); // negative indicates a sync issue so we retry
+        }
+#else
+        throw ConfigurationException("Session touch requires a StorageService.");
+#endif
+    }
+
+    m_lastAccess = now;
+}
+
+*/
\ No newline at end of file
diff --git a/tests/data/console-shibboleth.ini b/tests/data/console-shibboleth.ini
index 1fba6104..6bcd5083 100644
--- a/tests/data/console-shibboleth.ini
+++ b/tests/data/console-shibboleth.ini
@@ -1,5 +1,6 @@
 [global]
 skipHandlers = true
+skipAttributes = true
 # Use "partial" for partial matching
 regexMatching = full
 
diff --git a/tests/data/fatal-exts-shibboleth.ini b/tests/data/fatal-exts-shibboleth.ini
index a4d883c5..70a81156 100644
--- a/tests/data/fatal-exts-shibboleth.ini
+++ b/tests/data/fatal-exts-shibboleth.ini
@@ -1,5 +1,6 @@
 [global]
 skipHandlers = true
+skipAttributes = true
 
 [extensions]
 /path/to/extension.so = true
diff --git a/tests/data/impl/console-shibboleth.ini b/tests/data/impl/console-shibboleth.ini
index b841324a..8e205364 100644
--- a/tests/data/impl/console-shibboleth.ini
+++ b/tests/data/impl/console-shibboleth.ini
@@ -1,5 +1,6 @@
 [global]
 skipHandlers = true
+skipAttributes = true
 
 [logging]
 type = console
diff --git a/tests/data/nonfatal-exts-shibboleth.ini b/tests/data/nonfatal-exts-shibboleth.ini
index 7912aab0..e9ff5223 100644
--- a/tests/data/nonfatal-exts-shibboleth.ini
+++ b/tests/data/nonfatal-exts-shibboleth.ini
@@ -1,5 +1,6 @@
 [global]
 skipHandlers = true
+skipAttributes = true
 
 [extensions]
 /path/to/extension.so = false
diff --git a/tests/data/platform/iis/console-shibboleth.ini b/tests/data/platform/iis/console-shibboleth.ini
index 738c4845..bde240f9 100644
--- a/tests/data/platform/iis/console-shibboleth.ini
+++ b/tests/data/platform/iis/console-shibboleth.ini
@@ -1,5 +1,6 @@
 [global]
 skipHandlers = true
+skipAttributes = true
 
 [logging]
 type = console
diff --git a/tests/data/remoting/impl/shibboleth.ini b/tests/data/remoting/impl/shibboleth.ini
index b1baaf1e..8bd9c67c 100644
--- a/tests/data/remoting/impl/shibboleth.ini
+++ b/tests/data/remoting/impl/shibboleth.ini
@@ -1,5 +1,6 @@
 [global]
 skipHandlers = true
+skipAttributes = true
 
 [remoting]
 baseURL = https://localhost/idp/profile/sp
diff --git a/tests/data/syslog-shibboleth.ini b/tests/data/syslog-shibboleth.ini
index 7e164311..c9e78ee2 100644
--- a/tests/data/syslog-shibboleth.ini
+++ b/tests/data/syslog-shibboleth.ini
@@ -1,5 +1,6 @@
 [global]
 skipHandlers = true
+skipAttributes = true
 
 [logging]
 type = syslog
diff --git a/tests/data/util/reloadablefile/console-shibboleth.ini b/tests/data/util/reloadablefile/console-shibboleth.ini
index 6699f3b1..04d127d3 100644
--- a/tests/data/util/reloadablefile/console-shibboleth.ini
+++ b/tests/data/util/reloadablefile/console-shibboleth.ini
@@ -1,5 +1,6 @@
 [global]
 skipHandlers = true
+skipAttributes = true
 
 [logging]
 type = console

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.


More information about the commits mailing list