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/listKeys/action,Microsoft.Storage/storageAccounts/blobServices/containers/read,Microsoft.Storage/storageAccounts/blobServices/containers/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.
97pub fn expand(allow: &str, provider: &CloudProvider) -> String {
98    let parts: Vec<&str> = allow.split(',').map(|s| s.trim()).collect();
99    let mut expanded = Vec::new();
100
101    for part in parts {
102        if let Some(mapping) = MAPPINGS.iter().find(|m| m.key == part) {
103            let provider_actions = match provider {
104                CloudProvider::Aws => mapping.aws,
105                CloudProvider::Gcp => mapping.gcp,
106                CloudProvider::Azure => mapping.azure,
107            };
108            expanded.push(provider_actions.to_string());
109        } else {
110            expanded.push(part.to_string());
111        }
112    }
113
114    expanded.join(",")
115}
116
117/// List all available universal action keys.
118pub fn list_keys() -> Vec<&'static str> {
119    MAPPINGS.iter().map(|m| m.key).collect()
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125
126    #[test]
127    fn test_expand_storage_read_aws() {
128        let result = expand("storage:read", &CloudProvider::Aws);
129        assert!(result.contains("s3:GetObject"));
130        assert!(result.contains("s3:ListBucket"));
131    }
132
133    #[test]
134    fn test_expand_storage_read_gcp() {
135        let result = expand("storage:read", &CloudProvider::Gcp);
136        assert!(result.contains("storage.objects.get"));
137        assert!(result.contains("storage.buckets.list"));
138    }
139
140    #[test]
141    fn test_expand_storage_read_azure() {
142        let result = expand("storage:read", &CloudProvider::Azure);
143        assert!(result.contains("Microsoft.Storage/storageAccounts/read"));
144    }
145
146    #[test]
147    fn test_expand_multiple() {
148        let result = expand("storage:read, compute:read", &CloudProvider::Aws);
149        assert!(result.contains("s3:GetObject"));
150        assert!(result.contains("ec2:DescribeInstances"));
151    }
152
153    #[test]
154    fn test_passthrough_unknown() {
155        let result = expand("s3:PutObject", &CloudProvider::Aws);
156        assert_eq!(result, "s3:PutObject");
157    }
158
159    #[test]
160    fn test_mixed_universal_and_specific() {
161        let result = expand("storage:read,lambda:InvokeFunction", &CloudProvider::Aws);
162        assert!(result.contains("s3:GetObject"));
163        assert!(result.contains("lambda:InvokeFunction"));
164    }
165
166    #[test]
167    fn test_all_keys_expand() {
168        for key in list_keys() {
169            let aws = expand(key, &CloudProvider::Aws);
170            let gcp = expand(key, &CloudProvider::Gcp);
171            let azure = expand(key, &CloudProvider::Azure);
172            assert!(!aws.is_empty(), "AWS expansion empty for {}", key);
173            assert!(!gcp.is_empty(), "GCP expansion empty for {}", key);
174            assert!(!azure.is_empty(), "Azure expansion empty for {}", key);
175        }
176    }
177}