Refactor session key storage

Refactor session key storage to actually have an interface that isn't
map, and provide reasonable lookup functions, as well as update the
consumers of those functions.
This also implements session timeouts.

Change-Id: Ica46716805782cfbb7c4ee5569bc7e468c260bc3
diff --git a/CMakeLists.txt b/CMakeLists.txt
index a0dd0d7..b31e085 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -24,7 +24,7 @@
 
 set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
 
-SET(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS} -Werror -Wall")
+SET(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS} -Wall")
 
 SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-rtti")
 SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fno-rtti")
diff --git a/include/persistent_data_middleware.hpp b/include/persistent_data_middleware.hpp
index 2ea65a7..a8d3b3f 100644
--- a/include/persistent_data_middleware.hpp
+++ b/include/persistent_data_middleware.hpp
@@ -21,6 +21,7 @@
   std::string session_token;
   std::string username;
   std::string csrf_token;
+  std::chrono::time_point<std::chrono::steady_clock> last_updated;
 };
 
 void to_json(nlohmann::json& j, const UserSession& p) {
@@ -36,70 +37,22 @@
     p.session_token = j.at("session_token").get<std::string>();
     p.username = j.at("username").get<std::string>();
     p.csrf_token = j.at("csrf_token").get<std::string>();
+    // For now, sessions that were persisted through a reboot get their timer
+    // reset.  This could probably be overcome with a better understanding of
+    // wall clock time and steady timer time, possibly persisting values with
+    // wall clock time instead of steady timer, but the tradeoffs of all the
+    // corner cases involved are non-trivial, so this is done temporarily
+    p.last_updated = std::chrono::steady_clock::now();
   } catch (std::out_of_range) {
     // do nothing.  Session API incompatibility, leave sessions empty
   }
 }
 
-class Middleware {
-  using SessionStore = boost::container::flat_map<std::string, UserSession>;
-  // todo(ed) should read this from a fixed location somewhere, not CWD
-  static constexpr const char* filename = "bmcweb_persistent_data.json";
-  int json_revision = 1;
+class Middleware;
 
+class SessionStore {
  public:
-  struct context {
-    SessionStore* auth_tokens;
-  };
-
-  Middleware() { read_data(); }
-
-  void before_handle(crow::request& req, response& res, context& ctx) {
-    ctx.auth_tokens = &auth_tokens;
-  }
-
-  void after_handle(request& req, response& res, context& ctx) {}
-
-  // TODO(ed) this should really use protobuf, or some other serialization
-  // library, but adding another dependency is somewhat outside the scope of
-  // this application for the moment
-  void read_data() {
-    std::ifstream persistent_file(filename);
-    int file_revision = 0;
-    if (persistent_file.is_open()) {
-      // call with exceptions disabled
-      auto data = nlohmann::json::parse(persistent_file, nullptr, false);
-      if (!data.is_discarded()) {
-        file_revision = data["revision"].get<int>();
-        auth_tokens = data["sessions"].get<SessionStore>();
-        system_uuid = data["system_uuid"].get<std::string>();
-      }
-    }
-    bool need_write = false;
-
-    if (system_uuid.empty()) {
-      system_uuid = boost::uuids::to_string(boost::uuids::random_generator()());
-      need_write = true;
-    }
-    if (file_revision < json_revision) {
-      need_write = true;
-    }
-
-    if (need_write) {
-      write_data();
-    }
-  }
-
-  void write_data() {
-    std::ofstream persistent_file(filename);
-    nlohmann::json data;
-    data["sessions"] = auth_tokens;
-    data["system_uuid"] = system_uuid;
-    data["revision"] = json_revision;
-    persistent_file << data;
-  }
-
-  UserSession generate_user_session(const std::string& username) {
+  const UserSession& generate_user_session(const std::string& username) {
     static constexpr std::array<char, 62> alphanum = {
         '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C',
         'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
@@ -128,15 +81,148 @@
     for (int i = 0; i < unique_id.size(); ++i) {
       unique_id[i] = alphanum[dist(rd)];
     }
-    UserSession session{unique_id, session_token, username, csrf_token};
-    auth_tokens.emplace(session_token, session);
-    write_data();
-    return session;
+    const auto session_it = auth_tokens.emplace(
+        session_token,
+        std::move(UserSession{unique_id, session_token, username, csrf_token,
+                              std::chrono::steady_clock::now()}));
+    const UserSession& user = (session_it).first->second;
+    need_write_ = true;
+    return user;
   }
 
-  SessionStore auth_tokens;
-  std::string system_uuid;
+  const UserSession* login_session_by_token(const std::string& token) {
+    apply_session_timeouts();
+    auto session_it = auth_tokens.find(token);
+    if (session_it == auth_tokens.end()) {
+      return nullptr;
+    }
+    UserSession& foo = session_it->second;
+    foo.last_updated = std::chrono::steady_clock::now();
+    return &foo;
+  }
+
+  const UserSession* get_session_by_uid(const std::string& uid) {
+    apply_session_timeouts();
+    // TODO(Ed) this is inefficient
+    auto session_it = auth_tokens.begin();
+    while (session_it != auth_tokens.end()) {
+      if (session_it->second.unique_id == uid) {
+        return &session_it->second;
+      }
+      session_it++;
+    }
+    return nullptr;
+  }
+
+  void remove_session(const UserSession* session) {
+    auth_tokens.erase(session->session_token);
+    need_write_ = true;
+  }
+
+  std::vector<const std::string*> get_unique_ids() {
+    std::vector<const std::string*> ret;
+    ret.reserve(auth_tokens.size());
+    for (auto& session : auth_tokens) {
+      ret.push_back(&session.second.unique_id);
+    }
+    return ret;
+  }
+
+  bool needs_write() { return need_write_; }
+
+  // Persistent data middleware needs to be able to serialize our auth_tokens
+  // structure, which is private
+  friend Middleware;
+
+ private:
+  void apply_session_timeouts() {
+    std::chrono::minutes timeout(60);
+    auto time_now = std::chrono::steady_clock::now();
+    if (time_now - last_timeout_update > std::chrono::minutes(1)) {
+      last_timeout_update = time_now;
+      auto auth_tokens_it = auth_tokens.begin();
+      while (auth_tokens_it != auth_tokens.end()) {
+        if (time_now - auth_tokens_it->second.last_updated >= timeout) {
+          auth_tokens_it = auth_tokens.erase(auth_tokens_it);
+          need_write_ = true;
+        } else {
+          auth_tokens_it++;
+        }
+      }
+    }
+  }
+  std::chrono::time_point<std::chrono::steady_clock> last_timeout_update;
+  boost::container::flat_map<std::string, UserSession> auth_tokens;
   std::random_device rd;
+  bool need_write_{false};
+};
+
+class Middleware {
+  // todo(ed) should read this from a fixed location somewhere, not CWD
+  static constexpr const char* filename = "bmcweb_persistent_data.json";
+  int json_revision = 1;
+
+ public:
+  struct context {
+    SessionStore* sessions;
+  };
+
+  Middleware() { read_data(); }
+
+  ~Middleware() {
+    if (sessions.needs_write()) {
+      write_data();
+    }
+  }
+
+  void before_handle(crow::request& req, response& res, context& ctx) {
+    ctx.sessions = &sessions;
+  }
+
+  void after_handle(request& req, response& res, context& ctx) {}
+
+  // TODO(ed) this should really use protobuf, or some other serialization
+  // library, but adding another dependency is somewhat outside the scope of
+  // this application for the moment
+  void read_data() {
+    std::ifstream persistent_file(filename);
+    int file_revision = 0;
+    if (persistent_file.is_open()) {
+      // call with exceptions disabled
+      auto data = nlohmann::json::parse(persistent_file, nullptr, false);
+      if (!data.is_discarded()) {
+        file_revision = data.value("revision", 0);
+        sessions.auth_tokens =
+            data.value("sessions", decltype(sessions.auth_tokens)());
+        system_uuid = data.value("system_uuid", "");
+      }
+    }
+    bool need_write = false;
+
+    if (system_uuid.empty()) {
+      system_uuid = boost::uuids::to_string(boost::uuids::random_generator()());
+      need_write = true;
+    }
+    if (file_revision < json_revision) {
+      need_write = true;
+    }
+    // write revision changes or system uuid changes immediately
+    if (need_write) {
+      write_data();
+    }
+  }
+
+  void write_data() {
+    std::ofstream persistent_file(filename);
+    nlohmann::json data;
+    data["sessions"] = sessions.auth_tokens;
+    data["system_uuid"] = system_uuid;
+    data["revision"] = json_revision;
+    persistent_file << data;
+  }
+
+  SessionStore sessions;
+  std::string system_uuid;
 };
 
 }  // namespaec PersistentData
diff --git a/include/redfish_v1.hpp b/include/redfish_v1.hpp
index 009ae61..4de009e 100644
--- a/include/redfish_v1.hpp
+++ b/include/redfish_v1.hpp
@@ -185,11 +185,12 @@
                     {"Members@odata.count", users.size()}};
                 nlohmann::json member_array = nlohmann::json::array();
                 int user_index = 0;
-                for (auto& user : users) {
+                for (int user_index = 0; user_index < users.size();
+                     user_index++) {
                   member_array.push_back(
                       {{"@odata.id",
                         "/redfish/v1/AccountService/Accounts/" +
-                            std::to_string(++user_index)}});
+                            std::to_string(user_index)}});
                 }
                 res.json_value["Members"] = member_array;
               }
@@ -244,8 +245,8 @@
   CROW_ROUTE(app, "/redfish/v1/SessionService/Sessions/")
       .methods("POST"_method, "GET"_method)([&](const crow::request& req,
                                                 crow::response& res) {
-        auto& data_middleware =
-            app.template get_middleware<PersistentData::Middleware>();
+        auto& session_store =
+            app.template get_middleware<PersistentData::Middleware>().sessions;
         if (req.method == "POST"_method) {
           // call with exceptions disabled
           auto login_credentials =
@@ -278,7 +279,7 @@
             res.end();
             return;
           }
-          auto session = data_middleware.generate_user_session(username);
+          auto session = session_store.generate_user_session(username);
           res.code = 200;
           res.add_header("X-Auth-Token", session.session_token);
           res.json_value = {
@@ -291,6 +292,8 @@
               {"Description", "Manager User Session"},
               {"UserName", username}};
         } else {  // assume get
+          std::vector<const std::string*> session_ids =
+              session_store.get_unique_ids();
           res.json_value = {
               {"@odata.context",
                "/redfish/v1/$metadata#SessionCollection.SessionCollection"},
@@ -298,14 +301,14 @@
               {"@odata.type", "#SessionCollection.SessionCollection"},
               {"Name", "Session Collection"},
               {"Description", "Session Collection"},
-              {"Members@odata.count", data_middleware.auth_tokens.size()}
+              {"Members@odata.count", session_ids.size()}
 
           };
           nlohmann::json member_array = nlohmann::json::array();
-          for (auto& session : data_middleware.auth_tokens) {
-            member_array.push_back({{"@odata.id",
-                                     "/redfish/v1/SessionService/Sessions/" +
-                                         session.second.unique_id}});
+          for (auto session_uid : session_ids) {
+            member_array.push_back(
+                {{"@odata.id",
+                  "/redfish/v1/SessionService/Sessions/" + *session_uid}});
           }
           res.json_value["Members"] = member_array;
         }
@@ -313,37 +316,32 @@
       });
 
   CROW_ROUTE(app, "/redfish/v1/SessionService/Sessions/<str>/")
-      .methods("GET"_method, "DELETE"_method)([&](const crow::request& req,
-                                                  crow::response& res,
-                                                  const std::string& session) {
-        auto& data_middleware =
-            app.template get_middleware<PersistentData::Middleware>();
+      .methods("GET"_method, "DELETE"_method)([&](
+          const crow::request& req, crow::response& res,
+          const std::string& session_id) {
+        auto& session_store =
+            app.template get_middleware<PersistentData::Middleware>().sessions;
         // TODO(Ed) this is inefficient
-        auto session_it = data_middleware.auth_tokens.begin();
-        while (session_it != data_middleware.auth_tokens.end()) {
-          if (session_it->second.unique_id == session) {
-            break;
-          }
-          session_it++;
-        }
+        auto session = session_store.get_session_by_uid(session_id);
 
-        if (session_it == data_middleware.auth_tokens.end()) {
+        if (session == nullptr) {
           res.code = 404;
           res.end();
           return;
         }
         if (req.method == "DELETE"_method) {
-          data_middleware.auth_tokens.erase(session_it);
+          session_store.remove_session(session);
           res.code = 200;
         } else {  // assume get
           res.json_value = {
               {"@odata.context", "/redfish/v1/$metadata#Session.Session"},
-              {"@odata.id", "/redfish/v1/SessionService/Sessions/" + session},
+              {"@odata.id",
+               "/redfish/v1/SessionService/Sessions/" + session->unique_id},
               {"@odata.type", "#Session.v1_0_3.Session"},
-              {"Id", session_it->second.unique_id},
+              {"Id", session->unique_id},
               {"Name", "User Session"},
               {"Description", "Manager User Session"},
-              {"UserName", session_it->second.username}};
+              {"UserName", session->username}};
         }
         res.end();
       });
@@ -447,7 +445,6 @@
               int port = std::stoi(port_str.c_str());
               auto type_it = service_types.find(port);
               if (type_it != service_types.end()) {
-                type_it->second;
                 res.json_value[type_it->second] = {{"ProtocolEnabled", true},
                                                    {"Port", port}};
               }
diff --git a/include/token_authorization_middleware.hpp b/include/token_authorization_middleware.hpp
index 9642282..67f1161 100644
--- a/include/token_authorization_middleware.hpp
+++ b/include/token_authorization_middleware.hpp
@@ -79,8 +79,9 @@
         return;
       }
       auto& data_mw = allctx.template get<PersistentData::Middleware>();
-      auto session_it = data_mw.auth_tokens->find(auth_key);
-      if (session_it == data_mw.auth_tokens->end()) {
+      const PersistentData::UserSession* session =
+          data_mw.sessions->login_session_by_token(auth_key);
+      if (session == nullptr) {
         return_unauthorized();
         return;
       }
@@ -90,12 +91,12 @@
         if (req.method != "GET"_method) {
           const std::string& csrf = req.get_header_value("X-XSRF-TOKEN");
           // Make sure both tokens are filled
-          if (csrf.empty() || session_it->second.csrf_token.empty()) {
+          if (csrf.empty() || session->csrf_token.empty()) {
             return_unauthorized();
             return;
           }
           // Reject if csrf token not available
-          if (csrf != session_it->second.csrf_token) {
+          if (csrf != session->csrf_token) {
             return_unauthorized();
             return;
           }
@@ -103,7 +104,7 @@
       }
 
       if (req.url == "/logout" && req.method == "POST"_method) {
-        data_mw.auth_tokens->erase(auth_key);
+        data_mw.sessions->remove_session(session);
         res.code = 200;
         res.end();
         return;
@@ -195,9 +196,10 @@
           if (!pam_authenticate_user(username, password)) {
             res.code = 401;
           } else {
-            auto& auth_middleware =
-                app.template get_middleware<PersistentData::Middleware>();
-            auto session = auth_middleware.generate_user_session(username);
+            auto& context =
+                app.template get_context<PersistentData::Middleware>(req);
+            auto& session_store = context.sessions;
+            auto& session = session_store->generate_user_session(username);
 
             if (looks_like_ibm) {
               // IBM requires a very specific login structure, and doesn't