feat: plugin permission system

This commit is contained in:
Alexander Medvedev
2026-04-30 19:33:31 +02:00
parent 6581924760
commit bdc9b79e42
15 changed files with 357 additions and 15 deletions

View File

@@ -88,6 +88,7 @@ pub fn plugin_impl(_attr: TokenStream, item: TokenStream) -> TokenStream {
authors: env!("CARGO_PKG_AUTHORS").split(',').map(String::from).collect(),
description: env!("CARGO_PKG_DESCRIPTION").to_string(),
dependencies: Vec::new(),
permissions: Vec::new(),
}
});

View File

@@ -12,6 +12,7 @@ use tracing::{debug, warn};
pub mod fun;
pub mod logging;
pub mod networking;
pub mod plugins;
pub mod recipe;
pub mod resource_pack;
@@ -22,6 +23,7 @@ pub use networking::auth::AuthenticationConfig;
pub use networking::compression::CompressionConfig;
pub use networking::lan_broadcast::LANBroadcastConfig;
pub use networking::rcon::RCONConfig;
pub use plugins::PluginsConfig;
pub use pvp::PVPConfig;
pub use server_links::ServerLinksConfig;
@@ -74,6 +76,8 @@ pub struct AdvancedConfiguration {
pub fun: FunConfig,
/// Recipe-related configuration.
pub recipe: RecipeConfig,
/// Plugin-related configuration.
pub plugins: PluginsConfig,
}
/// Basic configuration for core server settings.

View File

@@ -0,0 +1,8 @@
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize, Default)]
#[serde(default)]
pub struct PluginsConfig {
/// List of permissions that are globally blocked for all plugins.
pub blocked_permissions: Vec<String>,
}

View File

@@ -7,7 +7,7 @@
//! # Quick start
//!
//! ```rust,ignore
//! use pumpkin_plugin_api::{Plugin, PluginMetadata, Context, register_plugin};
//! use pumpkin_plugin_api::{Plugin, PluginMetadata, Context, register_plugin, permissions::permissions};
//!
//! struct MyPlugin;
//!
@@ -20,6 +20,7 @@
//! authors: vec!["you".into()],
//! description: "An example plugin.".into(),
//! dependencies: vec![],
//! permissions: vec![permissions::NETWORK_DNS.into()],
//! }
//! }
//! }
@@ -34,6 +35,10 @@ use crate::{
pub mod commands;
pub mod events;
/// Constants for plugin permissions.
///
/// Use these in your `PluginMetadata` to request access to specific host features.
pub mod permissions;
pub mod scheduler;
pub mod command {
@@ -77,6 +82,8 @@ pub struct PluginMetadata {
pub description: String,
/// The list of plugin dependencies.
pub dependencies: Vec<String>,
/// The list of permissions requested by the plugin.
pub permissions: Vec<String>,
}
impl wit::exports::pumpkin::plugin::metadata::Guest for Component {
@@ -89,6 +96,7 @@ impl wit::exports::pumpkin::plugin::metadata::Guest for Component {
authors: metadata.authors,
description: metadata.description,
dependencies: metadata.dependencies,
permissions: metadata.permissions,
}
}
}

View File

@@ -0,0 +1,65 @@
/// Constants for plugin permissions.
///
/// Use these in your `PluginMetadata` to request access to specific host features.
/// Allows the plugin to perform DNS resolution.
pub const NETWORK_DNS: &str = "network.dns";
/// Allows the plugin to use TCP sockets.
pub const NETWORK_TCP: &str = "network.tcp";
/// Allows the plugin to use UDP sockets.
pub const NETWORK_UDP: &str = "network.udp";
/// Allows the plugin to initiate TCP connections.
pub const NETWORK_TCP_CONNECT: &str = "network.tcp.connect";
/// Allows the plugin to bind TCP listeners (accept inbound connections).
pub const NETWORK_TCP_BIND: &str = "network.tcp.bind";
/// Allows the plugin to send and receive UDP packets to specific destinations.
pub const NETWORK_UDP_CONNECT: &str = "network.udp.connect";
/// Allows the plugin to bind UDP sockets to local ports.
pub const NETWORK_UDP_BIND: &str = "network.udp.bind";
/// Allows the plugin to send datagram on non-connected UDP socket.
pub const NETWORK_UDP_OUTGOING_DATAGRAM: &str = "network.udp.outgoingdatagram";
/// Restricts all networking permissions to loopback addresses (localhost) only.
pub const NETWORK_LOOPBACK: &str = "network.loopback";
/// Allows the plugin to make outbound TCP/UDP connections.
/// **Warning:** This is a powerful permission.
pub const NETWORK_OUTBOUND: &str = "network.outbound";
/// Allows the plugin to read files from the server's file system outside of its data folder.
pub const FS_READ: &str = "fs.read";
/// Allows the plugin to write files to the server's file system outside of its data folder.
pub const FS_WRITE: &str = "fs.write";
/// Allows the plugin to read files within its own data folder (`plugins/<name>`).
pub const FS_READ_DATA: &str = "fs.read.data";
/// Allows the plugin to write files within its own data folder (`plugins/<name>`).
pub const FS_WRITE_DATA: &str = "fs.write.data";
/// Allows the plugin to read all environment variables.
pub const SYS_ENV: &str = "sys.env";
/// Allows the plugin to read specific environment variables.
/// Used with a prefix like "sys.env.PATH".
pub const SYS_ENV_PREFIX: &str = "sys.env.";
/// Allows the plugin to read system information (CPU, Memory, OS).
pub const SYS_INFO: &str = "sys.info";
/// Allows the plugin to read CPU information.
pub const SYS_INFO_CPU: &str = "sys.info.cpu";
/// Allows the plugin to read RAM information.
pub const SYS_INFO_RAM: &str = "sys.info.ram";
/// Allows the plugin to read OS information.
pub const SYS_INFO_OS: &str = "sys.info.os";

View File

@@ -11,6 +11,8 @@ interface metadata {
description: string,
/// Plugin dependencies.
dependencies: list<string>,
/// Permissions requested by the plugin.
permissions: list<string>,
}
get-metadata: func() -> plugin-metadata;

View File

@@ -23,6 +23,15 @@ interface server {
end,
}
record sys-info {
cpu-count: option<u32>,
total-memory: option<u64>,
used-memory: option<u64>,
os-name: option<string>,
os-version: option<string>,
pumpkin-version: string,
}
variant command-sender {
console,
player(player),
@@ -30,6 +39,10 @@ interface server {
/// Represents the global server instance.
resource server {
/// Returns system information (CPU, Memory, OS).
/// Fields are returned only if the plugin has the corresponding permissions.
get-sys-info: func() -> sys-info;
/// Returns the current difficulty level of the server.
get-difficulty: func() -> difficulty;

View File

@@ -66,6 +66,11 @@ impl Context {
}
}
#[must_use]
pub const fn get_metadata(&self) -> &PluginMetadata {
&self.metadata
}
/// Retrieves the data folder path for the plugin, creating it if it does not exist.
///
/// # Returns

View File

@@ -24,6 +24,8 @@ pub struct PluginMetadata {
pub description: String,
/// The dependencies of the plugin.
pub dependencies: Vec<String>,
/// The permissions requested by the plugin.
pub permissions: Vec<String>,
}
/// This type represents a future for the plugin.

View File

@@ -2,9 +2,11 @@ use std::{fs, path::Path, sync::Arc};
use thiserror::Error;
use tokio::sync::Mutex;
use wasmtime::{Cache, CacheConfig, Engine, Store, component::Component, component::Linker};
use wasmtime_wasi::WasiCtxBuilder;
use wasmtime_wasi::{WasiCtxBuilder, sockets::SocketAddrUse};
use crate::plugin::{Context, PluginMetadata, loader::wasm::wasm_host::state::PluginHostState};
use crate::plugin::{
Context, PluginMetadata, loader::wasm::wasm_host::state::PluginHostState, permissions,
};
pub mod args;
pub mod logging;
@@ -144,13 +146,117 @@ impl WasmPlugin {
let mut builder = WasiCtxBuilder::new();
let metadata = context.get_metadata();
let blocked_permissions = &context.server.advanced_config.plugins.blocked_permissions;
let filtered_permissions: Vec<String> = metadata
.permissions
.iter()
.filter(|p| !blocked_permissions.iter().any(|blocked| blocked == *p))
.cloned()
.collect();
let has_permission = |p: &str| filtered_permissions.iter().any(|perm| perm == p);
if has_permission(permissions::NETWORK_DNS) {
builder.allow_ip_name_lookup(true);
}
let tcp_allowed = has_permission(permissions::NETWORK_TCP);
let udp_allowed = has_permission(permissions::NETWORK_UDP);
let tcp_connect = tcp_allowed || has_permission(permissions::NETWORK_TCP_CONNECT);
let tcp_bind = tcp_allowed || has_permission(permissions::NETWORK_TCP_BIND);
let udp_connect = udp_allowed || has_permission(permissions::NETWORK_UDP_CONNECT);
let udp_bind = udp_allowed || has_permission(permissions::NETWORK_UDP_BIND);
let udp_outgoing_datagram =
udp_allowed || has_permission(permissions::NETWORK_UDP_OUTGOING_DATAGRAM);
let loopback_only = has_permission(permissions::NETWORK_LOOPBACK);
builder.allow_tcp(tcp_connect || tcp_bind);
builder.allow_udp(udp_connect || udp_bind);
builder.socket_addr_check(move |addr, reason| {
Box::pin(async move {
let ok = match reason {
SocketAddrUse::TcpConnect => tcp_connect,
SocketAddrUse::TcpBind => tcp_bind,
SocketAddrUse::UdpConnect => udp_connect,
SocketAddrUse::UdpBind => udp_bind,
SocketAddrUse::UdpOutgoingDatagram => udp_outgoing_datagram,
};
if loopback_only {
ok && addr.ip().is_loopback()
} else {
ok
}
})
});
if has_permission(permissions::NETWORK_OUTBOUND) {
builder.inherit_network();
}
// --- System Permissions ---
// Environment Variables
if has_permission(permissions::SYS_ENV) {
builder.inherit_env();
} else {
for (key, value) in std::env::vars() {
let perm = format!("{}{}", permissions::SYS_ENV_PREFIX, key);
if has_permission(&perm) {
builder.env(key, value);
}
}
}
let data_folder = context.get_data_folder();
let preopen_path =
if has_permission(permissions::FS_READ) || has_permission(permissions::FS_WRITE) {
Path::new(".")
} else {
data_folder.as_path()
};
// Determine permissions for the preopened directory
let (dir_perms, file_perms) = if has_permission(permissions::FS_WRITE) {
(
wasmtime_wasi::DirPerms::all(),
wasmtime_wasi::FilePerms::all(),
)
} else if has_permission(permissions::FS_READ) {
(
wasmtime_wasi::DirPerms::READ,
wasmtime_wasi::FilePerms::READ,
)
} else {
// Scoped to data folder
let can_write = has_permission(permissions::FS_WRITE_DATA);
if can_write {
(
wasmtime_wasi::DirPerms::all(),
wasmtime_wasi::FilePerms::all(),
)
} else {
// Default to READ if no write permission is given for data folder
// (Plugins should at least be able to read their own config)
(
wasmtime_wasi::DirPerms::READ,
wasmtime_wasi::FilePerms::READ,
)
}
};
builder.preopened_dir(
context.get_data_folder(),
context.get_data_folder().to_string_lossy(),
wasmtime_wasi::DirPerms::all(),
wasmtime_wasi::FilePerms::all(),
preopen_path,
preopen_path.to_string_lossy(),
dir_perms,
file_perms,
)?;
store.data_mut().permissions = filtered_permissions;
store.data_mut().wasi_ctx = builder.build();
store.data_mut().server = Some(context.server.clone());

View File

@@ -52,6 +52,7 @@ pub struct PluginHostState {
pub resource_table: ResourceTable,
pub plugin: Option<Weak<WasmPlugin>>,
pub server: Option<Arc<Server>>,
pub permissions: Vec<String>,
}
impl Default for PluginHostState {
@@ -69,6 +70,7 @@ impl PluginHostState {
resource_table,
plugin: None,
server: None,
permissions: Vec::new(),
}
}

View File

@@ -62,8 +62,11 @@ pub async fn init_plugin(
authors: metadata.authors,
description: metadata.description,
dependencies: metadata.dependencies,
permissions: metadata.permissions.clone(),
};
store.data_mut().permissions = metadata.permissions.clone();
Ok((
WasmPlugin {
plugin_instance: PluginInstance::V0_1(plugin),

View File

@@ -6,15 +6,18 @@ use crate::command::CommandSender;
use pumpkin::plugin::server::CommandSender as WasmCommandSender;
use super::player::text_component_from_resource;
use crate::plugin::loader::wasm::wasm_host::{
state::{PluginHostState, ServerResource},
wit::v0_1::pumpkin::{
self,
plugin::{
player::Player,
server::{Difficulty, Dimension, Server},
use crate::plugin::{
loader::wasm::wasm_host::{
state::{PluginHostState, ServerResource},
wit::v0_1::pumpkin::{
self,
plugin::{
player::Player,
server::{Difficulty, Dimension, Server, SysInfo},
},
},
},
permissions,
};
impl PluginHostState {
@@ -28,6 +31,42 @@ impl PluginHostState {
impl pumpkin::plugin::server::Host for PluginHostState {}
impl pumpkin::plugin::server::HostServer for PluginHostState {
async fn get_sys_info(&mut self, _res: Resource<Server>) -> wasmtime::Result<SysInfo> {
let has_perm = |p: &str| self.permissions.iter().any(|perm| perm == p);
let mut sys = sysinfo::System::new_all();
sys.refresh_all();
let cpu_count = if has_perm(permissions::SYS_INFO) || has_perm(permissions::SYS_INFO_CPU) {
Some(sys.cpus().len() as u32)
} else {
None
};
let (total_memory, used_memory) =
if has_perm(permissions::SYS_INFO) || has_perm(permissions::SYS_INFO_RAM) {
(Some(sys.total_memory()), Some(sys.used_memory()))
} else {
(None, None)
};
let (os_name, os_version) =
if has_perm(permissions::SYS_INFO) || has_perm(permissions::SYS_INFO_OS) {
(sysinfo::System::name(), sysinfo::System::os_version())
} else {
(None, None)
};
Ok(SysInfo {
cpu_count,
total_memory,
used_memory,
os_name,
os_version,
pumpkin_version: env!("CARGO_PKG_VERSION").to_string(),
})
}
async fn get_difficulty(&mut self, res: Resource<Server>) -> wasmtime::Result<Difficulty> {
let resource = self.get_server_res(&res)?;

View File

@@ -14,10 +14,15 @@ use tokio::{
sync::{Notify, RwLock},
task::JoinHandle,
};
use tracing::{debug, error, info};
use tracing::{debug, error, info, warn};
pub mod api;
pub mod loader;
/// Constants for plugin permissions.
///
/// Plugins can request these permissions in their metadata to access specific
/// host features.
pub mod permissions;
use crate::{LOGGER_IMPL, plugin::loader::wasm::WasmPluginLoader, server::Server};
pub use api::*;
@@ -462,6 +467,7 @@ impl PluginManager {
}
/// Spawn initialization for a single plugin
#[expect(clippy::too_many_lines)]
async fn spawn_plugin_initialization(
&self,
mut instance: Box<dyn Plugin>,
@@ -541,6 +547,13 @@ impl PluginManager {
state_notify.notify_waiters();
info!("Loaded {} ({})", metadata.name, metadata.version);
if !metadata.permissions.is_empty() {
warn!(
"Plugin \"{}\" uses the following permissions: {:?}",
metadata.name, metadata.permissions
);
}
}
Err(e) => {
// Handle initialization failure

View File

@@ -0,0 +1,71 @@
/// Constants for plugin permissions.
///
/// Plugins can request these permissions in their metadata to access specific
/// host features.
/// Allows the plugin to perform DNS resolution (resolving hostnames to IP addresses).
///
/// Corresponds to the `wasi:sockets/ip-name-lookup` interface.
pub const NETWORK_DNS: &str = "network.dns";
/// Allows the plugin to use TCP sockets.
pub const NETWORK_TCP: &str = "network.tcp";
/// Allows the plugin to use UDP sockets.
pub const NETWORK_UDP: &str = "network.udp";
/// Allows the plugin to initiate TCP connections.
pub const NETWORK_TCP_CONNECT: &str = "network.tcp.connect";
/// Allows the plugin to bind TCP listeners (accept inbound connections).
pub const NETWORK_TCP_BIND: &str = "network.tcp.bind";
/// Allows the plugin to send and receive UDP packets to specific destinations.
pub const NETWORK_UDP_CONNECT: &str = "network.udp.connect";
/// Allows the plugin to bind UDP sockets to local ports.
pub const NETWORK_UDP_BIND: &str = "network.udp.bind";
/// Allows the plugin to send datagram on non-connected UDP socket.
pub const NETWORK_UDP_OUTGOING_DATAGRAM: &str = "network.udp.outgoingdatagram";
/// Restricts all networking permissions to loopback addresses (localhost) only.
pub const NETWORK_LOOPBACK: &str = "network.loopback";
/// Allows the plugin to make outbound TCP/UDP connections.
///
/// This gives the plugin full access to the host's network interfaces.
/// **Warning:** This is a powerful permission and should only be granted to trusted plugins.
pub const NETWORK_OUTBOUND: &str = "network.outbound";
/// Allows the plugin to read files from the server's file system outside of its data folder.
pub const FS_READ: &str = "fs.read";
/// Allows the plugin to write files to the server's file system outside of its data folder.
pub const FS_WRITE: &str = "fs.write";
/// Allows the plugin to read files within its own data folder (`plugins/<name>`).
/// This is granted by default if any other FS permission is not specified, but can be explicitly requested.
pub const FS_READ_DATA: &str = "fs.read.data";
/// Allows the plugin to write files within its own data folder (`plugins/<name>`).
pub const FS_WRITE_DATA: &str = "fs.write.data";
/// Allows the plugin to read all environment variables.
pub const SYS_ENV: &str = "sys.env";
/// Allows the plugin to read specific environment variables.
/// Used with a prefix like "sys.env.PATH".
pub const SYS_ENV_PREFIX: &str = "sys.env.";
/// Allows the plugin to read system information (CPU, Memory, OS).
pub const SYS_INFO: &str = "sys.info";
/// Allows the plugin to read CPU information.
pub const SYS_INFO_CPU: &str = "sys.info.cpu";
/// Allows the plugin to read RAM information.
pub const SYS_INFO_RAM: &str = "sys.info.ram";
/// Allows the plugin to read OS information.
pub const SYS_INFO_OS: &str = "sys.info.os";