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.
- {object_storage_proxy-0.2.14 → object_storage_proxy-0.2.16}/Cargo.lock +1 -1
- {object_storage_proxy-0.2.14 → object_storage_proxy-0.2.16}/Cargo.toml +2 -1
- {object_storage_proxy-0.2.14 → object_storage_proxy-0.2.16}/PKG-INFO +70 -13
- {object_storage_proxy-0.2.14 → object_storage_proxy-0.2.16}/README.md +69 -12
- {object_storage_proxy-0.2.14 → object_storage_proxy-0.2.16}/pyproject.toml +3 -0
- object_storage_proxy-0.2.16/src/credentials/hmac_keystore.rs +34 -0
- {object_storage_proxy-0.2.14 → object_storage_proxy-0.2.16}/src/credentials/mod.rs +1 -0
- object_storage_proxy-0.2.16/src/credentials/models.rs +58 -0
- {object_storage_proxy-0.2.14 → object_storage_proxy-0.2.16}/src/credentials/secrets_proxy.rs +54 -0
- {object_storage_proxy-0.2.14 → object_storage_proxy-0.2.16}/src/credentials/signer.rs +179 -16
- {object_storage_proxy-0.2.14 → object_storage_proxy-0.2.16}/src/lib.rs +136 -61
- {object_storage_proxy-0.2.14 → object_storage_proxy-0.2.16}/src/parsers/cos_map.rs +1 -0
- object_storage_proxy-0.2.16/src/parsers/credentials.rs +82 -0
- object_storage_proxy-0.2.16/src/parsers/keystore.rs +59 -0
- {object_storage_proxy-0.2.14 → object_storage_proxy-0.2.16}/src/parsers/mod.rs +1 -0
- {object_storage_proxy-0.2.14 → object_storage_proxy-0.2.16}/src/utils/validator.rs +48 -0
- {object_storage_proxy-0.2.14 → object_storage_proxy-0.2.16}/test_server.py +28 -1
- {object_storage_proxy-0.2.14 → object_storage_proxy-0.2.16}/uv.lock +1 -1
- object_storage_proxy-0.2.14/src/credentials/models.rs +0 -25
- object_storage_proxy-0.2.14/src/parsers/credentials.rs +0 -24
- {object_storage_proxy-0.2.14 → object_storage_proxy-0.2.16}/.cargo/config.toml +0 -0
- {object_storage_proxy-0.2.14 → object_storage_proxy-0.2.16}/.github/workflows/ci.yml +0 -0
- {object_storage_proxy-0.2.14 → object_storage_proxy-0.2.16}/.gitignore +0 -0
- {object_storage_proxy-0.2.14 → object_storage_proxy-0.2.16}/LICENSE +0 -0
- {object_storage_proxy-0.2.14 → object_storage_proxy-0.2.16}/img/logo.svg +0 -0
- {object_storage_proxy-0.2.14 → object_storage_proxy-0.2.16}/img/request_lifecycle.svg +0 -0
- {object_storage_proxy-0.2.14 → object_storage_proxy-0.2.16}/img/request_stages.svg +0 -0
- {object_storage_proxy-0.2.14 → object_storage_proxy-0.2.16}/requirements.txt +0 -0
- {object_storage_proxy-0.2.14 → object_storage_proxy-0.2.16}/src/object_storage_proxy.pyi +0 -0
- {object_storage_proxy-0.2.14 → object_storage_proxy-0.2.16}/src/parsers/path.rs +0 -0
- {object_storage_proxy-0.2.14 → object_storage_proxy-0.2.16}/src/utils/mod.rs +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "object-storage-proxy"
|
|
3
|
-
version = "0.2.
|
|
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.
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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, ...):
|
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|
{object_storage_proxy-0.2.14 → object_storage_proxy-0.2.16}/src/credentials/secrets_proxy.rs
RENAMED
|
@@ -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
|
}
|