Compare commits

...
Author SHA1 Message Date
Hanif Koh d0df62b804 Keep the Orca Cloud Agent Tests Out of the System Keychain
The display-name tests call set_user_session(), which persists the session.
The agent they built was in keychain mode, so every run saved a fake
OrcaSlicer/Auth session into the system keychain of whoever ran the tests,
replacing their real Orca Cloud login on any desktop with a working keychain.

Move them next to the other agent tests and build the agent the same way:
encrypted-file mode with a throwaway config directory.
2026-09-29 14:00:37 +08:00
Hanif Koh 6803e62949 Stop Logged-Out Login Polling from Hitting the System Keychain
With stealth mode off the home page asks for the login status every 2 s,
and while nobody is logged in that ends in clear_user_secret(), which
opened the system keychain and deleted the OrcaSlicer/Auth entry on the
UI thread every tick. A working keychain cost a D-Bus round trip per tick;
a keychain that never answers blocked the UI for 25 s per tick. It also
deleted a login another running instance had just saved, and ignored
use_encrypted_token_file, so opting out of the keychain did not help.

Remember whether this process read a secret from the store or wrote one,
and only then touch the store when a logged-out poll asks for a logout.
A logged-out instance never touches the keychain, and in encrypted-file
mode the poll no longer opens the keychain at all. A secret this process
cannot read, such as a token file encrypted for another OS user sharing
the data directory, is left alone by the poll. An explicit logout still
wipes both backends, so a token stranded by switching the token storage
option cannot sign the account back in later.
2026-09-29 13:14:40 +08:00
5 changed files with 204 additions and 99 deletions
+23 -8
View File
@@ -818,7 +818,9 @@ int OrcaCloudServiceAgent::user_logout(bool request)
} }
} }
clear_session(); // An explicit logout also wipes the backend the token storage option is not using, so a token
// stranded by switching that option cannot sign the account back in later.
clear_session(/*all_backends=*/request);
return BAMBU_NETWORK_SUCCESS; return BAMBU_NETWORK_SUCCESS;
} }
@@ -1603,7 +1605,9 @@ void OrcaCloudServiceAgent::persist_user_secret(const std::string& secret)
} }
} }
(void) stored; if (stored) {
secret_stored = true;
}
} }
bool OrcaCloudServiceAgent::load_user_secret(std::string& out_secret) bool OrcaCloudServiceAgent::load_user_secret(std::string& out_secret)
@@ -1643,6 +1647,7 @@ bool OrcaCloudServiceAgent::load_user_secret(std::string& out_secret)
} }
if (integrity_ok && aes256gcm_decrypt(encoded_payload, key, plain) && !plain.empty()) { if (integrity_ok && aes256gcm_decrypt(encoded_payload, key, plain) && !plain.empty()) {
secret_stored = true;
out_secret = plain; out_secret = plain;
// Upgrade legacy payloads to signed format // Upgrade legacy payloads to signed format
if (payload.rfind("v2:", 0) != 0) { if (payload.rfind("v2:", 0) != 0) {
@@ -1660,6 +1665,7 @@ bool OrcaCloudServiceAgent::load_user_secret(std::string& out_secret)
if (store.Load(SECRET_STORE_SERVICE, username, secret) && secret.IsOk()) { if (store.Load(SECRET_STORE_SERVICE, username, secret) && secret.IsOk()) {
out_secret.assign(static_cast<const char*>(secret.GetData()), secret.GetSize()); out_secret.assign(static_cast<const char*>(secret.GetData()), secret.GetSize());
if (!out_secret.empty()) { if (!out_secret.empty()) {
secret_stored = true;
return true; return true;
} }
} }
@@ -1669,11 +1675,20 @@ bool OrcaCloudServiceAgent::load_user_secret(std::string& out_secret)
return false; return false;
} }
void OrcaCloudServiceAgent::clear_user_secret() void OrcaCloudServiceAgent::clear_user_secret(bool all_backends)
{ {
wxSecretStore store = wxSecretStore::GetDefault(); // Nothing this process loaded or saved: leave the store alone. Deleting would only cost a
if (store.IsOk()) { // keychain round trip (or a hang while the keychain is unresponsive) and could remove a
store.Delete(SECRET_STORE_SERVICE); // login another instance just saved.
if (!secret_stored.exchange(false) && !all_backends) {
return;
}
if (all_backends || !m_use_encrypted_token_file) {
wxSecretStore store = wxSecretStore::GetDefault();
if (store.IsOk()) {
store.Delete(SECRET_STORE_SERVICE);
}
} }
compute_fallback_path(); compute_fallback_path();
@@ -2022,13 +2037,13 @@ bool OrcaCloudServiceAgent::set_user_session(const json& session_json, bool noti
return success; return success;
} }
void OrcaCloudServiceAgent::clear_session() void OrcaCloudServiceAgent::clear_session(bool all_backends)
{ {
{ {
std::lock_guard<std::mutex> lock(session_mutex); std::lock_guard<std::mutex> lock(session_mutex);
session = SessionInfo{}; session = SessionInfo{};
} }
clear_user_secret(); clear_user_secret(all_backends);
} }
// ============================================================================ // ============================================================================
+7 -2
View File
@@ -324,7 +324,7 @@ public:
void persist_user_secret(const std::string& secret); void persist_user_secret(const std::string& secret);
bool load_user_secret(std::string& out_secret); bool load_user_secret(std::string& out_secret);
void clear_user_secret(); void clear_user_secret(bool all_backends = false);
// Token refresh helpers // Token refresh helpers
bool refresh_if_expiring(std::chrono::seconds skew, const std::string& reason); bool refresh_if_expiring(std::chrono::seconds skew, const std::string& reason);
@@ -342,7 +342,7 @@ public:
bool persist = true); bool persist = true);
// Accepts either nested Orca cloud / GoTrue session JSON or flat WebView token JSON. // Accepts either nested Orca cloud / GoTrue session JSON or flat WebView token JSON.
bool set_user_session(const nlohmann::json& session_json, bool notify_login = true); bool set_user_session(const nlohmann::json& session_json, bool notify_login = true);
void clear_session(); void clear_session(bool all_backends = false);
static std::string generate_uuid_for_setting_id(const std::string& name, const std::string& user_id = ""); static std::string generate_uuid_for_setting_id(const std::string& name, const std::string& user_id = "");
@@ -411,6 +411,11 @@ private:
// Member variables - auth state // Member variables - auth state
PkceBundle pkce_bundle; PkceBundle pkce_bundle;
std::string secret_fallback_path; std::string secret_fallback_path;
// Set once this process has read a secret from the store or written one. Unless the user logs
// out explicitly, clear_user_secret() only touches the store while it is set, so a logged-out
// instance (the GUI polls the login status every 2 s) makes no keychain calls and cannot wipe
// a login another instance saved.
std::atomic_bool secret_stored{false};
SessionHandler session_handler; SessionHandler session_handler;
OnLoginCompleteHandler on_login_complete_handler; OnLoginCompleteHandler on_login_complete_handler;
SessionInfo session; SessionInfo session;
+1
View File
@@ -10,6 +10,7 @@ add_executable(${_TEST_NAME}_tests
test_prebuild_queue.cpp test_prebuild_queue.cpp
test_staged_build.cpp test_staged_build.cpp
test_network_versions.cpp test_network_versions.cpp
test_orca_cloud_agent.cpp
test_action_source.cpp test_action_source.cpp
test_plugin_host_api.cpp test_plugin_host_api.cpp
test_plugin_capability_config.cpp test_plugin_capability_config.cpp
@@ -1,39 +1,6 @@
#include <catch2/catch_all.hpp> #include <catch2/catch_all.hpp>
#include "slic3r/Utils/Http.hpp" #include "slic3r/Utils/Http.hpp"
#include "slic3r/Utils/OrcaCloudServiceAgent.hpp"
namespace {
nlohmann::json flat_session_json(const nlohmann::json& fields)
{
nlohmann::json session = {
{"access_token", "test-token"},
{"user_id", "test-user-id"}
};
session.update(fields);
return session;
}
nlohmann::json nested_session_json(const nlohmann::json& metadata)
{
return {
{"access_token", "test-token"},
{"user", {
{"id", "test-user-id"},
{"user_metadata", metadata}
}}
};
}
std::string resolved_display_name(const nlohmann::json& session)
{
Slic3r::OrcaCloudServiceAgent agent("");
REQUIRE(agent.set_user_session(session, false));
return agent.get_user_nickname();
}
} // namespace
TEST_CASE("Check SSL certificates paths", "[Http][NotWorking]") { TEST_CASE("Check SSL certificates paths", "[Http][NotWorking]") {
@@ -53,62 +20,6 @@ TEST_CASE("Check SSL certificates paths", "[Http][NotWorking]") {
REQUIRE(status == 200); REQUIRE(status == 200);
} }
TEST_CASE("Orca cloud flat session resolves display name consistently", "[OrcaCloudServiceAgent]")
{
CHECK(resolved_display_name(flat_session_json({
{"username", "orca_username"},
{"display_name", "Display Name"},
{"nickname", "Nickname"}
})) == "Display Name");
CHECK(resolved_display_name(flat_session_json({
{"username", "orca_username"},
{"nickname", "Nickname"}
})) == "Nickname");
CHECK(resolved_display_name(flat_session_json({
{"username", "orca_username"},
{"full_name", "Full Name"}
})) == "Full Name");
CHECK(resolved_display_name(flat_session_json({
{"username", "orca_username"},
{"name", "Provider Name"}
})) == "Provider Name");
CHECK(resolved_display_name(flat_session_json({
{"username", "orca_username"}
})) == "orca_username");
}
TEST_CASE("Orca cloud nested session resolves display name consistently", "[OrcaCloudServiceAgent]")
{
CHECK(resolved_display_name(nested_session_json({
{"username", "orca_username"},
{"display_name", "Display Name"},
{"nickname", "Nickname"}
})) == "Display Name");
CHECK(resolved_display_name(nested_session_json({
{"username", "orca_username"},
{"nickname", "Nickname"}
})) == "Nickname");
CHECK(resolved_display_name(nested_session_json({
{"username", "orca_username"},
{"full_name", "Full Name"}
})) == "Full Name");
CHECK(resolved_display_name(nested_session_json({
{"username", "orca_username"},
{"name", "Provider Name"}
})) == "Provider Name");
CHECK(resolved_display_name(nested_session_json({
{"username", "orca_username"}
})) == "orca_username");
}
TEST_CASE("Http digest authentication", "[Http][NotWorking]") { TEST_CASE("Http digest authentication", "[Http][NotWorking]") {
Slic3r::Http g = Slic3r::Http::get("https://httpbingo.org/digest-auth/auth/guest/guest"); Slic3r::Http g = Slic3r::Http::get("https://httpbingo.org/digest-auth/auth/guest/guest");
+173
View File
@@ -0,0 +1,173 @@
#include <catch2/catch_all.hpp>
#include <boost/filesystem.hpp>
#include <boost/filesystem/fstream.hpp>
#include <memory>
#include <string>
#include "slic3r/Utils/OrcaCloudServiceAgent.hpp"
#include "test_utils.hpp"
using namespace Slic3r;
namespace fs = boost::filesystem;
namespace {
// The encrypted token file is the one secret backend a test can observe without a system
// keychain. Every agent pointed at the same directory shares it, like separate app instances
// share the keychain entry.
std::unique_ptr<OrcaCloudServiceAgent> make_file_backed_agent(const fs::path& dir)
{
auto agent = std::make_unique<OrcaCloudServiceAgent>(dir.string());
agent->set_use_encrypted_token_file(true);
agent->set_config_dir(dir.string());
return agent;
}
fs::path secret_file(const fs::path& dir) { return dir / secret_constants::USER_SECRET_FILENAME; }
nlohmann::json flat_session_json(const nlohmann::json& fields)
{
nlohmann::json session = {
{"access_token", "test-token"},
{"user_id", "test-user-id"}
};
session.update(fields);
return session;
}
nlohmann::json nested_session_json(const nlohmann::json& metadata)
{
return {
{"access_token", "test-token"},
{"user", {
{"id", "test-user-id"},
{"user_metadata", metadata}
}}
};
}
// set_user_session() persists the session, so it goes to a throwaway token file rather than the
// system keychain of whoever runs the tests.
std::string resolved_display_name(const nlohmann::json& session)
{
ScopedTemporaryDir dir("orca-secret");
auto agent = make_file_backed_agent(dir.path());
REQUIRE(agent->set_user_session(session, false));
return agent->get_user_nickname();
}
} // namespace
TEST_CASE("Logging out removes the secret this instance saved", "[OrcaCloudServiceAgent]")
{
ScopedTemporaryDir dir("orca-secret");
auto agent = make_file_backed_agent(dir.path());
agent->persist_user_secret("refresh-token");
REQUIRE(fs::exists(secret_file(dir.path())));
agent->user_logout(false);
CHECK_FALSE(fs::exists(secret_file(dir.path())));
}
TEST_CASE("Logging out removes a secret this instance loaded from the store", "[OrcaCloudServiceAgent]")
{
ScopedTemporaryDir dir("orca-secret");
make_file_backed_agent(dir.path())->persist_user_secret("refresh-token");
auto agent = make_file_backed_agent(dir.path());
std::string secret;
REQUIRE(agent->load_user_secret(secret));
CHECK(secret == "refresh-token");
agent->user_logout(false);
CHECK_FALSE(fs::exists(secret_file(dir.path())));
}
TEST_CASE("Logging out leaves a secret this instance never loaded or saved alone", "[OrcaCloudServiceAgent]")
{
ScopedTemporaryDir dir("orca-secret");
make_file_backed_agent(dir.path())->persist_user_secret("refresh-token");
// A logged-out instance is asked to log out on every login-status poll.
auto other = make_file_backed_agent(dir.path());
other->user_logout(false);
other->user_logout(false);
CHECK(fs::exists(secret_file(dir.path())));
std::string secret;
REQUIRE(make_file_backed_agent(dir.path())->load_user_secret(secret));
CHECK(secret == "refresh-token");
}
TEST_CASE("Logging out leaves a secret this instance could not read alone", "[OrcaCloudServiceAgent]")
{
ScopedTemporaryDir dir("orca-secret");
// Written under another encryption key, e.g. by another OS user sharing the data directory.
fs::ofstream(secret_file(dir.path())) << "v2:0000:not-a-payload-this-user-can-decrypt";
auto agent = make_file_backed_agent(dir.path());
std::string secret;
REQUIRE_FALSE(agent->load_user_secret(secret));
agent->user_logout(false);
CHECK(fs::exists(secret_file(dir.path())));
}
TEST_CASE("Orca cloud flat session resolves display name consistently", "[OrcaCloudServiceAgent]")
{
CHECK(resolved_display_name(flat_session_json({
{"username", "orca_username"},
{"display_name", "Display Name"},
{"nickname", "Nickname"}
})) == "Display Name");
CHECK(resolved_display_name(flat_session_json({
{"username", "orca_username"},
{"nickname", "Nickname"}
})) == "Nickname");
CHECK(resolved_display_name(flat_session_json({
{"username", "orca_username"},
{"full_name", "Full Name"}
})) == "Full Name");
CHECK(resolved_display_name(flat_session_json({
{"username", "orca_username"},
{"name", "Provider Name"}
})) == "Provider Name");
CHECK(resolved_display_name(flat_session_json({
{"username", "orca_username"}
})) == "orca_username");
}
TEST_CASE("Orca cloud nested session resolves display name consistently", "[OrcaCloudServiceAgent]")
{
CHECK(resolved_display_name(nested_session_json({
{"username", "orca_username"},
{"display_name", "Display Name"},
{"nickname", "Nickname"}
})) == "Display Name");
CHECK(resolved_display_name(nested_session_json({
{"username", "orca_username"},
{"nickname", "Nickname"}
})) == "Nickname");
CHECK(resolved_display_name(nested_session_json({
{"username", "orca_username"},
{"full_name", "Full Name"}
})) == "Full Name");
CHECK(resolved_display_name(nested_session_json({
{"username", "orca_username"},
{"name", "Provider Name"}
})) == "Provider Name");
CHECK(resolved_display_name(nested_session_json({
{"username", "orca_username"}
})) == "orca_username");
}