From b332a307d8a517c8f6def8f2930db3035fa46bb0 Mon Sep 17 00:00:00 2001 From: clementb49 Date: Thu, 15 Aug 2024 20:22:51 +0200 Subject: [PATCH 01/11] feat(secure_api_key): Adding keyring dependency to use secure credential store --- poetry.lock | 225 +++++++++++++++++++++++++++++++++++++++++++------ pyproject.toml | 1 + 2 files changed, 200 insertions(+), 26 deletions(-) diff --git a/poetry.lock b/poetry.lock index 881bbda5..5503d39f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -329,6 +329,55 @@ questionary = ">=2.0,<3.0" termcolor = ">=1.1,<3" tomlkit = ">=0.5.3,<1.0.0" +[[package]] +name = "cryptography" +version = "43.0.0" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = ">=3.7" +files = [ + {file = "cryptography-43.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:64c3f16e2a4fc51c0d06af28441881f98c5d91009b8caaff40cf3548089e9c74"}, + {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3dcdedae5c7710b9f97ac6bba7e1052b95c7083c9d0e9df96e02a1932e777895"}, + {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d9a1eca329405219b605fac09ecfc09ac09e595d6def650a437523fcd08dd22"}, + {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ea9e57f8ea880eeea38ab5abf9fbe39f923544d7884228ec67d666abd60f5a47"}, + {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:9a8d6802e0825767476f62aafed40532bd435e8a5f7d23bd8b4f5fd04cc80ecf"}, + {file = "cryptography-43.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cc70b4b581f28d0a254d006f26949245e3657d40d8857066c2ae22a61222ef55"}, + {file = "cryptography-43.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4a997df8c1c2aae1e1e5ac49c2e4f610ad037fc5a3aadc7b64e39dea42249431"}, + {file = "cryptography-43.0.0-cp37-abi3-win32.whl", hash = "sha256:6e2b11c55d260d03a8cf29ac9b5e0608d35f08077d8c087be96287f43af3ccdc"}, + {file = "cryptography-43.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:31e44a986ceccec3d0498e16f3d27b2ee5fdf69ce2ab89b52eaad1d2f33d8778"}, + {file = "cryptography-43.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:7b3f5fe74a5ca32d4d0f302ffe6680fcc5c28f8ef0dc0ae8f40c0f3a1b4fca66"}, + {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac1955ce000cb29ab40def14fd1bbfa7af2017cca696ee696925615cafd0dce5"}, + {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:299d3da8e00b7e2b54bb02ef58d73cd5f55fb31f33ebbf33bd00d9aa6807df7e"}, + {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ee0c405832ade84d4de74b9029bedb7b31200600fa524d218fc29bfa371e97f5"}, + {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cb013933d4c127349b3948aa8aaf2f12c0353ad0eccd715ca789c8a0f671646f"}, + {file = "cryptography-43.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fdcb265de28585de5b859ae13e3846a8e805268a823a12a4da2597f1f5afc9f0"}, + {file = "cryptography-43.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2905ccf93a8a2a416f3ec01b1a7911c3fe4073ef35640e7ee5296754e30b762b"}, + {file = "cryptography-43.0.0-cp39-abi3-win32.whl", hash = "sha256:47ca71115e545954e6c1d207dd13461ab81f4eccfcb1345eac874828b5e3eaaf"}, + {file = "cryptography-43.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:0663585d02f76929792470451a5ba64424acc3cd5227b03921dab0e2f27b1709"}, + {file = "cryptography-43.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2c6d112bf61c5ef44042c253e4859b3cbbb50df2f78fa8fae6747a7814484a70"}, + {file = "cryptography-43.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:844b6d608374e7d08f4f6e6f9f7b951f9256db41421917dfb2d003dde4cd6b66"}, + {file = "cryptography-43.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:51956cf8730665e2bdf8ddb8da0056f699c1a5715648c1b0144670c1ba00b48f"}, + {file = "cryptography-43.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:aae4d918f6b180a8ab8bf6511a419473d107df4dbb4225c7b48c5c9602c38c7f"}, + {file = "cryptography-43.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:232ce02943a579095a339ac4b390fbbe97f5b5d5d107f8a08260ea2768be8cc2"}, + {file = "cryptography-43.0.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5bcb8a5620008a8034d39bce21dc3e23735dfdb6a33a06974739bfa04f853947"}, + {file = "cryptography-43.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:08a24a7070b2b6804c1940ff0f910ff728932a9d0e80e7814234269f9d46d069"}, + {file = "cryptography-43.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e9c5266c432a1e23738d178e51c2c7a5e2ddf790f248be939448c0ba2021f9d1"}, + {file = "cryptography-43.0.0.tar.gz", hash = "sha256:b88075ada2d51aa9f18283532c9f60e72170041bba88d7f37e49cbb10275299e"}, +] + +[package.dependencies] +cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] +docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] +nox = ["nox"] +pep8test = ["check-sdist", "click", "mypy", "ruff"] +sdist = ["build"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["certifi", "cryptography-vectors (==43.0.0)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test-randomorder = ["pytest-randomly"] + [[package]] name = "cx-freeze" version = "7.2.0" @@ -891,6 +940,72 @@ files = [ {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, ] +[[package]] +name = "jaraco-classes" +version = "3.4.0" +description = "Utility functions for Python class constructs" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790"}, + {file = "jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd"}, +] + +[package.dependencies] +more-itertools = "*" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"] + +[[package]] +name = "jaraco-context" +version = "5.3.0" +description = "Useful decorators and context managers" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jaraco.context-5.3.0-py3-none-any.whl", hash = "sha256:3e16388f7da43d384a1a7cd3452e72e14732ac9fe459678773a3608a812bf266"}, + {file = "jaraco.context-5.3.0.tar.gz", hash = "sha256:c2f67165ce1f9be20f32f650f25d8edfc1646a8aeee48ae06fb35f90763576d2"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["portend", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"] + +[[package]] +name = "jaraco-functools" +version = "4.0.2" +description = "Functools like those found in stdlib" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jaraco.functools-4.0.2-py3-none-any.whl", hash = "sha256:c9d16a3ed4ccb5a889ad8e0b7a343401ee5b2a71cee6ed192d3f68bc351e94e3"}, + {file = "jaraco_functools-4.0.2.tar.gz", hash = "sha256:3460c74cd0d32bf82b9576bbb3527c4364d5b27a21f5158a62aed6c4b42e23f5"}, +] + +[package.dependencies] +more-itertools = "*" + +[package.extras] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +test = ["jaraco.classes", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"] + +[[package]] +name = "jeepney" +version = "0.8.0" +description = "Low-level, pure Python DBus protocol wrapper." +optional = false +python-versions = ">=3.7" +files = [ + {file = "jeepney-0.8.0-py3-none-any.whl", hash = "sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755"}, + {file = "jeepney-0.8.0.tar.gz", hash = "sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806"}, +] + +[package.extras] +test = ["async-timeout", "pytest", "pytest-asyncio (>=0.17)", "pytest-trio", "testpath", "trio"] +trio = ["async_generator", "trio"] + [[package]] name = "jinja2" version = "3.1.4" @@ -978,6 +1093,30 @@ files = [ {file = "jiter-0.5.0.tar.gz", hash = "sha256:1d916ba875bcab5c5f7d927df998c4cb694d27dceddf3392e58beaf10563368a"}, ] +[[package]] +name = "keyring" +version = "25.3.0" +description = "Store and access your passwords safely." +optional = false +python-versions = ">=3.8" +files = [ + {file = "keyring-25.3.0-py3-none-any.whl", hash = "sha256:8d963da00ccdf06e356acd9bf3b743208878751032d8599c6cc89eb51310ffae"}, + {file = "keyring-25.3.0.tar.gz", hash = "sha256:8d85a1ea5d6db8515b59e1c5d1d1678b03cf7fc8b8dcfb1651e8c4a524eb42ef"}, +] + +[package.dependencies] +"jaraco.classes" = "*" +"jaraco.context" = "*" +"jaraco.functools" = "*" +jeepney = {version = ">=0.4.2", markers = "sys_platform == \"linux\""} +pywin32-ctypes = {version = ">=0.2.0", markers = "sys_platform == \"win32\""} +SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""} + +[package.extras] +completion = ["shtab (>=1.1.0)"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +test = ["pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"] + [[package]] name = "lief" version = "0.14.1" @@ -1003,9 +1142,6 @@ files = [ {file = "lief-0.14.1-cp312-cp312-manylinux_2_28_x86_64.manylinux_2_27_x86_64.whl", hash = "sha256:497b88f9c9aaae999766ba188744ee35c5f38b4b64016f7dbb7037e9bf325382"}, {file = "lief-0.14.1-cp312-cp312-win32.whl", hash = "sha256:08bad88083f696915f8dcda4042a3bfc514e17462924ec8984085838b2261921"}, {file = "lief-0.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:e131d6158a085f8a72124136816fefc29405c725cd3695ce22a904e471f0f815"}, - {file = "lief-0.14.1-cp313-cp313-manylinux_2_28_x86_64.manylinux_2_27_x86_64.whl", hash = "sha256:f9ff9a6959fb6d0e553cca41cd1027b609d27c5073e98d9fad8b774fbb5746c2"}, - {file = "lief-0.14.1-cp313-cp313-win32.whl", hash = "sha256:95f295a7cc68f4e14ce7ea4ff8082a04f5313c2e5e63cc2bbe9d059190b7e4d5"}, - {file = "lief-0.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:cdc1123c2e27970f8c8353505fd578e634ab33193c8d1dff36dc159e25599a40"}, {file = "lief-0.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:df650fa05ca131e4dfeb42c77985e1eb239730af9944bc0aadb1dfac8576e0e8"}, {file = "lief-0.14.1-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:b4e76eeb48ca2925c6ca6034d408582615f2faa855f9bb11482e7acbdecc4803"}, {file = "lief-0.14.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:016e4fac91303466024154dd3c4b599e8b7c52882f72038b62a2be386d98c8f9"}, @@ -1105,6 +1241,17 @@ files = [ {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, ] +[[package]] +name = "more-itertools" +version = "10.4.0" +description = "More routines for operating on iterables, beyond itertools" +optional = false +python-versions = ">=3.8" +files = [ + {file = "more-itertools-10.4.0.tar.gz", hash = "sha256:fe0e63c4ab068eac62410ab05cccca2dc71ec44ba8ef29916a0090df061cf923"}, + {file = "more_itertools-10.4.0-py3-none-any.whl", hash = "sha256:0f7d9f83a0a8dcfa8a2694a770590d98a67ea943e3d9f5298309a484758c4e27"}, +] + [[package]] name = "nodeenv" version = "1.9.1" @@ -1179,13 +1326,13 @@ files = [ [[package]] name = "openai" -version = "1.41.1" +version = "1.42.0" description = "The official Python library for the openai API" optional = false python-versions = ">=3.7.1" files = [ - {file = "openai-1.41.1-py3-none-any.whl", hash = "sha256:56fb04105263f79559aff3ceea2e1dd16f8c5385e8238cb66cf0e6888fa8bfcf"}, - {file = "openai-1.41.1.tar.gz", hash = "sha256:e38e376efd91e0d4db071e2a6517b6b4cac1c2a6fd63efdc5ec6be10c5967c1b"}, + {file = "openai-1.42.0-py3-none-any.whl", hash = "sha256:dc91e0307033a4f94931e5d03cc3b29b9717014ad5e73f9f2051b6cb5eda4d80"}, + {file = "openai-1.42.0.tar.gz", hash = "sha256:c9d31853b4e0bc2dc8bd08003b462a006035655a701471695d0bfdc08529cde3"}, ] [package.dependencies] @@ -1672,6 +1819,17 @@ files = [ {file = "pywin32-306-cp39-cp39-win_amd64.whl", hash = "sha256:39b61c15272833b5c329a2989999dcae836b1eed650252ab1b7bfbe1d59f30f4"}, ] +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +description = "A (partial) reimplementation of pywin32 using ctypes/cffi" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755"}, + {file = "pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8"}, +] + [[package]] name = "pyyaml" version = "6.0.2" @@ -1785,31 +1943,46 @@ pyasn1 = ">=0.1.3" [[package]] name = "ruff" -version = "0.6.1" +version = "0.6.2" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.6.1-py3-none-linux_armv6l.whl", hash = "sha256:b4bb7de6a24169dc023f992718a9417380301b0c2da0fe85919f47264fb8add9"}, - {file = "ruff-0.6.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:45efaae53b360c81043e311cdec8a7696420b3d3e8935202c2846e7a97d4edae"}, - {file = "ruff-0.6.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:bc60c7d71b732c8fa73cf995efc0c836a2fd8b9810e115be8babb24ae87e0850"}, - {file = "ruff-0.6.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c7477c3b9da822e2db0b4e0b59e61b8a23e87886e727b327e7dcaf06213c5cf"}, - {file = "ruff-0.6.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a0af7ab3f86e3dc9f157a928e08e26c4b40707d0612b01cd577cc84b8905cc9"}, - {file = "ruff-0.6.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:392688dbb50fecf1bf7126731c90c11a9df1c3a4cdc3f481b53e851da5634fa5"}, - {file = "ruff-0.6.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5278d3e095ccc8c30430bcc9bc550f778790acc211865520f3041910a28d0024"}, - {file = "ruff-0.6.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fe6d5f65d6f276ee7a0fc50a0cecaccb362d30ef98a110f99cac1c7872df2f18"}, - {file = "ruff-0.6.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2e0dd11e2ae553ee5c92a81731d88a9883af8db7408db47fc81887c1f8b672e"}, - {file = "ruff-0.6.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d812615525a34ecfc07fd93f906ef5b93656be01dfae9a819e31caa6cfe758a1"}, - {file = "ruff-0.6.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:faaa4060f4064c3b7aaaa27328080c932fa142786f8142aff095b42b6a2eb631"}, - {file = "ruff-0.6.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:99d7ae0df47c62729d58765c593ea54c2546d5de213f2af2a19442d50a10cec9"}, - {file = "ruff-0.6.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9eb18dfd7b613eec000e3738b3f0e4398bf0153cb80bfa3e351b3c1c2f6d7b15"}, - {file = "ruff-0.6.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:c62bc04c6723a81e25e71715aa59489f15034d69bf641df88cb38bdc32fd1dbb"}, - {file = "ruff-0.6.1-py3-none-win32.whl", hash = "sha256:9fb4c4e8b83f19c9477a8745e56d2eeef07a7ff50b68a6998f7d9e2e3887bdc4"}, - {file = "ruff-0.6.1-py3-none-win_amd64.whl", hash = "sha256:c2ebfc8f51ef4aca05dad4552bbcf6fe8d1f75b2f6af546cc47cc1c1ca916b5b"}, - {file = "ruff-0.6.1-py3-none-win_arm64.whl", hash = "sha256:3bc81074971b0ffad1bd0c52284b22411f02a11a012082a76ac6da153536e014"}, - {file = "ruff-0.6.1.tar.gz", hash = "sha256:af3ffd8c6563acb8848d33cd19a69b9bfe943667f0419ca083f8ebe4224a3436"}, + {file = "ruff-0.6.2-py3-none-linux_armv6l.whl", hash = "sha256:5c8cbc6252deb3ea840ad6a20b0f8583caab0c5ef4f9cca21adc5a92b8f79f3c"}, + {file = "ruff-0.6.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:17002fe241e76544448a8e1e6118abecbe8cd10cf68fde635dad480dba594570"}, + {file = "ruff-0.6.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3dbeac76ed13456f8158b8f4fe087bf87882e645c8e8b606dd17b0b66c2c1158"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:094600ee88cda325988d3f54e3588c46de5c18dae09d683ace278b11f9d4d534"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:316d418fe258c036ba05fbf7dfc1f7d3d4096db63431546163b472285668132b"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d72b8b3abf8a2d51b7b9944a41307d2f442558ccb3859bbd87e6ae9be1694a5d"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2aed7e243be68487aa8982e91c6e260982d00da3f38955873aecd5a9204b1d66"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d371f7fc9cec83497fe7cf5eaf5b76e22a8efce463de5f775a1826197feb9df8"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8f310d63af08f583363dfb844ba8f9417b558199c58a5999215082036d795a1"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7db6880c53c56addb8638fe444818183385ec85eeada1d48fc5abe045301b2f1"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1175d39faadd9a50718f478d23bfc1d4da5743f1ab56af81a2b6caf0a2394f23"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5b939f9c86d51635fe486585389f54582f0d65b8238e08c327c1534844b3bb9a"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d0d62ca91219f906caf9b187dea50d17353f15ec9bb15aae4a606cd697b49b4c"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7438a7288f9d67ed3c8ce4d059e67f7ed65e9fe3aa2ab6f5b4b3610e57e3cb56"}, + {file = "ruff-0.6.2-py3-none-win32.whl", hash = "sha256:279d5f7d86696df5f9549b56b9b6a7f6c72961b619022b5b7999b15db392a4da"}, + {file = "ruff-0.6.2-py3-none-win_amd64.whl", hash = "sha256:d9f3469c7dd43cd22eb1c3fc16926fb8258d50cb1b216658a07be95dd117b0f2"}, + {file = "ruff-0.6.2-py3-none-win_arm64.whl", hash = "sha256:f28fcd2cd0e02bdf739297516d5643a945cc7caf09bd9bcb4d932540a5ea4fa9"}, + {file = "ruff-0.6.2.tar.gz", hash = "sha256:239ee6beb9e91feb8e0ec384204a763f36cb53fb895a1a364618c6abb076b3be"}, +] + +[[package]] +name = "secretstorage" +version = "3.3.3" +description = "Python bindings to FreeDesktop.org Secret Service API" +optional = false +python-versions = ">=3.6" +files = [ + {file = "SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99"}, + {file = "SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77"}, ] +[package.dependencies] +cryptography = ">=2.0" +jeepney = ">=0.6" + [[package]] name = "setuptools" version = "70.3.0" @@ -2212,4 +2385,4 @@ six = "*" [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "da435b10994d5ebd6ef2ee6c77b87fad6d984f3406a9de2646f16a19c2686d79" +content-hash = "414874ef5b0d7ec6243578d226b09245c578203744fa852fd257f0c3db308ff8" diff --git a/pyproject.toml b/pyproject.toml index 3766e9b8..fbc9512c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,6 +42,7 @@ sounddevice = "^0.5.0" numpy = "^2.1.0" google-generativeai = "^0.7.2" pillow = "^10.4.0" +keyring = "^25.3.0" [tool.poetry.group.dev.dependencies] ruff = "^0.6.1" From 5080863624f46868226f189949fa0637e469c5dd Mon Sep 17 00:00:00 2001 From: clementb49 Date: Sat, 17 Aug 2024 16:16:21 +0200 Subject: [PATCH 02/11] feat(secure_api_key): update cx_Freeze settings for keyring --- pyproject.toml | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index fbc9512c..6a63c7ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -90,19 +90,22 @@ excludes = [ "numpy._core.tests", "numpy.f2py", "numpy.fft", "numpy.ma", "numpy.polynomial", "numpy.random", "numpy.testing", "pip", "pydoc_data", "packaging", "setuptools", "setuptools_scm", "sqlite3", - "tarfile", "tomllib", "test", "tkinter", "unittest", - "win32api", "win32com", "wint32gui", "win32ui", "win32uiold", "winerror", "winreg", + "tomllib", "test", "tkinter", "unittest", + "win32com", "wint32gui", "win32ui", "win32uiold", "winerror", "winreg", ] include_files = ["basilisk/res"] -includes = ["numpy", "httplib2.socks"] -packages = ["numpy", "google.generativeai", "basilisk.provider_engine"] +includes = ["numpy", "httplib2.socks", "win32timezone"] +packages = ["numpy", "google.generativeai", "basilisk.provider_engine", "keyring"] zip_include_packages = [ "anyio", "annotated_types", "anthropic", "asyncio", "cachetools", "certifi", "cffi", "charset_normalizer", "concurrent", "collections", "colorama", "ctypes", "curses", "distro", "dotenv", "encodings", "email", "google", "googleapiclient", "grpc_status", "h11", "html", "httpcore", "http", "httplib2", "httpx", - "idna", "importlib", "jiter", "json", "logging", - "openai", "numpy", "PIL", "platformdirs", "proto", "psutil", "pyasn1", "pyasn1_modules", "pycparser", "pyparsing", "pydantic", "pydantic_core", "pydantic_settings", - "re", "rsa", "requests", "sniffio", "tokenizers", "tqdm", "uritemplate", "urllib", "urllib3", - "watchdog", "xml", "yaml", "zipfile", "zoneinfo", + "idna", "importlib", "importlib_metadata", + "jaraco", "jiter", "json", "keyring", "logging", + "openai", "numpy", "more_itertools", + "PIL", "platformdirs", "proto", "psutil", "pyasn1", "pyasn1_modules", "pycparser", "pyparsing", "pydantic", "pydantic_core", "pydantic_settings", "pywin32_system32", + "re", "rsa", "requests", "sniffio", "tarfile", "tokenizers", "tqdm", "uritemplate", "urllib", "urllib3", + "watchdog", "win32api", "win32ctypes", "win32timezone", + "xml", "yaml", "zipfile", "zipp", "zoneinfo", "basilisk"] From 93e2e86480572e277dcf79443db9e5263bc0e594 Mon Sep 17 00:00:00 2001 From: clementb49 Date: Sat, 17 Aug 2024 16:22:20 +0200 Subject: [PATCH 03/11] feat(secure_api_key): update account model to store and retreve api key from system keyring --- basilisk/account.py | 58 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 53 insertions(+), 5 deletions(-) diff --git a/basilisk/account.py b/basilisk/account.py index 349df2c2..f5c78f83 100644 --- a/basilisk/account.py +++ b/basilisk/account.py @@ -7,6 +7,7 @@ from typing import Any, Iterable, Optional from uuid import uuid4 +import keyring from pydantic import ( UUID4, BaseModel, @@ -15,7 +16,6 @@ OnErrorOmit, RootModel, SecretStr, - ValidationError, field_serializer, field_validator, model_serializer, @@ -29,6 +29,11 @@ log = getLogger(__name__) +class ApiKeyStorageMethodEnum(Enum): + plain = "plain" + system = "system" + + class AccountSource(Enum): ENV_VAR = "env_var" CONFIG = "config" @@ -57,15 +62,32 @@ class Account(BaseModel): provider: Provider = Field( validation_alias="provider_id", serialization_alias="provider_id" ) + api_key_storage_method: Optional[ApiKeyStorageMethodEnum] = Field( + default=ApiKeyStorageMethodEnum.plain + ) api_key: Optional[SecretStr] = Field(default=None) organizations: Optional[list[AccountOrganization]] = Field(default=None) active_organization_id: Optional[UUID4] = Field(default=None) source: AccountSource = Field(default=AccountSource.CONFIG, exclude=True) + @staticmethod + def get_keyring_service_name(provider_name: str, account_name: str) -> str: + return f"basilisk_{provider_name}_{account_name}" + def __init__(self, **data: Any): try: + if ( + data.get("api_key_storage_method", "plain") + != ApiKeyStorageMethodEnum.plain.value + ): + keyring_service_name = self.get_keyring_service_name( + data["provider_id"], data["name"] + ) + data["api_key"] = keyring.get_password( + data["api_key_storage_method"], keyring_service_name + ) super().__init__(**data) - except ValidationError as e: + except Exception as e: log.error( f"Error in account {e} the account will not be accessible" ) @@ -76,8 +98,19 @@ def serialize_provider(value: Provider) -> str: return value.id @field_serializer("api_key", when_used="json") - def dump_secret(self, value: SecretStr) -> str: - return value.get_secret_value() + def dump_secret(self, value: SecretStr) -> Optional[str]: + if self.api_key_storage_method == ApiKeyStorageMethodEnum.plain: + return value.get_secret_value() + elif self.api_key_storage_method == ApiKeyStorageMethodEnum.system: + keyring_service_name = self.get_keyring_service_name( + self.provider.id, self.name + ) + keyring.set_password( + self.api_key_storage_method.value, + keyring_service_name, + value.get_secret_value(), + ) + return None @field_validator("provider", mode="plain") @classmethod @@ -140,6 +173,15 @@ def active_organization_key(self) -> Optional[SecretStr]: self.active_organization.key if self.active_organization else None ) + def delete_keyring_password(self): + if self.api_key_storage_method == ApiKeyStorageMethodEnum.system: + keyring_service_name = self.get_keyring_service_name( + self.provider.id, self.name + ) + keyring.delete_password( + self.api_key_storage_method.value, keyring_service_name + ) + class AccountManager(RootModel): root: list[OnErrorOmit[Account]] = Field(default=list()) @@ -197,7 +239,12 @@ def serialize_account_config(self) -> list[dict[str, Any]]: lambda x: x.source == AccountSource.CONFIG, self.root ) return [ - acc.model_dump(mode="json", by_alias=True, exclude_none=True) + acc.model_dump( + mode="json", + by_alias=True, + exclude_none=True, + exclude_defaults=True, + ) for acc in accounts_config ] @@ -215,6 +262,7 @@ def get_accounts_by_provider( return filter(lambda x: x.provider.name == provider_name, self.root) def remove(self, account: Account): + account.delete_keyring_password() self.root.remove(account) def clear(self): From 5f5557536efe1743d4de5a2678750e1eb0b4cda4 Mon Sep 17 00:00:00 2001 From: clementb49 Date: Sat, 17 Aug 2024 16:27:55 +0200 Subject: [PATCH 04/11] feat(secure_api_key): Update account dialog to choose the api key storage method --- basilisk/gui/account_dialog.py | 86 +++++++++++++++++++++++++++++----- 1 file changed, 73 insertions(+), 13 deletions(-) diff --git a/basilisk/gui/account_dialog.py b/basilisk/gui/account_dialog.py index 445a98b5..9e7adc08 100644 --- a/basilisk/gui/account_dialog.py +++ b/basilisk/gui/account_dialog.py @@ -8,6 +8,7 @@ Account, AccountOrganization, AccountSource, + ApiKeyStorageMethodEnum, get_account_source_labels, ) from basilisk.config import conf @@ -15,6 +16,13 @@ log = getLogger(__name__) +api_storage_methods = { + # Translators: A label for the API key storage method in the account dialog + ApiKeyStorageMethodEnum.plain: _("Plain text"), + # Translators: A label for the API key storage method in the account dialog + ApiKeyStorageMethodEnum.system: _("System keyring"), +} + class EditAccountOrganizationDialog(wx.Dialog): def __init__( @@ -306,21 +314,50 @@ def init_ui(self): sizer = wx.BoxSizer(wx.VERTICAL) panel.SetSizer(sizer) - label = wx.StaticText(panel, label=_("&Name:"), style=wx.ALIGN_LEFT) + label = wx.StaticText( + panel, + # Translators: A label in account dialog + label=_("&Name:"), + style=wx.ALIGN_LEFT, + ) sizer.Add(label, 0, wx.ALL, 5) self.name = wx.TextCtrl(panel) sizer.Add(self.name, 0, wx.EXPAND) - label = wx.StaticText(panel, label=_("&Provider:"), style=wx.ALIGN_LEFT) + label = wx.StaticText( + panel, + # Translators: A label in account dialog + label=_("&Provider:"), + style=wx.ALIGN_LEFT, + ) sizer.Add(label, 0, wx.ALL, 5) - choices = [provider.name for provider in providers] + provider_choices = [provider.name for provider in providers] self.provider = wx.ComboBox( - panel, choices=choices, style=wx.CB_READONLY + panel, choices=provider_choices, style=wx.CB_READONLY ) self.provider.Bind(wx.EVT_COMBOBOX, lambda e: self.update_ui()) sizer.Add(self.provider, 0, wx.EXPAND) - label = wx.StaticText(panel, label=_("API &key:"), style=wx.ALIGN_LEFT) + label = wx.StaticText( + panel, + style=wx.ALIGN_LEFT, + # Translators: A label in account dialog + label=_("API &key storage method:"), + ) + sizer.Add(label, 0, wx.ALL, 5) + self.api_key_storage_method = wx.ComboBox( + panel, + choices=list(api_storage_methods.values()), + style=wx.CB_READONLY, + ) + sizer.Add(self.api_key_storage_method, 0, wx.EXPAND) + self.api_key_storage_method.Disable() + label = wx.StaticText( + panel, + style=wx.ALIGN_LEFT, + # Translators: A label in account dialog + label=_("API &key:"), + ) sizer.Add(label, 0, wx.ALL, 5) self.api_key = wx.TextCtrl(panel) self.api_key.Disable() @@ -356,7 +393,13 @@ def init_data(self): index = i break self.provider.SetSelection(index) - if self.account.api_key: + if self.account.api_key and self.account.api_key_storage_method: + index = -1 + for i, method in enumerate(api_storage_methods.keys()): + if method == self.account.api_key_storage_method: + index = i + break + self.api_key_storage_method.SetSelection(index) self.api_key.SetValue(self.account.api_key.get_secret_value()) self.organization.Enable( self.account.provider.organization_mode_available @@ -399,7 +442,9 @@ def update_ui(self): return provider_name = self.provider.GetValue() provider = get_provider(name=provider_name) - self.api_key.Enable(provider.require_api_key) + if provider.require_api_key: + self.api_key.Enable() + self.api_key_storage_method.Enable() if self.account: self.organization.Enable(provider.organization_mode_available) @@ -417,11 +462,19 @@ def on_ok(self, event): return provider_name = self.provider.GetValue() provider = get_provider(name=provider_name) - if provider.require_api_key and not self.api_key.GetValue(): - msg = _("Please enter an API key. It is required for this provider") - wx.MessageBox(msg, _("Error"), wx.OK | wx.ICON_ERROR) - self.api_key.SetFocus() - return + if provider.require_api_key: + if self.api_key_storage_method.GetSelection() == -1: + msg = _("Please select an API key storage method") + wx.MessageBox(msg, _("Error"), wx.OK | wx.ICON_ERROR) + self.api_key_storage_method.SetFocus() + return + if not self.api_key.GetValue(): + msg = _( + "Please enter an API key. It is required for this provider" + ) + wx.MessageBox(msg, _("Error"), wx.OK | wx.ICON_ERROR) + self.api_key.SetFocus() + return organization_index = self.organization.GetSelection() active_organization = None if organization_index > 0: @@ -431,12 +484,19 @@ def on_ok(self, event): if self.account: self.account.name = self.name.GetValue() self.account.provider = provider - self.account.api_key = SecretStr(self.api_key.GetValue()) + if provider.require_api_key: + self.account.api_key_storage_method = list( + api_storage_methods.keys() + )[self.api_key_storage_method.GetSelection()] + self.account.api_key = SecretStr(self.api_key.GetValue()) self.account.active_organization_id = active_organization else: self.account = Account( name=self.name.GetValue(), provider=provider, + apii_key_storage_method=list(api_storage_methods.keys())[ + self.api_key_storage_method.GetSelection() + ], api_key=SecretStr(self.api_key.GetValue()), active_organization_id=active_organization, source=AccountSource.CONFIG, From aa04abc961ad2b27b078bb9f51040eda5ed425c0 Mon Sep 17 00:00:00 2001 From: clementb49 Date: Sat, 17 Aug 2024 21:07:56 +0200 Subject: [PATCH 05/11] refactor(secure_api_key): use a field validator instead of init to get keyring secret --- basilisk/account.py | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/basilisk/account.py b/basilisk/account.py index f5c78f83..b2f7145e 100644 --- a/basilisk/account.py +++ b/basilisk/account.py @@ -16,6 +16,7 @@ OnErrorOmit, RootModel, SecretStr, + ValidationInfo, field_serializer, field_validator, model_serializer, @@ -76,20 +77,11 @@ def get_keyring_service_name(provider_name: str, account_name: str) -> str: def __init__(self, **data: Any): try: - if ( - data.get("api_key_storage_method", "plain") - != ApiKeyStorageMethodEnum.plain.value - ): - keyring_service_name = self.get_keyring_service_name( - data["provider_id"], data["name"] - ) - data["api_key"] = keyring.get_password( - data["api_key_storage_method"], keyring_service_name - ) super().__init__(**data) except Exception as e: log.error( - f"Error in account {e} the account will not be accessible" + f"Error in account {e} the account will not be accessible", + exc_info=e, ) raise e @@ -97,6 +89,24 @@ def __init__(self, **data: Any): def serialize_provider(value: Provider) -> str: return value.id + @field_validator("api_key", mode="after") + @classmethod + def validate_api_key( + cls, value: Optional[SecretStr], info: ValidationInfo + ) -> Optional[SecretStr]: + data = info.data + if data["api_key_storage_method"] == ApiKeyStorageMethodEnum.plain: + return value + keyring_service_name = cls.get_keyring_service_name( + data["provider"].id, data["name"] + ) + value = SecretStr( + keyring.get_password( + data["api_key_storage_method"].value, keyring_service_name + ) + ) + return value + @field_serializer("api_key", when_used="json") def dump_secret(self, value: SecretStr) -> Optional[str]: if self.api_key_storage_method == ApiKeyStorageMethodEnum.plain: From e57df8d6a248db9d0628c653243b31bed648f422 Mon Sep 17 00:00:00 2001 From: clementb49 Date: Sat, 24 Aug 2024 16:07:53 +0200 Subject: [PATCH 06/11] feat(secure_api_key): use app name for service name and account id for username in system keyring --- basilisk/account.py | 28 ++++------------------------ 1 file changed, 4 insertions(+), 24 deletions(-) diff --git a/basilisk/account.py b/basilisk/account.py index b2f7145e..78c462d3 100644 --- a/basilisk/account.py +++ b/basilisk/account.py @@ -25,6 +25,7 @@ import basilisk.global_vars as global_vars +from .consts import APP_NAME from .provider import Provider, get_provider, providers log = getLogger(__name__) @@ -71,10 +72,6 @@ class Account(BaseModel): active_organization_id: Optional[UUID4] = Field(default=None) source: AccountSource = Field(default=AccountSource.CONFIG, exclude=True) - @staticmethod - def get_keyring_service_name(provider_name: str, account_name: str) -> str: - return f"basilisk_{provider_name}_{account_name}" - def __init__(self, **data: Any): try: super().__init__(**data) @@ -97,14 +94,7 @@ def validate_api_key( data = info.data if data["api_key_storage_method"] == ApiKeyStorageMethodEnum.plain: return value - keyring_service_name = cls.get_keyring_service_name( - data["provider"].id, data["name"] - ) - value = SecretStr( - keyring.get_password( - data["api_key_storage_method"].value, keyring_service_name - ) - ) + value = SecretStr(keyring.get_password(APP_NAME, str(data["id"]))) return value @field_serializer("api_key", when_used="json") @@ -112,13 +102,8 @@ def dump_secret(self, value: SecretStr) -> Optional[str]: if self.api_key_storage_method == ApiKeyStorageMethodEnum.plain: return value.get_secret_value() elif self.api_key_storage_method == ApiKeyStorageMethodEnum.system: - keyring_service_name = self.get_keyring_service_name( - self.provider.id, self.name - ) keyring.set_password( - self.api_key_storage_method.value, - keyring_service_name, - value.get_secret_value(), + APP_NAME, str(self.id), value.get_secret_value() ) return None @@ -185,12 +170,7 @@ def active_organization_key(self) -> Optional[SecretStr]: def delete_keyring_password(self): if self.api_key_storage_method == ApiKeyStorageMethodEnum.system: - keyring_service_name = self.get_keyring_service_name( - self.provider.id, self.name - ) - keyring.delete_password( - self.api_key_storage_method.value, keyring_service_name - ) + keyring.delete_password(APP_NAME, str(self.id)) class AccountManager(RootModel): From 9fa277c5687d38877da627022b034de05763f867 Mon Sep 17 00:00:00 2001 From: clementb49 Date: Sun, 25 Aug 2024 00:01:04 +0200 Subject: [PATCH 07/11] fix(secure_api_key): change api key field validation method to fix account creation --- basilisk/account.py | 19 ++++++++++++++----- basilisk/gui/account_dialog.py | 2 +- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/basilisk/account.py b/basilisk/account.py index 78c462d3..e5a390f0 100644 --- a/basilisk/account.py +++ b/basilisk/account.py @@ -86,16 +86,25 @@ def __init__(self, **data: Any): def serialize_provider(value: Provider) -> str: return value.id - @field_validator("api_key", mode="after") + @field_validator("api_key", mode="before") @classmethod def validate_api_key( - cls, value: Optional[SecretStr], info: ValidationInfo + cls, value: Optional[Any], info: ValidationInfo ) -> Optional[SecretStr]: + if isinstance(value, SecretStr): + return value data = info.data if data["api_key_storage_method"] == ApiKeyStorageMethodEnum.plain: - return value - value = SecretStr(keyring.get_password(APP_NAME, str(data["id"]))) - return value + if not isinstance(value, str): + raise ValueError("API key must be a string") + return SecretStr(value) + elif data["api_key_storage_method"] == ApiKeyStorageMethodEnum.system: + value = keyring.get_password(APP_NAME, str(data["id"])) + if not value: + raise ValueError("API key not found in keyring") + return SecretStr(value) + else: + raise ValueError("Invalid API key storage method") @field_serializer("api_key", when_used="json") def dump_secret(self, value: SecretStr) -> Optional[str]: diff --git a/basilisk/gui/account_dialog.py b/basilisk/gui/account_dialog.py index 9e7adc08..67b03bd1 100644 --- a/basilisk/gui/account_dialog.py +++ b/basilisk/gui/account_dialog.py @@ -494,7 +494,7 @@ def on_ok(self, event): self.account = Account( name=self.name.GetValue(), provider=provider, - apii_key_storage_method=list(api_storage_methods.keys())[ + api_key_storage_method=list(api_storage_methods.keys())[ self.api_key_storage_method.GetSelection() ], api_key=SecretStr(self.api_key.GetValue()), From 8de56dd0acddf70fd82c074f2d82354980266b80 Mon Sep 17 00:00:00 2001 From: clementb49 Date: Sun, 25 Aug 2024 00:06:33 +0200 Subject: [PATCH 08/11] feat(secure_api_key): implement secure storage for organization key Refactor validation method for api_key Restore custom __init__ method for account --- basilisk/account.py | 60 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 48 insertions(+), 12 deletions(-) diff --git a/basilisk/account.py b/basilisk/account.py index e5a390f0..ef3a717c 100644 --- a/basilisk/account.py +++ b/basilisk/account.py @@ -31,7 +31,7 @@ log = getLogger(__name__) -class ApiKeyStorageMethodEnum(Enum): +class KeyStorageMethodEnum(Enum): plain = "plain" system = "system" @@ -45,12 +45,45 @@ class AccountOrganization(BaseModel): model_config = ConfigDict(populate_by_name=True) id: UUID4 = Field(default_factory=uuid4) name: str + key_storage_method: KeyStorageMethodEnum = Field( + default=KeyStorageMethodEnum.plain + ) key: SecretStr source: AccountSource = Field(default=AccountSource.CONFIG, exclude=True) + @field_validator("key", mode="before") + @classmethod + def validate_key( + cls, value: Optional[Any], info: ValidationInfo + ) -> SecretStr: + if isinstance(value, SecretStr): + return value + data = info.data + if data["key_storage_method"] == KeyStorageMethodEnum.plain: + if not isinstance(value, str): + raise ValueError("Key must be a string") + return SecretStr(value) + elif data["key_storage_method"] == KeyStorageMethodEnum.system: + value = keyring.get_password(APP_NAME, str(data["id"])) + if not value: + raise ValueError("Key not found in keyring") + return SecretStr(value) + else: + raise ValueError("Invalid key storage method") + @field_serializer("key", when_used="json") def dump_secret(self, value: SecretStr) -> str: - return value.get_secret_value() + if self.key_storage_method == KeyStorageMethodEnum.plain: + return value.get_secret_value() + elif self.key_storage_method == KeyStorageMethodEnum.system: + keyring.set_password( + APP_NAME, str(self.id), value.get_secret_value() + ) + return None + + def delete_keyring_password(self): + if self.key_storage_method == KeyStorageMethodEnum.system: + keyring.delete_password(APP_NAME, str(self.id)) class Account(BaseModel): @@ -64,8 +97,8 @@ class Account(BaseModel): provider: Provider = Field( validation_alias="provider_id", serialization_alias="provider_id" ) - api_key_storage_method: Optional[ApiKeyStorageMethodEnum] = Field( - default=ApiKeyStorageMethodEnum.plain + api_key_storage_method: Optional[KeyStorageMethodEnum] = Field( + default=KeyStorageMethodEnum.plain ) api_key: Optional[SecretStr] = Field(default=None) organizations: Optional[list[AccountOrganization]] = Field(default=None) @@ -94,11 +127,11 @@ def validate_api_key( if isinstance(value, SecretStr): return value data = info.data - if data["api_key_storage_method"] == ApiKeyStorageMethodEnum.plain: + if data["api_key_storage_method"] == KeyStorageMethodEnum.plain: if not isinstance(value, str): raise ValueError("API key must be a string") return SecretStr(value) - elif data["api_key_storage_method"] == ApiKeyStorageMethodEnum.system: + elif data["api_key_storage_method"] == KeyStorageMethodEnum.system: value = keyring.get_password(APP_NAME, str(data["id"])) if not value: raise ValueError("API key not found in keyring") @@ -108,9 +141,9 @@ def validate_api_key( @field_serializer("api_key", when_used="json") def dump_secret(self, value: SecretStr) -> Optional[str]: - if self.api_key_storage_method == ApiKeyStorageMethodEnum.plain: + if self.api_key_storage_method == KeyStorageMethodEnum.plain: return value.get_secret_value() - elif self.api_key_storage_method == ApiKeyStorageMethodEnum.system: + elif self.api_key_storage_method == KeyStorageMethodEnum.system: keyring.set_password( APP_NAME, str(self.id), value.get_secret_value() ) @@ -167,9 +200,9 @@ def active_organization(self) -> Optional[AccountOrganization]: @property def active_organization_name(self) -> Optional[str]: - if not self.active_organization: - return None - return self.active_organization.name + return ( + self.active_organization.name if self.active_organization else None + ) @property def active_organization_key(self) -> Optional[SecretStr]: @@ -178,7 +211,10 @@ def active_organization_key(self) -> Optional[SecretStr]: ) def delete_keyring_password(self): - if self.api_key_storage_method == ApiKeyStorageMethodEnum.system: + if self.organisations: + for org in self.organisations: + org.delete_keyring_password() + if self.api_key_storage_method == KeyStorageMethodEnum.system: keyring.delete_password(APP_NAME, str(self.id)) From 8a377add2a64c09c35961fd9433fb1cc79b5a5d0 Mon Sep 17 00:00:00 2001 From: clementb49 Date: Sun, 25 Aug 2024 00:11:46 +0200 Subject: [PATCH 09/11] feat(secure_api_key): implement orgazization GUI to secure API key --- basilisk/gui/account_dialog.py | 212 ++++++++++++++++++--------------- 1 file changed, 118 insertions(+), 94 deletions(-) diff --git a/basilisk/gui/account_dialog.py b/basilisk/gui/account_dialog.py index 67b03bd1..790fef8f 100644 --- a/basilisk/gui/account_dialog.py +++ b/basilisk/gui/account_dialog.py @@ -8,7 +8,7 @@ Account, AccountOrganization, AccountSource, - ApiKeyStorageMethodEnum, + KeyStorageMethodEnum, get_account_source_labels, ) from basilisk.config import conf @@ -16,11 +16,11 @@ log = getLogger(__name__) -api_storage_methods = { +key_storage_methods = { # Translators: A label for the API key storage method in the account dialog - ApiKeyStorageMethodEnum.plain: _("Plain text"), + KeyStorageMethodEnum.plain: _("Plain text"), # Translators: A label for the API key storage method in the account dialog - ApiKeyStorageMethodEnum.system: _("System keyring"), + KeyStorageMethodEnum.system: _("System keyring"), } @@ -47,12 +47,35 @@ def init_ui(self): sizer = wx.BoxSizer(wx.VERTICAL) panel.SetSizer(sizer) - label = wx.StaticText(panel, label=_("&Name:"), style=wx.ALIGN_LEFT) + label = wx.StaticText( + panel, + # Translators: A label in account dialog + label=_("&Name:"), + style=wx.ALIGN_LEFT, + ) sizer.Add(label, 0, wx.ALL, 5) self.name = wx.TextCtrl(panel) sizer.Add(self.name, 0, wx.EXPAND) - label = wx.StaticText(panel, label=_("API &Key:"), style=wx.ALIGN_LEFT) + label = wx.StaticText( + panel, + # Translators: A label in account dialog + label=_("Organisation key storage &method:"), + style=wx.ALIGN_LEFT, + ) + sizer.Add(label, 0, wx.ALL, 5) + self.key_storage_method = wx.ComboBox( + panel, + choices=list(key_storage_methods.values()), + style=wx.CB_READONLY, + ) + sizer.Add(self.key_storage_method, 0, wx.EXPAND) + label = wx.StaticText( + panel, + # Translators: A label in account dialog + label=_("Organisation &Key:"), + style=wx.ALIGN_LEFT, + ) sizer.Add(label, 0, wx.ALL, 5) self.key = wx.TextCtrl(panel) sizer.Add(self.key, 0, wx.EXPAND) @@ -71,9 +94,18 @@ def init_ui(self): sizer.Add(bSizer, 0, wx.ALL, 5) def init_data(self): - if self.organization: - self.name.SetValue(self.organization.name) - self.key.SetValue(self.organization.key.get_secret_value()) + if not self.organization: + self.key_storage_method.SetSelection(0) + return + + self.name.SetValue(self.organization.name) + index = -1 + for i, method in enumerate(key_storage_methods.keys()): + if method == self.organization.key_storage_method: + index = i + break + self.key_storage_method.SetSelection(index) + self.key.SetValue(self.organization.key.get_secret_value()) def update_data(self): pass @@ -84,17 +116,28 @@ def on_ok(self, event): wx.MessageBox(msg, _("Error"), wx.OK | wx.ICON_ERROR) self.name.SetFocus() return + if self.key_storage_method.GetSelection() == -1: + msg = _("Please select a key storage method") + wx.MessageBox(msg, _("Error"), wx.OK | wx.ICON_ERROR) + self.key_storage_method.SetFocus() + return if not self.key.GetValue(): msg = _("Please enter a key") wx.MessageBox(msg, _("Error"), wx.OK | wx.ICON_ERROR) self.key.SetFocus() return + key_storage_method = list(key_storage_methods.keys())[ + self.key_storage_method.GetSelection() + ] if self.organization: self.organization.name = self.name.GetValue() + self.organization.key_storage_method = (key_storage_method,) self.organization.key = SecretStr(self.key.GetValue()) else: self.organization = AccountOrganization( - name=self.name.GetValue(), key=self.key.GetValue() + name=self.name.GetValue(), + key_storage_method=key_storage_method, + key=SecretStr(self.key.GetValue()), ) self.EndModal(wx.ID_OK) @@ -144,6 +187,7 @@ def init_ui(self): self.organization_list.Bind( wx.EVT_LIST_ITEM_SELECTED, self.on_item_selected ) + self.organization_list.Bind(wx.EVT_KEY_DOWN, self.on_org_list_key_down) sizer.Add(self.organization_list, 1, wx.EXPAND) add_btn = wx.Button(panel, label=_("&Add")) @@ -187,10 +231,6 @@ def update_data(self): ) def update_ui(self): - if not self.organizations: - self.edit_btn.Disable() - self.remove_btn.Disable() - return selected_item = self.organization_list.GetFirstSelected() if selected_item == -1: self.edit_btn.Disable() @@ -205,18 +245,7 @@ def update_ui(self): self.remove_btn.Enable() def on_item_selected(self, event): - selected_item = self.organization_list.GetFirstSelected() - if selected_item == -1: - self.edit_btn.Disable() - self.remove_btn.Disable() - return - organization = self.organizations[selected_item] - if organization.source == AccountSource.ENV_VAR: - self.edit_btn.Disable() - self.remove_btn.Disable() - return - self.edit_btn.Enable() - self.remove_btn.Enable() + self.update_ui() def on_add(self, event): dialog = EditAccountOrganizationDialog(self, _("Add organization")) @@ -251,13 +280,11 @@ def on_edit(self, event): if dialog.ShowModal() == wx.ID_OK: organization = dialog.organization self.organizations[selected_item] = organization - self.organization_list.SetStringItem( - selected_item, 0, organization.name - ) - self.organization_list.SetStringItem( + self.organization_list.SetItem(selected_item, 0, organization.name) + self.organization_list.SetItem( selected_item, 1, organization.key.get_secret_value() ) - self.organization_list.SetStringItem( + self.organization_list.SetItem( selected_item, 2, self.account_source_labels.get( @@ -273,19 +300,20 @@ def on_edit(self, event): self.organization_list.EnsureVisible(selected_item) def on_remove(self, event): - item = self.organization_list.GetFirstSelected() - organization_id = self.organizations[item].id - organization_name = self.organizations[item].name + index = self.organization_list.GetFirstSelected() + organization = self.organizations[index] # Translators: A confirmation message in account dialog for removing organization msg = _("Are you sure you want to remove the organization {}?").format( - organization_name + organization.name ) if wx.MessageBox(msg, _("Confirmation"), wx.YES_NO) != wx.YES: return - self.organizations.pop(item) - self.organization_list.DeleteItem(item) - if self.account.active_organization_id == organization_id: + organization.delete_keyring_password() + self.organization_list.Select(index - 1) + self.organization_list.DeleteItem(index) + if self.account.active_organization_id == organization.id: self.account.active_organization_id = None + self.organizations.pop(index) self.update_ui() def onOK(self, event): @@ -295,6 +323,14 @@ def onOK(self, event): def onCancel(self, event): self.EndModal(wx.ID_CANCEL) + def on_org_list_key_down(self, event: wx.KeyEvent): + if event.GetKeyCode() == wx.WXK_RETURN: + self.on_edit(event) + elif event.GetKeyCode() == wx.WXK_DELETE: + self.on_remove(event) + else: + event.Skip() + class EditAccountDialog(wx.Dialog): def __init__(self, parent, title, size=(400, 400), account: Account = None): @@ -347,7 +383,7 @@ def init_ui(self): sizer.Add(label, 0, wx.ALL, 5) self.api_key_storage_method = wx.ComboBox( panel, - choices=list(api_storage_methods.values()), + choices=list(key_storage_methods.values()), style=wx.CB_READONLY, ) sizer.Add(self.api_key_storage_method, 0, wx.EXPAND) @@ -385,55 +421,41 @@ def init_ui(self): sizer.Add(bSizer, 0, wx.ALL, 5) def init_data(self): - if self.account: - self.name.SetValue(self.account.name) + if not self.account: + self.api_key_storage_method.SetSelection(0) + return + self.name.SetValue(self.account.name) + index = -1 + for i, provider in enumerate(providers): + if provider.name == self.account.provider.name: + index = i + break + self.provider.SetSelection(index) + if self.account.api_key and self.account.api_key_storage_method: index = -1 - for i, provider in enumerate(providers): - if provider.name == self.account.provider.name: + for i, method in enumerate(key_storage_methods.keys()): + if method == self.account.api_key_storage_method: index = i break - self.provider.SetSelection(index) - if self.account.api_key and self.account.api_key_storage_method: - index = -1 - for i, method in enumerate(api_storage_methods.keys()): - if method == self.account.api_key_storage_method: - index = i - break - self.api_key_storage_method.SetSelection(index) - self.api_key.SetValue(self.account.api_key.get_secret_value()) - self.organization.Enable( - self.account.provider.organization_mode_available - ) - if not self.account.provider.organization_mode_available: - return - if self.account.organizations: - choices = [_("Personal")] + [ - organization.name - for organization in self.account.organizations - ] - self.organization.SetItems(choices) - if self.account.active_organization_id: - index = -1 - for i, organization in enumerate(self.account.organizations): - if organization.id == self.account.active_organization_id: - index = i + 1 - break - self.organization.SetSelection(index) - else: - self.organization.SetSelection(0) - - if self.account.active_organization_id: - index = -1 - for i, organization in enumerate( - self.account.organizations - ): - if ( - organization.id - == self.account.active_organization_id - ): - index = i + 1 - break - self.organization.SetSelection(index) + self.api_key_storage_method.SetSelection(index) + self.api_key.SetValue(self.account.api_key.get_secret_value()) + self.organization.Enable( + self.account.provider.organization_mode_available + ) + if not self.account.provider.organization_mode_available: + return + if self.account.organizations: + choices = [_("Personal")] + [ + organization.name for organization in self.account.organizations + ] + self.organization.SetItems(choices) + if self.account.active_organization_id: + index = -1 + for i, organization in enumerate(self.account.organizations): + if organization.id == self.account.active_organization_id: + index = i + 1 + break + self.organization.SetSelection(index) def update_ui(self): provider_index = self.provider.GetSelection() @@ -481,23 +503,25 @@ def on_ok(self, event): active_organization = self.account.organizations[ organization_index - 1 ].id + api_key_storage_method = None + api_key = None + if provider.require_api_key: + api_key_storage_method = list(key_storage_methods.keys())[ + self.api_key_storage_method.GetSelection() + ] + api_key = SecretStr(self.api_key.GetValue()) if self.account: self.account.name = self.name.GetValue() self.account.provider = provider - if provider.require_api_key: - self.account.api_key_storage_method = list( - api_storage_methods.keys() - )[self.api_key_storage_method.GetSelection()] - self.account.api_key = SecretStr(self.api_key.GetValue()) + self.account.api_key_storage_method = api_key_storage_method + self.account.api_key = api_key self.account.active_organization_id = active_organization else: self.account = Account( name=self.name.GetValue(), provider=provider, - api_key_storage_method=list(api_storage_methods.keys())[ - self.api_key_storage_method.GetSelection() - ], - api_key=SecretStr(self.api_key.GetValue()), + api_key_storage_method=api_key_storage_method, + api_key=api_key, active_organization_id=active_organization, source=AccountSource.CONFIG, ) From 62e88044a9b2956040998d4d2240a76f43cd8716 Mon Sep 17 00:00:00 2001 From: clementb49 Date: Sun, 25 Aug 2024 15:06:34 +0200 Subject: [PATCH 10/11] fix(secure_api_key): fix saving key storage method on account organisation edition --- basilisk/gui/account_dialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/basilisk/gui/account_dialog.py b/basilisk/gui/account_dialog.py index 790fef8f..83c8f7d9 100644 --- a/basilisk/gui/account_dialog.py +++ b/basilisk/gui/account_dialog.py @@ -131,7 +131,7 @@ def on_ok(self, event): ] if self.organization: self.organization.name = self.name.GetValue() - self.organization.key_storage_method = (key_storage_method,) + self.organization.key_storage_method = key_storage_method self.organization.key = SecretStr(self.key.GetValue()) else: self.organization = AccountOrganization( From 82437eb69dda93dbe2cc984115448c2a78c60fa5 Mon Sep 17 00:00:00 2001 From: clementb49 Date: Sun, 25 Aug 2024 16:10:28 +0200 Subject: [PATCH 11/11] fix: reset active_organization property when necessary --- basilisk/account.py | 6 ++++++ basilisk/gui/account_dialog.py | 14 +++++++------- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/basilisk/account.py b/basilisk/account.py index ef3a717c..40c123d0 100644 --- a/basilisk/account.py +++ b/basilisk/account.py @@ -198,6 +198,12 @@ def active_organization(self) -> Optional[AccountOrganization]: None, ) + def reset_active_organization(self): + try: + del self.active_organization + except AttributeError: + pass + @property def active_organization_name(self) -> Optional[str]: return ( diff --git a/basilisk/gui/account_dialog.py b/basilisk/gui/account_dialog.py index 83c8f7d9..80fc7e35 100644 --- a/basilisk/gui/account_dialog.py +++ b/basilisk/gui/account_dialog.py @@ -663,8 +663,9 @@ def on_manage_organizations(self, event): self, _("Manage organizations"), account ) if dialog.ShowModal() == wx.ID_OK: + dialog.account.reset_active_organization() self.account_manager[index] = dialog.account - self.account_list.SetStringItem( + self.account_list.SetItem( index, 2, self._get_organization_name(dialog.account) ) dialog.Destroy() @@ -713,15 +714,14 @@ def on_edit(self, event): dialog = EditAccountDialog(self, _("Edit account"), account=account) if dialog.ShowModal() == wx.ID_OK: account = dialog.account - if "active_organization" in account.__dict__: - del account.__dict__["active_organization"] + account.reset_active_organization() self.account_manager[index] = account - self.account_list.SetStringItem(index, 0, account.name) - self.account_list.SetStringItem(index, 1, account.provider.name) - self.account_list.SetStringItem( + self.account_list.SetItem(index, 0, account.name) + self.account_list.SetItem(index, 1, account.provider.name) + self.account_list.SetItem( index, 2, self._get_organization_name(account) ) - self.account_list.SetStringItem( + self.account_list.SetItem( index, 3, self.account_source_labels.get(account.source, _("Unknown")),