Skip to content

Commit c50d339

Browse files
committed
feat(permissions): add environment variable permission handling and related tests
Signed-off-by: Jiaxiao Zhou <duibao55328@gmail.com>
1 parent e92e09d commit c50d339

File tree

3 files changed

+242
-1
lines changed

3 files changed

+242
-1
lines changed

crates/mcp-server/src/tools.rs

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ pub async fn handle_tools_call(
5050
"grant-network-permission" => {
5151
handle_grant_network_permission(&req, lifecycle_manager).await
5252
}
53+
"grant-environment-variable-permission" => {
54+
handle_grant_environment_variable_permission(&req, lifecycle_manager).await
55+
}
5356
_ => handle_component_call(&req, lifecycle_manager).await,
5457
};
5558

@@ -214,6 +217,37 @@ fn get_builtin_tools() -> Vec<Tool> {
214217
),
215218
annotations: None,
216219
},
220+
Tool {
221+
name: Cow::Borrowed("grant-environment-variable-permission"),
222+
description: Some(Cow::Borrowed(
223+
"Grants environment variable access permission to a component, allowing it to access specific environment variables."
224+
)),
225+
input_schema: Arc::new(
226+
serde_json::from_value(json!({
227+
"type": "object",
228+
"properties": {
229+
"component_id": {
230+
"type": "string",
231+
"description": "ID of the component to grant environment variable permission to"
232+
},
233+
"details": {
234+
"type": "object",
235+
"properties": {
236+
"key": {
237+
"type": "string",
238+
"description": "Environment variable key to grant access to"
239+
}
240+
},
241+
"required": ["key"],
242+
"additionalProperties": false
243+
}
244+
},
245+
"required": ["component_id", "details"]
246+
}))
247+
.unwrap_or_default(),
248+
),
249+
annotations: None,
250+
},
217251
]
218252
}
219253

@@ -358,20 +392,75 @@ async fn handle_grant_network_permission(
358392
}
359393
}
360394

395+
#[instrument(skip(lifecycle_manager))]
396+
async fn handle_grant_environment_variable_permission(
397+
req: &CallToolRequestParam,
398+
lifecycle_manager: &LifecycleManager,
399+
) -> Result<CallToolResult> {
400+
let args = extract_args_from_request(req)?;
401+
402+
let component_id = args
403+
.get("component_id")
404+
.and_then(|v| v.as_str())
405+
.ok_or_else(|| anyhow::anyhow!("Missing required argument: 'component_id'"))?;
406+
407+
let details = args
408+
.get("details")
409+
.ok_or_else(|| anyhow::anyhow!("Missing required argument: 'details'"))?;
410+
411+
info!(
412+
"Granting environment variable permission to component {}",
413+
component_id
414+
);
415+
416+
let result = lifecycle_manager
417+
.grant_permission(component_id, "environment", details)
418+
.await;
419+
420+
match result {
421+
Ok(()) => {
422+
let status_text = serde_json::to_string(&json!({
423+
"status": "permission granted",
424+
"component_id": component_id,
425+
"permission_type": "environment",
426+
"details": details
427+
}))?;
428+
429+
let contents = vec![Content::text(status_text)];
430+
431+
Ok(CallToolResult {
432+
content: contents,
433+
is_error: None,
434+
})
435+
}
436+
Err(e) => {
437+
error!("Failed to grant environment variable permission: {}", e);
438+
Err(anyhow::anyhow!(
439+
"Failed to grant environment variable permission to component {}: {}",
440+
component_id,
441+
e
442+
))
443+
}
444+
}
445+
}
446+
361447
#[cfg(test)]
362448
mod tests {
363449
use super::*;
364450

365451
#[test]
366452
fn test_get_builtin_tools() {
367453
let tools = get_builtin_tools();
368-
assert_eq!(tools.len(), 6);
454+
assert_eq!(tools.len(), 7);
369455
assert!(tools.iter().any(|t| t.name == "load-component"));
370456
assert!(tools.iter().any(|t| t.name == "unload-component"));
371457
assert!(tools.iter().any(|t| t.name == "list-components"));
372458
assert!(tools.iter().any(|t| t.name == "get-policy"));
373459
assert!(tools.iter().any(|t| t.name == "grant-storage-permission"));
374460
assert!(tools.iter().any(|t| t.name == "grant-network-permission"));
461+
assert!(tools
462+
.iter()
463+
.any(|t| t.name == "grant-environment-variable-permission"));
375464
}
376465

377466
#[tokio::test]

crates/wassette/src/lib.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ pub enum PermissionRule {
4141
uri: String,
4242
access: Vec<AccessType>,
4343
},
44+
Environment {
45+
key: String,
46+
},
4447
}
4548

4649
/// Access types for storage permissions
@@ -1027,6 +1030,15 @@ impl LifecycleManager {
10271030
access: access_types?,
10281031
})
10291032
}
1033+
"environment" => {
1034+
let key = details
1035+
.get("key")
1036+
.and_then(|v| v.as_str())
1037+
.ok_or_else(|| anyhow!("Missing 'key' field for environment permission"))?;
1038+
Ok(PermissionRule::Environment {
1039+
key: key.to_string(),
1040+
})
1041+
}
10301042
_ => Err(anyhow!("Unknown permission type: {}", permission_type)),
10311043
}
10321044
}
@@ -1151,6 +1163,29 @@ impl LifecycleManager {
11511163
}
11521164
}
11531165
}
1166+
PermissionRule::Environment { key } => {
1167+
// For environment permissions, we need to create a struct with key field
1168+
let env_perms = policy
1169+
.permissions
1170+
.environment
1171+
.get_or_insert_with(Default::default);
1172+
let allow_list = env_perms.allow.get_or_insert_with(Vec::new);
1173+
1174+
// Create a simple struct with the key field
1175+
let env_allow = serde_json::json!({ "key": key });
1176+
if let Ok(env_allow_struct) = serde_json::from_value(env_allow) {
1177+
// Avoid duplicates by checking if key already exists
1178+
if !allow_list.iter().any(|existing| {
1179+
if let Ok(existing_json) = serde_json::to_value(existing) {
1180+
existing_json.get("key").and_then(|k| k.as_str()) == Some(&key)
1181+
} else {
1182+
false
1183+
}
1184+
}) {
1185+
allow_list.push(env_allow_struct);
1186+
}
1187+
}
1188+
}
11541189
}
11551190
Ok(())
11561191
}
@@ -1200,6 +1235,11 @@ impl LifecycleManager {
12001235
return Err(anyhow!("Storage access cannot be empty"));
12011236
}
12021237
}
1238+
PermissionRule::Environment { key } => {
1239+
if key.is_empty() {
1240+
return Err(anyhow!("Environment variable key cannot be empty"));
1241+
}
1242+
}
12031243
}
12041244
Ok(())
12051245
}

tests/grant_permission_integration_test.rs

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -635,3 +635,115 @@ async fn test_grant_permission_sequential_grants() -> Result<()> {
635635

636636
Ok(())
637637
}
638+
639+
#[cfg(any(target_os = "linux", target_os = "macos"))]
640+
#[test(tokio::test)]
641+
async fn test_grant_permission_environment_variable_basic() -> Result<()> {
642+
let (manager, _tempdir) = setup_lifecycle_manager().await?;
643+
let component_path = build_fetch_component().await?;
644+
645+
let (component_id, _) = manager
646+
.load_component(&format!("file://{}", component_path.to_str().unwrap()))
647+
.await?;
648+
649+
// Test granting environment variable permission
650+
let result = manager
651+
.grant_permission(
652+
&component_id,
653+
"environment",
654+
&serde_json::json!({"key": "API_KEY"}),
655+
)
656+
.await;
657+
658+
assert!(result.is_ok());
659+
660+
// Verify policy file was created and contains the permission
661+
let policy_info = manager.get_policy_info(&component_id).await;
662+
assert!(policy_info.is_some());
663+
let policy_info = policy_info.unwrap();
664+
665+
// Verify policy contains the permission
666+
let policy_content = tokio::fs::read_to_string(&policy_info.local_path).await?;
667+
assert!(policy_content.contains("API_KEY"));
668+
assert!(policy_content.contains("environment"));
669+
670+
Ok(())
671+
}
672+
673+
#[cfg(any(target_os = "linux", target_os = "macos"))]
674+
#[test(tokio::test)]
675+
async fn test_grant_permission_environment_variable_multiple() -> Result<()> {
676+
let (manager, _tempdir) = setup_lifecycle_manager().await?;
677+
let component_path = build_fetch_component().await?;
678+
679+
let (component_id, _) = manager
680+
.load_component(&format!("file://{}", component_path.to_str().unwrap()))
681+
.await?;
682+
683+
// Grant multiple environment variable permissions
684+
let api_key_result = manager
685+
.grant_permission(
686+
&component_id,
687+
"environment",
688+
&serde_json::json!({"key": "API_KEY"}),
689+
)
690+
.await;
691+
692+
let config_url_result = manager
693+
.grant_permission(
694+
&component_id,
695+
"environment",
696+
&serde_json::json!({"key": "CONFIG_URL"}),
697+
)
698+
.await;
699+
700+
assert!(api_key_result.is_ok());
701+
assert!(config_url_result.is_ok());
702+
703+
// Verify policy file contains all permissions
704+
let policy_info = manager.get_policy_info(&component_id).await;
705+
assert!(policy_info.is_some());
706+
let policy_info = policy_info.unwrap();
707+
let policy_content = tokio::fs::read_to_string(&policy_info.local_path).await?;
708+
709+
assert!(policy_content.contains("API_KEY"));
710+
assert!(policy_content.contains("CONFIG_URL"));
711+
assert!(policy_content.contains("environment"));
712+
713+
Ok(())
714+
}
715+
716+
#[cfg(any(target_os = "linux", target_os = "macos"))]
717+
#[test(tokio::test)]
718+
async fn test_grant_permission_environment_variable_duplicate_prevention() -> Result<()> {
719+
let (manager, _tempdir) = setup_lifecycle_manager().await?;
720+
let component_path = build_fetch_component().await?;
721+
722+
let (component_id, _) = manager
723+
.load_component(&format!("file://{}", component_path.to_str().unwrap()))
724+
.await?;
725+
726+
// Grant the same environment variable permission twice
727+
let details = serde_json::json!({"key": "API_KEY"});
728+
let first_result = manager
729+
.grant_permission(&component_id, "environment", &details)
730+
.await;
731+
let second_result = manager
732+
.grant_permission(&component_id, "environment", &details)
733+
.await;
734+
735+
assert!(first_result.is_ok());
736+
assert!(second_result.is_ok());
737+
738+
// Verify policy file contains only one instance
739+
let policy_info = manager.get_policy_info(&component_id).await;
740+
assert!(policy_info.is_some());
741+
let policy_info = policy_info.unwrap();
742+
let policy_content = tokio::fs::read_to_string(&policy_info.local_path).await?;
743+
744+
// Count occurrences of the environment key - should be exactly 1
745+
let occurrences = policy_content.matches("API_KEY").count();
746+
assert_eq!(occurrences, 1);
747+
748+
Ok(())
749+
}

0 commit comments

Comments
 (0)