1use crate::session::CloudProvider;
2
3struct 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
92pub 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
131pub 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}