Skip to main content

tryaudex_core/
universal.rs

1use crate::session::CloudProvider;
2
3/// A mapping from a cloud-agnostic action key to provider-specific permissions.
4struct Mapping {
5    key: &'static str,
6    aws: &'static str,
7    gcp: &'static str,
8    azure: &'static str,
9}
10
11const MAPPINGS: &[Mapping] = &[
12    Mapping {
13        key: "storage:read",
14        aws: "s3:GetObject,s3:ListBucket,s3:GetBucketLocation,s3:ListAllMyBuckets",
15        gcp: "storage.objects.get,storage.objects.list,storage.buckets.get,storage.buckets.list",
16        azure: "Microsoft.Storage/storageAccounts/read,Microsoft.Storage/storageAccounts/blobServices/containers/read",
17    },
18    Mapping {
19        key: "storage:write",
20        aws: "s3:PutObject,s3:DeleteObject",
21        gcp: "storage.objects.create,storage.objects.delete",
22        azure: "Microsoft.Storage/storageAccounts/write,Microsoft.Storage/storageAccounts/blobServices/containers/write",
23    },
24    Mapping {
25        key: "storage:readwrite",
26        aws: "s3:GetObject,s3:PutObject,s3:DeleteObject,s3:ListBucket,s3:GetBucketLocation,s3:ListAllMyBuckets",
27        gcp: "storage.objects.get,storage.objects.list,storage.objects.create,storage.objects.delete,storage.buckets.get,storage.buckets.list",
28        azure: "Microsoft.Storage/storageAccounts/read,Microsoft.Storage/storageAccounts/write,Microsoft.Storage/storageAccounts/blobServices/containers/read,Microsoft.Storage/storageAccounts/blobServices/containers/write,Microsoft.Storage/storageAccounts/blobServices/containers/blobs/read,Microsoft.Storage/storageAccounts/blobServices/containers/blobs/write",
29    },
30    Mapping {
31        key: "compute:read",
32        aws: "ec2:DescribeInstances,ec2:DescribeSecurityGroups,ec2:DescribeSubnets,ec2:DescribeVpcs,ec2:DescribeImages",
33        gcp: "compute.instances.get,compute.instances.list,compute.zones.list,compute.regions.list",
34        azure: "Microsoft.Compute/virtualMachines/read,Microsoft.Compute/virtualMachines/instanceView/read,Microsoft.Network/networkInterfaces/read,Microsoft.Network/publicIPAddresses/read",
35    },
36    Mapping {
37        key: "functions:read",
38        aws: "lambda:GetFunction,lambda:ListFunctions",
39        gcp: "cloudfunctions.functions.get,cloudfunctions.functions.list",
40        azure: "Microsoft.Web/sites/read,Microsoft.Web/sites/functions/read",
41    },
42    Mapping {
43        key: "functions:deploy",
44        aws: "lambda:UpdateFunctionCode,lambda:UpdateFunctionConfiguration,lambda:GetFunction,lambda:ListFunctions",
45        gcp: "cloudfunctions.functions.get,cloudfunctions.functions.list,cloudfunctions.functions.update,cloudfunctions.functions.create",
46        azure: "Microsoft.Web/sites/read,Microsoft.Web/sites/write,Microsoft.Web/sites/functions/read,Microsoft.Web/sites/functions/write,Microsoft.Web/sites/restart/action",
47    },
48    Mapping {
49        key: "database:read",
50        aws: "dynamodb:GetItem,dynamodb:Query,dynamodb:Scan,dynamodb:BatchGetItem,dynamodb:DescribeTable,dynamodb:ListTables",
51        gcp: "bigquery.jobs.create,bigquery.tables.getData,bigquery.tables.list,bigquery.datasets.get",
52        azure: "Microsoft.DocumentDB/databaseAccounts/read,Microsoft.DocumentDB/databaseAccounts/sqlDatabases/read,Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/read",
53    },
54    Mapping {
55        key: "database:readwrite",
56        aws: "dynamodb:GetItem,dynamodb:PutItem,dynamodb:UpdateItem,dynamodb:DeleteItem,dynamodb:Query,dynamodb:Scan,dynamodb:BatchGetItem,dynamodb:BatchWriteItem,dynamodb:DescribeTable,dynamodb:ListTables",
57        gcp: "bigquery.jobs.create,bigquery.tables.getData,bigquery.tables.list,bigquery.tables.updateData,bigquery.datasets.get",
58        azure: "Microsoft.DocumentDB/databaseAccounts/read,Microsoft.DocumentDB/databaseAccounts/write,Microsoft.DocumentDB/databaseAccounts/sqlDatabases/read,Microsoft.DocumentDB/databaseAccounts/sqlDatabases/write,Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/read,Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/write",
59    },
60    Mapping {
61        key: "secrets:read",
62        aws: "ssm:GetParameter,ssm:GetParameters,ssm:GetParametersByPath,ssm:DescribeParameters",
63        gcp: "secretmanager.versions.access,secretmanager.secrets.get,secretmanager.secrets.list",
64        azure: "Microsoft.KeyVault/vaults/read,Microsoft.KeyVault/vaults/secrets/read",
65    },
66    Mapping {
67        key: "containers:pull",
68        aws: "ecr:GetAuthorizationToken,ecr:BatchCheckLayerAvailability,ecr:GetDownloadUrlForLayer,ecr:BatchGetImage,ecr:DescribeRepositories",
69        gcp: "artifactregistry.repositories.get,artifactregistry.repositories.downloadArtifacts",
70        azure: "Microsoft.ContainerRegistry/registries/read,Microsoft.ContainerRegistry/registries/pull/read",
71    },
72    Mapping {
73        key: "containers:push",
74        aws: "ecr:GetAuthorizationToken,ecr:BatchCheckLayerAvailability,ecr:GetDownloadUrlForLayer,ecr:BatchGetImage,ecr:PutImage,ecr:InitiateLayerUpload,ecr:UploadLayerPart,ecr:CompleteLayerUpload,ecr:DescribeRepositories,ecr:CreateRepository",
75        gcp: "artifactregistry.repositories.get,artifactregistry.repositories.uploadArtifacts,artifactregistry.repositories.downloadArtifacts",
76        azure: "Microsoft.ContainerRegistry/registries/read,Microsoft.ContainerRegistry/registries/pull/read,Microsoft.ContainerRegistry/registries/push/write",
77    },
78    Mapping {
79        key: "logs:read",
80        aws: "logs:GetLogEvents,logs:DescribeLogGroups,logs:DescribeLogStreams,logs:FilterLogEvents",
81        gcp: "logging.logEntries.list,logging.logs.list,logging.logServices.list",
82        azure: "Microsoft.Insights/logs/read,Microsoft.OperationalInsights/workspaces/query/read",
83    },
84    Mapping {
85        key: "queue:readwrite",
86        aws: "sqs:SendMessage,sqs:ReceiveMessage,sqs:DeleteMessage,sqs:GetQueueAttributes,sqs:ListQueues",
87        gcp: "pubsub.topics.publish,pubsub.subscriptions.consume,pubsub.topics.list,pubsub.subscriptions.list",
88        azure: "Microsoft.ServiceBus/namespaces/queues/read,Microsoft.ServiceBus/namespaces/queues/write",
89    },
90];
91
92/// Expand cloud-agnostic allow strings into provider-specific permissions.
93///
94/// Recognizes keys like `storage:read`, `compute:read`, `functions:deploy`.
95/// Unknown tokens are passed through unchanged, allowing mixing:
96/// `--allow "storage:read,s3:PutObject"` expands storage:read but keeps s3:PutObject.
97///
98/// NOTE: Universal keys map to semantically similar but not identical services
99/// across providers (e.g., `database:read` maps to DynamoDB on AWS but BigQuery
100/// on GCP). Review the expanded permissions for your target provider.
101pub fn expand(allow: &str, provider: &CloudProvider) -> String {
102    let parts: Vec<&str> = allow.split(',').map(|s| s.trim()).collect();
103    let mut expanded = Vec::new();
104    let mut used_universal = false;
105
106    for part in parts {
107        if let Some(mapping) = MAPPINGS.iter().find(|m| m.key == part) {
108            let provider_actions = match provider {
109                CloudProvider::Aws => mapping.aws,
110                CloudProvider::Gcp => mapping.gcp,
111                CloudProvider::Azure => mapping.azure,
112            };
113            expanded.push(provider_actions.to_string());
114            used_universal = true;
115        } else {
116            expanded.push(part.to_string());
117        }
118    }
119
120    if used_universal && !matches!(provider, CloudProvider::Aws) {
121        tracing::info!(
122            provider = ?provider,
123            "Universal syntax expands to different services per provider — \
124             verify the expanded permissions match your intent"
125        );
126    }
127
128    expanded.join(",")
129}
130
131/// List all available universal action keys.
132pub fn list_keys() -> Vec<&'static str> {
133    MAPPINGS.iter().map(|m| m.key).collect()
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    #[test]
141    fn test_expand_storage_read_aws() {
142        let result = expand("storage:read", &CloudProvider::Aws);
143        assert!(result.contains("s3:GetObject"));
144        assert!(result.contains("s3:ListBucket"));
145    }
146
147    #[test]
148    fn test_expand_storage_read_gcp() {
149        let result = expand("storage:read", &CloudProvider::Gcp);
150        assert!(result.contains("storage.objects.get"));
151        assert!(result.contains("storage.buckets.list"));
152    }
153
154    #[test]
155    fn test_expand_storage_read_azure() {
156        let result = expand("storage:read", &CloudProvider::Azure);
157        assert!(result.contains("Microsoft.Storage/storageAccounts/read"));
158    }
159
160    #[test]
161    fn test_expand_multiple() {
162        let result = expand("storage:read, compute:read", &CloudProvider::Aws);
163        assert!(result.contains("s3:GetObject"));
164        assert!(result.contains("ec2:DescribeInstances"));
165    }
166
167    #[test]
168    fn test_passthrough_unknown() {
169        let result = expand("s3:PutObject", &CloudProvider::Aws);
170        assert_eq!(result, "s3:PutObject");
171    }
172
173    #[test]
174    fn test_mixed_universal_and_specific() {
175        let result = expand("storage:read,lambda:InvokeFunction", &CloudProvider::Aws);
176        assert!(result.contains("s3:GetObject"));
177        assert!(result.contains("lambda:InvokeFunction"));
178    }
179
180    #[test]
181    fn test_all_keys_expand() {
182        for key in list_keys() {
183            let aws = expand(key, &CloudProvider::Aws);
184            let gcp = expand(key, &CloudProvider::Gcp);
185            let azure = expand(key, &CloudProvider::Azure);
186            assert!(!aws.is_empty(), "AWS expansion empty for {}", key);
187            assert!(!gcp.is_empty(), "GCP expansion empty for {}", key);
188            assert!(!azure.is_empty(), "Azure expansion empty for {}", key);
189        }
190    }
191}