Skip to content

Commit 6303e79

Browse files
committed
Merge branch 'kv-validate'
2 parents d9f24b4 + 5f57bb6 commit 6303e79

File tree

5 files changed

+174
-12
lines changed

5 files changed

+174
-12
lines changed

.github/workflows/check.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: Check
22
on:
33
push:
44
branches:
5-
- main
5+
- "*"
66
pull_request:
77
branches:
88
- "*"

.github/workflows/test.yml

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,7 @@ name: test
33
on:
44
push:
55
branches:
6-
- main
7-
- "release/**"
8-
- "dev/**"
6+
- "**"
97
pull_request:
108
branches:
119
- "**"
@@ -20,7 +18,7 @@ jobs:
2018
fail-fast: false
2119
matrix:
2220
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
23-
nats_version: ["v2.10.26", "v2.11.0", "main"]
21+
nats_version: ["v2.10.29", "v2.11.4", "main"]
2422

2523
steps:
2624
- name: Check out repository

nats/js/errors.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,13 @@ def __str__(self) -> str:
254254
return "nats: history limited to a max of 64"
255255

256256

257+
class InvalidKeyError(Error):
258+
"""
259+
Raised when trying to put an object in Key Value with an invalid key.
260+
"""
261+
pass
262+
263+
257264
class InvalidBucketNameError(Error):
258265
"""
259266
Raised when trying to create a KV or OBJ bucket with invalid name.

nats/js/kv.py

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import asyncio
1818
import datetime
1919
import logging
20+
import re
2021
from dataclasses import dataclass
2122
from typing import TYPE_CHECKING, List, Optional
2223

@@ -34,6 +35,14 @@
3435

3536
logger = logging.getLogger(__name__)
3637

38+
VALID_KEY_RE = re.compile(r'^[-/_=\.a-zA-Z0-9]+$')
39+
40+
41+
def _is_key_valid(key: str) -> bool:
42+
if len(key) == 0 or key[0] == '.' or key[-1] == '.':
43+
return False
44+
return bool(VALID_KEY_RE.match(key))
45+
3746

3847
class KeyValue:
3948
"""
@@ -124,10 +133,18 @@ def __init__(
124133
self._js = js
125134
self._direct = direct
126135

127-
async def get(self, key: str, revision: Optional[int] = None) -> Entry:
136+
async def get(
137+
self,
138+
key: str,
139+
revision: Optional[int] = None,
140+
validate_keys: bool = True
141+
) -> Entry:
128142
"""
129143
get returns the latest value for the key.
130144
"""
145+
if validate_keys and not _is_key_valid(key):
146+
raise nats.js.errors.InvalidKeyError
147+
131148
entry = None
132149
try:
133150
entry = await self._get(key, revision)
@@ -179,21 +196,33 @@ async def _get(self, key: str, revision: Optional[int] = None) -> Entry:
179196

180197
return entry
181198

182-
async def put(self, key: str, value: bytes) -> int:
199+
async def put(
200+
self, key: str, value: bytes, validate_keys: bool = True
201+
) -> int:
183202
"""
184203
put will place the new value for the key into the store
185204
and return the revision number.
186205
"""
206+
if validate_keys and not _is_key_valid(key):
207+
raise nats.js.errors.InvalidKeyError(key)
208+
187209
pa = await self._js.publish(f"{self._pre}{key}", value)
188210
return pa.seq
189211

190-
async def create(self, key: str, value: bytes) -> int:
212+
async def create(
213+
self, key: str, value: bytes, validate_keys: bool = True
214+
) -> int:
191215
"""
192216
create will add the key/value pair iff it does not exist.
193217
"""
218+
if validate_keys and not _is_key_valid(key):
219+
raise nats.js.errors.InvalidKeyError(key)
220+
194221
pa = None
195222
try:
196-
pa = await self.update(key, value, last=0)
223+
pa = await self.update(
224+
key, value, last=0, validate_keys=validate_keys
225+
)
197226
except nats.js.errors.KeyWrongLastSequenceError as err:
198227
# In case of attempting to recreate an already deleted key,
199228
# the client would get a KeyWrongLastSequenceError. When this happens,
@@ -213,16 +242,28 @@ async def create(self, key: str, value: bytes) -> int:
213242
# to recreate using the last revision.
214243
raise err
215244
except nats.js.errors.KeyDeletedError as err:
216-
pa = await self.update(key, value, last=err.entry.revision)
245+
pa = await self.update(
246+
key,
247+
value,
248+
last=err.entry.revision,
249+
validate_keys=validate_keys
250+
)
217251

218252
return pa
219253

220254
async def update(
221-
self, key: str, value: bytes, last: Optional[int] = None
255+
self,
256+
key: str,
257+
value: bytes,
258+
last: Optional[int] = None,
259+
validate_keys: bool = True
222260
) -> int:
223261
"""
224262
update will update the value iff the latest revision matches.
225263
"""
264+
if validate_keys and not _is_key_valid(key):
265+
raise nats.js.errors.InvalidKeyError(key)
266+
226267
hdrs = {}
227268
if not last:
228269
last = 0
@@ -243,10 +284,18 @@ async def update(
243284
raise err
244285
return pa.seq
245286

246-
async def delete(self, key: str, last: Optional[int] = None) -> bool:
287+
async def delete(
288+
self,
289+
key: str,
290+
last: Optional[int] = None,
291+
validate_keys: bool = True
292+
) -> bool:
247293
"""
248294
delete will place a delete marker and remove all previous revisions.
249295
"""
296+
if validate_keys and not _is_key_valid(key):
297+
raise nats.js.errors.InvalidKeyError(key)
298+
250299
hdrs = {}
251300
hdrs[KV_OP] = KV_DEL
252301

tests/test_js.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2689,6 +2689,111 @@ async def error_handler(e):
26892689
with pytest.raises(BadBucketError):
26902690
await js.key_value(bucket="TEST3")
26912691

2692+
@async_test
2693+
async def test_bucket_name_validation(self):
2694+
nc = await nats.connect()
2695+
js = nc.jetstream()
2696+
2697+
invalid_bucket_names = [
2698+
" x y",
2699+
"x ",
2700+
"x!",
2701+
"xx$",
2702+
"*",
2703+
">",
2704+
"x.>",
2705+
"x.*",
2706+
".",
2707+
".x",
2708+
".x.",
2709+
"x.",
2710+
]
2711+
2712+
for bucket_name in invalid_bucket_names:
2713+
with self.subTest(bucket_name):
2714+
with pytest.raises(InvalidBucketNameError):
2715+
await js.create_key_value(
2716+
bucket=bucket_name, history=5, ttl=3600
2717+
)
2718+
2719+
with pytest.raises(InvalidBucketNameError):
2720+
await js.key_value(bucket_name)
2721+
2722+
with pytest.raises(InvalidBucketNameError):
2723+
await js.delete_key_value(bucket_name)
2724+
2725+
@async_test
2726+
async def test_key_validation(self):
2727+
nc = await nats.connect()
2728+
js = nc.jetstream()
2729+
2730+
kv = await js.create_key_value(bucket="TEST", history=5, ttl=3600)
2731+
invalid_keys = [
2732+
" x y",
2733+
"x ",
2734+
"x!",
2735+
"xx$",
2736+
"*",
2737+
">",
2738+
"x.>",
2739+
"x.*",
2740+
".",
2741+
".x",
2742+
".x.",
2743+
"x.",
2744+
]
2745+
2746+
for key in invalid_keys:
2747+
with self.subTest(key):
2748+
# Invalid put (empty)
2749+
with pytest.raises(InvalidKeyError):
2750+
await kv.put(key, b'')
2751+
2752+
with pytest.raises(InvalidKeyError):
2753+
await kv.get(key)
2754+
2755+
with pytest.raises(InvalidKeyError):
2756+
await kv.update(key, b'')
2757+
2758+
@async_test
2759+
async def test_key_validation_bypass(self):
2760+
nc = await nats.connect()
2761+
js = nc.jetstream()
2762+
2763+
kv = await js.create_key_value(bucket="TEST", history=5, ttl=3600)
2764+
invalid_keys = [
2765+
"x!",
2766+
"x.>",
2767+
"x.*",
2768+
]
2769+
2770+
for key in invalid_keys:
2771+
with self.subTest(key):
2772+
# Should succeed with validate_keys=False
2773+
seq = await kv.put(key, b'test_value', validate_keys=False)
2774+
assert seq > 0
2775+
2776+
# Should be able to get with validate_keys=False
2777+
entry = await kv.get(key, validate_keys=False)
2778+
assert entry.value == b'test_value'
2779+
2780+
# Should be able to update with validate_keys=False
2781+
seq2 = await kv.update(
2782+
key, b'updated_value', last=seq, validate_keys=False
2783+
)
2784+
assert seq2 > seq
2785+
2786+
# Should be able to delete with validate_keys=False
2787+
result = await kv.delete(key, validate_keys=False)
2788+
assert result is True
2789+
2790+
# Should still fail with default validate_keys=True
2791+
with pytest.raises(InvalidKeyError):
2792+
await kv.put(key, b'fail')
2793+
2794+
with pytest.raises(InvalidKeyError):
2795+
await kv.get(key)
2796+
26922797
@async_test
26932798
async def test_kv_basic(self):
26942799
errors = []
@@ -2700,6 +2805,9 @@ async def error_handler(e):
27002805
nc = await nats.connect(error_cb=error_handler)
27012806
js = nc.jetstream()
27022807

2808+
with pytest.raises(nats.js.errors.InvalidBucketNameError):
2809+
await js.create_key_value(bucket="notok!")
2810+
27032811
bucket = "TEST"
27042812
kv = await js.create_key_value(
27052813
bucket=bucket,

0 commit comments

Comments
 (0)