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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UfmeJ533rQDnPBkMtjerV6
This commit is contained in:
Jonathan M Hsieh
2026-09-11 02:33:55 +00:00
co-authored by Claude Opus 5
parent e541a3be93
commit 2f4344e538
3 changed files with 26 additions and 2 deletions
+6 -1
View File
@@ -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_]*$")
@@ -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:
+1 -1
View File
@@ -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")
}