mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-20 14:08:20 +00:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37dcc65752 | ||
|
|
cdb7eda54f | ||
|
|
e7d6cc32a7 |
No files matched your search
Vendored
+6
-2
@@ -59,9 +59,13 @@ else ()
|
|||||||
set(_cross_compile_arg --host=${TOOLCHAIN_PREFIX})
|
set(_cross_compile_arg --host=${TOOLCHAIN_PREFIX})
|
||||||
endif ()
|
endif ()
|
||||||
|
|
||||||
|
# 6.3.0 is the first release whose arm64 assembly avoids x18, which macOS
|
||||||
|
# reserves and zeroes on every return to user space; 6.2.1 crashes on Apple
|
||||||
|
# silicon whenever CGAL falls back to exact arithmetic.
|
||||||
ExternalProject_Add(dep_GMP
|
ExternalProject_Add(dep_GMP
|
||||||
URL https://github.com/SoftFever/OrcaSlicer_deps/releases/download/gmp-6.2.1/gmp-6.2.1.tar.bz2
|
URL https://ftp.gnu.org/gnu/gmp/gmp-6.3.0.tar.xz
|
||||||
URL_HASH SHA256=eae9326beb4158c386e39a356818031bd28f3124cf915f8c5b1dc4c7a36b4d7c
|
https://github.com/SoftFever/OrcaSlicer_deps/releases/download/gmp-6.3.0/gmp-6.3.0.tar.xz
|
||||||
|
URL_HASH SHA256=a3c2b80201b89e68616f4ad30bc66aee4927c3ce50e33929ca819d5c43538898
|
||||||
DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/GMP
|
DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/GMP
|
||||||
PATCH_COMMAND git apply ${GMP_DIRECTORY_FLAG} --verbose ${CMAKE_CURRENT_LIST_DIR}/0001-GMP_GCC15.patch
|
PATCH_COMMAND git apply ${GMP_DIRECTORY_FLAG} --verbose ${CMAKE_CURRENT_LIST_DIR}/0001-GMP_GCC15.patch
|
||||||
BUILD_IN_SOURCE ON
|
BUILD_IN_SOURCE ON
|
||||||
|
|||||||
@@ -226,10 +226,10 @@ modules:
|
|||||||
sha256: f353e7b99bd0cbfc27ac9082de51acf32a8bc0b3e21ff9661ecca6f205ec1d81
|
sha256: f353e7b99bd0cbfc27ac9082de51acf32a8bc0b3e21ff9661ecca6f205ec1d81
|
||||||
dest: external-packages/OpenVDB
|
dest: external-packages/OpenVDB
|
||||||
|
|
||||||
# GMP 6.2.1
|
# GMP 6.3.0
|
||||||
- type: file
|
- type: file
|
||||||
url: https://github.com/SoftFever/OrcaSlicer_deps/releases/download/gmp-6.2.1/gmp-6.2.1.tar.bz2
|
url: https://ftp.gnu.org/gnu/gmp/gmp-6.3.0.tar.xz
|
||||||
sha256: eae9326beb4158c386e39a356818031bd28f3124cf915f8c5b1dc4c7a36b4d7c
|
sha256: a3c2b80201b89e68616f4ad30bc66aee4927c3ce50e33929ca819d5c43538898
|
||||||
dest: external-packages/GMP
|
dest: external-packages/GMP
|
||||||
|
|
||||||
# MPFR 4.2.2
|
# MPFR 4.2.2
|
||||||
|
|||||||
@@ -353,16 +353,18 @@ void PluginAuditManager::add_denied_filename(const std::string& filename)
|
|||||||
std::vector<std::string> PluginAuditManager::default_denied_filenames()
|
std::vector<std::string> PluginAuditManager::default_denied_filenames()
|
||||||
{
|
{
|
||||||
// AppConfig::config_path() picks .conf vs .ini on the USE_JSON_CONFIG ifdef and the app key
|
// AppConfig::config_path() picks .conf vs .ini on the USE_JSON_CONFIG ifdef and the app key
|
||||||
// on its mode, and is a non-static member we cannot call without an instance. Denying all
|
// on its mode, and is a non-static member we cannot call without an instance. Denying all
|
||||||
// four names is cheaper and more robust than replicating that; the two unused names cost one
|
// four names is cheaper and more robust than replicating that; the two unused names cost one
|
||||||
// string comparison each. Single-sourced here so install_hook() and the tests seed from the
|
// string comparison each. Keep the plugin install-state sidecar here too: it controls which
|
||||||
// same list and cannot drift apart.
|
// packages load on startup and must remain host-managed. Single-sourced so install_hook() and
|
||||||
|
// the tests seed from the same list and cannot drift apart.
|
||||||
return {
|
return {
|
||||||
SLIC3R_APP_KEY ".conf",
|
SLIC3R_APP_KEY ".conf",
|
||||||
GCODEVIEWER_APP_KEY ".conf",
|
GCODEVIEWER_APP_KEY ".conf",
|
||||||
SLIC3R_APP_KEY ".ini",
|
SLIC3R_APP_KEY ".ini",
|
||||||
GCODEVIEWER_APP_KEY ".ini",
|
GCODEVIEWER_APP_KEY ".ini",
|
||||||
secret_constants::USER_SECRET_FILENAME,
|
secret_constants::USER_SECRET_FILENAME,
|
||||||
|
".install_state.json",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -498,6 +500,12 @@ AuditDecision PluginAuditManager::check_path_access(const boost::filesystem::pat
|
|||||||
candidate = absolute_candidate;
|
candidate = absolute_candidate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Importing Python modules reads their source and may inspect package directories. These
|
||||||
|
// reads are normal interpreter work, not plugin data access; keep the exemption read-only
|
||||||
|
// and exclude third-party packages from the stdlib roots.
|
||||||
|
if (!is_write && is_stdlib_path(candidate))
|
||||||
|
return {true, ""};
|
||||||
|
|
||||||
// A root that doesn't allow writes only matches a read-shaped request; a write/create/
|
// A root that doesn't allow writes only matches a read-shaped request; a write/create/
|
||||||
// delete-shaped one falls through to "outside allowed root" for that root even though the
|
// delete-shaped one falls through to "outside allowed root" for that root even though the
|
||||||
// path is physically inside it.
|
// path is physically inside it.
|
||||||
@@ -528,6 +536,81 @@ AuditDecision PluginAuditManager::check_open(const std::string& path_str, const
|
|||||||
return check_path_access(boost::filesystem::path(path_str), is_write);
|
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)
|
void PluginAuditManager::report_violation(const AuditViolation& violation)
|
||||||
{
|
{
|
||||||
m_last_violation = violation;
|
m_last_violation = violation;
|
||||||
@@ -763,7 +846,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
|
// 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
|
// persists per plugin: the permission list matches targets by exact string, and the
|
||||||
// started function's repr embeds an address that changes every run.
|
// started function's repr embeds an address that changes every run.
|
||||||
targets.emplace_back(kThreadPermissionTarget);
|
targets.emplace_back("thread");
|
||||||
return targets;
|
return targets;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
@@ -819,20 +902,6 @@ bool has_permission(const std::vector<std::string>& granted, const std::string&
|
|||||||
return std::find(granted.begin(), granted.end(), target) != granted.end();
|
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,
|
bool persist_permission(const std::string& plugin_key,
|
||||||
PluginInstallState& state,
|
PluginInstallState& state,
|
||||||
std::vector<std::string>& permission_list,
|
std::vector<std::string>& permission_list,
|
||||||
@@ -842,7 +911,15 @@ bool persist_permission(const std::string& plugin_key,
|
|||||||
if (!PluginManager::instance().try_get_plugin_descriptor(plugin_key, descriptor) || descriptor.plugin_root.empty())
|
if (!PluginManager::instance().try_get_plugin_descriptor(plugin_key, descriptor) || descriptor.plugin_root.empty())
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
seed_install_metadata(descriptor, state);
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
if (!has_permission(permission_list, target))
|
if (!has_permission(permission_list, target))
|
||||||
permission_list.push_back(target);
|
permission_list.push_back(target);
|
||||||
@@ -928,13 +1005,6 @@ bool prompt_for_targets(AuditEventCategory category, const std::string& plugin_n
|
|||||||
return dialog.ShowModal() == wxID_YES;
|
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
|
// 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.
|
// 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)
|
void persist_grant(const std::string& plugin_key, AuditEventCategory category, bool by_event, const std::vector<std::string>& keys)
|
||||||
@@ -953,6 +1023,22 @@ void persist_grant(const std::string& plugin_key, AuditEventCategory category, b
|
|||||||
|
|
||||||
static PluginAuditManager::PermissionPrompt s_permission_prompt_for_testing;
|
static PluginAuditManager::PermissionPrompt s_permission_prompt_for_testing;
|
||||||
|
|
||||||
|
static const char* audit_category_name(AuditEventCategory category)
|
||||||
|
{
|
||||||
|
switch (category) {
|
||||||
|
case AuditEventCategory::FsRead: return "FsRead";
|
||||||
|
case AuditEventCategory::FsReadWrite: return "FsReadWrite";
|
||||||
|
case AuditEventCategory::FsCreate: return "FsCreate";
|
||||||
|
case AuditEventCategory::FsDelete: return "FsDelete";
|
||||||
|
case AuditEventCategory::Http: return "Http";
|
||||||
|
case AuditEventCategory::Socket: return "Socket";
|
||||||
|
case AuditEventCategory::ProcessCreate: return "ProcessCreate";
|
||||||
|
case AuditEventCategory::ProcessReplace: return "ProcessReplace";
|
||||||
|
case AuditEventCategory::Threading: return "Threading";
|
||||||
|
default: return "None";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Asks the user to grant an audited event, returning true when they approve an inline prompt.
|
// Asks the user to grant an audited event, returning true when they approve an inline prompt.
|
||||||
// `keys` are what a Yes saves: the unresolved targets, or the event-grant key when the event has
|
// `keys` are what a Yes saves: the unresolved targets, or the event-grant key when the event has
|
||||||
// no target (by_event), which the prompt does not show as a target.
|
// no target (by_event), which the prompt does not show as a target.
|
||||||
@@ -961,7 +1047,7 @@ static PluginAuditManager::PermissionPrompt s_permission_prompt_for_testing;
|
|||||||
// SlicingPipeline hook runs on the slicing worker thread (see PluginHooks.cpp), and
|
// SlicingPipeline hook runs on the slicing worker thread (see PluginHooks.cpp), and
|
||||||
// BackgroundSlicingProcess::stop()/stop_internal() park the UI thread until that worker stops.
|
// 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
|
// Blocking the worker on a marshaled modal -- which is safe for the plugin-load worker that
|
||||||
// request_permissions runs on -- would therefore deadlock the application (the
|
// 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
|
// 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
|
// 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
|
// prompt); the grant is persisted once the user accepts, so a later attempt succeeds without
|
||||||
@@ -970,6 +1056,16 @@ bool request_permission(AuditEventCategory category, const std::string& plugin_k
|
|||||||
const std::string& event_name, bool by_event, const std::vector<std::string>& keys)
|
const std::string& event_name, bool by_event, const std::vector<std::string>& keys)
|
||||||
{
|
{
|
||||||
const std::vector<std::string> shown = by_event ? std::vector<std::string>{} : keys;
|
const std::vector<std::string> shown = by_event ? std::vector<std::string>{} : keys;
|
||||||
|
std::string targets_text = by_event ? "<none>" : "";
|
||||||
|
for (const auto& target : shown) {
|
||||||
|
if (!targets_text.empty())
|
||||||
|
targets_text += ", ";
|
||||||
|
targets_text += target;
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_LOG_TRIVIAL(info) << "[AUDIT PROMPT] plugin=" << plugin_key << " event=" << event_name
|
||||||
|
<< " category=" << audit_category_name(category) << " targets=[" << targets_text << "]"
|
||||||
|
<< (by_event && !keys.empty() ? " grant=" + keys.front() : std::string{});
|
||||||
|
|
||||||
if (s_permission_prompt_for_testing)
|
if (s_permission_prompt_for_testing)
|
||||||
return s_permission_prompt_for_testing(event_name, shown);
|
return s_permission_prompt_for_testing(event_name, shown);
|
||||||
@@ -1085,105 +1181,6 @@ int audit_plugin_event(PluginAuditManager& mgr, const std::string& event_name, A
|
|||||||
|
|
||||||
} // namespace PluginAuditDetail
|
} // 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()
|
void PluginAuditManager::record_root_frame()
|
||||||
{
|
{
|
||||||
PyObject* frame = reinterpret_cast<PyObject*>(PyEval_GetFrame()); // borrowed reference
|
PyObject* frame = reinterpret_cast<PyObject*>(PyEval_GetFrame()); // borrowed reference
|
||||||
@@ -1235,6 +1232,18 @@ bool PluginAuditManager::is_stdlib_file(const std::string& filename)
|
|||||||
return it->second;
|
return it->second;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool PluginAuditManager::is_stdlib_path(const boost::filesystem::path& path)
|
||||||
|
{
|
||||||
|
if (path.empty())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
const auto inside_any = [&path](const std::vector<boost::filesystem::path>& dirs) {
|
||||||
|
return std::any_of(dirs.begin(), dirs.end(),
|
||||||
|
[&path](const boost::filesystem::path& dir) { return is_inside_allowed_root(path, dir); });
|
||||||
|
};
|
||||||
|
return inside_any(PluginAuditDetail::s_stdlib_dirs) && !inside_any(PluginAuditDetail::s_site_package_dirs);
|
||||||
|
}
|
||||||
|
|
||||||
void PluginAuditManager::release_frames(std::vector<PyObject*>& frames)
|
void PluginAuditManager::release_frames(std::vector<PyObject*>& frames)
|
||||||
{
|
{
|
||||||
if (frames.empty())
|
if (frames.empty())
|
||||||
@@ -1303,7 +1312,7 @@ int PluginAuditManager::audit_hook(const char* event, PyObject* args, void* user
|
|||||||
|
|
||||||
void PluginAuditManager::install_hook()
|
void PluginAuditManager::install_hook()
|
||||||
{
|
{
|
||||||
// Tells the stdlib's own frames apart, for the operation-root frames (see record_root_frame).
|
// Identifies stdlib frames for operation roots and stdlib paths for read-only access.
|
||||||
PluginAuditDetail::collect_stdlib_dirs();
|
PluginAuditDetail::collect_stdlib_dirs();
|
||||||
|
|
||||||
// data_dir() is the primary globally-allowed root during plugin execution: read+write. It
|
// data_dir() is the primary globally-allowed root during plugin execution: read+write. It
|
||||||
@@ -1320,8 +1329,9 @@ void PluginAuditManager::install_hook()
|
|||||||
// "cert" denied-path keyword seeded below.
|
// "cert" denied-path keyword seeded below.
|
||||||
add_global_allowed_root(resources_dir(), /*allow_write=*/false);
|
add_global_allowed_root(resources_dir(), /*allow_write=*/false);
|
||||||
|
|
||||||
// The user's app config and cloud credentials live directly inside data_dir(), so the
|
// The user's app config and cloud credentials live directly inside data_dir(), and plugin
|
||||||
// root just granted would otherwise expose them to any plugin. Deny them by name.
|
// install state lives beneath it, so the root just granted would otherwise expose them to
|
||||||
|
// plugins. Deny them by name.
|
||||||
//
|
//
|
||||||
// Seeded here rather than by each secret's owner because install_hook() runs during lazy
|
// Seeded here rather than by each secret's owner because install_hook() runs during lazy
|
||||||
// interpreter init and therefore provably precedes any plugin bytecode, whereas
|
// interpreter init and therefore provably precedes any plugin bytecode, whereas
|
||||||
|
|||||||
@@ -13,12 +13,6 @@
|
|||||||
|
|
||||||
namespace Slic3r {
|
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 {
|
struct AuditDecision {
|
||||||
bool allowed = true;
|
bool allowed = true;
|
||||||
std::string reason;
|
std::string reason;
|
||||||
@@ -136,12 +130,12 @@ public:
|
|||||||
AuditDecision check_path_access(const boost::filesystem::path& candidate, bool is_write);
|
AuditDecision check_path_access(const boost::filesystem::path& candidate, bool is_write);
|
||||||
AuditDecision check_open(const std::string& path, const std::string& mode);
|
AuditDecision check_open(const std::string& path, const std::string& mode);
|
||||||
|
|
||||||
// Ask the user to grant the requested permissions. The request may originate on a plugin
|
// Ask the user to grant the requested filesystem-read paths. The request may originate on a
|
||||||
// load worker, so the implementation marshals the modal dialog to the wx main thread.
|
// 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
|
// Returns true only when every missing path was granted and persisted; denial aborts the plugin
|
||||||
// only when every missing target was granted and persisted; denial aborts the plugin load
|
// load without adding a permission.
|
||||||
// without adding a permission.
|
bool request_filesystem_read_permissions(const std::string& plugin_key,
|
||||||
bool request_permissions(const std::string& plugin_key, const PluginPermissions& requested);
|
const std::vector<std::string>& paths);
|
||||||
|
|
||||||
void report_violation(const AuditViolation& violation);
|
void report_violation(const AuditViolation& violation);
|
||||||
bool audit_denial_pending() const;
|
bool audit_denial_pending() const;
|
||||||
@@ -179,6 +173,11 @@ private:
|
|||||||
|
|
||||||
static int audit_hook(const char* event, PyObject* args, void* user_data);
|
static int audit_hook(const char* event, PyObject* args, void* user_data);
|
||||||
|
|
||||||
|
// True for paths inside the interpreter's stdlib, excluding site-packages. Unlike
|
||||||
|
// is_stdlib_file(), this query is not cached because filesystem paths may resolve through
|
||||||
|
// symlinks that can change.
|
||||||
|
static bool is_stdlib_path(const boost::filesystem::path& path);
|
||||||
|
|
||||||
// Drops the references in `frames`, keeping the caller's error and denial state intact.
|
// Drops the references in `frames`, keeping the caller's error and denial state intact.
|
||||||
void release_frames(std::vector<PyObject*>& frames);
|
void release_frames(std::vector<PyObject*>& frames);
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ thread_local std::string g_active_plugin_key;
|
|||||||
std::mutex g_registry_mutex;
|
std::mutex g_registry_mutex;
|
||||||
std::unordered_map<std::string, std::vector<py::object>> g_pending_capabilities;
|
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, py::object> g_pending_package;
|
||||||
std::unordered_map<std::string, PluginPermissions> g_pending_permissions;
|
std::unordered_map<std::string, std::vector<std::string>> g_pending_fs_read_permissions;
|
||||||
struct PluginInstanceHandle
|
struct PluginInstanceHandle
|
||||||
{
|
{
|
||||||
// The C++ plugin interface points into a Python object. Keep both alive through one
|
// 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_package.erase(package);
|
||||||
}
|
}
|
||||||
|
|
||||||
g_pending_permissions.erase(plugin_key);
|
g_pending_fs_read_permissions.erase(plugin_key);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
@@ -113,7 +113,7 @@ void PythonPluginBridge::begin_plugin_capture(const std::string& plugin_key)
|
|||||||
// for this same entry path.
|
// for this same entry path.
|
||||||
g_pending_capabilities.erase(plugin_key);
|
g_pending_capabilities.erase(plugin_key);
|
||||||
g_pending_package.erase(plugin_key);
|
g_pending_package.erase(plugin_key);
|
||||||
g_pending_permissions.erase(plugin_key);
|
g_pending_fs_read_permissions.erase(plugin_key);
|
||||||
}
|
}
|
||||||
// From now until finalize/cancel, @orca.plugin and register_capability() calls made by
|
// From now until finalize/cancel, @orca.plugin and register_capability() calls made by
|
||||||
// Python code on this thread are attributed to this plugin.
|
// 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);
|
std::lock_guard<std::mutex> lock(g_registry_mutex);
|
||||||
g_pending_capabilities.erase(capture_key);
|
g_pending_capabilities.erase(capture_key);
|
||||||
g_pending_package.erase(capture_key);
|
g_pending_package.erase(capture_key);
|
||||||
g_pending_permissions.erase(capture_key);
|
g_pending_fs_read_permissions.erase(capture_key);
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -177,27 +177,23 @@ std::vector<CapturedCapability> PythonPluginBridge::finalize_plugin_capture(cons
|
|||||||
// Permission declarations are collected while register_capabilities() runs so the
|
// Permission declarations are collected while register_capabilities() runs so the
|
||||||
// plugin can describe its needs without touching wx from the Python load worker. Ask
|
// plugin can describe its needs without touching wx from the Python load worker. Ask
|
||||||
// only after registration returns, before capability instances are materialized.
|
// only after registration returns, before capability instances are materialized.
|
||||||
PluginPermissions requested_permissions;
|
std::vector<std::string> fs_read_permissions;
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(g_registry_mutex);
|
std::lock_guard<std::mutex> lock(g_registry_mutex);
|
||||||
auto it = g_pending_permissions.find(capture_key);
|
auto it = g_pending_fs_read_permissions.find(capture_key);
|
||||||
if (it != g_pending_permissions.end()) {
|
if (it != g_pending_fs_read_permissions.end()) {
|
||||||
requested_permissions = std::move(it->second);
|
fs_read_permissions = std::move(it->second);
|
||||||
g_pending_permissions.erase(it);
|
g_pending_fs_read_permissions.erase(it);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const bool has_permission_request =
|
if (!fs_read_permissions.empty()) {
|
||||||
!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;
|
bool granted = false;
|
||||||
{
|
{
|
||||||
py::gil_scoped_release release;
|
py::gil_scoped_release release;
|
||||||
granted = PluginAuditManager::instance().request_permissions(plugin_key, requested_permissions);
|
granted = PluginAuditManager::instance().request_filesystem_read_permissions(plugin_key, fs_read_permissions);
|
||||||
}
|
}
|
||||||
if (!granted) {
|
if (!granted) {
|
||||||
error = _u8L("Plugin permission request denied");
|
error = _u8L("Plugin filesystem read permission request denied");
|
||||||
BOOST_LOG_TRIVIAL(warning) << error << " for key " << plugin_key;
|
BOOST_LOG_TRIVIAL(warning) << error << " for key " << plugin_key;
|
||||||
discard_pending_for_key();
|
discard_pending_for_key();
|
||||||
clear_active_key();
|
clear_active_key();
|
||||||
@@ -326,7 +322,7 @@ void PythonPluginBridge::cancel_plugin_capture(const std::string& plugin_key)
|
|||||||
// may already have registered under this key.
|
// may already have registered under this key.
|
||||||
g_pending_capabilities.erase(plugin_key);
|
g_pending_capabilities.erase(plugin_key);
|
||||||
g_pending_package.erase(plugin_key);
|
g_pending_package.erase(plugin_key);
|
||||||
g_pending_permissions.erase(plugin_key);
|
g_pending_fs_read_permissions.erase(plugin_key);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (g_active_plugin_key == plugin_key)
|
if (g_active_plugin_key == plugin_key)
|
||||||
@@ -353,7 +349,7 @@ void PythonPluginBridge::clear_pending_captures()
|
|||||||
(void) pkg.release();
|
(void) pkg.release();
|
||||||
}
|
}
|
||||||
g_pending_package.clear();
|
g_pending_package.clear();
|
||||||
g_pending_permissions.clear();
|
g_pending_fs_read_permissions.clear();
|
||||||
g_active_plugin_key.clear();
|
g_active_plugin_key.clear();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -362,7 +358,7 @@ void PythonPluginBridge::clear_pending_captures()
|
|||||||
BOOST_LOG_TRIVIAL(info) << "Clearing " << g_pending_capabilities.size() << " pending Python plugin capture(s)";
|
BOOST_LOG_TRIVIAL(info) << "Clearing " << g_pending_capabilities.size() << " pending Python plugin capture(s)";
|
||||||
g_pending_capabilities.clear();
|
g_pending_capabilities.clear();
|
||||||
g_pending_package.clear();
|
g_pending_package.clear();
|
||||||
g_pending_permissions.clear();
|
g_pending_fs_read_permissions.clear();
|
||||||
g_active_plugin_key.clear();
|
g_active_plugin_key.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -562,54 +558,24 @@ void bind_python_api(pybind11::module_& m)
|
|||||||
|
|
||||||
m.def(
|
m.def(
|
||||||
"request_permissions",
|
"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())
|
if (g_active_plugin_key.empty())
|
||||||
throw py::value_error("request_permissions() called outside plugin discovery context");
|
throw py::value_error("request_permissions() called outside plugin discovery context");
|
||||||
|
|
||||||
std::lock_guard<std::mutex> lock(g_registry_mutex);
|
std::lock_guard<std::mutex> lock(g_registry_mutex);
|
||||||
PluginPermissions& requested = g_pending_permissions[g_active_plugin_key];
|
auto& requested = g_pending_fs_read_permissions[g_active_plugin_key];
|
||||||
|
for (const std::string& path : fs_read) {
|
||||||
const auto add_targets = [](std::vector<std::string>& bucket, const char* name,
|
if (path.empty())
|
||||||
const std::vector<std::string>& targets) {
|
throw py::value_error("request_permissions(fs_read=...) does not accept empty paths");
|
||||||
for (const std::string& target : targets) {
|
if (std::find(requested.begin(), requested.end(), path) == requested.end())
|
||||||
if (target.empty())
|
requested.push_back(path);
|
||||||
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>{},
|
py::arg("fs_read") = std::vector<std::string>{},
|
||||||
py::arg("fs_readwrite") = std::vector<std::string>{},
|
R"pbdoc(Request filesystem read permissions while OrcaSlicer loads your module.
|
||||||
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.
|
|
||||||
|
|
||||||
Each bucket takes the target strings the plugin expects the corresponding audit event to
|
The host presents the request to the user after register_capabilities() returns. Denying the
|
||||||
report: filesystem paths for fs_read and fs_readwrite, URLs for network_http, socket addresses
|
request aborts the plugin load.)pbdoc");
|
||||||
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) {
|
m.def("plugin", [](py::object cls) {
|
||||||
if (g_active_plugin_key.empty())
|
if (g_active_plugin_key.empty())
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ void seed_denied_keywords()
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
TEST_CASE("Plugin audit denies app config and token filenames anywhere", "[audit]")
|
TEST_CASE("Plugin audit denies host-managed filenames anywhere", "[audit]")
|
||||||
{
|
{
|
||||||
seed_denied_names();
|
seed_denied_names();
|
||||||
const PluginAuditManager& mgr = PluginAuditManager::instance();
|
const PluginAuditManager& mgr = PluginAuditManager::instance();
|
||||||
@@ -81,6 +81,7 @@ TEST_CASE("Plugin audit denies app config and token filenames anywhere", "[audit
|
|||||||
CHECK(mgr.is_denied_filename(fs::path(SLIC3R_APP_KEY ".ini")));
|
CHECK(mgr.is_denied_filename(fs::path(SLIC3R_APP_KEY ".ini")));
|
||||||
CHECK(mgr.is_denied_filename(fs::path(GCODEVIEWER_APP_KEY ".ini")));
|
CHECK(mgr.is_denied_filename(fs::path(GCODEVIEWER_APP_KEY ".ini")));
|
||||||
CHECK(mgr.is_denied_filename(fs::path(secret_constants::USER_SECRET_FILENAME)));
|
CHECK(mgr.is_denied_filename(fs::path(secret_constants::USER_SECRET_FILENAME)));
|
||||||
|
CHECK(mgr.is_denied_filename(fs::path(".install_state.json")));
|
||||||
}
|
}
|
||||||
|
|
||||||
SECTION("companions holding the same secrets are denied by the prefix rule")
|
SECTION("companions holding the same secrets are denied by the prefix rule")
|
||||||
@@ -136,6 +137,7 @@ TEST_CASE("Plugin audit deny beats allowed roots", "[audit]")
|
|||||||
|
|
||||||
const fs::path conf = fs::path(data_dir()) / (SLIC3R_APP_KEY ".conf");
|
const fs::path conf = fs::path(data_dir()) / (SLIC3R_APP_KEY ".conf");
|
||||||
const fs::path token = fs::path(data_dir()) / secret_constants::USER_SECRET_FILENAME;
|
const fs::path token = fs::path(data_dir()) / secret_constants::USER_SECRET_FILENAME;
|
||||||
|
const fs::path install_state = fs::path(data_dir()) / "plugins" / "example" / ".install_state.json";
|
||||||
|
|
||||||
SECTION("a non-denied file inside the allowed root is writable (root really grants writes)")
|
SECTION("a non-denied file inside the allowed root is writable (root really grants writes)")
|
||||||
{
|
{
|
||||||
@@ -176,6 +178,17 @@ TEST_CASE("Plugin audit deny beats allowed roots", "[audit]")
|
|||||||
CHECK_FALSE(decision.allowed);
|
CHECK_FALSE(decision.allowed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SECTION("plugin install state is blocked for reads and writes despite the allowed root")
|
||||||
|
{
|
||||||
|
AuditDecision read_decision = mgr.check_open(install_state.string(), "r");
|
||||||
|
CHECK_FALSE(read_decision.allowed);
|
||||||
|
CHECK(read_decision.reason == "denied filename");
|
||||||
|
|
||||||
|
AuditDecision write_decision = mgr.check_open(install_state.string(), "w");
|
||||||
|
CHECK_FALSE(write_decision.allowed);
|
||||||
|
CHECK(write_decision.reason == "denied filename");
|
||||||
|
}
|
||||||
|
|
||||||
SECTION("a traversal path resolving to the config is blocked")
|
SECTION("a traversal path resolving to the config is blocked")
|
||||||
{
|
{
|
||||||
const fs::path traversal = fs::path(data_dir()) / "plugins" / ".." / (SLIC3R_APP_KEY ".conf");
|
const fs::path traversal = fs::path(data_dir()) / "plugins" / ".." / (SLIC3R_APP_KEY ".conf");
|
||||||
@@ -1119,3 +1132,37 @@ TEST_CASE("Stdlib code is recognized through a symlinked Python home", "[audit][
|
|||||||
CHECK_FALSE(PluginAuditManager::is_stdlib_file((harness.outside.path() / "plugin.py").string()));
|
CHECK_FALSE(PluginAuditManager::is_stdlib_file((harness.outside.path() / "plugin.py").string()));
|
||||||
CHECK_FALSE(PluginAuditManager::is_stdlib_file("<string>"));
|
CHECK_FALSE(PluginAuditManager::is_stdlib_file("<string>"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("A plugin may read the CPython stdlib without a prompt, but not site-packages or for writing", "[audit][Python]")
|
||||||
|
{
|
||||||
|
AuditHarness harness;
|
||||||
|
if (!harness.initialized)
|
||||||
|
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
|
||||||
|
harness.add_plugin(PLUGIN);
|
||||||
|
py::gil_scoped_acquire gil;
|
||||||
|
PythonSide python(harness);
|
||||||
|
|
||||||
|
const py::dict paths = py::module_::import("sysconfig").attr("get_paths")();
|
||||||
|
const fs::path stdlib = py::str(paths["stdlib"]).cast<std::string>();
|
||||||
|
const fs::path purelib = py::str(paths["purelib"]).cast<std::string>();
|
||||||
|
// os.py ships with every CPython installation. A site-packages path is third-party code even
|
||||||
|
// when the directory is empty: the prompt must come before the missing file is noticed.
|
||||||
|
const fs::path stdlib_file = stdlib / "os.py";
|
||||||
|
REQUIRE(fs::exists(stdlib_file));
|
||||||
|
|
||||||
|
python.globals["STDLIB_FILE"] = stdlib_file.string();
|
||||||
|
python.globals["SITE_PACKAGE_FILE"] = (purelib / "regression_probe.py").string();
|
||||||
|
python.run_as(PLUGIN, R"PY(
|
||||||
|
read_outcome = attempt(lambda: open(STDLIB_FILE).read())
|
||||||
|
write_outcome = attempt(lambda: open(STDLIB_FILE, "r+").close())
|
||||||
|
site_packages_outcome = attempt(lambda: open(SITE_PACKAGE_FILE).close())
|
||||||
|
)PY");
|
||||||
|
|
||||||
|
CHECK(python.str("read_outcome") == "ok");
|
||||||
|
CHECK(python.str("write_outcome") == "denied");
|
||||||
|
CHECK(python.str("site_packages_outcome") == "denied");
|
||||||
|
// Only the write and the site-packages read are asked about; the stdlib read is implicit.
|
||||||
|
const std::string write_prompt = "open: " + python.str("STDLIB_FILE") + "\n";
|
||||||
|
const std::string site_packages_prompt = "open: " + python.str("SITE_PACKAGE_FILE") + "\n";
|
||||||
|
CHECK(describe(harness.prompts) == write_prompt + site_packages_prompt);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user