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 __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 .Cmd import Cmd, Parent
from .lib.base import Attrs from .lib.base import Attrs
if TYPE_CHECKING: def octal_mode(s: str) -> int:
from argparse import ArgumentParser, Namespace try:
return int(s, 8)
except ValueError:
raise ArgumentTypeError(f'Not an octal mode: {s}')
class CmdCompileTemplates(Cmd): # export class CmdCompileTemplates(Cmd): # export
@ -30,5 +34,9 @@ class CmdCompileTemplates(Cmd): # export
'--group', '-g', default = None, help = 'Default output file group' '--group', '-g', default = None, help = 'Default output file group'
) )
parser.add_argument( 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': elif key == 'group':
ret.group = val or None ret.group = val or None
elif key == 'mode': elif key == 'mode':
# -- octal, with or without a leading zero
if val: if val:
try: try:
if re.fullmatch(r'0[0-7]+', val): ret.mode = int(val, 8)
ret.mode = int(val, 8)
else:
ret.mode = int(val, 0)
except ValueError: except ValueError:
ret.mode = None ret.mode = None
@ -126,6 +124,11 @@ class FilesContext:
attrs = self._parse_attributes(new_content) attrs = self._parse_attributes(new_content)
if attrs is None: if attrs is None:
attrs = default_attrs 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 is not None:
if attrs.owner is not None: if attrs.owner is not None:

View file

@ -175,7 +175,7 @@ def main() -> None:
os.makedirs(target + '/etc') os.makedirs(target + '/etc')
with open(target + '/etc/foo.conf.jw-tmpl', 'w') as f: with open(target + '/etc/foo.conf.jw-tmpl', 'w') as f:
f.write( f.write(
f'# conf: owner={USER}, group={GROUP}, mode=0640\n' f'# conf: owner={USER}, group={GROUP}, mode=640\n'
'host=HOSTNAME\n' 'host=HOSTNAME\n'
'token=TOKEN\n' 'token=TOKEN\n'
) )
@ -297,8 +297,15 @@ def main() -> None:
assert not os.path.exists(baz_secret) assert not os.path.exists(baz_secret)
assert stat.S_IMODE(os.stat(foo_secret).st_mode) == 0o400 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( out, _ = run_cli(
distro, [ distro, [
'secrets', 'secrets',
@ -307,6 +314,8 @@ def main() -> None:
USER, USER,
'--group', '--group',
GROUP, GROUP,
'--mode',
'400',
PKG, PKG,
] ]
) )
@ -314,13 +323,10 @@ def main() -> None:
st = os.stat(foo_out) st = os.stat(foo_out)
assert (st.st_uid, stat.S_IMODE(st.st_mode)) == (os.getuid(), 0o640), st assert (st.st_uid, stat.S_IMODE(st.st_mode)) == (os.getuid(), 0o640), st
with open(foo_out) as f: with open(foo_out) as f:
assert f.read() == ( assert f.read() == ('host=jannet\n'
f'# conf: owner={USER}, group={GROUP}, mode=0640\n' 'token=host-token\n')
'host=jannet\n'
'token=host-token\n'
)
st = os.stat(bar_out) 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: with open(bar_out) as f:
assert f.read() == 'plain=plain-value\n' assert f.read() == 'plain=plain-value\n'
@ -334,16 +340,15 @@ def main() -> None:
USER, USER,
'--group', '--group',
GROUP, GROUP,
'--mode',
'400',
PKG, PKG,
] ]
) )
assert out == [], out assert out == [], out
with open(foo_out) as f: with open(foo_out) as f:
assert f.read() == ( assert f.read() == ('host=jannet\n'
f'# conf: owner={USER}, group={GROUP}, mode=0640\n' 'token=host-token\n')
'host=jannet\n'
'token=host-token\n'
)
with open(bar_out) as f: with open(bar_out) as f:
assert f.read() == 'plain=plain-value\n' assert f.read() == 'plain=plain-value\n'