Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ Issues = "https://github.com/oraylis/datam8/issues"

[dependency-groups]
dev = [
"pytest~=8.3.0", # TODO: upgrade to 9.x.x
"pytest-cases~=3.8.0",
"pytest~=9.0.3",
"pytest-cases~=3.10.1",
"coverage~=7.13.5",
"hatchling~=1.27.0",
"httpx~=0.25.0",
Expand All @@ -61,6 +61,8 @@ build-backend = "hatchling.build"

[tool.uv]
exclude-newer = "1 weeks"
# Allow critical CA trust-store fixes without waiting for the global cooldown (GHSA-6q48-6c7w-9gw2).
exclude-newer-package = { certifi = false }

#############
# tool.ruff #
Expand Down
9 changes: 8 additions & 1 deletion scripts/check-docs.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,14 @@ export function headingAnchors(source) {
for (const line of proseLines(source)) {
const heading = line.match(/^#{1,6}\s+(.+?)\s*#*$/)?.[1];
if (!heading) continue;
const slug = heading.toLowerCase().replace(/<[^>]*>/g, "")
let text = heading.toLowerCase();
let previous;
// Remove innermost tags until stripping cannot expose another tag.
do {
previous = text;
text = text.replace(/<[^<>]*>/g, "");
} while (text !== previous);
const slug = text
.replace(/[^\p{L}\p{N}\p{M}_\-\s]/gu, "").replace(/\s/g, "-");
const count = counts.get(slug) || 0;
counts.set(slug, count + 1);
Expand Down
13 changes: 13 additions & 0 deletions scripts/check-docs.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,19 @@ test("reports empty alt text and heading spacing", () => {
assert.equal(errors.length, 2);
});

test("heading anchors remove inline and reintroduced HTML tags", () => {
const source = "# Guide\n\n## <em>Überblick</em> 中文\n\n## Save\n\n## Save\n\n## <scrip<script>t>Read</script>\n\n## <scrip<script>is removed</script>t>alert(123)</script>\n";
assert.deepEqual([...headingAnchors(source)], [
"guide", "überblick-中文", "save", "save-1", "read", "alert123",
]);
});

test("heading anchors handle exposed and incomplete tags", () => {
assert.ok(headingAnchors("# <scr<script>ipt>Save</script>\n").has("save"));
assert.ok(headingAnchors("# <<b>em>Save</em>\n").has("save"));
assert.ok(headingAnchors("# <script Save\n").has("script-save"));
});

test("capture evidence rejects incomplete exports, missing images and failed acceptance", () => {
const manifest = {
frontendCommit: "a".repeat(40), backendCommit: "b".repeat(40),
Expand Down
15 changes: 3 additions & 12 deletions src/datam8/secrets.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,24 +91,22 @@ def __init__(self) -> None:
#

def __set_password(self, service: str, value: str, /) -> None:
logger.debug("Setting '%s'", service)
logger.debug("Setting secret")
keyring.set_password(service, self.__username, value)

def __get_password(self, service: str, /) -> str | None:
logger.debug("Getting '%s'", service)
logger.debug("Getting secret")
return keyring.get_password(service, self.__username)

def __unset_password(self, service: str, /) -> None:
logger.debug("Unsetting '%s'", service)
logger.debug("Unsetting secret")
keyring.delete_password(service, self.__username)

def __register_secret(self, path: PurePosixPath, /) -> None:
service_name = self.__create_service_name()
secrets = self.__get_password(service_name)
posix_path = path.as_posix()

logger.debug(f"Before register: {secrets}")

if secrets is None or secrets == "":
secrets = posix_path
elif f",{path.as_posix()}," in f",{secrets},":
Expand All @@ -118,25 +116,20 @@ def __register_secret(self, path: PurePosixPath, /) -> None:
secrets = f"{secrets},{path.as_posix()}"

self.__set_password(service_name, secrets)
logger.debug(f"After register: {secrets}")

def __unregister_secret(self, path: PurePosixPath, /) -> None:
service_name = self.__create_service_name()
current_secrets = self.__get_password(service_name)
current_secrets = [] if current_secrets is None else current_secrets.split(",")
posix_path = path.as_posix()

logger.debug(f"Before unregister: {current_secrets}")

if posix_path not in current_secrets:
logger.warning("Trying to unregister non existing secret")
return

path_index = current_secrets.index(posix_path)
current_secrets.pop(path_index)

logger.debug(f"After unregister: {current_secrets}")

self.__set_password(service_name, ",".join(current_secrets))

def __create_service_name(self, path: PurePosixPath | None = None, /) -> str:
Expand Down Expand Up @@ -191,8 +184,6 @@ def list_secrets(self) -> list[PurePosixPath]:

secret_list = secrets.split(",")

logger.debug("Secret registry: '%s' [%s]", secrets, len(secret_list))

return [PurePosixPath(p) for p in secret_list]

def get_secret(self, path: PurePosixPath | str, /) -> str | None:
Expand Down
35 changes: 35 additions & 0 deletions tests/test_017_plugin_secret_sql_regressions.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import asyncio
import logging
from pathlib import PurePosixPath
from types import SimpleNamespace

Expand Down Expand Up @@ -67,6 +68,40 @@ def test_secret_route_upserts_without_duplicate_registry_entries(fake_secret_bac
assert SecretResolver().list_secrets() == [PurePosixPath(path)]


def test_secret_operations_do_not_log_values_or_registry_metadata(
fake_secret_backend, caplog: pytest.LogCaptureFixture
) -> None:
resolver = SecretResolver()
path = "datasources/security-regression/password"
first_value = "initial-password-sentinel"
updated_value = "updated-password-sentinel"
registry_marker = "registry-only-sentinel"
# Register a distinct marker before capture to exercise registry contents
# independently of the path used for subsequent operations.
resolver.set_secret(registry_marker, "unused-registry-password")

with caplog.at_level(logging.DEBUG, logger="datam8.secrets"):
resolver.set_secret(path, first_value)
assert resolver.get_secret(path) == first_value
resolver.set_secret(path, updated_value, force=True)
assert resolver.get_secret(path) == updated_value
assert resolver.list_secrets() == [PurePosixPath(registry_marker), PurePosixPath(path)]
resolver.unset_secret(path)
assert resolver.get_secret(path) is None
assert resolver.list_secrets() == [PurePosixPath(registry_marker)]
resolver.set_secret(path, first_value)
resolver.clean()
assert resolver.list_secrets() == []
assert not fake_secret_backend

records = [record for record in caplog.records if record.name == "datam8.secrets"]
assert records, "DEBUG logging must be exercised"
for record in records:
assert not record.args
for sensitive in (first_value, updated_value, registry_marker, path, "test-solution"):
assert sensitive not in record.getMessage()


def _make_sql_plugin() -> SqlServer:
plugin = SqlServer.__new__(SqlServer)
plugin._data_source = SimpleNamespace(name="AdventureWorks") # type: ignore[ty:invalid-assignment]
Expand Down
41 changes: 23 additions & 18 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading