From 2f4344e5385cd301b2bd5446bbd6bc4249800c49 Mon Sep 17 00:00:00 2001 From: Jonathan M Hsieh Date: Wed, 9 Sep 2026 20:47:46 +0000 Subject: [PATCH] fix(secrets): admit periods inside Secret names, and rule out the edges LanceDB namespace names permit periods, so excluding them here put Secrets out of reach inside any namespace a user already has one in -- unreachable to create, alter, describe, drop or bind, with no way to recover but renaming the namespace. A period is ruled out at either end instead, which is RFC 1123's shape and Kubernetes' rule for object names. That stops `.` and `..` and anything reading as a hidden file or a path fragment in a listing or an error. Nothing in the layout depends on it -- the service encodes every segment -- so it buys legibility rather than safety, and it is free only until a name is stored. Names and path segments follow one rule: the two are joined nowhere, but a rule that held for one and not the other would be worse than either. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01UfmeJ533rQDnPBkMtjerV6 --- python/python/lancedb/secrets.py | 7 ++++++- .../tests/test_first_class_function_slice2.py | 19 +++++++++++++++++++ rust/lancedb/src/remote/client.rs | 2 +- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/python/python/lancedb/secrets.py b/python/python/lancedb/secrets.py index b16f32314..908b408c8 100644 --- a/python/python/lancedb/secrets.py +++ b/python/python/lancedb/secrets.py @@ -14,7 +14,12 @@ from __future__ import annotations import re -_SECRET_NAME = re.compile(r"^[A-Za-z0-9_-]{1,255}$") +# Periods are legal inside a name: excluding them would put Secrets out of reach +# of any namespace whose name carries one, which LanceDB namespaces allow. Not +# at either end, which is RFC 1123's shape and Kubernetes' rule for object +# names: it rules out `.` and `..` and anything that reads as a hidden file or a +# path fragment in a listing. Matches the service, which rejects the same shapes. +_SECRET_NAME = re.compile(r"^[A-Za-z0-9]([A-Za-z0-9_.-]{0,253}[A-Za-z0-9])?$") _ENV_VARIABLE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$") diff --git a/python/python/tests/test_first_class_function_slice2.py b/python/python/tests/test_first_class_function_slice2.py index d04a2b5f5..7a62008e3 100644 --- a/python/python/tests/test_first_class_function_slice2.py +++ b/python/python/tests/test_first_class_function_slice2.py @@ -281,6 +281,7 @@ def test_a_credential_value_is_rejected_in_the_binding_position(): ("openai-prod", "not-a-var", "invalid environment variable name"), ("openai-prod", "API-TOKEN", "invalid environment variable name"), ("not a name", "API_TOKEN", "invalid Secret name"), + ("openai$prod", "API_TOKEN", "invalid Secret name"), ], ) def test_a_binding_validates_both_names_locally(secret, variable, message): @@ -288,6 +289,24 @@ def test_a_binding_validates_both_names_locally(secret, variable, message): EnvVarSecret(secret=secret, env_variable=variable) +def test_a_period_is_legal_inside_a_name_and_not_at_its_edges(): + """LanceDB namespaces already permit a period, so a Secret must be nameable + alongside them -- inside the name. + + At either end it is ruled out instead, which is RFC 1123's shape and + Kubernetes' rule for object names: it stops `.` and `..` and anything that + reads as a hidden file or a path fragment in a listing. The service rejects + the same shapes, so this is a local answer to the same rule rather than a + second one. + """ + binding = EnvVarSecret(secret="openai.prod.v1", env_variable="OPENAI_API_KEY") + assert binding.secret == "openai.prod.v1" + + for name in [".", "..", ".hidden", "trailing.", "-lead", "trail-", "_x"]: + with pytest.raises(ValueError, match="invalid Secret name"): + EnvVarSecret(secret=name, env_variable="OPENAI_API_KEY") + + def _main_udf_source( *, threshold: int = 20, input_annotation: str = "int", comparison: str = ">=" ) -> str: diff --git a/rust/lancedb/src/remote/client.rs b/rust/lancedb/src/remote/client.rs index 6eaa8fcb1..2b70a9415 100644 --- a/rust/lancedb/src/remote/client.rs +++ b/rust/lancedb/src/remote/client.rs @@ -412,7 +412,7 @@ fn validate_dns_hostname(hostname: &str) -> Result<()> { /// remembering to extend this. Every secrets route is denied, not only the two /// that carry a value: their bodies hold names and page tokens, which are worth /// nothing in a debug log next to the risk of a new verb landing here unnoticed. -pub(crate) fn route_carries_credential(path: &str) -> bool { +fn route_carries_credential(path: &str) -> bool { path.split('/').any(|segment| segment == "secrets") }