Skip to main content

tryaudex_core/
config.rs

1use std::collections::HashMap;
2use std::path::PathBuf;
3
4use serde::{Deserialize, Serialize};
5
6use crate::error::{AvError, Result};
7
8/// A named policy profile (e.g. `s3-readonly`, `lambda-deploy`).
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct Profile {
11    /// Comma-separated IAM actions (same format as --allow)
12    pub allow: String,
13    /// Optional comma-separated resource ARNs
14    pub resource: Option<String>,
15    /// Optional description for display
16    pub description: Option<String>,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize, Default)]
20pub struct Config {
21    /// Default cloud provider ("aws", "gcp", or "azure")
22    pub provider: Option<String>,
23    /// Default IAM role ARN to assume (AWS)
24    pub role_arn: Option<String>,
25    /// Default TTL for sessions (e.g. "15m")
26    pub ttl: Option<String>,
27    /// Default budget limit in USD
28    pub budget: Option<f64>,
29    /// Default AWS region
30    pub region: Option<String>,
31    /// Actions that are always denied regardless of --allow
32    pub deny: Option<Vec<String>>,
33    /// AWS permissions boundary policy ARN — applied to every AssumeRole call
34    /// as an additional ceiling beyond the inline session policy.
35    pub permissions_boundary: Option<String>,
36    /// Network policy: restrict credentials to specific source IPs or VPCs.
37    #[serde(default)]
38    pub network: Option<crate::policy::NetworkPolicy>,
39    /// Named policy profiles
40    #[serde(default)]
41    pub profiles: HashMap<String, Profile>,
42    /// GCP service account email for impersonation
43    pub gcp_service_account: Option<String>,
44    /// GCP project ID
45    pub gcp_project: Option<String>,
46    /// Azure subscription ID
47    pub azure_subscription: Option<String>,
48    /// Azure tenant ID
49    pub azure_tenant: Option<String>,
50    /// Team policy configuration
51    #[serde(default)]
52    pub team: Option<crate::team::TeamConfig>,
53    /// Approval workflow configuration
54    #[serde(default)]
55    pub approval: Option<crate::approval::ApprovalConfig>,
56    /// Audit forwarding configuration
57    #[serde(default)]
58    pub audit: Option<crate::forward::ForwardConfig>,
59    /// Role mapping configuration
60    #[serde(default)]
61    pub roles: Option<crate::roles::RoleMappingConfig>,
62    /// Rate limiting configuration
63    #[serde(default)]
64    pub ratelimit: Option<crate::ratelimit::RateLimitConfig>,
65    /// SSO configuration
66    #[serde(default)]
67    pub sso: Option<crate::sso::SsoConfig>,
68    /// Vault credential backend configuration
69    #[serde(default)]
70    pub vault: Option<crate::vault::VaultConfig>,
71    /// Multi-account configuration
72    #[serde(default)]
73    pub account: Option<crate::account::AccountConfig>,
74    /// Credential broker configuration
75    #[serde(default)]
76    pub broker: Option<crate::broker::BrokerConfig>,
77    /// Database audit backend configuration
78    #[serde(default)]
79    pub dbaudit: Option<crate::dbaudit::DbAuditConfig>,
80    /// High availability configuration
81    #[serde(default)]
82    pub ha: Option<crate::ha::HaConfig>,
83}
84
85impl Config {
86    /// Load config from `~/.config/audex/config.toml`. Returns default if file doesn't exist.
87    pub fn load() -> Result<Self> {
88        let path = Self::path();
89        if !path.exists() {
90            return Ok(Self::default());
91        }
92        let contents = std::fs::read_to_string(&path)?;
93        toml::from_str(&contents).map_err(|e| {
94            AvError::InvalidPolicy(format!("Invalid config at {}: {}", path.display(), e))
95        })
96    }
97
98    /// Get a profile by name. Checks community:// prefix, then user config, then built-in profiles.
99    pub fn resolve_profile(&self, name: &str) -> Result<Profile> {
100        // Community policy library: `community://nextjs-vercel-deploy`
101        if name.starts_with("community://") {
102            return crate::community::resolve(name);
103        }
104        // Team policy library: `team://deploy-lambda`
105        if name.starts_with("team://") {
106            return crate::team::resolve_cached(name);
107        }
108        if let Some(p) = self.profiles.get(name) {
109            return Ok(p.clone());
110        }
111        if let Some(p) = builtin_profiles().get(name) {
112            return Ok(p.clone());
113        }
114        // Also check community policies without prefix
115        if let Ok(p) = crate::community::resolve(name) {
116            return Ok(p);
117        }
118        let mut available: Vec<String> = self.profiles.keys().cloned().collect();
119        available.extend(builtin_profiles().keys().cloned());
120        available.sort();
121        Err(AvError::InvalidPolicy(format!(
122            "Unknown profile '{}'. Available: {} (also try community:// policies)",
123            name,
124            available.join(", ")
125        )))
126    }
127
128    /// List all available profiles (user + built-in).
129    pub fn all_profiles(&self) -> Vec<(String, Profile)> {
130        let mut all: HashMap<String, Profile> = builtin_profiles();
131        // User profiles override built-ins
132        all.extend(self.profiles.clone());
133        let mut sorted: Vec<(String, Profile)> = all.into_iter().collect();
134        sorted.sort_by(|a, b| a.0.cmp(&b.0));
135        sorted
136    }
137
138    /// Config file path: `~/.config/audex/config.toml`
139    pub fn path() -> PathBuf {
140        dirs::config_dir()
141            .unwrap_or_else(|| PathBuf::from("."))
142            .join("audex")
143            .join("config.toml")
144    }
145}
146
147fn p(allow: &str, desc: &str) -> Profile {
148    Profile {
149        allow: allow.to_string(),
150        resource: None,
151        description: Some(desc.to_string()),
152    }
153}
154
155/// Built-in policy profiles that ship with Audex.
156pub fn builtin_profiles() -> HashMap<String, Profile> {
157    HashMap::from([
158        ("s3-readonly".into(), p(
159            "s3:GetObject,s3:ListBucket,s3:GetBucketLocation,s3:ListAllMyBuckets",
160            "Read-only S3 access",
161        )),
162        ("s3-readwrite".into(), p(
163            "s3:GetObject,s3:PutObject,s3:DeleteObject,s3:ListBucket,s3:GetBucketLocation,s3:ListAllMyBuckets",
164            "Read/write S3 access",
165        )),
166        ("lambda-deploy".into(), p(
167            "lambda:UpdateFunctionCode,lambda:UpdateFunctionConfiguration,lambda:GetFunction,lambda:ListFunctions",
168            "Deploy and manage Lambda functions",
169        )),
170        ("dynamodb-query".into(), p(
171            "dynamodb:GetItem,dynamodb:Query,dynamodb:Scan,dynamodb:BatchGetItem,dynamodb:DescribeTable,dynamodb:ListTables",
172            "Read-only DynamoDB access",
173        )),
174        ("dynamodb-readwrite".into(), p(
175            "dynamodb:GetItem,dynamodb:PutItem,dynamodb:UpdateItem,dynamodb:DeleteItem,dynamodb:Query,dynamodb:Scan,dynamodb:BatchGetItem,dynamodb:BatchWriteItem,dynamodb:DescribeTable,dynamodb:ListTables",
176            "Read/write DynamoDB access",
177        )),
178        ("ec2-readonly".into(), p(
179            "ec2:DescribeInstances,ec2:DescribeSecurityGroups,ec2:DescribeSubnets,ec2:DescribeVpcs,ec2:DescribeImages",
180            "Read-only EC2 access",
181        )),
182        ("cloudwatch-logs".into(), p(
183            "logs:GetLogEvents,logs:DescribeLogGroups,logs:DescribeLogStreams,logs:FilterLogEvents",
184            "Read CloudWatch Logs",
185        )),
186        ("ssm-readonly".into(), p(
187            "ssm:GetParameter,ssm:GetParameters,ssm:GetParametersByPath,ssm:DescribeParameters",
188            "Read SSM Parameter Store",
189        )),
190        ("sqs-readwrite".into(), p(
191            "sqs:SendMessage,sqs:ReceiveMessage,sqs:DeleteMessage,sqs:GetQueueAttributes,sqs:ListQueues",
192            "Read/write SQS access",
193        )),
194        ("sns-publish".into(), p(
195            "sns:Publish,sns:ListTopics,sns:GetTopicAttributes",
196            "Publish to SNS topics",
197        )),
198        ("ecr-push".into(), p(
199            "ecr:GetAuthorizationToken,ecr:BatchCheckLayerAvailability,ecr:GetDownloadUrlForLayer,ecr:BatchGetImage,ecr:PutImage,ecr:InitiateLayerUpload,ecr:UploadLayerPart,ecr:CompleteLayerUpload,ecr:DescribeRepositories,ecr:CreateRepository",
200            "Push Docker images to ECR",
201        )),
202        ("ecr-pull".into(), p(
203            "ecr:GetAuthorizationToken,ecr:BatchCheckLayerAvailability,ecr:GetDownloadUrlForLayer,ecr:BatchGetImage,ecr:DescribeRepositories",
204            "Pull Docker images from ECR",
205        )),
206        ("terraform-plan".into(), p(
207            "sts:GetCallerIdentity,s3:GetObject,s3:ListBucket,s3:GetBucketLocation,dynamodb:GetItem,dynamodb:PutItem,ec2:Describe*,iam:GetRole,iam:GetPolicy,iam:GetPolicyVersion,iam:ListRolePolicies,iam:ListAttachedRolePolicies,lambda:GetFunction,lambda:ListFunctions,logs:DescribeLogGroups,cloudformation:DescribeStacks,cloudformation:ListStacks",
208            "Terraform plan (read-only state + describe resources)",
209        )),
210        ("terraform-apply".into(), p(
211            "sts:GetCallerIdentity,s3:GetObject,s3:PutObject,s3:ListBucket,s3:GetBucketLocation,dynamodb:GetItem,dynamodb:PutItem,ec2:*,iam:GetRole,iam:GetPolicy,iam:GetPolicyVersion,iam:CreateRole,iam:DeleteRole,iam:AttachRolePolicy,iam:DetachRolePolicy,iam:PassRole,iam:ListRolePolicies,iam:ListAttachedRolePolicies,iam:PutRolePolicy,iam:DeleteRolePolicy,lambda:*,logs:*,cloudformation:*,apigateway:*",
212            "Terraform apply (full infrastructure management)",
213        )),
214        // GCP profiles
215        ("gcs-readonly".into(), p(
216            "storage.objects.get,storage.objects.list,storage.buckets.get,storage.buckets.list",
217            "Read-only Google Cloud Storage access",
218        )),
219        ("gcs-readwrite".into(), p(
220            "storage.objects.get,storage.objects.list,storage.objects.create,storage.objects.delete,storage.buckets.get,storage.buckets.list",
221            "Read/write Google Cloud Storage access",
222        )),
223        ("gce-readonly".into(), p(
224            "compute.instances.get,compute.instances.list,compute.zones.list,compute.regions.list",
225            "Read-only Compute Engine access",
226        )),
227        ("gcf-deploy".into(), p(
228            "cloudfunctions.functions.get,cloudfunctions.functions.list,cloudfunctions.functions.update,cloudfunctions.functions.create",
229            "Deploy Google Cloud Functions",
230        )),
231        ("bigquery-readonly".into(), p(
232            "bigquery.jobs.create,bigquery.tables.getData,bigquery.tables.list,bigquery.datasets.get",
233            "Read-only BigQuery access",
234        )),
235        // Azure profiles
236        ("azure-storage-readonly".into(), p(
237            "Microsoft.Storage/storageAccounts/read,Microsoft.Storage/storageAccounts/listKeys/action,Microsoft.Storage/storageAccounts/blobServices/containers/read",
238            "Read-only Azure Storage access",
239        )),
240        ("azure-storage-readwrite".into(), p(
241            "Microsoft.Storage/storageAccounts/read,Microsoft.Storage/storageAccounts/write,Microsoft.Storage/storageAccounts/listKeys/action,Microsoft.Storage/storageAccounts/blobServices/containers/read,Microsoft.Storage/storageAccounts/blobServices/containers/write",
242            "Read/write Azure Storage access",
243        )),
244        ("azure-compute-readonly".into(), p(
245            "Microsoft.Compute/virtualMachines/read,Microsoft.Compute/virtualMachines/instanceView/read,Microsoft.Network/networkInterfaces/read,Microsoft.Network/publicIPAddresses/read",
246            "Read-only Azure Compute access",
247        )),
248        ("azure-keyvault-readonly".into(), p(
249            "Microsoft.KeyVault/vaults/read,Microsoft.KeyVault/vaults/secrets/read",
250            "Read-only Azure Key Vault access",
251        )),
252        ("azure-functions-deploy".into(), p(
253            "Microsoft.Web/sites/read,Microsoft.Web/sites/write,Microsoft.Web/sites/functions/read,Microsoft.Web/sites/functions/write,Microsoft.Web/sites/restart/action",
254            "Deploy Azure Functions",
255        )),
256        ("azure-aks-readonly".into(), p(
257            "Microsoft.ContainerService/managedClusters/read,Microsoft.ContainerService/managedClusters/listClusterUserCredential/action",
258            "Read-only Azure Kubernetes Service access",
259        )),
260    ])
261}
262
263#[cfg(test)]
264mod tests {
265    use super::*;
266
267    #[test]
268    fn test_builtin_profile_resolve() {
269        let config = Config::default();
270        let profile = config.resolve_profile("s3-readonly").unwrap();
271        assert!(profile.allow.contains("s3:GetObject"));
272        assert!(profile.allow.contains("s3:ListBucket"));
273    }
274
275    #[test]
276    fn test_unknown_profile_error() {
277        let config = Config::default();
278        assert!(config.resolve_profile("nonexistent").is_err());
279    }
280
281    #[test]
282    fn test_user_profile_overrides_builtin() {
283        let mut config = Config::default();
284        config.profiles.insert(
285            "s3-readonly".into(),
286            Profile {
287                allow: "s3:GetObject".to_string(),
288                resource: Some("arn:aws:s3:::my-bucket/*".to_string()),
289                description: Some("Custom s3 readonly".to_string()),
290            },
291        );
292        let profile = config.resolve_profile("s3-readonly").unwrap();
293        // Should get user's version, not built-in
294        assert_eq!(profile.allow, "s3:GetObject");
295        assert!(profile.resource.is_some());
296    }
297
298    #[test]
299    fn test_all_profiles_includes_builtins() {
300        let config = Config::default();
301        let all = config.all_profiles();
302        assert!(all.len() >= 10);
303        assert!(all.iter().any(|(name, _)| name == "s3-readonly"));
304        assert!(all.iter().any(|(name, _)| name == "lambda-deploy"));
305    }
306
307    #[test]
308    fn test_config_toml_with_profiles() {
309        let toml_str = r#"
310role_arn = "arn:aws:iam::123456789012:role/MyRole"
311ttl = "30m"
312
313[profiles.my-custom]
314allow = "s3:GetObject,s3:PutObject"
315resource = "arn:aws:s3:::my-bucket/*"
316description = "Custom profile"
317"#;
318        let config: Config = toml::from_str(toml_str).unwrap();
319        assert_eq!(
320            config.role_arn.unwrap(),
321            "arn:aws:iam::123456789012:role/MyRole"
322        );
323        assert_eq!(config.ttl.unwrap(), "30m");
324        let profile = config.profiles.get("my-custom").unwrap();
325        assert!(profile.allow.contains("s3:GetObject"));
326        assert!(profile.resource.is_some());
327    }
328}