Skip to content

Commit 15d4821

Browse files
authored
feat: Implement plugin permission system (#132)
* fix: Cleanup senders before stopping the network stack * feat: Add plugin permission checks
1 parent 57a1a03 commit 15d4821

14 files changed

Lines changed: 108 additions & 69 deletions

File tree

controller/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ anyhow = "1.0.98"
1515

1616
# Getters and bitflags
1717
getset = "0.1.5"
18-
bitflags = "2.9.0"
18+
bitflags = { version = "2.9.0", features = ["serde"] }
1919

2020
# Signal handling
2121
ctrlc = "3.4.6"
Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,17 @@
11
# This configuration is crucial for granting the plugins their required permissions
2-
# https://httprafa.github.io/atomic-cloud/controller/plugins/wasm/permissions/
2+
# https://httprafa.github.io/atomic-cloud/plugins/wasm/permissions/
33

44
[[plugins]]
55
name = "local"
6-
inherit_stdio = false
7-
inherit_args = false
8-
inherit_env = false
9-
inherit_network = false
10-
allow_ip_name_lookup = false
11-
allow_http = false
12-
allow_process = true
13-
allow_remove_dir_all = true
6+
permissions = "ALLOW_PROCESS | ALLOW_REMOVE_DIR_ALL"
147
mounts = []
158

169
[[plugins]]
1710
name = "pelican"
18-
inherit_stdio = false
19-
inherit_args = false
20-
inherit_env = false
21-
inherit_network = true
22-
allow_ip_name_lookup = true
23-
allow_http = true
24-
allow_process = false
25-
allow_remove_dir_all = false
11+
permissions = "INHERIT_NETWORK | ALLOW_IP_NAME_LOOKUP | ALLOW_HTTP"
12+
mounts = []
13+
14+
[[plugins]]
15+
name = "cloudflare"
16+
permissions = "INHERIT_NETWORK | ALLOW_IP_NAME_LOOKUP | ALLOW_HTTP"
2617
mounts = []

controller/src/application.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,9 @@ impl Controller {
201201
// Cleanup node manager
202202
self.nodes.cleanup().await?;
203203

204+
// Cleanup subscription manager
205+
self.shared.subscribers.cleanup().await?;
206+
204207
// Cleanup screen manager
205208
self.shared.screens.cleanup().await?;
206209

controller/src/application/plugin/runtime/wasm.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use std::sync::Arc;
22

33
use anyhow::{anyhow, Result};
44
use common::error::FancyError;
5+
use config::Permissions;
56
use generated::{exports::plugin::system::bridge, plugin::system::data_types};
67
use listener::PluginListener;
78
use node::PluginNode;
@@ -51,6 +52,7 @@ pub(crate) struct PluginState {
5152

5253
/* Plugin */
5354
name: String,
55+
permissions: Permissions,
5456

5557
/* Wasmtime */
5658
wasi: WasiCtx,

controller/src/application/plugin/runtime/wasm/config.rs

Lines changed: 20 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use std::path::PathBuf;
22

33
use anyhow::Result;
4+
use bitflags::bitflags;
45
use regex::Regex;
56
use serde::{Deserialize, Serialize};
67
use simplelog::warn;
@@ -22,49 +23,33 @@ pub struct PluginsConfig {
2223
plugins: Vec<PluginConfig>,
2324
}
2425

25-
#[allow(
26-
clippy::struct_excessive_bools,
27-
reason = "Mybe refactor this in the future to use bitflags"
28-
)]
26+
bitflags! {
27+
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
28+
#[serde(transparent)]
29+
pub struct Permissions: u32 {
30+
const INHERIT_STDIO = 1;
31+
const INHERIT_ARGS = 1 << 1;
32+
const INHERIT_ENV = 1 << 2;
33+
const INHERIT_NETWORK = 1 << 3;
34+
const ALLOW_IP_NAME_LOOKUP = 1 << 4;
35+
const ALLOW_HTTP = 1 << 5;
36+
const ALLOW_PROCESS = 1 << 6;
37+
const ALLOW_REMOVE_DIR_ALL = 1 << 7;
38+
const ALL = Self::INHERIT_STDIO.bits() | Self::INHERIT_ARGS.bits() | Self::INHERIT_ENV.bits() | Self::INHERIT_NETWORK.bits() | Self::ALLOW_IP_NAME_LOOKUP.bits() | Self::ALLOW_HTTP.bits() | Self::ALLOW_PROCESS.bits() | Self::ALLOW_REMOVE_DIR_ALL.bits();
39+
}
40+
}
41+
2942
#[derive(Serialize, Deserialize)]
3043
pub struct PluginConfig {
3144
name: String,
32-
inherit_stdio: bool,
33-
inherit_args: bool,
34-
inherit_env: bool,
35-
inherit_network: bool,
36-
allow_ip_name_lookup: bool,
37-
allow_http: bool,
38-
allow_process: bool,
39-
allow_remove_dir_all: bool,
45+
permissions: Permissions,
4046

4147
mounts: Vec<Mount>,
4248
}
4349

4450
impl PluginConfig {
45-
pub fn has_inherit_stdio(&self) -> bool {
46-
self.inherit_stdio
47-
}
48-
pub fn has_inherit_args(&self) -> bool {
49-
self.inherit_args
50-
}
51-
pub fn has_inherit_env(&self) -> bool {
52-
self.inherit_env
53-
}
54-
pub fn has_inherit_network(&self) -> bool {
55-
self.inherit_network
56-
}
57-
pub fn has_allow_ip_name_lookup(&self) -> bool {
58-
self.allow_ip_name_lookup
59-
}
60-
pub fn _has_allow_http(&self) -> bool {
61-
self.allow_http
62-
}
63-
pub fn _has_allow_process(&self) -> bool {
64-
self.allow_process
65-
}
66-
pub fn _has_allow_remove_dir_all(&self) -> bool {
67-
self.allow_remove_dir_all
51+
pub fn get_permissions(&self) -> &Permissions {
52+
&self.permissions
6853
}
6954
pub fn get_mounts(&self) -> &[Mount] {
7055
&self.mounts

controller/src/application/plugin/runtime/wasm/ext/file.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
use anyhow::Result;
1+
use anyhow::{anyhow, Result};
22
use tokio::fs::remove_dir_all;
33

44
use crate::application::plugin::runtime::wasm::{
5+
config::Permissions,
56
generated::plugin::system::{
67
self,
78
types::{Directory, ErrorMessage},
@@ -11,6 +12,13 @@ use crate::application::plugin::runtime::wasm::{
1112

1213
impl system::file::Host for PluginState {
1314
async fn remove_dir_all(&mut self, directory: Directory) -> Result<Result<(), ErrorMessage>> {
15+
// Check if the plugin has permissions
16+
if !self.permissions.contains(Permissions::ALLOW_REMOVE_DIR_ALL) {
17+
return Err(anyhow!(
18+
"Plugin tried to call remove_dir_all without the required permissions"
19+
));
20+
}
21+
1422
Ok(remove_dir_all(Self::get_directory(&self.name, &directory))
1523
.await
1624
.map_err(|error| format!("Failed to remove directory: {error}")))

controller/src/application/plugin/runtime/wasm/ext/http.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
use anyhow::Result;
1+
use anyhow::{anyhow, Result};
22
use simplelog::warn;
33
use tokio::task::spawn_blocking;
44

55
use crate::application::plugin::runtime::wasm::{
6+
config::Permissions,
67
generated::plugin::system::{
78
self,
89
http::{Header, Method, Response},
@@ -19,6 +20,13 @@ impl system::http::Host for PluginState {
1920
headers: Vec<Header>,
2021
body: Option<Vec<u8>>,
2122
) -> Result<Option<Response>> {
23+
// Check if the plugin has permissions
24+
if !self.permissions.contains(Permissions::ALLOW_HTTP) {
25+
return Err(anyhow!(
26+
"Plugin tried to send a http request without the required permissions"
27+
));
28+
}
29+
2230
let name = self.name.clone();
2331
Ok(spawn_blocking(move || {
2432
let mut request = match method {

controller/src/application/plugin/runtime/wasm/ext/process.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use std::{
33
process::{self, Stdio},
44
};
55

6-
use anyhow::Result;
6+
use anyhow::{anyhow, Result};
77
use simplelog::debug;
88
use tokio::{
99
io::{AsyncBufReadExt, AsyncWriteExt, BufReader, BufWriter},
@@ -15,6 +15,7 @@ use tokio::{
1515
use wasmtime::component::Resource;
1616

1717
use crate::application::plugin::runtime::wasm::{
18+
config::Permissions,
1819
generated::plugin::system::{
1920
self,
2021
process::ExitStatus,
@@ -26,7 +27,7 @@ use crate::application::plugin::runtime::wasm::{
2627
#[cfg(unix)]
2728
use std::os::unix::process::ExitStatusExt;
2829

29-
const STREAM_BUFFER: usize = 64;
30+
const STREAM_BUFFER: usize = 128;
3031

3132
pub struct ProcessBuilder {
3233
command: String,
@@ -139,6 +140,13 @@ impl system::process::HostProcessBuilder for PluginState {
139140
&mut self,
140141
instance: Resource<ProcessBuilder>,
141142
) -> Result<Result<Resource<Process>, ErrorMessage>> {
143+
// Check if the plugin has permissions
144+
if !self.permissions.contains(Permissions::ALLOW_PROCESS) {
145+
return Err(anyhow!(
146+
"Plugin tried to spawn a process without the required permissions"
147+
));
148+
}
149+
142150
let builder = self.resources.get(&instance)?;
143151
debug!("Spawning process: {} {:?}", builder.command, builder.args);
144152

controller/src/application/plugin/runtime/wasm/init.rs

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use crate::{
2121
};
2222

2323
use super::{
24-
config::{verify_engine_config, PluginsConfig},
24+
config::{verify_engine_config, Permissions, PluginsConfig},
2525
epoch::EpochInvoker,
2626
generated, Plugin, PluginState,
2727
};
@@ -195,20 +195,30 @@ impl Plugin {
195195
generated::Plugin::add_to_linker(&mut linker, |state: &mut PluginState| state)?;
196196

197197
let mut wasi = WasiCtxBuilder::new();
198+
let mut permissions = Permissions::empty();
198199
if let Some(config) = plugins_config.find_config(name) {
199-
if config.has_inherit_stdio() {
200+
if config
201+
.get_permissions()
202+
.contains(Permissions::INHERIT_STDIO)
203+
{
200204
wasi.inherit_stdio();
201205
}
202-
if config.has_inherit_args() {
206+
if config.get_permissions().contains(Permissions::INHERIT_ARGS) {
203207
wasi.inherit_args();
204208
}
205-
if config.has_inherit_env() {
209+
if config.get_permissions().contains(Permissions::INHERIT_ENV) {
206210
wasi.inherit_env();
207211
}
208-
if config.has_inherit_network() {
212+
if config
213+
.get_permissions()
214+
.contains(Permissions::INHERIT_NETWORK)
215+
{
209216
wasi.inherit_network();
210217
}
211-
if config.has_allow_ip_name_lookup() {
218+
if config
219+
.get_permissions()
220+
.contains(Permissions::ALLOW_IP_NAME_LOOKUP)
221+
{
212222
wasi.allow_ip_name_lookup(true);
213223
}
214224
for mount in config.get_mounts() {
@@ -219,6 +229,7 @@ impl Plugin {
219229
FilePerms::all(),
220230
)?;
221231
}
232+
permissions = config.get_permissions().clone();
222233
}
223234
let wasi = wasi
224235
.preopened_dir(
@@ -237,6 +248,7 @@ impl Plugin {
237248
tasks,
238249
shared,
239250
name: name.to_string(),
251+
permissions,
240252
wasi,
241253
resources,
242254
},

controller/src/application/subscriber/manager.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,4 +79,17 @@ impl SubscriberManager {
7979
self.plugin.server_change_ready.cleanup().await;
8080
Ok(())
8181
}
82+
83+
pub async fn cleanup(&self) -> Result<()> {
84+
// Drop all sender
85+
self.network.channel.clear().await;
86+
self.network.transfer.clear().await;
87+
self.network.power.clear().await;
88+
self.network.ready.clear().await;
89+
90+
self.plugin.server_start.clear().await;
91+
self.plugin.server_stop.clear().await;
92+
self.plugin.server_change_ready.clear().await;
93+
Ok(())
94+
}
8295
}

0 commit comments

Comments
 (0)