Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 20 additions & 32 deletions iam-policy-autopilot-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,14 +82,14 @@ struct GeneratePolicyCliConfig {
account: String,
/// Output individual policies instead of merged policy
individual_policies: bool,
/// Show method to action mappings alongside policies
show_action_mappings: bool,
/// Upload policies to AWS with optional custom name prefix
upload_policies: Option<String>,
/// Enable minimal policy size by allowing cross-service merging
minimal_policy_size: bool,
/// Disable file system caching for service references
disable_cache: bool,
/// Generate explanations for why actions were added
explain: bool,
}

impl GeneratePolicyCliConfig {
Expand Down Expand Up @@ -280,16 +280,6 @@ for each method call. Disables --upload-policy, if provided."
)]
individual_policies: bool,

/// Include method to action mappings alongside the generated policies
#[arg(
hide = true,
long = "show-action-mappings",
long_help = "When enabled, outputs detailed method to action \
mappings alongside the generated policies. This provides granular visibility into which SDK method calls \
require which specific IAM actions and their associated resources. Disables --upload-policy, if provided."
)]
show_action_mappings: bool,

/// Upload generated policies to AWS IAM with optional custom name prefix
#[arg(long = "upload-policies", num_args = 0..=1, require_equals = false, default_missing_value = "",
long_help = "Upload the generated policies to AWS IAM using the iam:CreatePolicy API. \
Expand Down Expand Up @@ -327,6 +317,15 @@ Use this flag to force fresh data retrieval on every run."
long_help = SERVICE_HINTS_LONG_HELP,
)]
service_hints: Option<Vec<String>>,

/// Generate explanations for why actions were added
#[arg(
long = "explain",
long_help = "When enabled, generates detailed explanations for why each IAM action \
was added to the policy. Explanations include the initial operation with location information, FAS (https://docs.aws.amazon.com/IAM/latest/UserGuide/access_forward_access_sessions.html) expansion chains. The output format \
may change in future versions."
)]
explain: bool,
},

/// Start MCP server
Expand Down Expand Up @@ -425,35 +424,24 @@ async fn handle_generate_policy(config: &GeneratePolicyCliConfig) -> Result<()>
service_names: names.clone(),
});

let (policies, method_action_mappings) = generate_policies(&GeneratePolicyConfig {
let result = generate_policies(&GeneratePolicyConfig {
extract_sdk_calls_config: ExtractSdkCallsConfig {
source_files: config.shared.source_files.to_owned(),
language: config.shared.language.to_owned(),
service_hints,
},
aws_context: AwsContext::new(config.region.clone(), config.account.clone()),
generate_action_mappings: config.show_action_mappings,
individual_policies: config.individual_policies,
minimize_policy_size: config.minimal_policy_size,
disable_file_system_cache: config.disable_cache,
generate_explanations: config.explain,
})
.await?;

// Handle policy output based on configuration
if config.show_action_mappings {
// Output combined format with mappings and policies
output::output_combined_policy_mappings(
method_action_mappings,
policies,
config.shared.pretty,
)
.context("Failed to output combined policy and mappings")?;

trace!("Combined policy and mappings output written to stdout");
} else if config.individual_policies {
if config.individual_policies {
// Output individual policies
trace!("Outputting {} individual policies", policies.len());
output::output_iam_policies(policies, None, config.shared.pretty)
trace!("Outputting {} individual policies", result.policies.len());
output::output_iam_policies(result, None, config.shared.pretty)
.context("Failed to output individual IAM policies")?;
} else {
// Default behavior: output merged policy with optional upload
Expand All @@ -466,7 +454,7 @@ async fn handle_generate_policy(config: &GeneratePolicyCliConfig) -> Result<()>

let custom_name = config.upload_policies.as_deref().filter(|s| !s.is_empty());
let batch_response = uploader
.upload_policies(&policies, custom_name)
.upload_policies(&result.policies, custom_name)
.await
.context("Failed to upload policies to AWS IAM")?;

Expand All @@ -492,7 +480,7 @@ async fn handle_generate_policy(config: &GeneratePolicyCliConfig) -> Result<()>
None
};

output::output_iam_policies(policies, upload_result, config.shared.pretty)
output::output_iam_policies(result, upload_result, config.shared.pretty)
.context("Failed to output merged IAM policy")?
}

Expand Down Expand Up @@ -564,11 +552,11 @@ async fn main() {
region,
account,
individual_policies,
show_action_mappings,
upload_policies,
minimal_policy_size,
disable_cache,
service_hints,
explain,
} => {
// Initialize logging
if let Err(e) = init_logging(debug) {
Expand All @@ -587,10 +575,10 @@ async fn main() {
region,
account,
individual_policies,
show_action_mappings,
upload_policies,
minimal_policy_size,
disable_cache,
explain,
};

match handle_generate_policy(&config).await {
Expand Down
60 changes: 6 additions & 54 deletions iam-policy-autopilot-cli/src/output.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
use anyhow::{Context, Result};
use iam_policy_autopilot_access_denied::{DenialType, PlanResult};
use iam_policy_autopilot_policy_generation::policy_generation::{
MethodActionMapping, PolicyWithMetadata,
};
use iam_policy_autopilot_policy_generation::api::model::GeneratePoliciesResult;
use iam_policy_autopilot_tools::BatchUploadResponse;
use log::debug;
use std::io::{self, Write};
Expand Down Expand Up @@ -163,63 +161,17 @@ pub(crate) fn print_unsupported_denial(denial_type: &DenialType, reason: &str) {
#[serde(rename_all = "PascalCase")]
struct PolicyOutput {
/// The generated policies with type information
policies: Vec<PolicyWithMetadata>,
/// and explanations for why actions were added
#[serde(flatten)]
result: GeneratePoliciesResult,
/// Upload results (only present when --upload-policies is used)
#[serde(skip_serializing_if = "Option::is_none")]
upload_result: Option<BatchUploadResponse>,
}

/// Combined output structure when showing action mappings alongside policies
#[derive(Debug, Clone, serde::Serialize)]
#[serde(rename_all = "PascalCase")]
struct CombinedPolicyOutput {
/// Method to action mappings
method_action_mappings: Vec<MethodActionMapping>,
/// The generated policies with type information
policies: Vec<PolicyWithMetadata>,
/// Upload results (only present when --upload-policies is used)
#[serde(skip_serializing_if = "Option::is_none")]
upload_result: Option<BatchUploadResponse>,
}

/// Output combined policy and mappings as JSON to stdout
pub(crate) fn output_combined_policy_mappings(
method_action_mappings: Vec<MethodActionMapping>,
policies: Vec<PolicyWithMetadata>,
pretty: bool,
) -> Result<()> {
debug!(
"Formatting combined policy and mappings output as JSON (pretty: {})",
pretty
);

let combined_output = CombinedPolicyOutput {
method_action_mappings,
policies,
upload_result: None,
};

let json_output = if pretty {
iam_policy_autopilot_policy_generation::JsonProvider::stringify_pretty(&combined_output)
.context("Failed to serialize combined output to pretty JSON")?
} else {
iam_policy_autopilot_policy_generation::JsonProvider::stringify(&combined_output)
.context("Failed to serialize combined output to JSON")?
};

// Output to stdout (not using println! to avoid extra newline in compact mode)
print!("{}", json_output);
if pretty {
println!(); // Add newline for pretty output
}

debug!("Combined policy and mappings JSON output written to stdout");
Ok(())
}

/// Output IAM policies as JSON to stdout
pub(crate) fn output_iam_policies(
policies: Vec<PolicyWithMetadata>,
result: GeneratePoliciesResult,
upload_result: Option<BatchUploadResponse>,
pretty: bool,
) -> Result<()> {
Expand All @@ -229,7 +181,7 @@ pub(crate) fn output_iam_policies(
);

let policy_output = PolicyOutput {
policies,
result,
upload_result,
};

Expand Down
34 changes: 20 additions & 14 deletions iam-policy-autopilot-mcp-server/src/tools/generate_policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ pub async fn generate_application_policies(
service_names: hints,
});

let (policies, _) = api::generate_policies(&GeneratePolicyConfig {
let result = api::generate_policies(&GeneratePolicyConfig {
individual_policies: false,
extract_sdk_calls_config: ExtractSdkCallsConfig {
source_files: input.source_files.into_iter().map(|f| f.into()).collect(),
Expand All @@ -61,16 +61,17 @@ pub async fn generate_application_policies(
service_hints,
},
aws_context: AwsContext::new(region, account),
generate_action_mappings: false,
minimize_policy_size: false,

// true by default, if we want to allow the user to change it we should
// accept it as part of the cli input when starting the mcp server
disable_file_system_cache: true,
generate_explanations: false,
})
.await?;

let policies = policies
let policies = result
.policies
.into_iter()
.map(|policy| serde_json::to_string(&policy.policy).context("Failed to serialize policy"))
.collect::<Result<Vec<String>, Error>>()?;
Expand All @@ -82,26 +83,23 @@ pub async fn generate_application_policies(
#[cfg(test)]
mod api {
use anyhow::Result;
use iam_policy_autopilot_policy_generation::{
api::model::GeneratePolicyConfig, policy_generation::PolicyWithMetadata,
MethodActionMapping,
use iam_policy_autopilot_policy_generation::api::model::{
GeneratePoliciesResult, GeneratePolicyConfig,
};

// Static mutable return value
pub static mut MOCK_RETURN_VALUE: Option<
Result<(Vec<PolicyWithMetadata>, Vec<MethodActionMapping>)>,
> = None;
pub static mut MOCK_RETURN_VALUE: Option<Result<GeneratePoliciesResult>> = None;

pub async fn generate_policies(
_config: &GeneratePolicyConfig,
) -> Result<(Vec<PolicyWithMetadata>, Vec<MethodActionMapping>)> {
) -> Result<GeneratePoliciesResult> {
#[allow(static_mut_refs)]
unsafe {
MOCK_RETURN_VALUE.take().unwrap()
}
}

pub fn set_mock_return(value: Result<(Vec<PolicyWithMetadata>, Vec<MethodActionMapping>)>) {
pub fn set_mock_return(value: Result<GeneratePoliciesResult>) {
unsafe { MOCK_RETURN_VALUE = Some(value) }
}
}
Expand All @@ -113,7 +111,7 @@ mod tests {

use super::*;
use iam_policy_autopilot_policy_generation::{
IamPolicy, PolicyType, PolicyWithMetadata, Statement,
api::model::GeneratePoliciesResult, IamPolicy, PolicyType, PolicyWithMetadata, Statement,
};

use anyhow::anyhow;
Expand Down Expand Up @@ -143,7 +141,12 @@ mod tests {
policy_type: PolicyType::Identity,
};

api::set_mock_return(Ok((vec![policy], vec![])));
use iam_policy_autopilot_policy_generation::api::model::GeneratePoliciesResult;

api::set_mock_return(Ok(GeneratePoliciesResult {
policies: vec![policy],
explanations: None,
}));
let result = generate_application_policies(input).await;

println!("{result:?}");
Expand Down Expand Up @@ -223,7 +226,10 @@ mod tests {
policy_type: PolicyType::Identity,
};

api::set_mock_return(Ok((vec![policy], vec![])));
api::set_mock_return(Ok(GeneratePoliciesResult {
policies: vec![policy],
explanations: None,
}));
let result = generate_application_policies(input).await;

assert!(result.is_ok());
Expand Down
Loading