Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: log pollution is possible when long values saved to options #1

Merged
merged 1 commit into from
Apr 24, 2024
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
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ help:


changelog: ## compile changelog
git changelog -c conventional -o CHANGELOG.md
git changelog
21 changes: 14 additions & 7 deletions ckanext/editable_config/model/option.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from typing import Any, cast

import sqlalchemy as sa
from sqlalchemy.orm import Mapped
from typing_extensions import Self

import ckan.plugins.toolkit as tk
Expand All @@ -13,13 +14,20 @@
from ckanext.editable_config import shared


class Option(tk.BaseModel):
__tablename__ = "editable_config_option"
class Option(tk.BaseModel): # pyright: ignore[reportUntypedBaseClass]
__table__ = sa.Table(
"editable_config_option",
tk.BaseModel.metadata,
sa.Column("key", sa.Text, primary_key=True),
sa.Column("value", sa.Text, nullable=False),
sa.Column("updated_at", sa.DateTime, nullable=False),
sa.Column("prev_value", sa.Text, nullable=False),
)

key = sa.Column(sa.Text, primary_key=True)
value = sa.Column(sa.Text, nullable=False)
updated_at = sa.Column(sa.DateTime, nullable=False)
prev_value = sa.Column(sa.Text, nullable=False)
key: Mapped[str]
value: Mapped[str]
updated_at: Mapped[datetime]
prev_value: Mapped[str]

@classmethod
def get(cls, key: str) -> Self | None:
Expand All @@ -32,7 +40,6 @@ def get(cls, key: str) -> Self | None:
@classmethod
def set(cls, key: str, value: Any) -> Self:
"""Create/update an option."""
option: Self
safe_value = shared.value_as_string(key, value)

if option := cls.get(key):
Expand Down
6 changes: 5 additions & 1 deletion ckanext/editable_config/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,11 @@ def configure(self, config_: CKANConfig):
)
return

inspector = sa.inspect(model.meta.engine)
engine = model.meta.engine
if not engine:
return

inspector = sa.inspect(engine)
self._editable_config_enabled = inspector.has_table("editable_config_option")
if not self._editable_config_enabled:
log.critical(
Expand Down
24 changes: 18 additions & 6 deletions ckanext/editable_config/shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,18 @@ class OptionDict(TypedDict):
prev_value: str


def shorten_for_log(value: Any, width: int = 80, placeholder: str = "...") -> str:
"""Prepare value for logging.

Trasforms value into string and truncates it to specified length, appending
placeholder if result was reduced in size.
"""
result = str(value)
if len(result) > width + len(placeholder):
result = result[:width] + placeholder
return result


def get_declaration(key: str) -> DeclaredOption[Any] | None:
"""Return existing declaration or None."""
if key in cd:
Expand Down Expand Up @@ -77,7 +89,7 @@ def convert_core_overrides(names: Iterable[str]):
)
options = {op.key: op.value for op in q}

log.debug("Convert core overrides into editable config: %s", options)
log.debug("Convert core overrides into editable config: %s", list(options))
change(
{"ignore_auth": True},
{
Expand Down Expand Up @@ -168,8 +180,8 @@ def _apply_changes(self) -> int:
log.debug(
"Change %s from %s to %s",
option.key,
tk.config[option.key],
option.value,
shorten_for_log(tk.config[option.key]),
shorten_for_log(option.value),
)
tk.config[option.key] = option.value
count += 1
Expand All @@ -190,8 +202,8 @@ def _remove_keys(self, keys: Collection[str] | None) -> int:
log.debug(
"Reset %s from %s to %s",
key,
tk.config[key],
src_conf[key],
shorten_for_log(tk.config[key]),
shorten_for_log(src_conf[key]),
)

tk.config[key] = src_conf[key]
Expand All @@ -200,7 +212,7 @@ def _remove_keys(self, keys: Collection[str] | None) -> int:
log.debug(
"Remove %s with value %s",
key,
tk.config[key],
shorten_for_log(tk.config[key]),
)
tk.config.pop(key)
else:
Expand Down
7 changes: 0 additions & 7 deletions ckanext/editable_config/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
from ckan import model
from ckan.tests import factories
from ckan.tests.helpers import call_action

from ckanext.editable_config.model import Option
from ckanext.editable_config.shared import apply_config_overrides

Expand All @@ -20,12 +19,6 @@ def reset_last_check():
apply_config_overrides._last_check = None


@pytest.fixture(scope="session")
def reset_db_once(reset_db, migrate_db_for):
reset_db()
migrate_db_for("editable_config")


@pytest.fixture()
def clean_db(reset_db, migrate_db_for):
reset_db()
Expand Down
10 changes: 5 additions & 5 deletions ckanext/editable_config/tests/logic/test_action.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@


@pytest.mark.ckan_config(config.WHITELIST, ["ckan.site_title", "ckan.site_description"])
@pytest.mark.usefixtures("with_plugins", "non_clean_db")
@pytest.mark.usefixtures("with_plugins", "clean_db")
class TestList:
def test_default(self):
result = call_action("editable_config_list")
Expand All @@ -18,7 +18,7 @@ def test_pattern(self):
assert set(result) == {"ckan.site_title"}


@pytest.mark.usefixtures("with_plugins", "non_clean_db", "with_autoclean")
@pytest.mark.usefixtures("with_plugins", "clean_db", "with_autoclean")
class TestUpdate:
def test_empty(self):
result = call_action("editable_config_update")
Expand Down Expand Up @@ -50,7 +50,7 @@ def test_one_per_group(self, faker, option_factory, ckan_config):
assert ckan_config[reset_option["key"]] is None


@pytest.mark.usefixtures("with_plugins", "non_clean_db", "with_autoclean")
@pytest.mark.usefixtures("with_plugins", "clean_db", "with_autoclean")
class TestChange:
def test_undeclared(self, option_factory, faker):
"""Undeclared options are not allowed."""
Expand Down Expand Up @@ -98,7 +98,7 @@ def test_additional_validators(self):
)


@pytest.mark.usefixtures("with_plugins", "non_clean_db", "with_autoclean")
@pytest.mark.usefixtures("with_plugins", "clean_db", "with_autoclean")
class TestRevert:
def test_revert_missing(self):
with pytest.raises(tk.ObjectNotFound):
Expand All @@ -114,7 +114,7 @@ def test_revert_initial(self, ckan_config, faker, option_factory):
assert result["ckan.site_title"]["prev_value"] == updated


@pytest.mark.usefixtures("with_plugins", "non_clean_db", "with_autoclean")
@pytest.mark.usefixtures("with_plugins", "clean_db", "with_autoclean")
class TestReset:
def test_reset_missing(self):
with pytest.raises(tk.ObjectNotFound):
Expand Down
2 changes: 1 addition & 1 deletion ckanext/editable_config/tests/model/test_option.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from ckanext.editable_config.model import Option


@pytest.mark.usefixtures("with_plugins", "non_clean_db")
@pytest.mark.usefixtures("with_plugins", "clean_db")
class TestOption:
def test_get(self, faker, option_factory):
"""Option.get returns exition option or None."""
Expand Down
2 changes: 1 addition & 1 deletion ckanext/editable_config/tests/test_fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import ckan.plugins.toolkit as tk


@pytest.mark.usefixtures("with_plugins", "non_clean_db")
@pytest.mark.usefixtures("with_plugins", "clean_db")
class TestOptionFactory:
def test_key_and_value_required(self, option_factory):
"""OptionFactory requires key and value."""
Expand Down
2 changes: 1 addition & 1 deletion ckanext/editable_config/tests/test_shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def test_value_as_string():
assert shared.value_as_string("ckan.plugins", ["hello", "world"]) == "hello world"


@pytest.mark.usefixtures("with_plugins", "non_clean_db")
@pytest.mark.usefixtures("with_plugins", "clean_db")
class TestUpdater:
def test_apply_new_updates(self, faker, ckan_config, freezer, autoclean_option):
"""New updates are applied."""
Expand Down
7 changes: 6 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ filterwarnings = [
"ignore::DeprecationWarning",
]

[tool.git-changelog]
output = "CHANGELOG.md"
convention = "conventional"
parse-trailers = true

[tool.pyright]
pythonVersion = "3.8"
include = ["ckanext"]
Expand All @@ -76,7 +81,7 @@ reportFunctionMemberAccess = true # non-standard member accesses for functions
reportMissingImports = true
reportMissingModuleSource = true
reportMissingTypeStubs = false
reportImportCycles = true
reportImportCycles = false
reportUnusedImport = true
reportUnusedClass = true
reportUnusedFunction = true
Expand Down
6 changes: 6 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ babel.extractors =
ckan = ckan.lib.extract:extract_ckan

[options.extras_require]
test =
pytest-ckan

dev =
%(test)s
git-changelog

[extract_messages]
keywords = translate isPlural
Expand Down
Loading