Plugin Capability System¶
Overview¶
Implementations SHOULD use a capability-based security model for plugins, granting only the minimum permissions required for each plugin's functionality.
Design Principles¶
Principle of Least Privilege¶
Each plugin should have access to only what it needs:
+---------------------------------------------------------+
| Capability Model |
+---------------------------------------------------------+
| Plugin A (price fetcher) |
| [x] read_prices |
| [x] network_https (api.example.com only) |
| [ ] write_directives |
| [ ] filesystem |
+---------------------------------------------------------+
| Plugin B (auto_accounts) |
| [x] read_accounts |
| [x] write_directives (open only) |
| [ ] network |
| [ ] filesystem |
+---------------------------------------------------------+
Capability Tokens¶
Capabilities are unforgeable tokens that grant specific permissions:
/// A capability token granting specific permissions
pub struct Capability {
kind: CapabilityKind,
scope: CapabilityScope,
// Cryptographically signed by host
}
pub enum CapabilityKind {
ReadDirectives,
WriteDirectives,
ReadAccounts,
ReadPrices,
Network,
FileRead,
FileWrite,
}
pub enum CapabilityScope {
All,
Filtered { accounts: Vec<AccountPattern> },
Domain { hosts: Vec<String> },
Path { prefix: PathBuf },
}
Standard Capabilities¶
Directive Access¶
| Capability | Description | Risk Level |
|---|---|---|
read:directives |
Read all directives | Low |
read:directives:transactions |
Read transactions only | Low |
read:directives:prices |
Read price directives | Low |
write:directives |
Emit new directives | Medium |
write:directives:open |
Emit Open directives only | Low |
modify:directives |
Modify existing directives | High |
Account Access¶
| Capability | Description | Risk Level |
|---|---|---|
read:accounts |
Read account names | Low |
read:accounts:balances |
Read account balances | Medium |
read:accounts:pattern:Assets:* |
Read matching accounts | Low |
Network Access¶
| Capability | Description | Risk Level |
|---|---|---|
network:none |
No network access (default) | None |
network:https:*.example.com |
HTTPS to specific domains | Medium |
network:all |
Unrestricted network | High |
Filesystem Access¶
| Capability | Description | Risk Level |
|---|---|---|
file:none |
No filesystem access (default) | None |
file:read:ledger |
Read files in ledger directory | Low |
file:read:path:/data/* |
Read from specific path | Medium |
file:write:path:/output/* |
Write to specific path | High |
Capability Declaration¶
In Plugin Manifest¶
Plugins declare required capabilities:
# plugin.toml
[plugin]
name = "price_fetcher"
version = "1.0.0"
[capabilities.required]
read = ["directives:prices", "accounts"]
network = ["https:api.exchangerate.com"]
[capabilities.optional]
file = ["read:ledger"] # For caching
At Load Time¶
Host verifies and grants capabilities:
fn load_plugin(path: &Path, policy: &SecurityPolicy) -> Result<Plugin> {
let manifest = PluginManifest::load(path)?;
// Check required capabilities against policy
for cap in &manifest.capabilities.required {
if !policy.allows(cap) {
return Err(PluginError::CapabilityDenied(cap.clone()));
}
}
// Grant capabilities as tokens
let capabilities = manifest.capabilities.required
.iter()
.map(|c| policy.grant(c))
.collect();
Plugin::new(path, capabilities)
}
Capability Enforcement¶
At Runtime¶
impl PluginRuntime {
fn read_accounts(&self, pattern: &str) -> Result<Vec<Account>> {
// Check capability
self.require_capability(Capability::ReadAccounts)?;
// If scoped, filter results
let accounts = self.ledger.accounts();
if let Some(scope) = self.capability_scope(Capability::ReadAccounts) {
return Ok(accounts.filter(|a| scope.matches(a)).collect());
}
Ok(accounts.collect())
}
fn emit_directive(&mut self, directive: Directive) -> Result<()> {
// Check capability
self.require_capability(Capability::WriteDirectives)?;
// If scoped, validate directive type
if let Some(scope) = self.capability_scope(Capability::WriteDirectives) {
if !scope.allows_directive(&directive) {
return Err(PluginError::CapabilityViolation);
}
}
self.output.push(directive);
Ok(())
}
}
Capability Revocation¶
Capabilities can be revoked mid-execution:
impl PluginRuntime {
fn revoke(&mut self, capability: CapabilityKind) {
self.capabilities.remove(&capability);
// Future calls using this capability will fail
}
}
Security Policies¶
Built-in Policies¶
pub enum SecurityPolicy {
/// No capabilities (maximum security)
Deny,
/// Read-only access to directives
ReadOnly,
/// Standard plugin permissions
Standard,
/// Custom policy
Custom(CapabilitySet),
}
impl SecurityPolicy {
pub fn standard() -> Self {
Self::Custom(CapabilitySet::new()
.add(Capability::ReadDirectives)
.add(Capability::ReadAccounts)
.add(Capability::WriteDirectives)
)
}
}
User Configuration¶
; Global plugin policy
option "plugin_policy" "standard"
; Per-plugin overrides
plugin "price_fetcher" {
capabilities: [
"read:directives:prices",
"network:https:api.exchangerate.com"
]
}
plugin "custom_validator" {
capabilities: [
"read:directives",
"read:accounts"
]
}
Command Line¶
# Run with strict policy
beancount --plugin-policy=readonly ledger.beancount
# Grant specific capability to plugin
beancount --plugin-capability=price_fetcher:network:https:* ledger.beancount
Capability Audit¶
Logging¶
impl PluginRuntime {
fn use_capability(&self, cap: &Capability) {
// Log all capability usage
log::info!(
plugin = self.name,
capability = %cap,
"Plugin used capability"
);
}
}
Audit Report¶
Plugin Capability Audit
=======================
price_fetcher v1.0.0
Used capabilities:
- read:directives:prices (152 times)
- network:https:api.exchangerate.com (12 requests)
Unused declared capabilities:
- file:read:ledger (optional, never used)
auto_accounts v2.1.0
Used capabilities:
- read:accounts (1 time)
- write:directives:open (3 times)
Attempted denied capabilities:
- write:directives:transaction (BLOCKED)
Error Messages¶
error: Plugin capability denied
--> main.beancount:3:1
|
3 | plugin "untrusted_plugin"
| ^^^^^^^^^^^^^^^^^^^^^^^^^ requires capability not granted
|
= plugin: untrusted_plugin
= required: network:https:*
= policy: standard (no network access)
= hint: add --plugin-capability=untrusted_plugin:network:https:example.com
error: Plugin capability violation
--> [plugin: auto_accounts]
|
= attempted: write transaction directive
= granted: write:directives:open only
= hint: plugin tried to emit Transaction but only has Open permission
Recommendations¶
- Declare capabilities explicitly in plugin manifests
- Default to minimal permissions - deny by default
- Scope capabilities narrowly - use patterns and domains
- Audit capability usage - log all access
- Review capability requests before granting
- Separate read and write capabilities
- Time-limit capabilities for sensitive operations