From 1d0092e92dcf519158ec0a80d70db244b7974770 Mon Sep 17 00:00:00 2001 From: ckunki Date: Tue, 10 Oct 2023 16:44:51 +0200 Subject: [PATCH 1/8] Initial commit --- .github/actions/action.yml | 20 ++++ .github/workflows/ci_build.yaml | 20 ++++ .gitignore | 5 + doc/changes/changelog.md | 12 +++ doc/changes/changes_0.0.1.md | 9 ++ error_code_config.yml | 3 + notebook_connector/__init__.py | 0 notebook_connector/poetry.lock | 142 +++++++++++++++++++++++++ notebook_connector/secret_store.py | 161 +++++++++++++++++++++++++++++ poetry.lock | 133 ++++++++++++++++++++++++ pyproject.toml | 31 ++++++ test/__init__.py | 0 test/conftest.py | 13 +++ test/test_secret_store.py | 56 ++++++++++ 14 files changed, 605 insertions(+) create mode 100644 .github/actions/action.yml create mode 100644 .github/workflows/ci_build.yaml create mode 100644 .gitignore create mode 100644 doc/changes/changelog.md create mode 100644 doc/changes/changes_0.0.1.md create mode 100644 error_code_config.yml create mode 100644 notebook_connector/__init__.py create mode 100644 notebook_connector/poetry.lock create mode 100644 notebook_connector/secret_store.py create mode 100644 poetry.lock create mode 100644 pyproject.toml create mode 100644 test/__init__.py create mode 100644 test/conftest.py create mode 100644 test/test_secret_store.py diff --git a/.github/actions/action.yml b/.github/actions/action.yml new file mode 100644 index 0000000..0c49983 --- /dev/null +++ b/.github/actions/action.yml @@ -0,0 +1,20 @@ +name: 'Prepare Poetry environment' +description: 'This composite actions checks out out the project, installs Poetry, and install the project in the Poetry environment' +inputs: + python-version: + description: 'The Python version to use' + required: true + default: '3.8' +runs: + using: "composite" + steps: + - uses: actions/setup-python@v2 + with: + python-version: ${{ inputs.python-version }} + - uses: abatilo/actions-poetry@v2 + with: + poetry-version: 1.4.0 + - name: Poetry install + run: poetry install + shell: bash + diff --git a/.github/workflows/ci_build.yaml b/.github/workflows/ci_build.yaml new file mode 100644 index 0000000..0ba4491 --- /dev/null +++ b/.github/workflows/ci_build.yaml @@ -0,0 +1,20 @@ +name: CI Build + +on: + push: + branches-ignore: + - "main" + +jobs: + run_unit_tests: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Setup Python & Poetry Environment + uses: ./.github/actions/prepare_poetry_env + + - name: Run pytest + run: poetry run pytest diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9ce8408 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.idea +.pytest_cache +dist +__pycache__/ +/TAGS diff --git a/doc/changes/changelog.md b/doc/changes/changelog.md new file mode 100644 index 0000000..5aa79e5 --- /dev/null +++ b/doc/changes/changelog.md @@ -0,0 +1,12 @@ +# Changes + +* [0.0.1](changes_0.0.1.md) + + +```{toctree} +--- +hidden: +--- +changes_0.0.1 + +``` \ No newline at end of file diff --git a/doc/changes/changes_0.0.1.md b/doc/changes/changes_0.0.1.md new file mode 100644 index 0000000..7d6e01f --- /dev/null +++ b/doc/changes/changes_0.0.1.md @@ -0,0 +1,9 @@ +# Notebook Connector 0.0.1, released t.b.d. + +## Summary + +This release adds the initial implementation of the secret store + +## Changes + +* #1: Added secret store diff --git a/error_code_config.yml b/error_code_config.yml new file mode 100644 index 0000000..b3779b0 --- /dev/null +++ b/error_code_config.yml @@ -0,0 +1,3 @@ +error-tags: + NC: + highest-index: 0 diff --git a/notebook_connector/__init__.py b/notebook_connector/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/notebook_connector/poetry.lock b/notebook_connector/poetry.lock new file mode 100644 index 0000000..d650aeb --- /dev/null +++ b/notebook_connector/poetry.lock @@ -0,0 +1,142 @@ +# This file is automatically @generated by Poetry and should not be changed by hand. + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.1.3" +description = "Backport of PEP 654 (exception groups)" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.1.3-py3-none-any.whl", hash = "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"}, + {file = "exceptiongroup-1.1.3.tar.gz", hash = "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "packaging" +version = "23.1" +description = "Core utilities for Python packages" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, + {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, +] + +[[package]] +name = "pluggy" +version = "1.3.0" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, + {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pytest" +version = "7.4.2" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-7.4.2-py3-none-any.whl", hash = "sha256:1d881c6124e08ff0a1bb75ba3ec0bfd8b5354a01c194ddd5a0a870a48d99b002"}, + {file = "pytest-7.4.2.tar.gz", hash = "sha256:a766259cfab564a2ad52cb1aae1b881a75c3eb7e34ca3779697c23ed47c47069"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-mock" +version = "3.11.1" +description = "Thin-wrapper around the mock package for easier use with pytest" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-mock-3.11.1.tar.gz", hash = "sha256:7f6b125602ac6d743e523ae0bfa71e1a697a2f5534064528c6ff84c2f7c2fc7f"}, + {file = "pytest_mock-3.11.1-py3-none-any.whl", hash = "sha256:21c279fff83d70763b05f8874cc9cfb3fcacd6d354247a976f9529d19f9acf39"}, +] + +[package.dependencies] +pytest = ">=5.0" + +[package.extras] +dev = ["pre-commit", "pytest-asyncio", "tox"] + +[[package]] +name = "sqlcipher3-binary" +version = "0.5.2" +description = "DB-API 2.0 interface for SQLCipher 3.x" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "sqlcipher3_binary-0.5.2-cp310-cp310-manylinux_2_24_x86_64.whl", hash = "sha256:2f38bfffa0b6c23898c2542216d37c494df95ec1587711ebad3c055efe7d4643"}, + {file = "sqlcipher3_binary-0.5.2-cp311-cp311-manylinux_2_24_x86_64.whl", hash = "sha256:a41f18f77e6988a812ed099c7afda835a6ce3697baed440c2f775c6633aea33a"}, + {file = "sqlcipher3_binary-0.5.2-cp36-cp36m-manylinux_2_24_x86_64.whl", hash = "sha256:df1abf166a9e1b42ba53eaecd539efaf408abb986867be4f72c366a8e267af72"}, + {file = "sqlcipher3_binary-0.5.2-cp37-cp37m-manylinux_2_24_x86_64.whl", hash = "sha256:217845d0fa6c88962ebc2e415a49e85cda694d8d79f2a8907b94c946b40892c3"}, + {file = "sqlcipher3_binary-0.5.2-cp38-cp38-manylinux_2_24_x86_64.whl", hash = "sha256:18f83b12770bb90a8fcceb9a33a8abb2c4b65aee5c3e22493bbf4d8cfa6da0ff"}, + {file = "sqlcipher3_binary-0.5.2-cp39-cp39-manylinux_2_24_x86_64.whl", hash = "sha256:419b45efbbffaebe55a4ce8abb9e561606e87229e81bd524a739a047863b51ad"}, +] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[metadata] +lock-version = "2.0" +python-versions = ">=3.8.0,<4.0" +content-hash = "4f2989612a99ffcf93b9c8cc88249563dbcb1214355ad8e68c0bf0e9eacb0631" diff --git a/notebook_connector/secret_store.py b/notebook_connector/secret_store.py new file mode 100644 index 0000000..27bf6eb --- /dev/null +++ b/notebook_connector/secret_store.py @@ -0,0 +1,161 @@ +import contextlib +import logging +import os +from dataclasses import dataclass +from sqlcipher3 import dbapi2 as sqlcipher +from typing import Optional, Union +from inspect import cleandoc + + +_logger = logging.getLogger(__name__) + + +@dataclass(frozen=True) +class Table: + name: str + columns: list[str] + + +SECRETS_TABLE = Table("secrets", ["user", "password"]) +CONFIG_ITEMS_TABLE = Table("config_items", ["item"]) + + +@dataclass(frozen=True) +class Credentials: + user: str + password: str + + +class InvalidPassword(Exception): + """Signal potentially incorrect master password.""" + + +class Secrets: + def __init__(self, db_file: str, master_password: str) -> None: + self.db_file = db_file + self._master_password = master_password + self._con = None + + def close(self) -> None: + if self._con is not None: + self._con.close() + self._con = None + + def connection(self) -> sqlcipher.Connection: + if self._con is None: + db_file_found = os.path.exists(self.db_file) + if not db_file_found: + _logger.info(f"Creating file {self.db_file}") + self._con = sqlcipher.connect(self.db_file) + self._use_master_password() + self._initialize(db_file_found) + return self._con + + def _initialize(self, db_file_found: bool) -> None: + if db_file_found: + self._verify_access() + return + + def create_table(table: Table) -> None: + _logger.info(f'Creating table "{table.name}".') + columns = " ,".join(table.columns) + with self._cursor() as cur: + cur.execute(f"CREATE TABLE {table.name} (key, {columns})") + + for table in (SECRETS_TABLE, CONFIG_ITEMS_TABLE): + create_table(table) + + def _use_master_password(self) -> None: + """ + If database is unencrypted then this method encrypts it. + If database is already encrypted then this method enables to access the data. + """ + if self._master_password is not None: + sanitized = self._master_password.replace("'", "\\'") + with self._cursor() as cur: + cur.execute(f"PRAGMA key = '{sanitized}'") + + def _verify_access(self): + try: + with self._cursor() as cur: + cur.execute("SELECT * FROM sqlite_master") + except sqlcipher.DatabaseError as ex: + print(f'exception {ex}') + if str(ex) == "file is not a database": + raise InvalidPassword( + cleandoc( + f""" + Cannot access + database file {self.db_file}. + This also happens if master password is incorrect. + """) + ) from ex + else: + raise ex + + @contextlib.contextmanager + def _cursor(self) -> sqlcipher.Cursor: + cur = self.connection().cursor() + try: + yield cur + self.connection().commit() + except: + self.connection().rollback() + raise + finally: + cur.close() + + def _save_data(self, table: Table, key: str, data: list[str]) -> "Secrets": + def entry_exists(cur) -> None: + res = cur.execute( + f"SELECT * FROM {table.name} WHERE key=?", + [key]) + return res and res.fetchone() + + def update(cur) -> None: + columns = ", ".join(f"{c}=?" for c in table.columns) + cur.execute( + f"UPDATE {table.name} SET {columns} WHERE key=?", + data + [key]) + + def insert(cur) -> None: + columns = ",".join(table.columns) + value_slots = ", ".join("?" for c in table.columns) + cur.execute( + ( + f"INSERT INTO {table.name}" + f" (key,{columns})" + f" VALUES (?, {value_slots})" + ), + [key] + data) + + with self._cursor() as cur: + if entry_exists(cur): + update(cur) + else: + insert(cur) + return self + + def save(self, key: str, data: Union[str, Credentials]) -> "Secrets": + """key represents a system, service, or application""" + if isinstance(data, str): + return self._save_data(CONFIG_ITEMS_TABLE, key, [data]) + if isinstance(data, Credentials): + return self._save_data(SECRETS_TABLE, key, [data.user, data.password]) + raise Exception("Unsupported type of data: " + type(data).__name__) + + def _data(self, table: Table, key: str) -> Optional[list[str]]: + columns = ", ".join(table.columns) + with self._cursor() as cur: + res = cur.execute( + f"SELECT {columns} FROM {table.name} WHERE key=?", + [key]) + return res.fetchone() if res else None + + def credentials(self, key: str) -> Optional[Credentials]: + row = self._data(SECRETS_TABLE, key) + return Credentials(row[0], row[1]) if row else None + + def config(self, key: str) -> Optional[str]: + row = self._data(CONFIG_ITEMS_TABLE, key) + return row[0] if row else None diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..3547ab9 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,133 @@ +# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.1.3" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.1.3-py3-none-any.whl", hash = "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"}, + {file = "exceptiongroup-1.1.3.tar.gz", hash = "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "packaging" +version = "23.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, + {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, +] + +[[package]] +name = "pluggy" +version = "1.3.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, + {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pytest" +version = "7.4.2" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-7.4.2-py3-none-any.whl", hash = "sha256:1d881c6124e08ff0a1bb75ba3ec0bfd8b5354a01c194ddd5a0a870a48d99b002"}, + {file = "pytest-7.4.2.tar.gz", hash = "sha256:a766259cfab564a2ad52cb1aae1b881a75c3eb7e34ca3779697c23ed47c47069"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-mock" +version = "3.11.1" +description = "Thin-wrapper around the mock package for easier use with pytest" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-mock-3.11.1.tar.gz", hash = "sha256:7f6b125602ac6d743e523ae0bfa71e1a697a2f5534064528c6ff84c2f7c2fc7f"}, + {file = "pytest_mock-3.11.1-py3-none-any.whl", hash = "sha256:21c279fff83d70763b05f8874cc9cfb3fcacd6d354247a976f9529d19f9acf39"}, +] + +[package.dependencies] +pytest = ">=5.0" + +[package.extras] +dev = ["pre-commit", "pytest-asyncio", "tox"] + +[[package]] +name = "sqlcipher3-binary" +version = "0.5.2" +description = "DB-API 2.0 interface for SQLCipher 3.x" +optional = false +python-versions = "*" +files = [ + {file = "sqlcipher3_binary-0.5.2-cp310-cp310-manylinux_2_24_x86_64.whl", hash = "sha256:2f38bfffa0b6c23898c2542216d37c494df95ec1587711ebad3c055efe7d4643"}, + {file = "sqlcipher3_binary-0.5.2-cp311-cp311-manylinux_2_24_x86_64.whl", hash = "sha256:a41f18f77e6988a812ed099c7afda835a6ce3697baed440c2f775c6633aea33a"}, + {file = "sqlcipher3_binary-0.5.2-cp36-cp36m-manylinux_2_24_x86_64.whl", hash = "sha256:df1abf166a9e1b42ba53eaecd539efaf408abb986867be4f72c366a8e267af72"}, + {file = "sqlcipher3_binary-0.5.2-cp37-cp37m-manylinux_2_24_x86_64.whl", hash = "sha256:217845d0fa6c88962ebc2e415a49e85cda694d8d79f2a8907b94c946b40892c3"}, + {file = "sqlcipher3_binary-0.5.2-cp38-cp38-manylinux_2_24_x86_64.whl", hash = "sha256:18f83b12770bb90a8fcceb9a33a8abb2c4b65aee5c3e22493bbf4d8cfa6da0ff"}, + {file = "sqlcipher3_binary-0.5.2-cp39-cp39-manylinux_2_24_x86_64.whl", hash = "sha256:419b45efbbffaebe55a4ce8abb9e561606e87229e81bd524a739a047863b51ad"}, +] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[metadata] +lock-version = "2.0" +python-versions = ">=3.8.0,<4.0" +content-hash = "4f2989612a99ffcf93b9c8cc88249563dbcb1214355ad8e68c0bf0e9eacb0631" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..b35a9ca --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,31 @@ +[tool.poetry] +name = "notebook-connector" +version = "0.0.1" +description = "Components, tools, APIs, and configurations in order to connect Jupyter notebooks to various other systems." + +license = "MIT" + +authors = [ + "Christoph Kuhnke " +] + +[tool.poetry.dependencies] +python = ">=3.8.0,<4.0" +sqlcipher3-binary = ">=0.5.0" + + +[build-system] +requires = ["poetry_core>=1.0.0"] +build-backend = "poetry.core.masonry.api" + + +[tool.poetry.dev-dependencies] +pytest = "^7.1.1" +pytest-mock = "^3.7.0" + +[tool.pytest.ini_options] +minversion = "6.0" + +testpaths = [ + "test" +] diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/conftest.py b/test/conftest.py new file mode 100644 index 0000000..afef0f1 --- /dev/null +++ b/test/conftest.py @@ -0,0 +1,13 @@ +import pytest +from pathlib import Path +from notebook_connector.secret_store import Secrets + + +@pytest.fixture +def sample_file(tmp_path: Path) -> Path: + return tmp_path / "sample_database.db" + + +@pytest.fixture +def secrets(sample_file) -> Path: + return Secrets(sample_file, master_password="abc") diff --git a/test/test_secret_store.py b/test/test_secret_store.py new file mode 100644 index 0000000..fc78ca3 --- /dev/null +++ b/test/test_secret_store.py @@ -0,0 +1,56 @@ +import os +import pytest +from notebook_connector.secret_store import Credentials, InvalidPassword, Secrets +from sqlcipher3 import dbapi2 as sqlcipher + + +def test_no_database_file(secrets): + assert not os.path.exists(secrets.db_file) + + +def test_database_file_from_credentials(secrets): + assert secrets.credentials("a") is None + assert os.path.exists(secrets.db_file) + + +def test_database_file_from_config_item(secrets): + assert secrets.config("a") is None + assert os.path.exists(secrets.db_file) + + +def test_credentials(secrets): + credentials = Credentials("user", "password") + secrets.save("key", credentials).close() + assert secrets.credentials("key") == credentials + + +def test_config_item(secrets): + config_item = "some configuration" + secrets.save("key", config_item).close() + assert secrets.config("key") == config_item + + +def test_update_credentials(secrets): + initial = Credentials("user", "password") + secrets.save("key", initial).close() + other = Credentials("other", "changed") + secrets.save("key", other) + secrets.close() + assert secrets.credentials("key") == other + + +def test_update_config_item(secrets): + initial = "initial value" + secrets.save("key", initial).close() + other = "other value" + secrets.save("key", other).close() + assert secrets.config("key") == other + + +def test_wrong_password(sample_file): + secrets = Secrets(sample_file, "correct password") + secrets.save("key", Credentials("usr", "pass")).close() + invalid = Secrets(sample_file, "wrong password") + with pytest.raises(InvalidPassword) as ex: + invalid.credentials("key") + assert "master password is incorrect" in str(ex.value) From f3537c832a75a6a7a6fd44954977d4a20c410d5a Mon Sep 17 00:00:00 2001 From: ckunki Date: Tue, 10 Oct 2023 16:49:43 +0200 Subject: [PATCH 2/8] added folder --- .github/actions/{ => prepare_poetry_env}/action.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/actions/{ => prepare_poetry_env}/action.yml (100%) diff --git a/.github/actions/action.yml b/.github/actions/prepare_poetry_env/action.yml similarity index 100% rename from .github/actions/action.yml rename to .github/actions/prepare_poetry_env/action.yml From d9bf24016c715abcfc6ed0716919286817a6f573 Mon Sep 17 00:00:00 2001 From: ckunki Date: Tue, 10 Oct 2023 16:51:20 +0200 Subject: [PATCH 3/8] fixed type hints --- notebook_connector/secret_store.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/notebook_connector/secret_store.py b/notebook_connector/secret_store.py index 27bf6eb..34d2711 100644 --- a/notebook_connector/secret_store.py +++ b/notebook_connector/secret_store.py @@ -3,7 +3,7 @@ import os from dataclasses import dataclass from sqlcipher3 import dbapi2 as sqlcipher -from typing import Optional, Union +from typing import List, Optional, Union from inspect import cleandoc @@ -13,7 +13,7 @@ @dataclass(frozen=True) class Table: name: str - columns: list[str] + columns: List[str] SECRETS_TABLE = Table("secrets", ["user", "password"]) @@ -105,7 +105,7 @@ def _cursor(self) -> sqlcipher.Cursor: finally: cur.close() - def _save_data(self, table: Table, key: str, data: list[str]) -> "Secrets": + def _save_data(self, table: Table, key: str, data: List[str]) -> "Secrets": def entry_exists(cur) -> None: res = cur.execute( f"SELECT * FROM {table.name} WHERE key=?", @@ -144,7 +144,7 @@ def save(self, key: str, data: Union[str, Credentials]) -> "Secrets": return self._save_data(SECRETS_TABLE, key, [data.user, data.password]) raise Exception("Unsupported type of data: " + type(data).__name__) - def _data(self, table: Table, key: str) -> Optional[list[str]]: + def _data(self, table: Table, key: str) -> Optional[List[str]]: columns = ", ".join(table.columns) with self._cursor() as cur: res = cur.execute( From d558e2404990e5a3e9b5c4d6d5fd0560959b529d Mon Sep 17 00:00:00 2001 From: ckunki Date: Wed, 11 Oct 2023 10:01:34 +0200 Subject: [PATCH 4/8] Fixed review finding. --- notebook_connector/secret_store.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/notebook_connector/secret_store.py b/notebook_connector/secret_store.py index 34d2711..2a4d992 100644 --- a/notebook_connector/secret_store.py +++ b/notebook_connector/secret_store.py @@ -1,6 +1,7 @@ import contextlib import logging import os +import pathlib from dataclasses import dataclass from sqlcipher3 import dbapi2 as sqlcipher from typing import List, Optional, Union @@ -43,7 +44,7 @@ def close(self) -> None: def connection(self) -> sqlcipher.Connection: if self._con is None: - db_file_found = os.path.exists(self.db_file) + db_file_found = pathlib.Path.exists(self.db_file) if not db_file_found: _logger.info(f"Creating file {self.db_file}") self._con = sqlcipher.connect(self.db_file) From bf428f642cb21cdf90f4e5a3d709b555dc8e215b Mon Sep 17 00:00:00 2001 From: ckunki Date: Wed, 11 Oct 2023 10:06:45 +0200 Subject: [PATCH 5/8] Fixed review finding. --- notebook_connector/secret_store.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/notebook_connector/secret_store.py b/notebook_connector/secret_store.py index 2a4d992..d50c904 100644 --- a/notebook_connector/secret_store.py +++ b/notebook_connector/secret_store.py @@ -2,6 +2,7 @@ import logging import os import pathlib +from pathlib import Path from dataclasses import dataclass from sqlcipher3 import dbapi2 as sqlcipher from typing import List, Optional, Union @@ -32,7 +33,7 @@ class InvalidPassword(Exception): class Secrets: - def __init__(self, db_file: str, master_password: str) -> None: + def __init__(self, db_file: Path, master_password: str) -> None: self.db_file = db_file self._master_password = master_password self._con = None From 7445978573e0695f47b28d51f5dc7719f0660e43 Mon Sep 17 00:00:00 2001 From: Christoph Kuhnke Date: Wed, 11 Oct 2023 10:07:17 +0200 Subject: [PATCH 6/8] Apply suggestions from code review Co-authored-by: Christoph Pirkl --- .github/actions/prepare_poetry_env/action.yml | 1 - .github/workflows/ci_build.yaml | 5 ++++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/actions/prepare_poetry_env/action.yml b/.github/actions/prepare_poetry_env/action.yml index 0c49983..17dcbc7 100644 --- a/.github/actions/prepare_poetry_env/action.yml +++ b/.github/actions/prepare_poetry_env/action.yml @@ -16,5 +16,4 @@ runs: poetry-version: 1.4.0 - name: Poetry install run: poetry install - shell: bash diff --git a/.github/workflows/ci_build.yaml b/.github/workflows/ci_build.yaml index 0ba4491..59c8738 100644 --- a/.github/workflows/ci_build.yaml +++ b/.github/workflows/ci_build.yaml @@ -8,9 +8,12 @@ on: jobs: run_unit_tests: runs-on: ubuntu-latest + concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup Python & Poetry Environment From f1523f4e1a52fc38cc841eb6bf262bb99c9c7327 Mon Sep 17 00:00:00 2001 From: ckunki Date: Wed, 11 Oct 2023 10:08:26 +0200 Subject: [PATCH 7/8] Fixed review finding: removed file notebook_connector/poetry.lock --- notebook_connector/poetry.lock | 142 --------------------------------- 1 file changed, 142 deletions(-) delete mode 100644 notebook_connector/poetry.lock diff --git a/notebook_connector/poetry.lock b/notebook_connector/poetry.lock deleted file mode 100644 index d650aeb..0000000 --- a/notebook_connector/poetry.lock +++ /dev/null @@ -1,142 +0,0 @@ -# This file is automatically @generated by Poetry and should not be changed by hand. - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -category = "dev" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] - -[[package]] -name = "exceptiongroup" -version = "1.1.3" -description = "Backport of PEP 654 (exception groups)" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "exceptiongroup-1.1.3-py3-none-any.whl", hash = "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"}, - {file = "exceptiongroup-1.1.3.tar.gz", hash = "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9"}, -] - -[package.extras] -test = ["pytest (>=6)"] - -[[package]] -name = "iniconfig" -version = "2.0.0" -description = "brain-dead simple config-ini parsing" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, - {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, -] - -[[package]] -name = "packaging" -version = "23.1" -description = "Core utilities for Python packages" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, - {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, -] - -[[package]] -name = "pluggy" -version = "1.3.0" -description = "plugin and hook calling mechanisms for python" -category = "dev" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, - {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, -] - -[package.extras] -dev = ["pre-commit", "tox"] -testing = ["pytest", "pytest-benchmark"] - -[[package]] -name = "pytest" -version = "7.4.2" -description = "pytest: simple powerful testing with Python" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "pytest-7.4.2-py3-none-any.whl", hash = "sha256:1d881c6124e08ff0a1bb75ba3ec0bfd8b5354a01c194ddd5a0a870a48d99b002"}, - {file = "pytest-7.4.2.tar.gz", hash = "sha256:a766259cfab564a2ad52cb1aae1b881a75c3eb7e34ca3779697c23ed47c47069"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} -iniconfig = "*" -packaging = "*" -pluggy = ">=0.12,<2.0" -tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} - -[package.extras] -testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] - -[[package]] -name = "pytest-mock" -version = "3.11.1" -description = "Thin-wrapper around the mock package for easier use with pytest" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "pytest-mock-3.11.1.tar.gz", hash = "sha256:7f6b125602ac6d743e523ae0bfa71e1a697a2f5534064528c6ff84c2f7c2fc7f"}, - {file = "pytest_mock-3.11.1-py3-none-any.whl", hash = "sha256:21c279fff83d70763b05f8874cc9cfb3fcacd6d354247a976f9529d19f9acf39"}, -] - -[package.dependencies] -pytest = ">=5.0" - -[package.extras] -dev = ["pre-commit", "pytest-asyncio", "tox"] - -[[package]] -name = "sqlcipher3-binary" -version = "0.5.2" -description = "DB-API 2.0 interface for SQLCipher 3.x" -category = "main" -optional = false -python-versions = "*" -files = [ - {file = "sqlcipher3_binary-0.5.2-cp310-cp310-manylinux_2_24_x86_64.whl", hash = "sha256:2f38bfffa0b6c23898c2542216d37c494df95ec1587711ebad3c055efe7d4643"}, - {file = "sqlcipher3_binary-0.5.2-cp311-cp311-manylinux_2_24_x86_64.whl", hash = "sha256:a41f18f77e6988a812ed099c7afda835a6ce3697baed440c2f775c6633aea33a"}, - {file = "sqlcipher3_binary-0.5.2-cp36-cp36m-manylinux_2_24_x86_64.whl", hash = "sha256:df1abf166a9e1b42ba53eaecd539efaf408abb986867be4f72c366a8e267af72"}, - {file = "sqlcipher3_binary-0.5.2-cp37-cp37m-manylinux_2_24_x86_64.whl", hash = "sha256:217845d0fa6c88962ebc2e415a49e85cda694d8d79f2a8907b94c946b40892c3"}, - {file = "sqlcipher3_binary-0.5.2-cp38-cp38-manylinux_2_24_x86_64.whl", hash = "sha256:18f83b12770bb90a8fcceb9a33a8abb2c4b65aee5c3e22493bbf4d8cfa6da0ff"}, - {file = "sqlcipher3_binary-0.5.2-cp39-cp39-manylinux_2_24_x86_64.whl", hash = "sha256:419b45efbbffaebe55a4ce8abb9e561606e87229e81bd524a739a047863b51ad"}, -] - -[[package]] -name = "tomli" -version = "2.0.1" -description = "A lil' TOML parser" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, -] - -[metadata] -lock-version = "2.0" -python-versions = ">=3.8.0,<4.0" -content-hash = "4f2989612a99ffcf93b9c8cc88249563dbcb1214355ad8e68c0bf0e9eacb0631" From 99db7ed28c629241ac2965003a4cd9d29d2e2fc5 Mon Sep 17 00:00:00 2001 From: ckunki Date: Wed, 11 Oct 2023 10:11:52 +0200 Subject: [PATCH 8/8] Fixed build error --- .github/actions/prepare_poetry_env/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/prepare_poetry_env/action.yml b/.github/actions/prepare_poetry_env/action.yml index 17dcbc7..2bf84c7 100644 --- a/.github/actions/prepare_poetry_env/action.yml +++ b/.github/actions/prepare_poetry_env/action.yml @@ -16,4 +16,4 @@ runs: poetry-version: 1.4.0 - name: Poetry install run: poetry install - + shell: bash