object-storage-proxy 0.2.14__tar.gz → 0.2.16__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. {object_storage_proxy-0.2.14 → object_storage_proxy-0.2.16}/Cargo.lock +1 -1
  2. {object_storage_proxy-0.2.14 → object_storage_proxy-0.2.16}/Cargo.toml +2 -1
  3. {object_storage_proxy-0.2.14 → object_storage_proxy-0.2.16}/PKG-INFO +70 -13
  4. {object_storage_proxy-0.2.14 → object_storage_proxy-0.2.16}/README.md +69 -12
  5. {object_storage_proxy-0.2.14 → object_storage_proxy-0.2.16}/pyproject.toml +3 -0
  6. object_storage_proxy-0.2.16/src/credentials/hmac_keystore.rs +34 -0
  7. {object_storage_proxy-0.2.14 → object_storage_proxy-0.2.16}/src/credentials/mod.rs +1 -0
  8. object_storage_proxy-0.2.16/src/credentials/models.rs +58 -0
  9. {object_storage_proxy-0.2.14 → object_storage_proxy-0.2.16}/src/credentials/secrets_proxy.rs +54 -0
  10. {object_storage_proxy-0.2.14 → object_storage_proxy-0.2.16}/src/credentials/signer.rs +179 -16
  11. {object_storage_proxy-0.2.14 → object_storage_proxy-0.2.16}/src/lib.rs +136 -61
  12. {object_storage_proxy-0.2.14 → object_storage_proxy-0.2.16}/src/parsers/cos_map.rs +1 -0
  13. object_storage_proxy-0.2.16/src/parsers/credentials.rs +82 -0
  14. object_storage_proxy-0.2.16/src/parsers/keystore.rs +59 -0
  15. {object_storage_proxy-0.2.14 → object_storage_proxy-0.2.16}/src/parsers/mod.rs +1 -0
  16. {object_storage_proxy-0.2.14 → object_storage_proxy-0.2.16}/src/utils/validator.rs +48 -0
  17. {object_storage_proxy-0.2.14 → object_storage_proxy-0.2.16}/test_server.py +28 -1
  18. {object_storage_proxy-0.2.14 → object_storage_proxy-0.2.16}/uv.lock +1 -1
  19. object_storage_proxy-0.2.14/src/credentials/models.rs +0 -25
  20. object_storage_proxy-0.2.14/src/parsers/credentials.rs +0 -24
  21. {object_storage_proxy-0.2.14 → object_storage_proxy-0.2.16}/.cargo/config.toml +0 -0
  22. {object_storage_proxy-0.2.14 → object_storage_proxy-0.2.16}/.github/workflows/ci.yml +0 -0
  23. {object_storage_proxy-0.2.14 → object_storage_proxy-0.2.16}/.gitignore +0 -0
  24. {object_storage_proxy-0.2.14 → object_storage_proxy-0.2.16}/LICENSE +0 -0
  25. {object_storage_proxy-0.2.14 → object_storage_proxy-0.2.16}/img/logo.svg +0 -0
  26. {object_storage_proxy-0.2.14 → object_storage_proxy-0.2.16}/img/request_lifecycle.svg +0 -0
  27. {object_storage_proxy-0.2.14 → object_storage_proxy-0.2.16}/img/request_stages.svg +0 -0
  28. {object_storage_proxy-0.2.14 → object_storage_proxy-0.2.16}/requirements.txt +0 -0
  29. {object_storage_proxy-0.2.14 → object_storage_proxy-0.2.16}/src/object_storage_proxy.pyi +0 -0
  30. {object_storage_proxy-0.2.14 → object_storage_proxy-0.2.16}/src/parsers/path.rs +0 -0
  31. {object_storage_proxy-0.2.14 → object_storage_proxy-0.2.16}/src/utils/mod.rs +0 -0
@@ -1701,7 +1701,7 @@ dependencies = [
1701
1701
 
1702
1702
  [[package]]
1703
1703
  name = "object-storage-proxy"
1704
- version = "0.2.14"
1704
+ version = "0.2.16"
1705
1705
  dependencies = [
1706
1706
  "async-trait",
1707
1707
  "chrono",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "object-storage-proxy"
3
- version = "0.2.14"
3
+ version = "0.2.16"
4
4
  edition = "2024"
5
5
 
6
6
  [dependencies]
@@ -40,6 +40,7 @@ regex = "1.11.1"
40
40
  [lib]
41
41
  name = "object_storage_proxy"
42
42
  crate-type = ["cdylib"]
43
+ path = "src/lib.rs"
43
44
 
44
45
  [package.metadata.maturin]
45
46
  bindings = "pyo3"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: object-storage-proxy
3
- Version: 0.2.14
3
+ Version: 0.2.16
4
4
  Classifier: Programming Language :: Rust
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  Classifier: Programming Language :: Python :: Implementation :: PyPy
@@ -30,11 +30,16 @@ Project-URL: BugTracker, https://github.com/opensourceworks-org/object-storage-p
30
30
 
31
31
  A fast and safe in-process reverse proxy server, based on Cloudflare's [pingora](https://github.com/cloudflare/pingora?tab=readme-ov-file), to reverse proxy AWS and IBM Cloud Object Storage buckets and integrate your Authentication and Authorization services.
32
32
 
33
- - [x] Takes a Python authorization callable function (allows you to plug in your own authorization services) and api_key fetch callback function and cos bucket dictionary.
33
+ Decouples frontend from backend authentication and authorization.
34
+
35
+
36
+ - [x] Takes a Python authorization callable (allows you to plug in your own authorization services) and api_key fetch callback function and cos bucket dictionary.
34
37
  - [x] The validation is cached with optional ttl (default 5min, keep it short).
35
38
  - [x] The apikey is used to authenticate against IBM's IAM endpoint and is cached and renewed on expiration. (IBM only)
36
- - [x] If no apikey is provided, a Python function can be passed in to fetch the apikey or hmac keys for any given bucket (run once).
37
- - [x] HMAC support: passing in access and secret id keys (or as json string from python credentials callable), will be used to sign the request (AWS/IBM/..)
39
+ - [x] If no apikey or hmac keypair is provided, a Python function can be passed in to fetch the apikey or hmac keys for any given bucket (run once).
40
+ - [x] HMAC support: passing in access and secret id keys (or as json string from python credentials callable), will be used to sign the upstream request (AWS/IBM/..)
41
+ - [x] frontend request is signed with your own custom keypair
42
+ - [x] supports running in cloud environments / proxy headers for request signature validation
38
43
 
39
44
  The bucket dict contains for each bucket:
40
45
 
@@ -85,6 +90,8 @@ The Python callables take two arguments:
85
90
  def your_request_authorizer(token: str, bucket: str) -> bool
86
91
  ```
87
92
 
93
+ Proxy Configuration
94
+
88
95
 
89
96
 
90
97
 
@@ -120,7 +127,7 @@ Pass in a function which maps bucket to instance (credentials), and a function t
120
127
  The advantage is we can plug in a python authentication function and another function for authorization, allowing for fine-grained control.
121
128
 
122
129
  ## authentication
123
- We use the standard aws hmac header.
130
+ We use the standard aws hmac header and aws v4 request signing algorithm.
124
131
 
125
132
  ## authorization
126
133
  Pass in a callable from python which will be called from rust. This will be cached (ttl) for consecutive requests.
@@ -177,7 +184,8 @@ def strtobool(val: str) -> bool:
177
184
  raise ValueError(f"invalid truth value {val!r}")
178
185
 
179
186
 
180
- def do_api_creds(bucket) -> str:
187
+ def do_api_creds(token: str, bucket: str) -> str:
188
+ """Fetch credentials (ro, rw, access_denied) for the given bucket, depending on the token. """
181
189
  apikey = os.getenv("COS_API_KEY")
182
190
  if not apikey:
183
191
  raise ValueError("COS_API_KEY environment variable not set")
@@ -186,11 +194,13 @@ def do_api_creds(bucket) -> str:
186
194
  return apikey
187
195
 
188
196
 
189
- def do_hmac_creds(bucket) -> str:
197
+ def do_hmac_creds(token: str, bucket: str) -> str:
198
+ """ Fetch HMAC credentials (ro, rw, access_denied) for the given bucket, depending on the token """
190
199
  access_key = os.getenv("ACCESS_KEY")
191
200
  secret_key = os.getenv("SECRET_KEY")
192
201
  if not access_key or not secret_key:
193
202
  raise ValueError("ACCESS_KEY or SECRET_KEY environment variable not set")
203
+
194
204
  print(f"Fetching HMAC credentials for {bucket}...")
195
205
 
196
206
  return json.dumps({
@@ -198,8 +208,27 @@ def do_hmac_creds(bucket) -> str:
198
208
  "secret_key": secret_key
199
209
  })
200
210
 
211
+ def lookup_secret_key(access_key: str) -> str | None:
212
+ # get all environment variables ending in ACCESS_KEY
213
+ access_keys = [{key:value} for key, value in os.environ.items() if key.endswith("ACCESS_KEY") and value==access_key ]
214
+
215
+ if len(access_keys) > 0:
216
+ access_key_var = next((k for k, v in access_keys[0].items() if v == access_key), None)
217
+
218
+ secret_key_var = access_key_var.replace("ACCESS_KEY", "SECRET_KEY")
219
+ return os.getenv(secret_key_var, None)
220
+ else:
221
+ print(f"no access keys found for : {access_key}")
222
+
201
223
 
202
224
  def do_validation(token: str, bucket: str) -> bool:
225
+ """ Authorize the request based on token for the given bucket.
226
+ You can plug in your own authorization service here.
227
+ The token is the authorization token passed in the request.
228
+ The bucket is the bucket name.
229
+ The function should return True if the request is authorized, False otherwise.
230
+ """
231
+
203
232
  print(f"PYTHON: Validating headers: {token} for {bucket}...")
204
233
  # return random.choice([True, False])
205
234
  return True
@@ -214,7 +243,6 @@ def main() -> None:
214
243
  osp.enable_request_counting()
215
244
  print("Request counting enabled")
216
245
 
217
-
218
246
  apikey = os.getenv("COS_API_KEY")
219
247
  if not apikey:
220
248
  raise ValueError("COS_API_KEY environment variable not set")
@@ -240,16 +268,48 @@ def main() -> None:
240
268
  # "secret_key": os.getenv("SECRET_KEY"),
241
269
  "port": 443,
242
270
  "ttl": 300
271
+ },
272
+ "proxy-bucket05": {
273
+ "host": "s3.eu-de.cloud-object-storage.appdomain.cloud",
274
+ "region": "eu-de",
275
+ "access_key": os.getenv("PROXY_BUCKET05_ACCESS_KEY"),
276
+ "secret_key": os.getenv("PROXY_BUCKET05_SECRET_KEY"),
277
+ "port": 443,
278
+ "ttl": 300
279
+ },
280
+ "proxy-aws-bucket01": {
281
+ "host": "s3.eu-west-3.amazonaws.com",
282
+ "region": "eu-west-3",
283
+ "access_key": os.getenv("AWS_ACCESS_KEY"),
284
+ "secret_key": os.getenv("AWS_SECRET_KEY"),
285
+ "port": 443,
286
+ "ttl": 300
243
287
  }
244
288
  }
245
289
 
290
+ hmac_keys= [
291
+ # {
292
+ # "access_key": os.getenv("LOCAL_ACCESS_KEY"),
293
+ # "secret_key": os.getenv("LOCAL_SECRET_KEY")
294
+ # },
295
+ {
296
+ "access_key": os.getenv("LOCAL2_ACCESS_KEY"),
297
+ "secret_key": os.getenv("LOCAL2_SECRET_KEY")
298
+ },
299
+
300
+ ]
301
+
246
302
  ra = ProxyServerConfig(
247
303
  cos_map=cos_map,
248
- bucket_creds_fetcher=do_hmac_creds, # or: do_api_creds
304
+ bucket_creds_fetcher=do_hmac_creds,
249
305
  validator=do_validation,
250
306
  http_port=6190,
251
- https_port=8443,
307
+ # https_port=8443,
252
308
  threads=1,
309
+ # verify=False,
310
+ hmac_keystore=hmac_keys,
311
+ skip_signature_validation=False,
312
+ hmac_fetcher=lookup_secret_key
253
313
  )
254
314
 
255
315
  start_server(ra)
@@ -258,9 +318,6 @@ def main() -> None:
258
318
  if __name__ == "__main__":
259
319
  main()
260
320
 
261
-
262
-
263
-
264
321
  ```
265
322
 
266
323
  Run with [aws-cli](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) (but could be anything compatible with the aws s3 api like polars, spark, presto, ...):
@@ -11,11 +11,16 @@
11
11
 
12
12
  A fast and safe in-process reverse proxy server, based on Cloudflare's [pingora](https://github.com/cloudflare/pingora?tab=readme-ov-file), to reverse proxy AWS and IBM Cloud Object Storage buckets and integrate your Authentication and Authorization services.
13
13
 
14
- - [x] Takes a Python authorization callable function (allows you to plug in your own authorization services) and api_key fetch callback function and cos bucket dictionary.
14
+ Decouples frontend from backend authentication and authorization.
15
+
16
+
17
+ - [x] Takes a Python authorization callable (allows you to plug in your own authorization services) and api_key fetch callback function and cos bucket dictionary.
15
18
  - [x] The validation is cached with optional ttl (default 5min, keep it short).
16
19
  - [x] The apikey is used to authenticate against IBM's IAM endpoint and is cached and renewed on expiration. (IBM only)
17
- - [x] If no apikey is provided, a Python function can be passed in to fetch the apikey or hmac keys for any given bucket (run once).
18
- - [x] HMAC support: passing in access and secret id keys (or as json string from python credentials callable), will be used to sign the request (AWS/IBM/..)
20
+ - [x] If no apikey or hmac keypair is provided, a Python function can be passed in to fetch the apikey or hmac keys for any given bucket (run once).
21
+ - [x] HMAC support: passing in access and secret id keys (or as json string from python credentials callable), will be used to sign the upstream request (AWS/IBM/..)
22
+ - [x] frontend request is signed with your own custom keypair
23
+ - [x] supports running in cloud environments / proxy headers for request signature validation
19
24
 
20
25
  The bucket dict contains for each bucket:
21
26
 
@@ -66,6 +71,8 @@ The Python callables take two arguments:
66
71
  def your_request_authorizer(token: str, bucket: str) -> bool
67
72
  ```
68
73
 
74
+ Proxy Configuration
75
+
69
76
 
70
77
 
71
78
 
@@ -101,7 +108,7 @@ Pass in a function which maps bucket to instance (credentials), and a function t
101
108
  The advantage is we can plug in a python authentication function and another function for authorization, allowing for fine-grained control.
102
109
 
103
110
  ## authentication
104
- We use the standard aws hmac header.
111
+ We use the standard aws hmac header and aws v4 request signing algorithm.
105
112
 
106
113
  ## authorization
107
114
  Pass in a callable from python which will be called from rust. This will be cached (ttl) for consecutive requests.
@@ -158,7 +165,8 @@ def strtobool(val: str) -> bool:
158
165
  raise ValueError(f"invalid truth value {val!r}")
159
166
 
160
167
 
161
- def do_api_creds(bucket) -> str:
168
+ def do_api_creds(token: str, bucket: str) -> str:
169
+ """Fetch credentials (ro, rw, access_denied) for the given bucket, depending on the token. """
162
170
  apikey = os.getenv("COS_API_KEY")
163
171
  if not apikey:
164
172
  raise ValueError("COS_API_KEY environment variable not set")
@@ -167,11 +175,13 @@ def do_api_creds(bucket) -> str:
167
175
  return apikey
168
176
 
169
177
 
170
- def do_hmac_creds(bucket) -> str:
178
+ def do_hmac_creds(token: str, bucket: str) -> str:
179
+ """ Fetch HMAC credentials (ro, rw, access_denied) for the given bucket, depending on the token """
171
180
  access_key = os.getenv("ACCESS_KEY")
172
181
  secret_key = os.getenv("SECRET_KEY")
173
182
  if not access_key or not secret_key:
174
183
  raise ValueError("ACCESS_KEY or SECRET_KEY environment variable not set")
184
+
175
185
  print(f"Fetching HMAC credentials for {bucket}...")
176
186
 
177
187
  return json.dumps({
@@ -179,8 +189,27 @@ def do_hmac_creds(bucket) -> str:
179
189
  "secret_key": secret_key
180
190
  })
181
191
 
192
+ def lookup_secret_key(access_key: str) -> str | None:
193
+ # get all environment variables ending in ACCESS_KEY
194
+ access_keys = [{key:value} for key, value in os.environ.items() if key.endswith("ACCESS_KEY") and value==access_key ]
195
+
196
+ if len(access_keys) > 0:
197
+ access_key_var = next((k for k, v in access_keys[0].items() if v == access_key), None)
198
+
199
+ secret_key_var = access_key_var.replace("ACCESS_KEY", "SECRET_KEY")
200
+ return os.getenv(secret_key_var, None)
201
+ else:
202
+ print(f"no access keys found for : {access_key}")
203
+
182
204
 
183
205
  def do_validation(token: str, bucket: str) -> bool:
206
+ """ Authorize the request based on token for the given bucket.
207
+ You can plug in your own authorization service here.
208
+ The token is the authorization token passed in the request.
209
+ The bucket is the bucket name.
210
+ The function should return True if the request is authorized, False otherwise.
211
+ """
212
+
184
213
  print(f"PYTHON: Validating headers: {token} for {bucket}...")
185
214
  # return random.choice([True, False])
186
215
  return True
@@ -195,7 +224,6 @@ def main() -> None:
195
224
  osp.enable_request_counting()
196
225
  print("Request counting enabled")
197
226
 
198
-
199
227
  apikey = os.getenv("COS_API_KEY")
200
228
  if not apikey:
201
229
  raise ValueError("COS_API_KEY environment variable not set")
@@ -221,16 +249,48 @@ def main() -> None:
221
249
  # "secret_key": os.getenv("SECRET_KEY"),
222
250
  "port": 443,
223
251
  "ttl": 300
252
+ },
253
+ "proxy-bucket05": {
254
+ "host": "s3.eu-de.cloud-object-storage.appdomain.cloud",
255
+ "region": "eu-de",
256
+ "access_key": os.getenv("PROXY_BUCKET05_ACCESS_KEY"),
257
+ "secret_key": os.getenv("PROXY_BUCKET05_SECRET_KEY"),
258
+ "port": 443,
259
+ "ttl": 300
260
+ },
261
+ "proxy-aws-bucket01": {
262
+ "host": "s3.eu-west-3.amazonaws.com",
263
+ "region": "eu-west-3",
264
+ "access_key": os.getenv("AWS_ACCESS_KEY"),
265
+ "secret_key": os.getenv("AWS_SECRET_KEY"),
266
+ "port": 443,
267
+ "ttl": 300
224
268
  }
225
269
  }
226
270
 
271
+ hmac_keys= [
272
+ # {
273
+ # "access_key": os.getenv("LOCAL_ACCESS_KEY"),
274
+ # "secret_key": os.getenv("LOCAL_SECRET_KEY")
275
+ # },
276
+ {
277
+ "access_key": os.getenv("LOCAL2_ACCESS_KEY"),
278
+ "secret_key": os.getenv("LOCAL2_SECRET_KEY")
279
+ },
280
+
281
+ ]
282
+
227
283
  ra = ProxyServerConfig(
228
284
  cos_map=cos_map,
229
- bucket_creds_fetcher=do_hmac_creds, # or: do_api_creds
285
+ bucket_creds_fetcher=do_hmac_creds,
230
286
  validator=do_validation,
231
287
  http_port=6190,
232
- https_port=8443,
288
+ # https_port=8443,
233
289
  threads=1,
290
+ # verify=False,
291
+ hmac_keystore=hmac_keys,
292
+ skip_signature_validation=False,
293
+ hmac_fetcher=lookup_secret_key
234
294
  )
235
295
 
236
296
  start_server(ra)
@@ -239,9 +299,6 @@ def main() -> None:
239
299
  if __name__ == "__main__":
240
300
  main()
241
301
 
242
-
243
-
244
-
245
302
  ```
246
303
 
247
304
  Run with [aws-cli](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) (but could be anything compatible with the aws s3 api like polars, spark, presto, ...):
@@ -26,6 +26,9 @@ dependencies = [
26
26
  [tool.maturin]
27
27
  features = ["pyo3/extension-module"]
28
28
 
29
+ [tool.uv.workspace]
30
+ members = ["integration"]
31
+
29
32
  [dependency-groups]
30
33
  dev = []
31
34
 
@@ -0,0 +1,34 @@
1
+ use pyo3::pyclass;
2
+
3
+ #[pyclass]
4
+ #[derive(Debug, Clone)]
5
+ pub struct HmacKeyStore {
6
+ access_key: String,
7
+ secret_key: String,
8
+
9
+ }
10
+
11
+ impl HmacKeyStore {
12
+ pub fn new(access_key: String, secret_key: String) -> Self {
13
+ HmacKeyStore {
14
+ access_key,
15
+ secret_key,
16
+ }
17
+ }
18
+
19
+ pub fn get_access_key(&self) -> &str {
20
+ &self.access_key
21
+ }
22
+
23
+ pub fn get_secret_key(&self) -> &str {
24
+ &self.secret_key
25
+ }
26
+ }
27
+ impl Default for HmacKeyStore {
28
+ fn default() -> Self {
29
+ HmacKeyStore {
30
+ access_key: String::new(),
31
+ secret_key: String::new(),
32
+ }
33
+ }
34
+ }
@@ -1,3 +1,4 @@
1
1
  pub mod models;
2
2
  pub mod secrets_proxy;
3
3
  pub mod signer;
4
+ pub mod hmac_keystore;
@@ -0,0 +1,58 @@
1
+ pub enum BucketCredential {
2
+ Hmac {
3
+ access_key: String,
4
+ secret_key: String,
5
+ },
6
+ ApiKey(String),
7
+ }
8
+
9
+ impl BucketCredential {
10
+ pub fn parse(raw: &str) -> Self {
11
+ if let Ok(json_val) = serde_json::from_str::<serde_json::Value>(raw) {
12
+ if let (Some(ak), Some(sk)) = (json_val.get("access_key"), json_val.get("secret_key")) {
13
+ return BucketCredential::Hmac {
14
+ access_key: ak.as_str().unwrap().to_owned(),
15
+ secret_key: sk.as_str().unwrap().to_owned(),
16
+ };
17
+ }
18
+ if let Some(apikey) = json_val.get("api_key").or_else(|| json_val.get("apikey")) {
19
+ return BucketCredential::ApiKey(apikey.as_str().unwrap().to_owned());
20
+ }
21
+ }
22
+
23
+ BucketCredential::ApiKey(raw.to_owned())
24
+ }
25
+ }
26
+
27
+ #[cfg(test)]
28
+ mod tests {
29
+ use super::*;
30
+
31
+
32
+ #[test]
33
+ fn parse_bucket_credential_variants() {
34
+ let hmac_json = r#"{ "access_key": "AK", "secret_key": "SK" }"#;
35
+ match BucketCredential::parse(hmac_json) {
36
+ BucketCredential::Hmac { access_key, secret_key } => {
37
+ assert_eq!(access_key, "AK");
38
+ assert_eq!(secret_key, "SK");
39
+ }
40
+ _ => panic!("Expected Hmac variant"),
41
+ }
42
+
43
+ let api_json = r#"{ "api_key": "APIKEY" }"#;
44
+ if let BucketCredential::ApiKey(k) = BucketCredential::parse(api_json) {
45
+ assert_eq!(k, "APIKEY");
46
+ } else {
47
+ panic!("Expected ApiKey variant");
48
+ }
49
+
50
+ let raw = "raw_token";
51
+ if let BucketCredential::ApiKey(k) = BucketCredential::parse(raw) {
52
+ assert_eq!(k, raw);
53
+ } else {
54
+ panic!("Expected fallback ApiKey variant");
55
+ }
56
+ }
57
+
58
+ }
@@ -170,6 +170,8 @@ pub(crate) async fn get_credential_for_bucket(
170
170
 
171
171
  #[cfg(test)]
172
172
  mod tests {
173
+ use std::time::{SystemTime, UNIX_EPOCH};
174
+
173
175
  use super::*;
174
176
  use wiremock::matchers::{method, path};
175
177
  use wiremock::{Mock, MockServer, ResponseTemplate};
@@ -267,4 +269,56 @@ mod tests {
267
269
  Err(format!("Failed to get token: {}", err_text).into())
268
270
  }
269
271
  }
272
+
273
+ #[tokio::test]
274
+ async fn secrets_cache_hit_returns_cached_value() {
275
+ let cache = SecretsCache::new();
276
+ let key = "test".to_string();
277
+
278
+ let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
279
+ cache.insert(key.clone(), "cached_token".to_string(), now + 3600);
280
+
281
+
282
+ let fetcher = || async { panic!("Should not be called on cache hit") };
283
+
284
+ let result = cache.get(&key, fetcher).await;
285
+ assert_eq!(result, Some("cached_token".to_string()));
286
+ }
287
+
288
+ #[tokio::test]
289
+ async fn secrets_cache_expired_renews_token() {
290
+ let cache = SecretsCache::new();
291
+ let key = "test2".to_string();
292
+ // expired token
293
+ let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
294
+ cache.insert(key.clone(), "old_token".to_string(), now);
295
+
296
+ // fetcher returns new token
297
+ let fetcher = move || async {
298
+ let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
299
+ Ok(IamResponse { access_token: "new_token".into(), expires_in: 3600, expiration: now + 7200 })
300
+ };
301
+
302
+ let result = cache.get(&key, fetcher).await;
303
+ assert_eq!(result, Some("new_token".to_string()));
304
+ }
305
+
306
+ #[tokio::test]
307
+ async fn secrets_cache_invalidate_works() {
308
+ let cache = SecretsCache::new();
309
+ let key = "test3".to_string();
310
+ let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
311
+ cache.insert(key.clone(), "token".to_string(), now + 3600);
312
+
313
+ cache.invalidate(&key);
314
+
315
+ // now fetcher must be called
316
+ let fetcher = move || async {
317
+ let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
318
+ Ok(IamResponse { access_token: "fresh_token".into(), expires_in: 3600, expiration: now + 3600 })
319
+ };
320
+ let result = cache.get(&key, fetcher).await;
321
+ assert_eq!(result, Some("fresh_token".to_string()));
322
+ }
323
+
270
324
  }