diff --git a/interfaces/auth.yaml b/interfaces/auth.yaml index 8a53531eb..d3c7beef1 100644 --- a/interfaces/auth.yaml +++ b/interfaces/auth.yaml @@ -19,6 +19,27 @@ cmds: description: The master pass group id type: string maxLength: 36 + withdraw_authorization: + description: >- + Withdraw granted authorization. + If only the evse_id is given, the granted authorization for this EVSE will be withdrawn. + If only the id_token is given, the granted authorization for every EVSE where this id_token is placed will be + withdrawn + If both parameters are given, the granted authorization for the given EVSE will be withdrawn, if the placed + id_token matches the given id_token + If no parameter is given, all granted authorizations for all EVSEs will be removed + arguments: + request: + description: The request + type: object + $ref: /authorization#/WithdrawAuthorizationRequest + result: + description: >- + Accepted in case requested authorization was removed + AuthorizationNotFound in case no match for request was found + Rejected in case module could not process the request for other reasons + type: string + $ref: /authorization#/WithdrawAuthorizationResponse vars: token_validation_status: description: Emits all events related to current token validation diff --git a/modules/Auth/Auth.cpp b/modules/Auth/Auth.cpp index c4f90d078..e3d79dcde 100644 --- a/modules/Auth/Auth.cpp +++ b/modules/Auth/Auth.cpp @@ -19,10 +19,8 @@ void Auth::init() { this->info.id, (!this->r_kvs.empty() ? this->r_kvs.at(0).get() : nullptr)); for (const auto& token_provider : this->r_token_provider) { - token_provider->subscribe_provided_token([this](ProvidedIdToken provided_token) { - std::thread t([this, provided_token]() { this->auth_handler->on_token(provided_token); }); - t.detach(); - }); + token_provider->subscribe_provided_token( + [this](ProvidedIdToken provided_token) { this->auth_handler->on_token(provided_token); }); } } @@ -136,4 +134,8 @@ void Auth::set_master_pass_group_id(const std::string& master_pass_group_id) { this->auth_handler->set_master_pass_group_id(master_pass_group_id); } +WithdrawAuthorizationResult Auth::handle_withdraw_authorization(const WithdrawAuthorizationRequest& request) { + return this->auth_handler->handle_withdraw_authorization(request); +} + } // namespace module diff --git a/modules/Auth/Auth.hpp b/modules/Auth/Auth.hpp index fca11b846..a9d1593df 100644 --- a/modules/Auth/Auth.hpp +++ b/modules/Auth/Auth.hpp @@ -83,6 +83,8 @@ class Auth : public Everest::ModuleBase { * @param master_pass_group_id master pass group id */ void set_master_pass_group_id(const std::string& master_pass_group_id); + + WithdrawAuthorizationResult handle_withdraw_authorization(const WithdrawAuthorizationRequest& request); // ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1 protected: diff --git a/modules/Auth/include/AuthHandler.hpp b/modules/Auth/include/AuthHandler.hpp index b6c66ff22..75a15f2b4 100644 --- a/modules/Auth/include/AuthHandler.hpp +++ b/modules/Auth/include/AuthHandler.hpp @@ -24,6 +24,28 @@ using namespace types::evse_manager; using namespace types::authorization; using namespace types::reservation; +namespace types { +namespace authorization { + +inline bool operator==(const IdToken& lhs, const IdToken& rhs) { + return lhs.value == rhs.value and lhs.type == rhs.type; +} + +inline bool operator==(const ProvidedIdToken& lhs, const ProvidedIdToken& rhs) { + return lhs.id_token == rhs.id_token; +} + +inline bool operator<(const IdToken& lhs, const IdToken& rhs) { + return lhs.value < rhs.value; +} + +inline bool operator<(const ProvidedIdToken& lhs, const ProvidedIdToken& rhs) { + return lhs.id_token < rhs.id_token; +} + +} // namespace authorization +} // namespace types + namespace module { enum class TokenHandlingResult { @@ -32,7 +54,8 @@ enum class TokenHandlingResult { REJECTED, USED_TO_STOP_TRANSACTION, TIMEOUT, - NO_CONNECTOR_AVAILABLE + NO_CONNECTOR_AVAILABLE, + WITHDRAWN }; namespace conversions { @@ -216,7 +239,20 @@ class AuthHandler { void register_publish_token_validation_status_callback( const std::function& callback); + WithdrawAuthorizationResult handle_withdraw_authorization(const WithdrawAuthorizationRequest& request); + private: + enum class SelectEvseReturnStatus { + EvseSelected, + Interrupted, + TimeOut + }; + + struct SelectEvseResult { + std::optional evse_id; + SelectEvseReturnStatus status; + }; + SelectionAlgorithm selection_algorithm; int connection_timeout; std::optional master_pass_group_id; @@ -227,9 +263,12 @@ class AuthHandler { std::map> evses; std::list plug_in_queue; - std::set tokens_in_process; + std::set tokens_in_process; std::condition_variable cv; + std::condition_variable processing_finished_cv; std::mutex event_mutex; + std::mutex withdraw_mutex; + std::unique_ptr withdraw_request; // callbacks std::function get_referenced_evses(const ProvidedIdToken& provided_token); int used_for_transaction(const std::vector& evse_ids, const std::string& id_token); - bool is_token_already_in_process(const std::string& id_token, const std::vector& referenced_evses); + bool is_token_already_in_process(const ProvidedIdToken& provided_id_token, + const std::vector& referenced_evses); bool any_evse_available(const std::vector& evse_ids); bool any_parent_id_present(const std::vector& evse_ids); bool equals_master_pass_group_id(const std::optional parent_id_token); @@ -260,9 +300,11 @@ class AuthHandler { * occurs that can be used to determine an evse. * * @param selected_evses - * @return int + * @param id_token The id token of the request. + * @return The status and optional evse id if an evse was selected. */ - int select_evse(const std::vector& selected_evses); + SelectEvseResult select_evse(const std::vector& selected_evses, const IdToken& id_token); + bool is_authorization_withdrawn(const std::vector& selected_evses, const IdToken& id_token); int get_latest_plugin(const std::vector& evse_ids); void notify_evse(int evse_id, const ProvidedIdToken& provided_token, const ValidationResult& validation_result); diff --git a/modules/Auth/lib/AuthHandler.cpp b/modules/Auth/lib/AuthHandler.cpp index bffd1f1e6..6f55ec299 100644 --- a/modules/Auth/lib/AuthHandler.cpp +++ b/modules/Auth/lib/AuthHandler.cpp @@ -34,6 +34,8 @@ std::string token_handling_result_to_string(const TokenHandlingResult& result) { return "TIMEOUT"; case TokenHandlingResult::USED_TO_STOP_TRANSACTION: return "USED_TO_STOP_TRANSACTION"; + case TokenHandlingResult::WITHDRAWN: + return "WITHDRAWN"; default: throw std::runtime_error("No known conversion for the given token handling result"); } @@ -79,9 +81,9 @@ TokenHandlingResult AuthHandler::on_token(const ProvidedIdToken& provided_token) EVLOG_info << "Received new token: " << everest::staging::helpers::redact(provided_token); const auto referenced_evses = this->get_referenced_evses(provided_token); - if (!this->is_token_already_in_process(provided_token.id_token.value, referenced_evses)) { + if (!this->is_token_already_in_process(provided_token, referenced_evses)) { // process token if not already in process - this->tokens_in_process.insert(provided_token.id_token.value); + this->tokens_in_process.insert(provided_token); this->publish_token_validation_status_callback(provided_token, TokenValidationStatus::Processing); result = this->handle_token(provided_token); } else { @@ -106,14 +108,18 @@ TokenHandlingResult AuthHandler::on_token(const ProvidedIdToken& provided_token) case TokenHandlingResult::USED_TO_STOP_TRANSACTION: this->publish_token_validation_status_callback(provided_token, TokenValidationStatus::Accepted); break; + case TokenHandlingResult::WITHDRAWN: + this->publish_token_validation_status_callback(provided_token, TokenValidationStatus::Withdrawn); + break; } if (result != TokenHandlingResult::ALREADY_IN_PROCESS) { - this->tokens_in_process.erase(provided_token.id_token.value); + this->tokens_in_process.erase(provided_token); } EVLOG_info << "Result for token: " << everest::staging::helpers::redact(provided_token.id_token.value) << ": " << conversions::token_handling_result_to_string(result); + this->processing_finished_cv.notify_all(); this->event_mutex.unlock(); return result; } @@ -272,52 +278,57 @@ TokenHandlingResult AuthHandler::handle_token(const ProvidedIdToken& provided_to - select the evse for the authorization request - process it against placed reservations - compare referenced_evses against the evses listed in the validation_result + - check if request has been withdrawn while selecting an evse */ this->event_mutex .unlock(); // unlock to allow other threads to continue processing in case select_evse is blocking - int evse_id = this->select_evse(referenced_evses); // might block - this->event_mutex.lock(); // lock again after evse is selected + const auto select_evse_result = + this->select_evse(referenced_evses, provided_token.id_token); // might block + this->event_mutex.lock(); // lock again after evse is selected + + if (not select_evse_result.evse_id.has_value()) { + if (select_evse_result.status == SelectEvseReturnStatus::TimeOut) { + return TokenHandlingResult::TIMEOUT; + } else if (select_evse_result.status == SelectEvseReturnStatus::Interrupted) { + return TokenHandlingResult::WITHDRAWN; + } + } + + int evse_id = select_evse_result.evse_id.value(); EVLOG_debug << "Selected evse#" << evse_id << " for token: " << everest::staging::helpers::redact(provided_token.id_token.value); - if (evse_id != -1) { // indicates timeout of evse selection - std::optional parent_id_token; - if (validation_result.parent_id_token.has_value()) { - parent_id_token = validation_result.parent_id_token.value().value; - } - const std::optional reservation_id = this->reservation_handler.matches_reserved_identifier( - provided_token.id_token.value, static_cast(evse_id), parent_id_token); - - if (validation_result.evse_ids.has_value() and - intersect(referenced_evses, validation_result.evse_ids.value()).empty()) { - EVLOG_debug << "Empty intersection between referenced evses and evses that are authorized"; - validation_result.authorization_status = AuthorizationStatus::NotAtThisLocation; - } else if (reservation_id == std::nullopt && - !this->reservation_handler.is_charging_possible(static_cast(evse_id))) { - validation_result.authorization_status = AuthorizationStatus::NotAtThisTime; - } else if (!this->reservation_handler.is_evse_reserved(static_cast(evse_id)) && - (reservation_id == std::nullopt)) { - EVLOG_info << "Providing authorization to evse#" << evse_id; + std::optional parent_id_token; + if (validation_result.parent_id_token.has_value()) { + parent_id_token = validation_result.parent_id_token.value().value; + } + const std::optional reservation_id = this->reservation_handler.matches_reserved_identifier( + provided_token.id_token.value, static_cast(evse_id), parent_id_token); + + if (validation_result.evse_ids.has_value() and + intersect(referenced_evses, validation_result.evse_ids.value()).empty()) { + EVLOG_debug << "Empty intersection between referenced evses and evses that are authorized"; + validation_result.authorization_status = AuthorizationStatus::NotAtThisLocation; + } else if (reservation_id == std::nullopt && + !this->reservation_handler.is_charging_possible(static_cast(evse_id))) { + validation_result.authorization_status = AuthorizationStatus::NotAtThisTime; + } else if (!this->reservation_handler.is_evse_reserved(static_cast(evse_id)) && + (reservation_id == std::nullopt)) { + EVLOG_info << "Providing authorization to evse#" << evse_id; + authorized = true; + } else { + EVLOG_debug << "Evse is reserved. Checking if token matches..."; + + if (reservation_id.has_value()) { + EVLOG_info << "Evse is reserved and token is valid for this reservation"; + this->reservation_handler.on_reservation_used(reservation_id.value()); authorized = true; + validation_result.reservation_id = reservation_id.value(); } else { - EVLOG_debug << "Evse is reserved. Checking if token matches..."; - - if (reservation_id.has_value()) { - EVLOG_info << "Evse is reserved and token is valid for this reservation"; - this->reservation_handler.on_reservation_used(reservation_id.value()); - authorized = true; - validation_result.reservation_id = reservation_id.value(); - } else { - EVLOG_info << "Evse is reserved but token is not valid for this reservation"; - validation_result.authorization_status = AuthorizationStatus::NotAtThisTime; - } + EVLOG_info << "Evse is reserved but token is not valid for this reservation"; + validation_result.authorization_status = AuthorizationStatus::NotAtThisTime; } - this->notify_evse(evse_id, provided_token, validation_result); - } else { - // in this case we dont need / cannot notify an evse, because no evse was selected - EVLOG_info << "Timeout while selecting evse for provided token: " - << everest::staging::helpers::redact(provided_token); - return TokenHandlingResult::TIMEOUT; } + this->notify_evse(evse_id, provided_token, validation_result); } i++; } @@ -386,16 +397,17 @@ int AuthHandler::used_for_transaction(const std::vector& evse_ids, const st return -1; } -bool AuthHandler::is_token_already_in_process(const std::string& id_token, const std::vector& referenced_evses) { +bool AuthHandler::is_token_already_in_process(const ProvidedIdToken& provided_id_token, + const std::vector& referenced_evses) { // checks if the token is currently already processed by the module (because already swiped) - if (this->tokens_in_process.find(id_token) != this->tokens_in_process.end()) { + if (this->tokens_in_process.find(provided_id_token) != this->tokens_in_process.end()) { return true; } else { // check if id_token was already used to authorize evse but no transaction has been started yet for (const auto evse_id : referenced_evses) { const auto& evse = this->evses.at(evse_id); - if (evse->identifier.has_value() && evse->identifier.value().id_token.value == id_token && + if (evse->identifier.has_value() && evse->identifier.value().id_token == provided_id_token.id_token && !evse->transaction_active) { return true; } @@ -449,10 +461,15 @@ int AuthHandler::get_latest_plugin(const std::vector& evse_ids) { return -1; } -int AuthHandler::select_evse(const std::vector& selected_evses) { +AuthHandler::SelectEvseResult AuthHandler::select_evse(const std::vector& selected_evses, + const IdToken& id_token) { std::unique_lock lk(this->event_mutex); + SelectEvseResult result; + if (selected_evses.size() == 1) { - return selected_evses.at(0); + result.status = SelectEvseReturnStatus::EvseSelected; + result.evse_id = selected_evses.at(0); + return result; } if (this->selection_algorithm == SelectionAlgorithm::PlugEvents) { @@ -463,35 +480,75 @@ int AuthHandler::select_evse(const std::vector& selected_evses) { EVLOG_debug << "No evse in authorization queue. Waiting for a plug in..."; // blocks until respective plugin for evse occured or until timeout if (!this->cv.wait_for(lk, std::chrono::seconds(this->connection_timeout), - [this, selected_evses] { return this->get_latest_plugin(selected_evses) != -1; })) { - return -1; + [this, selected_evses, id_token] { + return this->get_latest_plugin(selected_evses) != -1 || + this->is_authorization_withdrawn(selected_evses, id_token); + })) { + result.status = SelectEvseReturnStatus::TimeOut; + return result; } - EVLOG_debug << "Plug in at evse occured"; + EVLOG_debug << "Plug in at evse occured or authorization withdrawn"; + } + + if (this->is_authorization_withdrawn(selected_evses, id_token)) { + result.status = SelectEvseReturnStatus::Interrupted; + } else { + result.status = SelectEvseReturnStatus::EvseSelected; + result.evse_id = this->get_latest_plugin(selected_evses); } - return this->get_latest_plugin(selected_evses); + + return result; } else if (this->selection_algorithm == SelectionAlgorithm::FindFirst) { EVLOG_debug << "SelectionAlgorithm FindFirst: Selecting first available evse without an active transaction"; const auto selected_evse_id = this->get_latest_plugin(selected_evses); if (selected_evse_id != -1 and !this->evses.at(selected_evse_id)->transaction_active) { // an EV has been plugged in yet at the referenced evses - return this->get_latest_plugin(selected_evses); + result.status = SelectEvseReturnStatus::EvseSelected; + result.evse_id = this->get_latest_plugin(selected_evses); + return result; } else { // no EV has been plugged in yet at the referenced evses; choosing the first one where no // transaction is active for (const auto& evse_id : selected_evses) { const auto& evse = this->evses.at(evse_id); if (!evse->transaction_active) { - return evse_id; + result.status = SelectEvseReturnStatus::EvseSelected; + result.evse_id = evse_id; + return result; } } } - return -1; + result.status = SelectEvseReturnStatus::TimeOut; + return result; } else { throw std::runtime_error("SelectionAlgorithm not implemented: " + selection_algorithm_to_string(this->selection_algorithm)); } } +/// Checks if given \p withdraw_request matches given \p id_token and/or one evse of given \p selected_evses +bool does_withdraw_request_match(const WithdrawAuthorizationRequest& withdraw_request, + const std::vector& selected_evses, const IdToken& id_token) { + // Check if the withdraw request is specific to an EVSE + const bool has_evse_id = withdraw_request.evse_id.has_value(); + const bool is_evse_in_selected = + has_evse_id and (std::find(selected_evses.begin(), selected_evses.end(), withdraw_request.evse_id.value()) != + selected_evses.end()); + + // Check if the ID token matches or is absent + const bool id_token_matches = + not withdraw_request.id_token.has_value() or (withdraw_request.id_token.value() == id_token); + + return id_token_matches and (not has_evse_id or is_evse_in_selected); +} + +bool AuthHandler::is_authorization_withdrawn(const std::vector& selected_evses, const IdToken& id_token) { + if (withdraw_request == nullptr) { + return false; + } + return does_withdraw_request_match(*this->withdraw_request, selected_evses, id_token); +} + void AuthHandler::notify_evse(int evse_id, const ProvidedIdToken& provided_token, const ValidationResult& validation_result) { const auto evse_index = this->evses.at(evse_id)->evse_index; @@ -696,6 +753,9 @@ void AuthHandler::handle_session_event(const int evse_id, const SessionEvent& ev this->submit_event_for_connector(evse_id, connector_id, ConnectorEvent::ENABLE); check_reservations = true; break; + case SessionEventEnum::Deauthorized: + this->evses.at(evse_id)->identifier.reset(); + this->evses.at(evse_id)->timeout_timer.stop(); case SessionEventEnum::ReservationStart: break; case SessionEventEnum::ReservationEnd: { @@ -707,8 +767,6 @@ void AuthHandler::handle_session_event(const int evse_id, const SessionEvent& ev /// explicitly fall through all the SessionEventEnum values we are not handling case SessionEventEnum::Authorized: [[fallthrough]]; - case SessionEventEnum::Deauthorized: - [[fallthrough]]; case SessionEventEnum::AuthRequired: [[fallthrough]]; case SessionEventEnum::PrepareCharging: @@ -806,6 +864,102 @@ void AuthHandler::register_publish_token_validation_status_callback( this->publish_token_validation_status_callback = callback; } +WithdrawAuthorizationResult AuthHandler::handle_withdraw_authorization(const WithdrawAuthorizationRequest& request) { + std::lock_guard lg(this->withdraw_mutex); + EVLOG_info << "Witdrawing authorization" + << (request.evse_id.has_value() ? " evse: " + std::to_string(request.evse_id.value()) : "") + << (request.id_token.has_value() ? " id token: " + request.id_token.value().value : ""); + + if (request.evse_id.has_value() and this->evses.find(request.evse_id.value()) == this->evses.end()) { + return WithdrawAuthorizationResult::EvseNotFound; + } + + auto result = WithdrawAuthorizationResult::AuthorizationNotFound; // default + + // Wait for processing threads to finish executing + std::unique_lock lock(this->event_mutex); + + const auto is_withdraw_request_targeting_token_in_process = [this](const WithdrawAuthorizationRequest& request) { + for (const auto& token_in_process : this->tokens_in_process) { + auto referenced_evses = this->get_referenced_evses(token_in_process); + if (does_withdraw_request_match(request, referenced_evses, token_in_process.id_token)) { + return true; + } + } + return false; + }; + + this->withdraw_request = std::make_unique(request); + + if (is_withdraw_request_targeting_token_in_process(request)) { + // Notify processing threads that wait within select_evse + // This will interrupt the wait for a plug in in case the authorization is withdrawn by this request + this->cv.notify_all(); + + // Release the event_mutex lock and wait for threads to finish... + this->processing_finished_cv.wait( + lock, [&]() { return not is_withdraw_request_targeting_token_in_process(request); }); + + result = WithdrawAuthorizationResult::Accepted; + } + + // It might still be the case that the withdraw request is also targeting already granted authorization so we continue + // checking for this + + const auto withdraw_authorization_or_stop_transaction = [this](const EVSEContext& evse) { + if (evse.transaction_active) { + StopTransactionRequest req; + req.reason = StopTransactionReason::DeAuthorized; + this->stop_transaction_callback(evse.evse_index, req); + } else { + this->withdraw_authorization_callback(evse.evse_index); + } + }; + + if (request.evse_id.has_value() and request.id_token.has_value()) { + // evse_id and id_token is specified + // find if there is a granted authorization for id_token and evse_id + const auto evse_id = request.evse_id.value(); + const auto id_token = request.id_token.value(); + const auto& evse = this->evses.at(evse_id); + if (evse->identifier.has_value() && request.id_token.value() == evse->identifier.value().id_token) { + withdraw_authorization_or_stop_transaction(*evse); + result = WithdrawAuthorizationResult::Accepted; + } + + } else if (request.evse_id.has_value()) { + // only evse_id is specified + // find if there is a granted authorization for evse_id + const auto evse_id = request.evse_id.value(); + const auto& evse = this->evses.at(evse_id); + if (evse->identifier.has_value()) { + withdraw_authorization_or_stop_transaction(*evse); + result = WithdrawAuthorizationResult::Accepted; + } + } else if (request.id_token.has_value()) { + // only id_token is specified + // find if there is a granted authorization for id_token + for (const auto& evse : this->evses) { + if (evse.second->identifier.has_value() && + request.id_token.value() == evse.second->identifier.value().id_token) { + withdraw_authorization_or_stop_transaction(*evse.second); + result = WithdrawAuthorizationResult::Accepted; + } + } + } else { + // neither evse_id nor id_token is specified, withdraw all authorizations that have been granted + for (const auto& evse : this->evses) { + if (evse.second->identifier.has_value()) { + withdraw_authorization_or_stop_transaction(*evse.second); + result = WithdrawAuthorizationResult::Accepted; + } + } + } + + // result was either set in one of the if statements above or is still AuthorizationNotFound + return result; +} + void AuthHandler::submit_event_for_connector(const int32_t evse_id, const int32_t connector_id, const ConnectorEvent connector_event) { for (auto& connector : this->evses.at(evse_id)->connectors) { diff --git a/modules/Auth/main/authImpl.cpp b/modules/Auth/main/authImpl.cpp index 517d44a25..a7d14ad7f 100644 --- a/modules/Auth/main/authImpl.cpp +++ b/modules/Auth/main/authImpl.cpp @@ -23,5 +23,9 @@ void authImpl::handle_set_master_pass_group_id(std::string& master_pass_group_id this->mod->set_master_pass_group_id(master_pass_group_id); } +std::string authImpl::handle_withdraw_authorization(WithdrawAuthorizationRequest& request) { + return withdraw_authorization_result_to_string(this->mod->handle_withdraw_authorization(request)); +} + } // namespace main } // namespace module diff --git a/modules/Auth/main/authImpl.hpp b/modules/Auth/main/authImpl.hpp index da04d0d73..9f9e97324 100644 --- a/modules/Auth/main/authImpl.hpp +++ b/modules/Auth/main/authImpl.hpp @@ -36,6 +36,8 @@ class authImpl : public authImplBase { // command handler functions (virtual) virtual void handle_set_connection_timeout(int& connection_timeout) override; virtual void handle_set_master_pass_group_id(std::string& master_pass_group_id) override; + virtual std::string + handle_withdraw_authorization(types::authorization::WithdrawAuthorizationRequest& request) override; // ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1 // insert your protected definitions here diff --git a/modules/Auth/tests/auth_tests.cpp b/modules/Auth/tests/auth_tests.cpp index d4d7c0502..e151cee1f 100644 --- a/modules/Auth/tests/auth_tests.cpp +++ b/modules/Auth/tests/auth_tests.cpp @@ -18,18 +18,6 @@ using ::testing::MockFunction; using ::testing::StrictMock; class kvsIntf; - -namespace types { -namespace authorization { - -// Define operator== for types::authorization::IdToken -bool operator==(const types::authorization::IdToken& lhs, const types::authorization::IdToken& rhs) { - return lhs.value == rhs.value; -} - -} // namespace authorization -} // namespace types - namespace module { const static std::string VALID_TOKEN_1 = "VALID_RFID_1"; // SAME PARENT_ID @@ -82,6 +70,9 @@ class AuthTest : public ::testing::Test { testing::MockFunction send_callback_mock; StrictMock> mock_publish_token_validation_status_callback; + testing::MockFunction + mock_stop_transaction_callback; + testing::MockFunction mock_withdraw_authorization_callback_mock; void SetUp() override { std::vector evse_indices{0, 1}; @@ -105,12 +96,14 @@ class AuthTest : public ::testing::Test { this->auth_handler->register_withdraw_authorization_callback([this](int32_t evse_index) { EVLOG_debug << "DeAuthorize called with evse_index#" << evse_index; this->auth_receiver->deauthorize(evse_index); + this->mock_withdraw_authorization_callback_mock.Call(evse_index); }); this->auth_handler->register_stop_transaction_callback( [this](int32_t evse_index, const StopTransactionRequest& request) { EVLOG_debug << "Stop transaction called with evse_index#" << evse_index << " and reason " << stop_transaction_reason_to_string(request.reason); this->auth_receiver->deauthorize(evse_index); + this->mock_stop_transaction_callback.Call(evse_index, request); }); this->auth_handler->register_validate_token_callback([](const ProvidedIdToken& provided_token) { @@ -1567,4 +1560,206 @@ TEST_F(AuthTest, test_plug_in_time_out) { ASSERT_FALSE(this->auth_receiver->get_authorization(1)); } +/// \brief Test if a authorization can be withdrawn if an EV was connected and authorization was granted before +TEST_F(AuthTest, test_withdraw_authorization) { + const SessionEvent session_event = get_session_started_event(types::evse_manager::StartSessionReason::EVConnected); + this->auth_handler->handle_session_event(1, session_event); + + std::vector connectors{1}; + ProvidedIdToken provided_token = get_provided_token(VALID_TOKEN_1, connectors); + + EXPECT_CALL(mock_publish_token_validation_status_callback, + Call(Field(&ProvidedIdToken::id_token, provided_token.id_token), TokenValidationStatus::Processing)); + EXPECT_CALL(mock_publish_token_validation_status_callback, + Call(Field(&ProvidedIdToken::id_token, provided_token.id_token), TokenValidationStatus::Accepted)); + EXPECT_CALL(mock_withdraw_authorization_callback_mock, Call(0)).Times(1); + EXPECT_CALL(mock_stop_transaction_callback, Call(_, _)).Times(0); + + const auto result = this->auth_handler->on_token(provided_token); + ASSERT_TRUE(result == TokenHandlingResult::ACCEPTED); + ASSERT_TRUE(this->auth_receiver->get_authorization(0)); + ASSERT_FALSE(this->auth_receiver->get_authorization(1)); + + types::authorization::WithdrawAuthorizationRequest withdraw_request; + withdraw_request.evse_id = 1; + + auto withdraw_result = this->auth_handler->handle_withdraw_authorization(withdraw_request); + + ASSERT_EQ(withdraw_result, WithdrawAuthorizationResult::Accepted); + ASSERT_FALSE(this->auth_receiver->get_authorization(0)); + + SessionEvent deauthorized_event; + deauthorized_event.event = SessionEventEnum::Deauthorized; + this->auth_handler->handle_session_event(1, deauthorized_event); + + withdraw_result = this->auth_handler->handle_withdraw_authorization(withdraw_request); + ASSERT_EQ(withdraw_result, WithdrawAuthorizationResult::AuthorizationNotFound); +} + +/// \brief Test if a authorization can be withdrawn while authorization process is still selecting an EVSE +TEST_F(AuthTest, test_withdraw_authorization_while_waiting_for_ev_plugin) { + + std::vector connectors{1, 2}; + ProvidedIdToken provided_token1 = get_provided_token(VALID_TOKEN_1, connectors); + ProvidedIdToken provided_token2 = get_provided_token(VALID_TOKEN_2, connectors); + + std::mutex mtx; + std::condition_variable cv; + bool processing_called1 = false; + bool processing_called2 = false; + + EXPECT_CALL(mock_publish_token_validation_status_callback, + Call(Field(&ProvidedIdToken::id_token, provided_token1.id_token), TokenValidationStatus::Processing)) + .WillOnce(Invoke([&]() { + std::unique_lock lock(mtx); + processing_called1 = true; + cv.notify_all(); // Notify that the processing call happened + })); + ; + EXPECT_CALL(mock_publish_token_validation_status_callback, + Call(Field(&ProvidedIdToken::id_token, provided_token1.id_token), TokenValidationStatus::Accepted)); + EXPECT_CALL(mock_publish_token_validation_status_callback, + Call(Field(&ProvidedIdToken::id_token, provided_token1.id_token), TokenValidationStatus::Withdrawn)); + EXPECT_CALL(mock_withdraw_authorization_callback_mock, Call(0)).Times(0); + EXPECT_CALL(mock_stop_transaction_callback, Call(_, _)).Times(0); + + EXPECT_CALL(mock_publish_token_validation_status_callback, + Call(Field(&ProvidedIdToken::id_token, provided_token2.id_token), TokenValidationStatus::Processing)) + .WillOnce(Invoke([&]() { + std::unique_lock lock(mtx); + processing_called2 = true; + cv.notify_all(); // Notify that the processing call happened + })); + ; + EXPECT_CALL(mock_publish_token_validation_status_callback, + Call(Field(&ProvidedIdToken::id_token, provided_token2.id_token), TokenValidationStatus::Accepted)); + EXPECT_CALL(mock_publish_token_validation_status_callback, + Call(Field(&ProvidedIdToken::id_token, provided_token2.id_token), TokenValidationStatus::Withdrawn)); + EXPECT_CALL(mock_withdraw_authorization_callback_mock, Call(0)).Times(0); + EXPECT_CALL(mock_stop_transaction_callback, Call(_, _)).Times(0); + + TokenHandlingResult result1; + TokenHandlingResult result2; + + std::thread t1([this, provided_token1, &result1]() { result1 = this->auth_handler->on_token(provided_token1); }); + std::thread t2([this, provided_token2, &result2]() { result2 = this->auth_handler->on_token(provided_token2); }); + + WithdrawAuthorizationResult withdraw_authorization_result1; + types::authorization::WithdrawAuthorizationRequest withdraw_request1; + withdraw_request1.id_token = {VALID_TOKEN_1, types::authorization::IdTokenType::ISO14443}; + + WithdrawAuthorizationResult withdraw_authorization_result2; + types::authorization::WithdrawAuthorizationRequest withdraw_request2; + withdraw_request2.id_token = {VALID_TOKEN_2, types::authorization::IdTokenType::ISO14443}; + + // Wait for TokenValidationStatus::Processing to be triggered + { + std::unique_lock lock(mtx); + cv.wait(lock, [&]() { return processing_called1 and processing_called2; }); + } + + std::thread t3([this, withdraw_request1, &withdraw_authorization_result1]() { + withdraw_authorization_result1 = this->auth_handler->handle_withdraw_authorization(withdraw_request1); + }); + std::thread t4([this, withdraw_request2, &withdraw_authorization_result2]() { + withdraw_authorization_result2 = this->auth_handler->handle_withdraw_authorization(withdraw_request2); + }); + + t1.join(); + t2.join(); + t3.join(); + t4.join(); + + ASSERT_EQ(result1, TokenHandlingResult::WITHDRAWN); + ASSERT_EQ(withdraw_authorization_result1, WithdrawAuthorizationResult::Accepted); + ASSERT_EQ(result2, TokenHandlingResult::WITHDRAWN); + ASSERT_EQ(withdraw_authorization_result2, WithdrawAuthorizationResult::Accepted); + + ASSERT_FALSE(this->auth_receiver->get_authorization(0)); + ASSERT_FALSE(this->auth_receiver->get_authorization(1)); +} + +/// \brief Test if a authorization is not withdrawn if the token does not match and the authorization requests ends with +/// a timeout +TEST_F(AuthTest, test_withdraw_authorization_while_waiting_for_ev_plugin_timeout) { + + std::vector connectors{1, 2}; + ProvidedIdToken provided_token = get_provided_token(VALID_TOKEN_1, connectors); + + std::mutex mtx; + std::condition_variable cv; + bool processing_called = false; + + EXPECT_CALL(mock_publish_token_validation_status_callback, + Call(Field(&ProvidedIdToken::id_token, provided_token.id_token), TokenValidationStatus::Processing)) + .WillOnce(Invoke([&]() { + std::unique_lock lock(mtx); + processing_called = true; + cv.notify_all(); // Notify that the processing call happened + })); + ; + EXPECT_CALL(mock_publish_token_validation_status_callback, + Call(Field(&ProvidedIdToken::id_token, provided_token.id_token), TokenValidationStatus::Accepted)); + EXPECT_CALL(mock_publish_token_validation_status_callback, + Call(Field(&ProvidedIdToken::id_token, provided_token.id_token), TokenValidationStatus::TimedOut)); + EXPECT_CALL(mock_withdraw_authorization_callback_mock, Call(_)).Times(0); + EXPECT_CALL(mock_stop_transaction_callback, Call(_, _)).Times(0); + + TokenHandlingResult result; + std::thread t1([this, provided_token, &result]() { result = this->auth_handler->on_token(provided_token); }); + + WithdrawAuthorizationResult withdraw_authorization_result; + types::authorization::WithdrawAuthorizationRequest withdraw_request; + withdraw_request.id_token = {VALID_TOKEN_2, types::authorization::IdTokenType::ISO14443}; + + // Wait for TokenValidationStatus::Processing to be triggered + { + std::unique_lock lock(mtx); + cv.wait(lock, [&]() { return processing_called; }); + } + + std::thread t2([this, withdraw_request, &withdraw_authorization_result]() { + withdraw_authorization_result = this->auth_handler->handle_withdraw_authorization(withdraw_request); + }); + + t1.join(); + t2.join(); + + ASSERT_EQ(result, TokenHandlingResult::TIMEOUT); + ASSERT_EQ(withdraw_authorization_result, WithdrawAuthorizationResult::AuthorizationNotFound); + + ASSERT_FALSE(this->auth_receiver->get_authorization(0)); + ASSERT_FALSE(this->auth_receiver->get_authorization(1)); +} + +/// \brief Test if a authorization can be withdrawn during an active transaction +TEST_F(AuthTest, test_withdraw_authorization_during_transaction) { + const SessionEvent session_started_event = + get_session_started_event(types::evse_manager::StartSessionReason::EVConnected); + this->auth_handler->handle_session_event(1, session_started_event); + + std::vector connectors{1}; + ProvidedIdToken provided_token = get_provided_token(VALID_TOKEN_1, connectors); + + EXPECT_CALL(mock_publish_token_validation_status_callback, + Call(Field(&ProvidedIdToken::id_token, provided_token.id_token), TokenValidationStatus::Processing)); + EXPECT_CALL(mock_publish_token_validation_status_callback, + Call(Field(&ProvidedIdToken::id_token, provided_token.id_token), TokenValidationStatus::Accepted)); + + EXPECT_CALL(mock_withdraw_authorization_callback_mock, Call(_)).Times(0); + EXPECT_CALL(mock_stop_transaction_callback, Call(0, _)).Times(1); + + const auto result = this->auth_handler->on_token(provided_token); + ASSERT_TRUE(result == TokenHandlingResult::ACCEPTED); + ASSERT_TRUE(this->auth_receiver->get_authorization(0)); + ASSERT_FALSE(this->auth_receiver->get_authorization(1)); + + const SessionEvent transaction_started_event = get_transaction_started_event(provided_token); + this->auth_handler->handle_session_event(1, transaction_started_event); + + types::authorization::WithdrawAuthorizationRequest withdraw_request; + withdraw_request.id_token = {VALID_TOKEN_1, types::authorization::IdTokenType::ISO14443}; + this->auth_handler->handle_withdraw_authorization(withdraw_request); +} + } // namespace module diff --git a/tests/ocpp_tests/test_sets/ocpp16/ocpp_generic_interface_integration_tests.py b/tests/ocpp_tests/test_sets/ocpp16/ocpp_generic_interface_integration_tests.py index f78ab7757..6a8dad1d8 100644 --- a/tests/ocpp_tests/test_sets/ocpp16/ocpp_generic_interface_integration_tests.py +++ b/tests/ocpp_tests/test_sets/ocpp16/ocpp_generic_interface_integration_tests.py @@ -163,6 +163,7 @@ def _add_pm_command_mock(implementation_id, command, value, skip_implementation) skip_implementation, ) _add_pm_command_mock("auth", "set_connection_timeout", None, skip_implementation) + _add_pm_command_mock("auth", "withdraw_authorization", "Accepted", skip_implementation) _add_pm_command_mock("auth", "set_master_pass_group_id", None, skip_implementation) _add_pm_command_mock( "reservation", "cancel_reservation", "Accepted", skip_implementation diff --git a/types/authorization.yaml b/types/authorization.yaml index db548bfdf..75171f401 100644 --- a/types/authorization.yaml +++ b/types/authorization.yaml @@ -34,6 +34,7 @@ types: - Rejected # Occurs when the token has been authorized but no connector is selected within connection_timeout seconds - TimedOut + - Withdrawn CustomIdToken: description: Type for a custom id token with a free-form type type: object @@ -222,3 +223,34 @@ types: - KeyCode - Local - NoAuthorization + WithdrawAuthorizationRequest: + description: >- + Request to withdraw granted authorization. + If only the evse_id is given, the granted authorization for this EVSE will be withdrawn. + If only the id_token is given, the granted authorization for every EVSE where this id_token is placed will be + withdrawn + If both parameters are given, the granted authorization for the given EVSE will be withdrawn, if the placed + id_token matches the given id_token + If no parameter is given, all granted authorizations for all EVSEs will be removed + type: object + properties: + evse_id: + description: The evse id to withdraw authorization for. + type: integer + id_token: + description: The id token to withdraw authorization for. + type: object + $ref: /authorization#/IdToken + WithdrawAuthorizationResult: + description: >- + The result of a WithdrawAuthorizationRequest: + Accepted in case requested authorization was removed + AuthorizationNotFound in case no match for request was found + EvseNotFound in case no match for evse_id was found + Rejected in case module could not process the request for other reasons + type: string + enum: + - Accepted + - AuthorizationNotFound + - EvseNotFound + - Rejected