Merge pull request 'Passwords template redesigned' (#12) from passwd into develop
Reviewed-on: https://hattori.ztsh.eu/iac/karl/pulls/12
This commit is contained in:
commit
f1f1772c9a
5 changed files with 83 additions and 41 deletions
|
|
@ -7,13 +7,9 @@ from injectable import injectable, autowired, Autowired
|
||||||
from app.services import Passwords
|
from app.services import Passwords
|
||||||
|
|
||||||
|
|
||||||
class ComplexValueTemplate(Template):
|
class ValueTemplate(Template):
|
||||||
# Pozwala na kropki w nazwach placeholderów, np. ${user.name.first}
|
# Pozwala na kropki i ukośniki w nazwach placeholderów, np. ${user.name/first}
|
||||||
idpattern = r'[_a-zA-Z][_a-zA-Z0-9.]*'
|
idpattern = r'[_a-zA-Z][_a-zA-Z0-9.\/]*'
|
||||||
|
|
||||||
|
|
||||||
class SimpleValueTemplate(ComplexValueTemplate):
|
|
||||||
delimiter = '%'
|
|
||||||
|
|
||||||
|
|
||||||
@injectable
|
@injectable
|
||||||
|
|
@ -26,12 +22,9 @@ class Mo:
|
||||||
raw = ''
|
raw = ''
|
||||||
with open(mo_file, "r") as mo:
|
with open(mo_file, "r") as mo:
|
||||||
raw = mo.read()
|
raw = mo.read()
|
||||||
cmp = ComplexValueTemplate(raw)
|
parsed = ValueTemplate(raw)
|
||||||
rendered = cmp.substitute(self._passwords.get_values(cmp.get_identifiers()))
|
mappings = self._passwords.get_values(parsed.get_identifiers())
|
||||||
smp = SimpleValueTemplate(rendered)
|
rendered = parsed.safe_substitute(mappings)
|
||||||
ids = [_id + '.password' for _id in smp.get_identifiers()]
|
|
||||||
mappings = {k.replace('.password', ''): v for k, v in self._passwords.get_values(ids).items()}
|
|
||||||
rendered = smp.substitute(mappings)
|
|
||||||
de_mo_ified = str(mo_file).replace(".mo", "")
|
de_mo_ified = str(mo_file).replace(".mo", "")
|
||||||
with open(de_mo_ified, "w") as mo:
|
with open(de_mo_ified, "w") as mo:
|
||||||
mo.write(rendered)
|
mo.write(rendered)
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,30 @@ import keyring
|
||||||
from injectable import injectable
|
from injectable import injectable
|
||||||
from pykeepass import PyKeePass, create_database
|
from pykeepass import PyKeePass, create_database
|
||||||
|
|
||||||
|
class KeyRequest:
|
||||||
|
def __init__(self, prompt: str):
|
||||||
|
self.field_name = None
|
||||||
|
self.entry_name = None
|
||||||
|
self.path = None
|
||||||
|
self._parse_prompt(prompt)
|
||||||
|
|
||||||
|
def _parse_prompt(self, prompt: str):
|
||||||
|
prompt_parts = prompt.split("/")
|
||||||
|
key = None
|
||||||
|
match len(prompt_parts):
|
||||||
|
case 1:
|
||||||
|
self.field_name = 'password'
|
||||||
|
key = prompt_parts[0]
|
||||||
|
case 2:
|
||||||
|
self.field_name = prompt_parts[1]
|
||||||
|
key = prompt_parts[0]
|
||||||
|
case _:
|
||||||
|
key = None
|
||||||
|
if key is None:
|
||||||
|
return
|
||||||
|
key_parts = key.split(".")
|
||||||
|
self.path = key_parts[:] if len(key_parts) > 1 else None
|
||||||
|
self.entry_name = key_parts[-1]
|
||||||
|
|
||||||
@injectable(singleton=True)
|
@injectable(singleton=True)
|
||||||
class Passwords:
|
class Passwords:
|
||||||
|
|
@ -28,13 +52,10 @@ class Passwords:
|
||||||
def get_values(self, keys: list[str]) -> dict[str, str]:
|
def get_values(self, keys: list[str]) -> dict[str, str]:
|
||||||
output = {}
|
output = {}
|
||||||
for k in keys:
|
for k in keys:
|
||||||
key_parts = k.split(".")
|
request = KeyRequest(k)
|
||||||
path = key_parts[:-1] if len(key_parts) > 2 else None
|
|
||||||
entry_name = key_parts[-2]
|
|
||||||
field_name = key_parts[-1]
|
|
||||||
with self.open() as kp:
|
with self.open() as kp:
|
||||||
kp_entry = kp.find_entries(path=path, first=True, title=entry_name)
|
kp_entry = kp.find_entries(path=request.path, first=True, title=request.entry_name)
|
||||||
output[k] = self._get_field_value(kp_entry, field_name)
|
output[k] = self._get_field_value(kp_entry, request.field_name)
|
||||||
return output
|
return output
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
|
||||||
|
|
@ -20,11 +20,11 @@ dependencies = [
|
||||||
"keyring>=25.6.0",
|
"keyring>=25.6.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[dependency-groups]
|
||||||
dev = [
|
dev = [
|
||||||
"httpx>=0.27.0",
|
"httpx>=0.27.0",
|
||||||
"pytest>=8.3.0",
|
"pytest==9.0.1",
|
||||||
"pytest-asyncio>=0.23.0",
|
"pytest-asyncio>=1.3.0",
|
||||||
"ruff>=0.6.0",
|
"ruff>=0.6.0",
|
||||||
"mypy>=1.11.0",
|
"mypy>=1.11.0",
|
||||||
"types-Jinja2>=2.11.9",
|
"types-Jinja2>=2.11.9",
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
value: %{sample}
|
value: ${sample}
|
||||||
nested: %{some.nested.value}
|
nested: ${some.nested.value}
|
||||||
custom: ${custom.field}
|
custom: ${custom/field}
|
||||||
|
uname: ${sample/username}
|
||||||
|
invalid: ${double/slash/example}
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,50 @@
|
||||||
import os
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest import TestCase
|
|
||||||
|
|
||||||
|
import pytest
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
from app.services import Passwords
|
from app.services import Passwords
|
||||||
from app.services.mo import Mo
|
from app.services.mo import Mo
|
||||||
|
|
||||||
|
|
||||||
class TestMo(TestCase):
|
@pytest.fixture(scope='class')
|
||||||
def test_process(self):
|
def target_path():
|
||||||
target_path = Path('tests/files/test1/test.yaml')
|
p = Path('tests/files/test1/test.yaml')
|
||||||
|
# posprzątaj przed testem, gdyby plik istniał z poprzednich uruchomień
|
||||||
|
if p.exists():
|
||||||
|
p.unlink()
|
||||||
|
yield p
|
||||||
|
# sprzątanie po teście
|
||||||
|
if p.exists():
|
||||||
|
p.unlink()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope='class')
|
||||||
|
def test1_content(target_path: Path):
|
||||||
mo = Mo(Passwords())
|
mo = Mo(Passwords())
|
||||||
mo.process(Path('tests/files/test1/test.mo.yaml').absolute())
|
mo.process(Path('tests/files/test1/test.mo.yaml').absolute())
|
||||||
self.assertTrue(os.path.exists(target_path))
|
|
||||||
with open(target_path, 'r') as f:
|
assert target_path.exists()
|
||||||
content = f.read()
|
|
||||||
self.assertFalse(content.__contains__('${'))
|
content = target_path.read_text()
|
||||||
self.assertFalse(content.__contains__('%{'))
|
assert '${' not in content
|
||||||
parsed = yaml.load(content, Loader=yaml.FullLoader)
|
|
||||||
self.assertEqual('some_pass', parsed['value'])
|
yield yaml.load(content, Loader=yaml.FullLoader)
|
||||||
self.assertEqual('nested_pass', parsed['nested'])
|
|
||||||
self.assertEqual('custom_content', parsed['custom'])
|
|
||||||
|
class TestParsing:
|
||||||
|
|
||||||
|
def test_simple(self, test1_content: dict):
|
||||||
|
assert test1_content['value'] == 'some_pass'
|
||||||
|
|
||||||
|
def test_nested(self, test1_content: dict):
|
||||||
|
assert test1_content['nested'] == 'nested_pass'
|
||||||
|
|
||||||
|
def test_custom_field(self, test1_content: dict):
|
||||||
|
assert test1_content['custom'] == 'custom_content'
|
||||||
|
|
||||||
|
def test_username_field(self, test1_content: dict):
|
||||||
|
assert test1_content['uname'] == 'sample_username'
|
||||||
|
|
||||||
|
def test_invalid_key(self, test1_content: dict):
|
||||||
|
assert test1_content.get('invalid') == 'None'
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue