|
12 | 12 |
|
13 | 13 | mod common; |
14 | 14 |
|
15 | | -use aws_config::BehaviorVersion; |
16 | | -use aws_sdk_s3::config::{Credentials, Region}; |
17 | | -use aws_sdk_s3::Client; |
18 | | -use facet_common::proxy::s3::UpstreamStyle; |
19 | | -use crate::common::{ |
20 | | - get_available_port, launch_s3proxy, MinioInstance, ProxyConfig, |
21 | | - MINIO_ACCESS_KEY, MINIO_SECRET_KEY, TEST_BUCKET, TEST_KEY, |
| 15 | +use common::{ |
| 16 | + create_test_client, get_available_port, launch_s3proxy, setup_postgres_container, |
| 17 | + MinioInstance, PassthroughCredentialsResolver, ProxyConfig, TestJwtVerifier, |
| 18 | + MINIO_ACCESS_KEY, MINIO_SECRET_KEY, TEST_BUCKET, |
22 | 19 | }; |
23 | | - |
24 | | -const TEST_CONTENT: &str = "Hello from Pingora proxy test!"; |
25 | | -const VALID_SESSION_TOKEN: &str = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"; |
26 | | -const INVALID_SESSION_TOKEN: &str = "invalid-token"; |
| 20 | +use facet_common::auth::{AuthorizationEvaluator, Operation, PostgresAuthorizationEvaluator, Rule, RuleStore}; |
| 21 | +use facet_common::context::ParticipantContext; |
| 22 | +use facet_common::proxy::s3::{ |
| 23 | + DefaultS3OperationParser, S3Credentials, StaticParticipantContextResolver, UpstreamStyle, |
| 24 | +}; |
| 25 | +use std::sync::Arc; |
27 | 26 |
|
28 | 27 | #[tokio::test] |
29 | | -async fn test_s3_proxy_with_token_validation() { |
30 | | - // Start MinIO container |
31 | | - let minio = MinioInstance::launch().await; |
32 | | - minio.setup_bucket_with_file(TEST_BUCKET, TEST_KEY, TEST_CONTENT.as_bytes()).await; |
| 28 | +async fn test_s3_proxy_end_to_end_with_postgres() { |
| 29 | + // Launch Postgres container and initialize auth evaluator |
| 30 | + let (pool, _pg_container) = setup_postgres_container().await; |
| 31 | + let auth_evaluator = Arc::new(PostgresAuthorizationEvaluator::new(pool)); |
| 32 | + auth_evaluator.initialize().await.unwrap(); |
33 | 33 |
|
34 | | - // Get an available port for the proxy |
| 34 | + // Launch MinIO container as upstream S3 server |
| 35 | + let minio = MinioInstance::launch().await; |
| 36 | + minio.setup_bucket_with_file(TEST_BUCKET, "test-file.txt", b"Hello from MinIO!").await; |
| 37 | + minio.setup_bucket_with_file(TEST_BUCKET, "data/document.pdf", b"PDF content here").await; |
| 38 | + |
| 39 | + let participant_id = "user123"; |
| 40 | + let scope = "agreement-456"; |
| 41 | + let participant_context = ParticipantContext::builder() |
| 42 | + .identifier(participant_id) |
| 43 | + .audience("s3-proxy") |
| 44 | + .build(); |
| 45 | + |
| 46 | + // Create authorization rules in Postgres |
| 47 | + // Rule 1: Allow GetObject on all objects in test-bucket (note: URL path includes leading slash) |
| 48 | + let get_rule = Rule::new( |
| 49 | + scope.to_string(), |
| 50 | + vec!["s3:GetObject".to_string()], |
| 51 | + format!("^/{}/.*", TEST_BUCKET), |
| 52 | + ) |
| 53 | + .unwrap(); |
| 54 | + |
| 55 | + // Rule 2: Allow PutObject in test-bucket/uploads/ path (note: URL path includes leading slash) |
| 56 | + let put_rule = Rule::new( |
| 57 | + scope.to_string(), |
| 58 | + vec!["s3:PutObject".to_string()], |
| 59 | + format!("^/{}/uploads/.*", TEST_BUCKET), |
| 60 | + ) |
| 61 | + .unwrap(); |
| 62 | + |
| 63 | + auth_evaluator.save_rule(&participant_context, get_rule).await.unwrap(); |
| 64 | + auth_evaluator.save_rule(&participant_context, put_rule).await.unwrap(); |
| 65 | + |
| 66 | + // Verify rules are stored |
| 67 | + let rules = auth_evaluator.get_rules(&participant_context).await.unwrap(); |
| 68 | + assert_eq!(rules.len(), 2); |
| 69 | + |
| 70 | + // Configure and launch S3 proxy with Postgres auth |
35 | 71 | let proxy_port = get_available_port(); |
36 | | - launch_s3proxy(ProxyConfig::for_token_testing( |
37 | | - proxy_port, |
38 | | - minio.host.clone(), |
39 | | - UpstreamStyle::PathStyle, |
40 | | - None, |
41 | | - VALID_SESSION_TOKEN.to_string(), |
42 | | - "test-scope".to_string(), |
43 | | - ).await); |
44 | | - |
45 | | - // Configure SDK to use the proxy as a reverse proxy endpoint |
| 72 | + let proxy_config = ProxyConfig { |
| 73 | + port: proxy_port, |
| 74 | + upstream_endpoint: minio.host.clone(), |
| 75 | + upstream_style: UpstreamStyle::PathStyle, |
| 76 | + proxy_domain: None, |
| 77 | + credential_resolver: Arc::new(PassthroughCredentialsResolver { |
| 78 | + credentials: S3Credentials { |
| 79 | + access_key_id: MINIO_ACCESS_KEY.to_string(), |
| 80 | + secret_key: MINIO_SECRET_KEY.to_string(), |
| 81 | + region: "us-east-1".to_string(), |
| 82 | + }, |
| 83 | + }), |
| 84 | + participant_context_resolver: Arc::new(StaticParticipantContextResolver { |
| 85 | + participant_context: participant_context.clone(), |
| 86 | + }), |
| 87 | + token_verifier: Arc::new(TestJwtVerifier { |
| 88 | + scope: scope.to_string(), |
| 89 | + }), |
| 90 | + auth_evaluator: auth_evaluator.clone(), |
| 91 | + operation_parser: Some(Arc::new(DefaultS3OperationParser::new())), |
| 92 | + }; |
| 93 | + |
| 94 | + launch_s3proxy(proxy_config).await; |
| 95 | + |
| 96 | + // Test 1: GET request - should succeed (authorized by get_rule) |
46 | 97 | let proxy_url = format!("http://127.0.0.1:{}", proxy_port); |
| 98 | + let client = create_test_client(&proxy_url, Some("test-token".to_string())).await; |
47 | 99 |
|
48 | | - // Test Case 1: Valid token succeeds |
49 | | - let valid_config = aws_config::defaults(BehaviorVersion::latest()) |
50 | | - .credentials_provider(Credentials::new( |
51 | | - "", |
52 | | - "", |
53 | | - Some(VALID_SESSION_TOKEN.to_string()), // Valid token! |
54 | | - None, |
55 | | - "test", |
56 | | - )) |
57 | | - .region(Region::new("us-east-1")) |
58 | | - .endpoint_url(&proxy_url) // Point directly to the proxy |
59 | | - .load() |
60 | | - .await; |
61 | | - |
62 | | - let valid_client = Client::new(&valid_config); |
63 | | - |
64 | | - let result = valid_client |
| 100 | + let get_response = client |
65 | 101 | .get_object() |
66 | 102 | .bucket(TEST_BUCKET) |
67 | | - .key(TEST_KEY) |
| 103 | + .key("test-file.txt") |
68 | 104 | .send() |
69 | | - .await |
70 | | - .expect("Request with valid token should succeed"); |
71 | | - |
72 | | - let body = result.body.collect().await.expect("Failed to read body"); |
73 | | - let content = String::from_utf8(body.to_vec()).expect("Invalid UTF-8"); |
74 | | - |
75 | | - assert_eq!(content, TEST_CONTENT, "Content should match"); |
76 | | - |
77 | | - // Test Case 2: Invalid token fails |
78 | | - let invalid_config = aws_config::defaults(BehaviorVersion::latest()) |
79 | | - .credentials_provider(Credentials::new( |
80 | | - MINIO_ACCESS_KEY, |
81 | | - MINIO_SECRET_KEY, |
82 | | - Some(INVALID_SESSION_TOKEN.to_string()), // Invalid token! |
83 | | - None, |
84 | | - "test", |
85 | | - )) |
86 | | - .region(Region::new("us-east-1")) |
87 | | - .endpoint_url(&proxy_url) // Point directly to the proxy |
88 | | - .load() |
89 | 105 | .await; |
90 | 106 |
|
91 | | - let invalid_client = Client::new(&invalid_config); |
| 107 | + assert!(get_response.is_ok(), "GetObject should succeed with valid authorization. Error: {:?}", get_response.as_ref().err()); |
| 108 | + let body = get_response.unwrap().body.collect().await.unwrap(); |
| 109 | + assert_eq!(body.to_vec(), b"Hello from MinIO!"); |
92 | 110 |
|
93 | | - let result = invalid_client |
| 111 | + // Test 2: GET another file - should succeed (authorized by get_rule) |
| 112 | + let get_response2 = client |
94 | 113 | .get_object() |
95 | 114 | .bucket(TEST_BUCKET) |
96 | | - .key(TEST_KEY) |
| 115 | + .key("data/document.pdf") |
97 | 116 | .send() |
98 | 117 | .await; |
99 | 118 |
|
100 | | - assert!(result.is_err(), "Request with invalid token should fail"); |
101 | | - |
102 | | - // Test Case 3: Missing token fails |
103 | | - let no_token_config = aws_config::defaults(BehaviorVersion::latest()) |
104 | | - .credentials_provider(Credentials::new( |
105 | | - MINIO_ACCESS_KEY, |
106 | | - MINIO_SECRET_KEY, |
107 | | - None, // No token! |
108 | | - None, |
109 | | - "test", |
110 | | - )) |
111 | | - .region(Region::new("us-east-1")) |
112 | | - .endpoint_url(&proxy_url) // Point directly to the proxy |
113 | | - .load() |
114 | | - .await; |
| 119 | + assert!(get_response2.is_ok(), "GetObject should succeed for nested path"); |
| 120 | + let body2 = get_response2.unwrap().body.collect().await.unwrap(); |
| 121 | + assert_eq!(body2.to_vec(), b"PDF content here"); |
115 | 122 |
|
116 | | - let no_token_client = Client::new(&no_token_config); |
117 | | - |
118 | | - let result = no_token_client |
119 | | - .get_object() |
| 123 | + // Test 3: PUT request to uploads/ - should succeed (authorized by put_rule) |
| 124 | + let put_response = client |
| 125 | + .put_object() |
120 | 126 | .bucket(TEST_BUCKET) |
121 | | - .key(TEST_KEY) |
| 127 | + .key("uploads/new-file.txt") |
| 128 | + .body(aws_sdk_s3::primitives::ByteStream::from_static(b"New content uploaded through proxy")) |
122 | 129 | .send() |
123 | 130 | .await; |
124 | 131 |
|
125 | | - assert!(result.is_err(), "Request without token should fail"); |
| 132 | + assert!(put_response.is_ok(), "PutObject should succeed in uploads/ path"); |
| 133 | + |
| 134 | + assert!( |
| 135 | + minio.verify_object_content(TEST_BUCKET, "uploads/new-file.txt", b"New content uploaded through proxy").await, |
| 136 | + "Uploaded file should exist in MinIO with correct content" |
| 137 | + ); |
| 138 | + |
| 139 | + // Test 5: Verify authorization decisions are persisted in Postgres |
| 140 | + // Check that we can evaluate operations directly using the AuthorizationEvaluator trait |
| 141 | + let get_operation = Operation::builder() |
| 142 | + .scope(scope) |
| 143 | + .action("s3:GetObject") |
| 144 | + .resource(format!("/{}/test-file.txt", TEST_BUCKET)) |
| 145 | + .build(); |
| 146 | + |
| 147 | + let authorized = <PostgresAuthorizationEvaluator as AuthorizationEvaluator>::evaluate( |
| 148 | + &*auth_evaluator, |
| 149 | + &participant_context, |
| 150 | + get_operation, |
| 151 | + ) |
| 152 | + .await |
| 153 | + .unwrap(); |
| 154 | + assert!(authorized, "GetObject operation should be authorized"); |
| 155 | + |
| 156 | + let put_operation = Operation::builder() |
| 157 | + .scope(scope) |
| 158 | + .action("s3:PutObject") |
| 159 | + .resource(format!("/{}/uploads/new-file.txt", TEST_BUCKET)) |
| 160 | + .build(); |
| 161 | + |
| 162 | + let authorized_put = <PostgresAuthorizationEvaluator as AuthorizationEvaluator>::evaluate( |
| 163 | + &*auth_evaluator, |
| 164 | + &participant_context, |
| 165 | + put_operation, |
| 166 | + ) |
| 167 | + .await |
| 168 | + .unwrap(); |
| 169 | + assert!(authorized_put, "PutObject operation should be authorized for uploads/ path"); |
| 170 | + |
| 171 | + // Test 6: Verify unauthorized operation would be denied |
| 172 | + let delete_operation = Operation::builder() |
| 173 | + .scope(scope) |
| 174 | + .action("s3:DeleteObject") |
| 175 | + .resource(format!("/{}/test-file.txt", TEST_BUCKET)) |
| 176 | + .build(); |
| 177 | + |
| 178 | + let unauthorized = <PostgresAuthorizationEvaluator as AuthorizationEvaluator>::evaluate( |
| 179 | + &*auth_evaluator, |
| 180 | + &participant_context, |
| 181 | + delete_operation, |
| 182 | + ) |
| 183 | + .await |
| 184 | + .unwrap(); |
| 185 | + assert!(!unauthorized, "DeleteObject should not be authorized"); |
126 | 186 | } |
0 commit comments