Compare commits

..
9 changed files with 98 additions and 59 deletions
+2 -8
View File
@@ -2337,7 +2337,7 @@ bool PresetCollection::reset_project_embedded_presets()
return re_select; return re_select;
} }
void PresetCollection::set_sync_info_and_save(std::string name, std::string setting_id, std::string syncinfo, long long update_time, const std::string& user_id) void PresetCollection::set_sync_info_and_save(std::string name, std::string setting_id, std::string syncinfo, long long update_time)
{ {
lock(); lock();
const std::string canonical_name = this->canonical_preset_name(name); const std::string canonical_name = this->canonical_preset_name(name);
@@ -2355,10 +2355,7 @@ void PresetCollection::set_sync_info_and_save(std::string name, std::string sett
preset2.save_info(); preset2.save_info();
} }
} }
if (!setting_id.empty()) preset->setting_id = setting_id;
preset->setting_id = setting_id;
if (!user_id.empty())
preset->user_id = user_id;
if (update_time > 0) if (update_time > 0)
preset->updated_time = update_time; preset->updated_time = update_time;
if (preset->sync_info == "update") if (preset->sync_info == "update")
@@ -2648,9 +2645,6 @@ bool PresetCollection::load_user_preset(std::string name, std::map<std::string,
iter->base_id = based_id; iter->base_id = based_id;
iter->filament_id = cloud_filament_id; iter->filament_id = cloud_filament_id;
update_alias(*iter); update_alias(*iter);
// Persist the cloud-assigned identity to disk, mirroring the equal/newer branch
// above; otherwise the id stays only in memory and the next launch rewrites it.
iter->save_info();
//presets_loaded.emplace_back(*it->second); //presets_loaded.emplace_back(*it->second);
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", update the user preset %1% from cloud, type %2%, setting_id %3%, base_id %4%, sync_info %5% inherits %6%, filament_id %7%") BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", update the user preset %1% from cloud, type %2%, setting_id %3%, base_id %4%, sync_info %5% inherits %6%, filament_id %7%")
% iter->name %Preset::get_type_string(m_type) %iter->setting_id %iter->base_id %iter->sync_info %iter->inherits() % iter->filament_id; % iter->name %Preset::get_type_string(m_type) %iter->setting_id %iter->base_id %iter->sync_info %iter->inherits() % iter->filament_id;
+1 -1
View File
@@ -603,7 +603,7 @@ public:
void update_after_user_presets_loaded(); void update_after_user_presets_loaded();
//BBS: get user presets //BBS: get user presets
int get_user_presets(PresetBundle *preset_bundle, std::vector<Preset> &result_presets); int get_user_presets(PresetBundle *preset_bundle, std::vector<Preset> &result_presets);
void set_sync_info_and_save(std::string name, std::string setting_id, std::string syncinfo, long long update_time, const std::string& user_id); void set_sync_info_and_save(std::string name, std::string setting_id, std::string syncinfo, long long update_time);
bool need_sync(std::string name, std::string setting_id, long long update_time); bool need_sync(std::string name, std::string setting_id, long long update_time);
//BBS: add function to generate differed preset for save //BBS: add function to generate differed preset for save
+4 -4
View File
@@ -7204,11 +7204,11 @@ void GUI_App::sync_preset(Preset* preset, bool force)
BOOST_LOG_TRIVIAL(trace) << "sync_preset: sync operation: " << preset->sync_info << " success! preset = " << preset->name; BOOST_LOG_TRIVIAL(trace) << "sync_preset: sync operation: " << preset->sync_info << " success! preset = " << preset->name;
if (preset->type == Preset::Type::TYPE_FILAMENT) { if (preset->type == Preset::Type::TYPE_FILAMENT) {
preset_bundle->filaments.set_sync_info_and_save(preset->name, setting_id, updated_info, update_time, m_agent->get_user_id()); preset_bundle->filaments.set_sync_info_and_save(preset->name, setting_id, updated_info, update_time);
} else if (preset->type == Preset::Type::TYPE_PRINT) { } else if (preset->type == Preset::Type::TYPE_PRINT) {
preset_bundle->prints.set_sync_info_and_save(preset->name, setting_id, updated_info, update_time, m_agent->get_user_id()); preset_bundle->prints.set_sync_info_and_save(preset->name, setting_id, updated_info, update_time);
} else if (preset->type == Preset::Type::TYPE_PRINTER) { } else if (preset->type == Preset::Type::TYPE_PRINTER) {
preset_bundle->printers.set_sync_info_and_save(preset->name, setting_id, updated_info, update_time, m_agent->get_user_id()); preset_bundle->printers.set_sync_info_and_save(preset->name, setting_id, updated_info, update_time);
} }
} }
} }
@@ -7907,7 +7907,7 @@ void GUI_App::force_push_conflicting_preset(const std::string& setting_id)
? OrcaCloudServiceAgent::generate_uuid_for_setting_id(preset.name, user_id) ? OrcaCloudServiceAgent::generate_uuid_for_setting_id(preset.name, user_id)
: preset.setting_id; : preset.setting_id;
if (preset_id == setting_id) { if (preset_id == setting_id) {
coll->set_sync_info_and_save(preset.name, setting_id, "update", 0, user_id); coll->set_sync_info_and_save(preset.name, setting_id, "update", 0);
break; break;
} }
} }
+12 -36
View File
@@ -1129,19 +1129,6 @@ std::string OrcaCloudServiceAgent::request_setting_id(std::string name,
if (http_code) if (http_code)
*http_code = result.http_code; *http_code = result.http_code;
// 409 duplicate_profile_uuid in the create path means the deterministic id we
// just generated already exists in this account: the earlier create succeeded.
// Adopt it instead of failing, so sync_preset persists the id and stops retrying.
if (result.http_code == 409 && result.conflict_code == -2
&& !result.server_version.id.empty() && result.server_version.id == new_id) {
if (values_map && result.server_version.updated_time != 0)
(*values_map)[IOT_JSON_KEY_UPDATED_TIME] = std::to_string(result.server_version.updated_time);
if (http_code)
*http_code = 200;
BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: request_setting_id adopted existing profile id " << new_id << " (409 duplicate_profile_uuid)";
return new_id;
}
if (result.success) { if (result.success) {
if (values_map && result.new_updated_time != 0) { if (values_map && result.new_updated_time != 0) {
(*values_map)[IOT_JSON_KEY_UPDATED_TIME] = std::to_string(result.new_updated_time); (*values_map)[IOT_JSON_KEY_UPDATED_TIME] = std::to_string(result.new_updated_time);
@@ -1407,7 +1394,6 @@ SyncPushResult OrcaCloudServiceAgent::sync_push(const std::string& profile_id,
SyncPushResult result; SyncPushResult result;
result.success = false; result.success = false;
result.http_code = 0; result.http_code = 0;
result.conflict_code = 0;
result.server_deleted = false; result.server_deleted = false;
nlohmann::json body; nlohmann::json body;
@@ -1443,30 +1429,20 @@ SyncPushResult OrcaCloudServiceAgent::sync_push(const std::string& profile_id,
err_body = json; err_body = json;
if (json.is_null()) { if (json.is_null()) {
result.server_deleted = true; result.server_deleted = true;
} else if (json.is_object()) { } else {
result.conflict_code = json.value("code", 0); auto& profile_data = json["server_profile"];
if (json.contains("server_profile") && !json["server_profile"].is_null()) { result.server_version.id = profile_data.value("id", "");
auto& profile_data = json["server_profile"]; result.server_version.name = profile_data.value("name", "");
result.server_version.id = profile_data.value("id", ""); result.server_version.updated_time = profile_data.value(ORCA_JSON_KEY_UPDATE_TIME, 0);
result.server_version.name = profile_data.value("name", "");
result.server_version.updated_time = profile_data.value(ORCA_JSON_KEY_UPDATE_TIME, 0);
}
} }
} catch (...) {} } catch (...) {}
// Create-path duplicate_profile_uuid (-2) is an idempotent success: the deterministic id // Surface the conflict via the http-error callback with the local preset name injected.
// already exists, so the caller adopts the returned id. Skip the conflict notification, // The raw server body omits the name for tombstone (-3) conflicts (server_profile is null),
// otherwise every already-imported preset would raise a Pull/Force-push prompt on each launch. // but the GUI needs it to regenerate the deterministic setting_id for a force push.
const bool is_create = original_updated_time.empty(); if (!err_body.is_object())
const bool auto_resolved_duplicate = (is_create && result.conflict_code == -2); err_body = nlohmann::json::object();
if (!auto_resolved_duplicate) { err_body["name"] = name;
// Surface the conflict via the http-error callback with the local preset name injected. invoke_http_error_callback(409, err_body.dump());
// The raw server body omits the name for tombstone (-3) conflicts (server_profile is null),
// but the GUI needs it to regenerate the deterministic setting_id for a force push.
if (!err_body.is_object())
err_body = nlohmann::json::object();
err_body["name"] = name;
invoke_http_error_callback(409, err_body.dump());
}
result.error_message = response; result.error_message = response;
return result; return result;
} }
@@ -94,7 +94,6 @@ struct SyncPullResponse {
struct SyncPushResult { struct SyncPushResult {
bool success; bool success;
int http_code; int http_code;
int conflict_code;
long long new_updated_time; long long new_updated_time;
ProfileUpsert server_version; ProfileUpsert server_version;
bool server_deleted; bool server_deleted;
+74 -9
View File
@@ -115,6 +115,9 @@ static const std::unordered_map<std::string, AuditEventCategory> audit_event_cat
{"subprocess.Popen", AuditEventCategory::ProcessCreate}, {"subprocess.Popen", AuditEventCategory::ProcessCreate},
{"_winapi.CreateProcess", AuditEventCategory::ProcessCreate}, {"_winapi.CreateProcess", AuditEventCategory::ProcessCreate},
{"_posixsubprocess.fork_exec", AuditEventCategory::ProcessCreate}, {"_posixsubprocess.fork_exec", AuditEventCategory::ProcessCreate},
// threading
{"_thread.start_new_thread", AuditEventCategory::Threading},
}; };
// Returns the category event_name belongs to, or AuditEventCategory::None when it isn't audited. // Returns the category event_name belongs to, or AuditEventCategory::None when it isn't audited.
@@ -735,6 +738,12 @@ std::vector<std::string> audit_targets(const std::string& event_name, AuditEvent
} }
return targets; return targets;
} }
case AuditEventCategory::Threading:
// Thread creation exposes no user-supplied target. Use a fixed sentinel so the grant
// persists per plugin: the permission list matches targets by exact string, and the
// started function's repr embeds an address that changes every run.
targets.emplace_back("thread");
return targets;
default: default:
break; break;
} }
@@ -761,6 +770,7 @@ std::vector<std::string>* permission_list_for(AuditEventCategory category, Plugi
case AuditEventCategory::Http: return &permissions.network_http; case AuditEventCategory::Http: return &permissions.network_http;
case AuditEventCategory::Socket: return &permissions.network_socket; case AuditEventCategory::Socket: return &permissions.network_socket;
case AuditEventCategory::ProcessCreate: return &permissions.process; case AuditEventCategory::ProcessCreate: return &permissions.process;
case AuditEventCategory::Threading: return &permissions.threading;
default: return nullptr; default: return nullptr;
} }
} }
@@ -832,11 +842,74 @@ wxString audit_message(AuditEventCategory category, const wxString& plugin_name,
return wxString::Format(_L("Plugin \"%s\" is requesting to open a network connection to:\n%s"), plugin_name, target_list); return wxString::Format(_L("Plugin \"%s\" is requesting to open a network connection to:\n%s"), plugin_name, target_list);
case AuditEventCategory::ProcessCreate: case AuditEventCategory::ProcessCreate:
return wxString::Format(_L("Plugin \"%s\" is requesting to run the following command(s):\n%s"), plugin_name, target_list); return wxString::Format(_L("Plugin \"%s\" is requesting to run the following command(s):\n%s"), plugin_name, target_list);
case AuditEventCategory::Threading:
return wxString::Format(_L("Plugin \"%s\" is requesting permission to create a thread."), plugin_name);
default: default:
return wxString::Format(_L("Plugin \"%s\" is requesting permission for the Python audit event \"%s\"."), plugin_name, event_name); return wxString::Format(_L("Plugin \"%s\" is requesting permission for the Python audit event \"%s\"."), plugin_name, event_name);
} }
} }
// Builds and shows the modal permission prompt. Must run on the GUI thread.
bool prompt_for_targets(AuditEventCategory category, const std::string& plugin_name, const std::string& event_name,
const std::vector<std::string>& unresolved)
{
wxString target_list;
for (const auto& target : unresolved)
target_list += wxString::FromUTF8(target.c_str()) + "\n";
wxMessageDialog dialog(nullptr,
audit_message(category, wxString::FromUTF8(plugin_name.c_str()),
wxString::FromUTF8(event_name.c_str()), target_list),
_L("Plugin permission request"), wxYES_NO | wxICON_WARNING);
return dialog.ShowModal() == wxID_YES;
}
// Records a grant in the plugin's sidecar so it is not asked again. Reads the install state
// freshly because the async prompt outlives the caller's stack copy of it.
void persist_grant(const std::string& plugin_key, AuditEventCategory category, const std::vector<std::string>& targets)
{
PluginInstallState state;
if (!PluginManager::instance().get_install_state(plugin_key, state))
return;
std::vector<std::string>* permission_list = permission_list_for(category, state.permissions);
if (!permission_list)
return;
for (const auto& target : targets)
persist_permission(plugin_key, state, *permission_list, target);
}
// Requests permission for an audited event, returning true when it is already granted or the user
// approves an inline prompt.
//
// An audited event can fire on a thread the UI thread may itself be blocked waiting on: the
// SlicingPipeline hook runs on the slicing worker thread (see PluginHooks.cpp), and
// BackgroundSlicingProcess::stop()/stop_internal() park the UI thread until that worker stops.
// Blocking the worker on a marshaled modal -- which is safe for the plugin-load worker that
// request_filesystem_read_permissions runs on -- would therefore deadlock the application (the
// invariant PluginHostUi.cpp documents for slicing-hook UI calls). Off the main thread the prompt
// is therefore posted asynchronously and the current event denied (fail closed, like an unanswered
// prompt); the grant is persisted once the user accepts, so a later attempt succeeds without
// re-prompting.
bool request_permission(AuditEventCategory category, const std::string& plugin_key, const std::string& plugin_name,
const std::string& event_name, const std::vector<std::string>& unresolved)
{
if (wxTheApp == nullptr || GUI::wxGetApp().is_closing())
return false;
if (wxIsMainThread())
return prompt_for_targets(category, plugin_name, event_name, unresolved);
GUI::wxGetApp().CallAfter([category, plugin_key, plugin_name, event_name, unresolved]() {
if (wxTheApp == nullptr || GUI::wxGetApp().is_closing())
return;
if (prompt_for_targets(category, plugin_name, event_name, unresolved))
persist_grant(plugin_key, category, unresolved);
});
return false;
}
int decide_audited_event(PluginAuditManager& mgr, int decide_audited_event(PluginAuditManager& mgr,
PluginInstallState& state, PluginInstallState& state,
const std::string& plugin_key, const std::string& plugin_key,
@@ -857,15 +930,7 @@ int decide_audited_event(PluginAuditManager& mgr,
return 0; return 0;
} }
wxString target_list; if (!request_permission(category, plugin_key, plugin_name, event_name, unresolved))
for (const auto& target : unresolved)
target_list += wxString::FromUTF8(target.c_str()) + "\n";
wxMessageDialog dialog(nullptr,
audit_message(category, wxString::FromUTF8(plugin_name.c_str()),
wxString::FromUTF8(event_name.c_str()), target_list),
_L("Plugin permission request"), wxYES_NO | wxICON_WARNING);
if (dialog.ShowModal() != wxID_YES)
return report_denied(mgr, event_name, {false, "audit permission required"}); return report_denied(mgr, event_name, {false, "audit permission required"});
if (permission_list) if (permission_list)
+2
View File
@@ -807,6 +807,7 @@ bool read_install_state(const boost::filesystem::path& plugin_dir, PluginInstall
read_string_list("network_http", parsed.permissions.network_http); read_string_list("network_http", parsed.permissions.network_http);
read_string_list("network_socket", parsed.permissions.network_socket); read_string_list("network_socket", parsed.permissions.network_socket);
read_string_list("process", parsed.permissions.process); read_string_list("process", parsed.permissions.process);
read_string_list("threading", parsed.permissions.threading);
} }
if (state.contains("enabled") && state["enabled"].is_boolean()) if (state.contains("enabled") && state["enabled"].is_boolean())
@@ -850,6 +851,7 @@ bool write_install_state(const boost::filesystem::path& plugin_dir, const Plugin
{"network_http", state.permissions.network_http}, {"network_http", state.permissions.network_http},
{"network_socket", state.permissions.network_socket}, {"network_socket", state.permissions.network_socket},
{"process", state.permissions.process}, {"process", state.permissions.process},
{"threading", state.permissions.threading},
}; };
nlohmann::json capabilities = nlohmann::json::array(); nlohmann::json capabilities = nlohmann::json::array();
+1
View File
@@ -92,6 +92,7 @@ struct PluginPermissions
std::vector<std::string> network_http; std::vector<std::string> network_http;
std::vector<std::string> network_socket; std::vector<std::string> network_socket;
std::vector<std::string> process; std::vector<std::string> process;
std::vector<std::string> threading;
}; };
struct PluginInstallState { struct PluginInstallState {
@@ -122,6 +122,7 @@ TEST_CASE("install-state sidecar is the source of truth for a cloud plugin's ins
state.permissions.network_http = {"https://api.example.com"}; state.permissions.network_http = {"https://api.example.com"};
state.permissions.network_socket = {"192.168.45.6:443"}; state.permissions.network_socket = {"192.168.45.6:443"};
state.permissions.process = {"/usr/bin/curl"}; state.permissions.process = {"/usr/bin/curl"};
state.permissions.threading = {"thread"};
REQUIRE(write_install_state(plugin_dir, state)); REQUIRE(write_install_state(plugin_dir, state));
// Permission data is persisted in the same sidecar as the installation metadata. // Permission data is persisted in the same sidecar as the installation metadata.
@@ -132,6 +133,7 @@ TEST_CASE("install-state sidecar is the source of truth for a cloud plugin's ins
CHECK(persisted.permissions.network_http == state.permissions.network_http); CHECK(persisted.permissions.network_http == state.permissions.network_http);
CHECK(persisted.permissions.network_socket == state.permissions.network_socket); CHECK(persisted.permissions.network_socket == state.permissions.network_socket);
CHECK(persisted.permissions.process == state.permissions.process); CHECK(persisted.permissions.process == state.permissions.process);
CHECK(persisted.permissions.threading == state.permissions.threading);
// Reading the sidecar back onto a freshly-scanned descriptor (whose header version is still // Reading the sidecar back onto a freshly-scanned descriptor (whose header version is still
// 1.0.0) must surface the cloud-installed 1.2.0. This is what lets update_cloud_metadata compare // 1.0.0) must surface the cloud-installed 1.2.0. This is what lets update_cloud_metadata compare