Compare commits

...
Author SHA1 Message Date
609b3dcf9d
cmds.secrets.CompileTemplates: Fix conf line handling
Some checks failed
CI / Packaging - Kali Linux (pull_request) Failing after 9m25s
CI / Packaging - OpenSUSE Tumbleweed (pull_request) Failing after 5m24s
CI / Packaging test (pull_request) Has been cancelled
File attributes of the compiled output of "jw-pkg secrets
compile-templates" can be specified via the # conf line atop a .jw-tmpl
file, the parsing of that line is buggy, however.

Its mode is parsed as a decimal number unless it carries a leading zero, so
"mode=640" yields 640 decimal, 1200 octal, and the output file ends up
without usable permissions.  The same decimal parse applies to a conf line atop
the .jw-secret of a target, whose attributes are honored the same way, and
the --mode option of compile-templates passes its string value through to
Attrs, whose mode is typed int, so a target without a conf line of its own
crashes in _format_metadata().

Other than that, the # conf line itself should be removed from the compiled
output, but isn't.

This commit makes the conf line handling correct:

- _parse_attributes() parses the mode as octal, with or without a leading
  zero
- _compile_one_template_file() drops the conf line from the compiled
  content
- the --mode option is converted to an octal int by octal_mode() at the
  argument boundary

The integration test writes its conf line modes without leading zeros,
expects the conf line gone from the compiled output, takes one target's
mode from a conf line atop its .jw-secret, and passes a --mode that the
conf lines override.

Assisted-by: unsloth/Qwen3.8-27B-GGUF:Q4_K_M with pi.dev v1.1.0
Signed-off-by: Jan Lindemann <jan@janware.com>
2026-10-12 10:37:20 +02:00
3 changed files with 37 additions and 21 deletions

View file

@ -1,12 +1,16 @@
from __future__ import annotations
from typing import TYPE_CHECKING, override
from argparse import ArgumentParser, ArgumentTypeError, Namespace
from typing import override
from .Cmd import Cmd, Parent
from .lib.base import Attrs
if TYPE_CHECKING:
from argparse import ArgumentParser, Namespace
def octal_mode(s: str) -> int:
try:
return int(s, 8)
except ValueError:
raise ArgumentTypeError(f'Not an octal mode: {s}')
class CmdCompileTemplates(Cmd): # export
@ -30,5 +34,9 @@ class CmdCompileTemplates(Cmd): # export
'--group', '-g', default = None, help = 'Default output file group'
)
parser.add_argument(
'--mode', '-m', default = None, help = 'Default output file mode'
'--mode',
'-m',
default = None,
type = octal_mode,
help = 'Default output file mode (octal)',
)

View file

@ -76,12 +76,10 @@ class FilesContext:
elif key == 'group':
ret.group = val or None
elif key == 'mode':
# -- octal, with or without a leading zero
if val:
try:
if re.fullmatch(r'0[0-7]+', val):
ret.mode = int(val, 8)
else:
ret.mode = int(val, 0)
ret.mode = int(val, 8)
except ValueError:
ret.mode = None
@ -126,6 +124,11 @@ class FilesContext:
attrs = self._parse_attributes(new_content)
if attrs is None:
attrs = default_attrs
else:
# -- A conf line is the attribute spec of the output
# file, not part of its content
lines = new_content.splitlines(keepends = True)
new_content = ''.join(lines[1:])
if attrs is not None:
if attrs.owner is not None:

View file

@ -175,7 +175,7 @@ def main() -> None:
os.makedirs(target + '/etc')
with open(target + '/etc/foo.conf.jw-tmpl', 'w') as f:
f.write(
f'# conf: owner={USER}, group={GROUP}, mode=0640\n'
f'# conf: owner={USER}, group={GROUP}, mode=640\n'
'host=HOSTNAME\n'
'token=TOKEN\n'
)
@ -297,8 +297,15 @@ def main() -> None:
assert not os.path.exists(baz_secret)
assert stat.S_IMODE(os.stat(foo_secret).st_mode) == 0o400
# -- compile-templates: substitute the keys, apply the metadata
# -- The file attributes can also be specified atop the
# .jw-secret; bar takes its mode from there
os.unlink(bar_secret)
with open(bar_secret, 'w') as f:
f.write(f'# conf: owner={USER}, group={GROUP}, mode=640\n')
f.write('PLAIN=plain-value\n')
# -- compile-templates: substitute the keys, apply the
# metadata; the conf lines win over the --mode default
out, _ = run_cli(
distro, [
'secrets',
@ -307,6 +314,8 @@ def main() -> None:
USER,
'--group',
GROUP,
'--mode',
'400',
PKG,
]
)
@ -314,13 +323,10 @@ def main() -> None:
st = os.stat(foo_out)
assert (st.st_uid, stat.S_IMODE(st.st_mode)) == (os.getuid(), 0o640), st
with open(foo_out) as f:
assert f.read() == (
f'# conf: owner={USER}, group={GROUP}, mode=0640\n'
'host=jannet\n'
'token=host-token\n'
)
assert f.read() == ('host=jannet\n'
'token=host-token\n')
st = os.stat(bar_out)
assert (st.st_uid, stat.S_IMODE(st.st_mode)) == (os.getuid(), 0o400), st
assert (st.st_uid, stat.S_IMODE(st.st_mode)) == (os.getuid(), 0o640), st
with open(bar_out) as f:
assert f.read() == 'plain=plain-value\n'
@ -334,16 +340,15 @@ def main() -> None:
USER,
'--group',
GROUP,
'--mode',
'400',
PKG,
]
)
assert out == [], out
with open(foo_out) as f:
assert f.read() == (
f'# conf: owner={USER}, group={GROUP}, mode=0640\n'
'host=jannet\n'
'token=host-token\n'
)
assert f.read() == ('host=jannet\n'
'token=host-token\n')
with open(bar_out) as f:
assert f.read() == 'plain=plain-value\n'