Compare commits

...
3 changed files with 195 additions and 118 deletions

No files matched your search

+123 -86
View File
@@ -528,81 +528,6 @@ AuditDecision PluginAuditManager::check_open(const std::string& path_str, const
return check_path_access(boost::filesystem::path(path_str), is_write);
}
bool PluginAuditManager::request_filesystem_read_permissions(const std::string& plugin_key,
const std::vector<std::string>& paths)
{
if (plugin_key.empty() || paths.empty())
return true;
PluginDescriptor descriptor;
if (!PluginManager::instance().try_get_plugin_descriptor(plugin_key, descriptor) || descriptor.plugin_root.empty())
return false;
PluginInstallState state;
read_install_state(boost::filesystem::path(descriptor.plugin_root), state);
std::vector<std::string> missing;
for (const std::string& path : paths) {
if (std::find(state.permissions.fs_read.begin(), state.permissions.fs_read.end(), path) == state.permissions.fs_read.end())
missing.push_back(path);
}
if (missing.empty())
return true;
if (wxTheApp == nullptr || GUI::wxGetApp().is_closing())
return false;
auto show_dialog = [&descriptor, &missing]() {
wxString requested_paths;
for (const std::string& path : missing)
requested_paths += wxString::FromUTF8(path.c_str()) + "\n";
wxMessageDialog dialog(
nullptr,
wxString::Format(_L("Plugin \"%s\" requests filesystem read access to:\n%s"),
wxString::FromUTF8(descriptor.name.c_str()), requested_paths),
_L("Plugin permissions"),
wxYES_NO | wxICON_WARNING);
return dialog.ShowModal() == wxID_YES;
};
bool granted = false;
if (wxIsMainThread()) {
granted = show_dialog();
} else {
auto result = std::make_shared<std::promise<bool>>();
auto future = result->get_future();
GUI::wxGetApp().CallAfter([result, descriptor_name = descriptor.name, missing]() {
wxString requested_paths;
for (const std::string& path : missing)
requested_paths += wxString::FromUTF8(path.c_str()) + "\n";
wxMessageDialog dialog(
nullptr,
wxString::Format(_L("Plugin \"%s\" requests filesystem read access to:\n%s"),
wxString::FromUTF8(descriptor_name.c_str()), requested_paths),
_L("Plugin permissions"),
wxYES_NO | wxICON_WARNING);
result->set_value(dialog.ShowModal() == wxID_YES);
});
granted = future.get();
}
if (!granted)
return false;
if (state.plugin_name.empty()) {
state.installed_from = descriptor.is_cloud_plugin() ? "cloud" : "local";
state.installed_version = !descriptor.installed_version.empty() ? descriptor.installed_version : descriptor.version;
state.plugin_name = descriptor.name;
state.cloud_uuid = descriptor.cloud_uuid();
state.enabled = true;
}
state.permissions.fs_read.insert(state.permissions.fs_read.end(), missing.begin(), missing.end());
return write_install_state(boost::filesystem::path(descriptor.plugin_root), state);
}
void PluginAuditManager::report_violation(const AuditViolation& violation)
{
m_last_violation = violation;
@@ -838,7 +763,7 @@ std::vector<std::string> audit_targets(const std::string& event_name, AuditEvent
// 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");
targets.emplace_back(kThreadPermissionTarget);
return targets;
default:
break;
@@ -894,6 +819,20 @@ bool has_permission(const std::vector<std::string>& granted, const std::string&
return std::find(granted.begin(), granted.end(), target) != granted.end();
}
// A sandbox plugin may not have a sidecar yet. Seed the small amount of install metadata the
// writer needs so clicking Yes still creates the JSON file.
void seed_install_metadata(const PluginDescriptor& descriptor, PluginInstallState& state)
{
if (!state.plugin_name.empty())
return;
state.installed_from = descriptor.is_cloud_plugin() ? "cloud" : "local";
state.installed_version = !descriptor.installed_version.empty() ? descriptor.installed_version : descriptor.version;
state.plugin_name = descriptor.name;
state.cloud_uuid = descriptor.cloud_uuid();
state.enabled = true;
}
bool persist_permission(const std::string& plugin_key,
PluginInstallState& state,
std::vector<std::string>& permission_list,
@@ -903,15 +842,7 @@ bool persist_permission(const std::string& plugin_key,
if (!PluginManager::instance().try_get_plugin_descriptor(plugin_key, descriptor) || descriptor.plugin_root.empty())
return false;
// A sandbox plugin may not have a sidecar yet. Seed the small amount of install metadata
// needed by the writer so clicking Yes still creates the JSON file.
if (state.plugin_name.empty()) {
state.installed_from = descriptor.is_cloud_plugin() ? "cloud" : "local";
state.installed_version = !descriptor.installed_version.empty() ? descriptor.installed_version : descriptor.version;
state.plugin_name = descriptor.name;
state.cloud_uuid = descriptor.cloud_uuid();
state.enabled = true;
}
seed_install_metadata(descriptor, state);
if (!has_permission(permission_list, target))
permission_list.push_back(target);
@@ -997,6 +928,13 @@ bool prompt_for_targets(AuditEventCategory category, const std::string& plugin_n
return dialog.ShowModal() == wxID_YES;
}
// Shows a prompt whose body may cover several permission buckets. Must run on the GUI thread.
bool prompt_for_permission_message(const wxString& message)
{
wxMessageDialog dialog(nullptr, message, _L("Plugin permissions"), 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, bool by_event, const std::vector<std::string>& keys)
@@ -1023,7 +961,7 @@ static PluginAuditManager::PermissionPrompt s_permission_prompt_for_testing;
// 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
// request_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
@@ -1147,6 +1085,105 @@ int audit_plugin_event(PluginAuditManager& mgr, const std::string& event_name, A
} // namespace PluginAuditDetail
bool PluginAuditManager::request_permissions(const std::string& plugin_key, const PluginPermissions& requested)
{
if (plugin_key.empty())
return true;
PluginDescriptor descriptor;
if (!PluginManager::instance().try_get_plugin_descriptor(plugin_key, descriptor) || descriptor.plugin_root.empty())
return false;
PluginInstallState state;
read_install_state(boost::filesystem::path(descriptor.plugin_root), state);
// Per bucket, keep only the targets that no earlier grant already covers. Dropping these
// before prompting keeps a repeated declaration from asking again for what is on disk.
struct MissingBucket {
AuditEventCategory category;
std::vector<std::string> targets;
};
std::vector<MissingBucket> missing_buckets;
const auto collect_missing = [&missing_buckets](AuditEventCategory category,
const std::vector<std::string>& requested_targets,
const std::vector<std::string>& granted_targets) {
MissingBucket bucket{category, {}};
for (const std::string& target : requested_targets) {
if (target.empty())
continue;
if (std::find(granted_targets.begin(), granted_targets.end(), target) == granted_targets.end() &&
std::find(bucket.targets.begin(), bucket.targets.end(), target) == bucket.targets.end())
bucket.targets.push_back(target);
}
if (!bucket.targets.empty())
missing_buckets.push_back(std::move(bucket));
};
collect_missing(AuditEventCategory::FsRead, requested.fs_read, state.permissions.fs_read);
collect_missing(AuditEventCategory::FsReadWrite, requested.fs_readwrite, state.permissions.fs_readwrite);
collect_missing(AuditEventCategory::Http, requested.network_http, state.permissions.network_http);
collect_missing(AuditEventCategory::Socket, requested.network_socket, state.permissions.network_socket);
collect_missing(AuditEventCategory::ProcessCreate, requested.process, state.permissions.process);
collect_missing(AuditEventCategory::Threading, requested.threading, state.permissions.threading);
if (missing_buckets.empty())
return true;
if (wxTheApp == nullptr || GUI::wxGetApp().is_closing())
return false;
const auto build_message = [](const std::string& plugin_name, const std::vector<MissingBucket>& buckets) {
wxString message;
for (const MissingBucket& bucket : buckets) {
wxString targets;
for (const std::string& target : bucket.targets)
targets += wxString::FromUTF8(target.c_str()) + "\n";
if (!message.IsEmpty())
message += "\n\n";
message += PluginAuditDetail::audit_message(bucket.category, wxString::FromUTF8(plugin_name.c_str()),
wxEmptyString, targets, bucket.targets.size());
}
return message;
};
bool granted = false;
if (wxIsMainThread()) {
granted = PluginAuditDetail::prompt_for_permission_message(build_message(descriptor.name, missing_buckets));
} else {
auto result = std::make_shared<std::promise<bool>>();
auto future = result->get_future();
GUI::wxGetApp().CallAfter([result, plugin_name = descriptor.name, missing_buckets, build_message]() {
result->set_value(PluginAuditDetail::prompt_for_permission_message(build_message(plugin_name, missing_buckets)));
});
granted = future.get();
}
if (!granted)
return false;
// The dialog can stay open while another grant is persisted; re-read before saving so this
// request adds to the current state instead of replacing it.
PluginInstallState latest_state;
if (read_install_state(boost::filesystem::path(descriptor.plugin_root), latest_state))
state = std::move(latest_state);
PluginAuditDetail::seed_install_metadata(descriptor, state);
for (const MissingBucket& bucket : missing_buckets) {
std::vector<std::string>* permission_list =
PluginAuditDetail::permission_list_for(bucket.category, /*by_event=*/false, state.permissions);
if (!permission_list)
continue;
for (const std::string& target : bucket.targets)
if (std::find(permission_list->begin(), permission_list->end(), target) == permission_list->end())
permission_list->push_back(target);
}
return write_install_state(boost::filesystem::path(descriptor.plugin_root), state);
}
void PluginAuditManager::record_root_frame()
{
PyObject* frame = reinterpret_cast<PyObject*>(PyEval_GetFrame()); // borrowed reference
+12 -6
View File
@@ -13,6 +13,12 @@
namespace Slic3r {
struct PluginPermissions;
// Thread creation exposes no user-supplied target, so both the runtime audit event and a
// request_permissions(threading=True) declaration use this fixed target.
inline constexpr const char* kThreadPermissionTarget = "thread";
struct AuditDecision {
bool allowed = true;
std::string reason;
@@ -130,12 +136,12 @@ public:
AuditDecision check_path_access(const boost::filesystem::path& candidate, bool is_write);
AuditDecision check_open(const std::string& path, const std::string& mode);
// Ask the user to grant the requested filesystem-read paths. The request may originate on a
// plugin load worker, so the implementation marshals the modal dialog to the wx main thread.
// Returns true only when every missing path was granted and persisted; denial aborts the plugin
// load without adding a permission.
bool request_filesystem_read_permissions(const std::string& plugin_key,
const std::vector<std::string>& paths);
// Ask the user to grant the requested permissions. The request may originate on a plugin
// load worker, so the implementation marshals the modal dialog to the wx main thread.
// Targets already persisted for the plugin are not shown or asked for again. Returns true
// only when every missing target was granted and persisted; denial aborts the plugin load
// without adding a permission.
bool request_permissions(const std::string& plugin_key, const PluginPermissions& requested);
void report_violation(const AuditViolation& violation);
bool audit_denial_pending() const;
+60 -26
View File
@@ -49,7 +49,7 @@ thread_local std::string g_active_plugin_key;
std::mutex g_registry_mutex;
std::unordered_map<std::string, std::vector<py::object>> g_pending_capabilities;
std::unordered_map<std::string, py::object> g_pending_package;
std::unordered_map<std::string, std::vector<std::string>> g_pending_fs_read_permissions;
std::unordered_map<std::string, PluginPermissions> g_pending_permissions;
struct PluginInstanceHandle
{
// The C++ plugin interface points into a Python object. Keep both alive through one
@@ -88,7 +88,7 @@ void discard_pending_capture_without_python(const std::string& plugin_key)
g_pending_package.erase(package);
}
g_pending_fs_read_permissions.erase(plugin_key);
g_pending_permissions.erase(plugin_key);
}
} // namespace
@@ -113,7 +113,7 @@ void PythonPluginBridge::begin_plugin_capture(const std::string& plugin_key)
// for this same entry path.
g_pending_capabilities.erase(plugin_key);
g_pending_package.erase(plugin_key);
g_pending_fs_read_permissions.erase(plugin_key);
g_pending_permissions.erase(plugin_key);
}
// From now until finalize/cancel, @orca.plugin and register_capability() calls made by
// Python code on this thread are attributed to this plugin.
@@ -144,7 +144,7 @@ std::vector<CapturedCapability> PythonPluginBridge::finalize_plugin_capture(cons
std::lock_guard<std::mutex> lock(g_registry_mutex);
g_pending_capabilities.erase(capture_key);
g_pending_package.erase(capture_key);
g_pending_fs_read_permissions.erase(capture_key);
g_pending_permissions.erase(capture_key);
};
try {
@@ -177,23 +177,27 @@ std::vector<CapturedCapability> PythonPluginBridge::finalize_plugin_capture(cons
// Permission declarations are collected while register_capabilities() runs so the
// plugin can describe its needs without touching wx from the Python load worker. Ask
// only after registration returns, before capability instances are materialized.
std::vector<std::string> fs_read_permissions;
PluginPermissions requested_permissions;
{
std::lock_guard<std::mutex> lock(g_registry_mutex);
auto it = g_pending_fs_read_permissions.find(capture_key);
if (it != g_pending_fs_read_permissions.end()) {
fs_read_permissions = std::move(it->second);
g_pending_fs_read_permissions.erase(it);
auto it = g_pending_permissions.find(capture_key);
if (it != g_pending_permissions.end()) {
requested_permissions = std::move(it->second);
g_pending_permissions.erase(it);
}
}
if (!fs_read_permissions.empty()) {
const bool has_permission_request =
!requested_permissions.fs_read.empty() || !requested_permissions.fs_readwrite.empty() ||
!requested_permissions.network_http.empty() || !requested_permissions.network_socket.empty() ||
!requested_permissions.process.empty() || !requested_permissions.threading.empty();
if (has_permission_request) {
bool granted = false;
{
py::gil_scoped_release release;
granted = PluginAuditManager::instance().request_filesystem_read_permissions(plugin_key, fs_read_permissions);
granted = PluginAuditManager::instance().request_permissions(plugin_key, requested_permissions);
}
if (!granted) {
error = _u8L("Plugin filesystem read permission request denied");
error = _u8L("Plugin permission request denied");
BOOST_LOG_TRIVIAL(warning) << error << " for key " << plugin_key;
discard_pending_for_key();
clear_active_key();
@@ -322,7 +326,7 @@ void PythonPluginBridge::cancel_plugin_capture(const std::string& plugin_key)
// may already have registered under this key.
g_pending_capabilities.erase(plugin_key);
g_pending_package.erase(plugin_key);
g_pending_fs_read_permissions.erase(plugin_key);
g_pending_permissions.erase(plugin_key);
}
if (g_active_plugin_key == plugin_key)
@@ -349,7 +353,7 @@ void PythonPluginBridge::clear_pending_captures()
(void) pkg.release();
}
g_pending_package.clear();
g_pending_fs_read_permissions.clear();
g_pending_permissions.clear();
g_active_plugin_key.clear();
return;
}
@@ -358,7 +362,7 @@ void PythonPluginBridge::clear_pending_captures()
BOOST_LOG_TRIVIAL(info) << "Clearing " << g_pending_capabilities.size() << " pending Python plugin capture(s)";
g_pending_capabilities.clear();
g_pending_package.clear();
g_pending_fs_read_permissions.clear();
g_pending_permissions.clear();
g_active_plugin_key.clear();
}
@@ -558,24 +562,54 @@ void bind_python_api(pybind11::module_& m)
m.def(
"request_permissions",
[](const std::vector<std::string>& fs_read) {
[](const std::vector<std::string>& fs_read,
const std::vector<std::string>& fs_readwrite,
const std::vector<std::string>& network_http,
const std::vector<std::string>& network_socket,
const std::vector<std::string>& process,
bool threading) {
if (g_active_plugin_key.empty())
throw py::value_error("request_permissions() called outside plugin discovery context");
std::lock_guard<std::mutex> lock(g_registry_mutex);
auto& requested = g_pending_fs_read_permissions[g_active_plugin_key];
for (const std::string& path : fs_read) {
if (path.empty())
throw py::value_error("request_permissions(fs_read=...) does not accept empty paths");
if (std::find(requested.begin(), requested.end(), path) == requested.end())
requested.push_back(path);
}
PluginPermissions& requested = g_pending_permissions[g_active_plugin_key];
const auto add_targets = [](std::vector<std::string>& bucket, const char* name,
const std::vector<std::string>& targets) {
for (const std::string& target : targets) {
if (target.empty())
throw py::value_error(std::string("request_permissions(") + name +
"=...) does not accept empty values");
if (std::find(bucket.begin(), bucket.end(), target) == bucket.end())
bucket.push_back(target);
}
};
add_targets(requested.fs_read, "fs_read", fs_read);
add_targets(requested.fs_readwrite, "fs_readwrite", fs_readwrite);
add_targets(requested.network_http, "network_http", network_http);
add_targets(requested.network_socket, "network_socket", network_socket);
add_targets(requested.process, "process", process);
if (threading && std::find(requested.threading.begin(), requested.threading.end(),
kThreadPermissionTarget) == requested.threading.end())
requested.threading.emplace_back(kThreadPermissionTarget);
},
py::arg("fs_read") = std::vector<std::string>{},
R"pbdoc(Request filesystem read permissions while OrcaSlicer loads your module.
py::arg("fs_readwrite") = std::vector<std::string>{},
py::arg("network_http") = std::vector<std::string>{},
py::arg("network_socket") = std::vector<std::string>{},
py::arg("process") = std::vector<std::string>{},
py::arg("threading") = false,
R"pbdoc(Request permissions while OrcaSlicer loads your module.
The host presents the request to the user after register_capabilities() returns. Denying the
request aborts the plugin load.)pbdoc");
Each bucket takes the target strings the plugin expects the corresponding audit event to
report: filesystem paths for fs_read and fs_readwrite, URLs for network_http, socket addresses
for network_socket, and commands for process. threading=True requests permission to start
threads. Targets already granted to the plugin are not asked for again.
The host presents one request to the user after register_capabilities() returns and before any
capability instance is created. Denying the request aborts the plugin load.)pbdoc");
m.def("plugin", [](py::object cls) {
if (g_active_plugin_key.empty())