Skip to content

feat(declarativeSettings): sensitive field type support #361

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

Merged
merged 4 commits into from
May 29, 2025
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
80 changes: 32 additions & 48 deletions nc_py_api/_preferences_ex.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,21 @@ def delete(self, keys: str | list[str], not_fail=True) -> None:
if not not_fail:
raise e from None

def set_value(self, key: str, value: str, sensitive: bool | None = None) -> None:
"""Sets a value and if specified the sensitive flag for a key.

.. note:: A sensitive flag ensures key value are encrypted and truncated in Nextcloud logs.
Default for new records is ``False`` when sensitive is *unspecified*, if changes existing record and
sensitive is *unspecified* it will not change the existing `sensitive` flag.
"""
if not key:
raise ValueError("`key` parameter can not be empty")
require_capabilities("app_api", self._session.capabilities)
params: dict = {"configKey": key, "configValue": value}
if sensitive is not None:
params["sensitive"] = sensitive
self._session.ocs("POST", f"{self._session.ae_url}/{self._url_suffix}", json=params)


class _AsyncBasicAppCfgPref:
_url_suffix: str
Expand Down Expand Up @@ -104,72 +119,41 @@ async def delete(self, keys: str | list[str], not_fail=True) -> None:
if not not_fail:
raise e from None

async def set_value(self, key: str, value: str, sensitive: bool | None = None) -> None:
"""Sets a value and if specified the sensitive flag for a key.

.. note:: A sensitive flag ensures key value are encrypted and truncated in Nextcloud logs.
Default for new records is ``False`` when sensitive is *unspecified*, if changes existing record and
sensitive is *unspecified* it will not change the existing `sensitive` flag.
"""
if not key:
raise ValueError("`key` parameter can not be empty")
require_capabilities("app_api", await self._session.capabilities)
params: dict = {"configKey": key, "configValue": value}
if sensitive is not None:
params["sensitive"] = sensitive
await self._session.ocs("POST", f"{self._session.ae_url}/{self._url_suffix}", json=params)


class PreferencesExAPI(_BasicAppCfgPref):
"""User specific preferences API, avalaible as **nc.preferences_ex.<method>**."""
"""User specific preferences API, available as **nc.preferences_ex.<method>**."""

_url_suffix = "ex-app/preference"

def set_value(self, key: str, value: str) -> None:
"""Sets a value for a key."""
if not key:
raise ValueError("`key` parameter can not be empty")
require_capabilities("app_api", self._session.capabilities)
params = {"configKey": key, "configValue": value}
self._session.ocs("POST", f"{self._session.ae_url}/{self._url_suffix}", json=params)


class AsyncPreferencesExAPI(_AsyncBasicAppCfgPref):
"""User specific preferences API."""

_url_suffix = "ex-app/preference"

async def set_value(self, key: str, value: str) -> None:
"""Sets a value for a key."""
if not key:
raise ValueError("`key` parameter can not be empty")
require_capabilities("app_api", await self._session.capabilities)
params = {"configKey": key, "configValue": value}
await self._session.ocs("POST", f"{self._session.ae_url}/{self._url_suffix}", json=params)


class AppConfigExAPI(_BasicAppCfgPref):
"""Non-user(App) specific preferences API, avalaible as **nc.appconfig_ex.<method>**."""
"""Non-user(App) specific preferences API, available as **nc.appconfig_ex.<method>**."""

_url_suffix = "ex-app/config"

def set_value(self, key: str, value: str, sensitive: bool | None = None) -> None:
"""Sets a value and if specified the sensitive flag for a key.

.. note:: A sensitive flag ensures key values are truncated in Nextcloud logs.
Default for new records is ``False`` when sensitive is *unspecified*, if changes existing record and
sensitive is *unspecified* it will not change the existing `sensitive` flag.
"""
if not key:
raise ValueError("`key` parameter can not be empty")
require_capabilities("app_api", self._session.capabilities)
params: dict = {"configKey": key, "configValue": value}
if sensitive is not None:
params["sensitive"] = sensitive
self._session.ocs("POST", f"{self._session.ae_url}/{self._url_suffix}", json=params)


class AsyncAppConfigExAPI(_AsyncBasicAppCfgPref):
"""Non-user(App) specific preferences API."""

_url_suffix = "ex-app/config"

async def set_value(self, key: str, value: str, sensitive: bool | None = None) -> None:
"""Sets a value and if specified the sensitive flag for a key.

.. note:: A sensitive flag ensures key values are truncated in Nextcloud logs.
Default for new records is ``False`` when sensitive is *unspecified*, if changes existing record and
sensitive is *unspecified* it will not change the existing `sensitive` flag.
"""
if not key:
raise ValueError("`key` parameter can not be empty")
require_capabilities("app_api", await self._session.capabilities)
params: dict = {"configKey": key, "configValue": value}
if sensitive is not None:
params["sensitive"] = sensitive
await self._session.ocs("POST", f"{self._session.ae_url}/{self._url_suffix}", json=params)
2 changes: 1 addition & 1 deletion nc_py_api/_version.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""Version of nc_py_api."""

__version__ = "0.20.1"
__version__ = "0.20.2"
2 changes: 2 additions & 0 deletions nc_py_api/ex_app/ui/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ class SettingsField:
description: str = ""
placeholder: str = ""
label: str = ""
sensitive: bool = False
notify = False # to be supported in future

@classmethod
Expand All @@ -74,6 +75,7 @@ def to_dict(self) -> dict:
"placeholder": self.placeholder,
"label": self.label,
"notify": self.notify,
"sensitive": self.sensitive,
}


Expand Down
108 changes: 74 additions & 34 deletions tests/actual_tests/appcfg_prefs_ex_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,60 +233,100 @@ async def test_cfg_ex_get_typing_async(class_to_test):
assert r[1].value == "321"


def test_appcfg_sensitive(nc_app):
appcfg = nc_app.appconfig_ex
appcfg.delete("test_key")
appcfg.set_value("test_key", "123", sensitive=True)
assert appcfg.get_value("test_key") == "123"
assert appcfg.get_values(["test_key"])[0].value == "123"
appcfg.delete("test_key")
@pytest.mark.parametrize("class_to_test", (NC_APP.appconfig_ex, NC_APP.preferences_ex))
def test_appcfg_sensitive(nc_app, class_to_test):
class_to_test.delete("test_key")
class_to_test.set_value("test_key", "123", sensitive=True)
assert class_to_test.get_value("test_key") == "123"
assert class_to_test.get_values(["test_key"])[0].value == "123"
class_to_test.delete("test_key")
# next code tests `sensitive` value from the `AppAPI`
params = {"configKey": "test_key", "configValue": "123"}
result = nc_app._session.ocs("POST", f"{nc_app._session.ae_url}/{appcfg._url_suffix}", json=params)
assert not result["sensitive"] # by default if sensitive value is unspecified it is False
appcfg.delete("test_key")
params = {"configKey": "test_key", "configValue": "123", "sensitive": True}
result = nc_app._session.ocs("POST", f"{nc_app._session.ae_url}/{appcfg._url_suffix}", json=params)
result = nc_app._session.ocs("POST", f"{nc_app._session.ae_url}/{class_to_test._url_suffix}", json=params)
if class_to_test == NC_APP.appconfig_ex or nc_app.srv_version["major"] >= 32:
assert not result["sensitive"] # by default if sensitive value is unspecified it is False
class_to_test.delete("test_key")
params["sensitive"] = True
result = nc_app._session.ocs("POST", f"{nc_app._session.ae_url}/{class_to_test._url_suffix}", json=params)
assert result["configkey"] == "test_key"
assert result["configvalue"] == "123"
assert bool(result["sensitive"]) is True
if class_to_test == NC_APP.appconfig_ex or nc_app.srv_version["major"] >= 32:
assert bool(result["sensitive"]) is True
params.pop("sensitive") # if we not specify value, AppEcosystem should not change it.
result = nc_app._session.ocs("POST", f"{nc_app._session.ae_url}/{appcfg._url_suffix}", json=params)
result = nc_app._session.ocs("POST", f"{nc_app._session.ae_url}/{class_to_test._url_suffix}", json=params)
assert result["configkey"] == "test_key"
assert result["configvalue"] == "123"
assert bool(result["sensitive"]) is True
if class_to_test == NC_APP.appconfig_ex or nc_app.srv_version["major"] >= 32:
assert bool(result["sensitive"]) is True
params["sensitive"] = False
result = nc_app._session.ocs("POST", f"{nc_app._session.ae_url}/{appcfg._url_suffix}", json=params)
result = nc_app._session.ocs("POST", f"{nc_app._session.ae_url}/{class_to_test._url_suffix}", json=params)
assert result["configkey"] == "test_key"
assert result["configvalue"] == "123"
assert bool(result["sensitive"]) is False
if class_to_test == NC_APP.appconfig_ex or nc_app.srv_version["major"] >= 32:
assert bool(result["sensitive"]) is False
# test setting to empty value (sensitive=False)
params["configValue"] = ""
result = nc_app._session.ocs("POST", f"{nc_app._session.ae_url}/{class_to_test._url_suffix}", json=params)
assert result["configkey"] == "test_key"
assert result["configvalue"] == ""
if class_to_test == NC_APP.appconfig_ex or nc_app.srv_version["major"] >= 32:
assert bool(result["sensitive"]) is False
assert class_to_test.get_value("test_key") == ""
# test setting to empty value (sensitive=True)
params["sensitive"] = True
result = nc_app._session.ocs("POST", f"{nc_app._session.ae_url}/{class_to_test._url_suffix}", json=params)
assert result["configkey"] == "test_key"
assert result["configvalue"] == ""
if class_to_test == NC_APP.appconfig_ex or nc_app.srv_version["major"] >= 32:
assert bool(result["sensitive"]) is True
assert class_to_test.get_value("test_key") == ""


@pytest.mark.asyncio(scope="session")
async def test_appcfg_sensitive_async(anc_app):
appcfg = anc_app.appconfig_ex
await appcfg.delete("test_key")
await appcfg.set_value("test_key", "123", sensitive=True)
assert await appcfg.get_value("test_key") == "123"
assert (await appcfg.get_values(["test_key"]))[0].value == "123"
await appcfg.delete("test_key")
@pytest.mark.parametrize("class_to_test", (NC_APP_ASYNC.appconfig_ex, NC_APP_ASYNC.preferences_ex))
async def test_appcfg_sensitive_async(anc_app, class_to_test):
await class_to_test.delete("test_key")
await class_to_test.set_value("test_key", "123", sensitive=True)
assert await class_to_test.get_value("test_key") == "123"
assert (await class_to_test.get_values(["test_key"]))[0].value == "123"
await class_to_test.delete("test_key")
# next code tests `sensitive` value from the `AppAPI`
params = {"configKey": "test_key", "configValue": "123"}
result = await anc_app._session.ocs("POST", f"{anc_app._session.ae_url}/{appcfg._url_suffix}", json=params)
assert not result["sensitive"] # by default if sensitive value is unspecified it is False
await appcfg.delete("test_key")
params = {"configKey": "test_key", "configValue": "123", "sensitive": True}
result = await anc_app._session.ocs("POST", f"{anc_app._session.ae_url}/{appcfg._url_suffix}", json=params)
result = await anc_app._session.ocs("POST", f"{anc_app._session.ae_url}/{class_to_test._url_suffix}", json=params)
if class_to_test == NC_APP_ASYNC.appconfig_ex or (await anc_app.srv_version)["major"] >= 32:
assert not result["sensitive"] # by default if sensitive value is unspecified it is False
await class_to_test.delete("test_key")
params["sensitive"] = True
result = await anc_app._session.ocs("POST", f"{anc_app._session.ae_url}/{class_to_test._url_suffix}", json=params)
assert result["configkey"] == "test_key"
assert result["configvalue"] == "123"
assert bool(result["sensitive"]) is True
if class_to_test == NC_APP_ASYNC.appconfig_ex or (await anc_app.srv_version)["major"] >= 32:
assert bool(result["sensitive"]) is True
params.pop("sensitive") # if we not specify value, AppEcosystem should not change it.
result = await anc_app._session.ocs("POST", f"{anc_app._session.ae_url}/{appcfg._url_suffix}", json=params)
result = await anc_app._session.ocs("POST", f"{anc_app._session.ae_url}/{class_to_test._url_suffix}", json=params)
assert result["configkey"] == "test_key"
assert result["configvalue"] == "123"
assert bool(result["sensitive"]) is True
if class_to_test == NC_APP_ASYNC.appconfig_ex or (await anc_app.srv_version)["major"] >= 32:
assert bool(result["sensitive"]) is True
params["sensitive"] = False
result = await anc_app._session.ocs("POST", f"{anc_app._session.ae_url}/{appcfg._url_suffix}", json=params)
result = await anc_app._session.ocs("POST", f"{anc_app._session.ae_url}/{class_to_test._url_suffix}", json=params)
assert result["configkey"] == "test_key"
assert result["configvalue"] == "123"
assert bool(result["sensitive"]) is False
if class_to_test == NC_APP_ASYNC.appconfig_ex or (await anc_app.srv_version)["major"] >= 32:
assert bool(result["sensitive"]) is False
# test setting to empty value (sensitive=False)
params["configValue"] = ""
result = await anc_app._session.ocs("POST", f"{anc_app._session.ae_url}/{class_to_test._url_suffix}", json=params)
assert result["configkey"] == "test_key"
assert result["configvalue"] == ""
if class_to_test == NC_APP_ASYNC.appconfig_ex or (await anc_app.srv_version)["major"] >= 32:
assert bool(result["sensitive"]) is False
assert await class_to_test.get_value("test_key") == ""
# test setting to empty value (sensitive=True)
params["sensitive"] = True
result = await anc_app._session.ocs("POST", f"{anc_app._session.ae_url}/{class_to_test._url_suffix}", json=params)
assert result["configkey"] == "test_key"
assert result["configvalue"] == ""
if class_to_test == NC_APP_ASYNC.appconfig_ex or (await anc_app.srv_version)["major"] >= 32:
assert bool(result["sensitive"]) is True
assert await class_to_test.get_value("test_key") == ""