mirror of
https://github.com/d9pouces/id_configurator.git
synced 2026-11-09 01:47:21 +01:00
feat: can specify several users to configure
Signed-off-by: d9pouces <github@19pouces.net>
This commit is contained in:
@@ -15,6 +15,12 @@ CONFIG_WITH_CERT = {
|
||||
**MINIMAL_CONFIG,
|
||||
"configurations": {"certificates": [{"name": "my-ca", "content": "fake"}]},
|
||||
}
|
||||
CONFIG_WITH_GO_REPO = {
|
||||
**MINIMAL_CONFIG,
|
||||
"configurations": {
|
||||
"go": {"repositories": [{"name": "demo", "url": "https://example.invalid/"}]}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def write_config(tmp_path, data) -> str:
|
||||
@@ -129,6 +135,66 @@ def test_apply_runs_relevant_module(tmp_path) -> None:
|
||||
assert (dst / "etc/ssl/certs/ca-certificates.crt").read_bytes() == b"fake\n"
|
||||
|
||||
|
||||
def test_apply_with_user_option_writes_per_user_config(tmp_path) -> None:
|
||||
config_path = write_config(tmp_path, CONFIG_WITH_GO_REPO)
|
||||
dst = tmp_path / "dst"
|
||||
result = runner.invoke(
|
||||
cli.app,
|
||||
[
|
||||
"apply",
|
||||
"--config",
|
||||
config_path,
|
||||
"--dst",
|
||||
str(dst),
|
||||
"--user",
|
||||
"alice",
|
||||
"--user",
|
||||
"root",
|
||||
],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert (dst / "home/alice/.config/go/env").read_text(encoding="utf-8") == (
|
||||
"GOPROXY=https://example.invalid/\nGOSUMDB=off\n"
|
||||
)
|
||||
assert (dst / "root/.config/go/env").read_text(encoding="utf-8") == (
|
||||
"GOPROXY=https://example.invalid/\nGOSUMDB=off\n"
|
||||
)
|
||||
|
||||
|
||||
def test_apply_with_auto_users_reads_passwd_from_src(tmp_path) -> None:
|
||||
config_path = write_config(tmp_path, CONFIG_WITH_GO_REPO)
|
||||
src = tmp_path / "src"
|
||||
(src / "etc").mkdir(parents=True)
|
||||
(src / "etc/passwd").write_text(
|
||||
"root:x:0:0:root:/root:/bin/bash\n"
|
||||
"alice:x:1000:1000:Alice:/home/alice:/bin/bash\n"
|
||||
"daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
dst = tmp_path / "dst"
|
||||
result = runner.invoke(
|
||||
cli.app,
|
||||
[
|
||||
"apply",
|
||||
"--config",
|
||||
config_path,
|
||||
"--dst",
|
||||
str(dst),
|
||||
"--src",
|
||||
str(src),
|
||||
"--auto-users",
|
||||
],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0
|
||||
# root is always included despite its uid 0; daemon (nologin shell) is
|
||||
# a system account, excluded.
|
||||
assert (dst / "root/.config/go/env").exists()
|
||||
assert (dst / "home/alice/.config/go/env").exists()
|
||||
assert not (dst / "usr/sbin/.config/go/env").exists()
|
||||
|
||||
|
||||
def test_main_default_name_calls_app(monkeypatch) -> None:
|
||||
mock_app = MagicMock()
|
||||
monkeypatch.setattr(cli, "app", mock_app)
|
||||
|
||||
+132
-2
@@ -8,14 +8,16 @@ CONFIG = Configuration.model_validate(
|
||||
)
|
||||
|
||||
|
||||
def make_context(tmp_path, with_src: bool = True) -> Context:
|
||||
def make_context(
|
||||
tmp_path, with_src: bool = True, users=None, auto_users: bool = False
|
||||
) -> Context:
|
||||
dst = tmp_path / "dst"
|
||||
dst.mkdir()
|
||||
src = None
|
||||
if with_src:
|
||||
src = tmp_path / "src"
|
||||
src.mkdir()
|
||||
return Context(config=CONFIG, dst=dst, src=src)
|
||||
return Context(config=CONFIG, dst=dst, src=src, users=users, auto_users=auto_users)
|
||||
|
||||
|
||||
def test_dst_path_within_destination(tmp_path) -> None:
|
||||
@@ -97,3 +99,131 @@ def test_glob_src_matches_pattern(tmp_path) -> None:
|
||||
(context.src / "usr/lib/python-legacy").mkdir(parents=True)
|
||||
result = context.glob_src("usr/lib/python3.*")
|
||||
assert [entry.name for entry in result] == ["python3.11", "python3.12"]
|
||||
|
||||
|
||||
def test_users_defaults_to_empty_list(tmp_path) -> None:
|
||||
context = make_context(tmp_path, with_src=False)
|
||||
assert context.users == []
|
||||
|
||||
|
||||
def test_users_keeps_explicit_names_in_order_without_duplicates(tmp_path) -> None:
|
||||
context = make_context(tmp_path, with_src=False, users=["alice", "bob", "alice"])
|
||||
assert context.users == ["alice", "bob"]
|
||||
|
||||
|
||||
def test_auto_users_expands_from_passwd_excluding_disabled_shells(tmp_path) -> None:
|
||||
dst = tmp_path / "dst"
|
||||
dst.mkdir()
|
||||
src = tmp_path / "src"
|
||||
(src / "etc").mkdir(parents=True)
|
||||
(src / "etc/passwd").write_text(
|
||||
"root:x:0:0:root:/root:/bin/bash\n"
|
||||
"alice:x:1000:1000:Alice:/home/alice:/bin/bash\n"
|
||||
"daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
context = Context(config=CONFIG, dst=dst, src=src, auto_users=True)
|
||||
# root is always included despite its uid 0; daemon (nologin shell) is
|
||||
# excluded as a system account.
|
||||
assert context.users == ["root", "alice"]
|
||||
|
||||
|
||||
def test_auto_users_excludes_root_with_disabled_shell(tmp_path) -> None:
|
||||
dst = tmp_path / "dst"
|
||||
dst.mkdir()
|
||||
src = tmp_path / "src"
|
||||
(src / "etc").mkdir(parents=True)
|
||||
(src / "etc/passwd").write_text(
|
||||
"root:x:0:0:root:/root:/usr/sbin/nologin\n"
|
||||
"alice:x:1000:1000:Alice:/home/alice:/bin/bash\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
context = Context(config=CONFIG, dst=dst, src=src, auto_users=True)
|
||||
assert context.users == ["alice"]
|
||||
|
||||
|
||||
def test_auto_users_excludes_uid_below_default_uid_min(tmp_path) -> None:
|
||||
dst = tmp_path / "dst"
|
||||
dst.mkdir()
|
||||
src = tmp_path / "src"
|
||||
(src / "etc").mkdir(parents=True)
|
||||
(src / "etc/passwd").write_text(
|
||||
"svc:x:999:999:service account:/home/svc:/bin/bash\n"
|
||||
"alice:x:1000:1000:Alice:/home/alice:/bin/bash\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
context = Context(config=CONFIG, dst=dst, src=src, auto_users=True)
|
||||
assert context.users == ["alice"]
|
||||
|
||||
|
||||
def test_auto_users_honors_uid_min_from_login_defs(tmp_path) -> None:
|
||||
dst = tmp_path / "dst"
|
||||
dst.mkdir()
|
||||
src = tmp_path / "src"
|
||||
(src / "etc").mkdir(parents=True)
|
||||
(src / "etc/passwd").write_text(
|
||||
"svc:x:999:999:service account:/home/svc:/bin/bash\n"
|
||||
"alice:x:1000:1000:Alice:/home/alice:/bin/bash\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(src / "etc/login.defs").write_text("UID_MIN\t500\n", encoding="utf-8")
|
||||
context = Context(config=CONFIG, dst=dst, src=src, auto_users=True)
|
||||
assert context.users == ["svc", "alice"]
|
||||
|
||||
|
||||
def test_auto_users_combines_with_explicit_names_without_duplicates(tmp_path) -> None:
|
||||
dst = tmp_path / "dst"
|
||||
dst.mkdir()
|
||||
src = tmp_path / "src"
|
||||
src.mkdir()
|
||||
(src / "etc").mkdir()
|
||||
(src / "etc/passwd").write_text(
|
||||
"root:x:0:0:root:/root:/bin/bash\nalice:x:1000:1000:Alice:/home/alice:/bin/bash\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
context = Context(
|
||||
config=CONFIG, dst=dst, src=src, users=["bob", "alice"], auto_users=True
|
||||
)
|
||||
# explicit names come first, in order; auto-detected names are appended,
|
||||
# skipping "alice" since it's already present.
|
||||
assert context.users == ["bob", "alice", "root"]
|
||||
|
||||
|
||||
def test_auto_users_false_ignores_passwd(tmp_path) -> None:
|
||||
dst = tmp_path / "dst"
|
||||
dst.mkdir()
|
||||
src = tmp_path / "src"
|
||||
(src / "etc").mkdir(parents=True)
|
||||
(src / "etc/passwd").write_text(
|
||||
"alice:x:1000:1000:Alice:/home/alice:/bin/bash\n", encoding="utf-8"
|
||||
)
|
||||
context = Context(config=CONFIG, dst=dst, src=src)
|
||||
assert context.users == []
|
||||
|
||||
|
||||
def test_auto_users_without_src_is_empty(tmp_path) -> None:
|
||||
context = make_context(tmp_path, with_src=False, auto_users=True)
|
||||
assert context.users == []
|
||||
|
||||
|
||||
def test_home_dir_for_root(tmp_path) -> None:
|
||||
context = make_context(tmp_path, with_src=False)
|
||||
assert context.home_dir("root") == "root"
|
||||
|
||||
|
||||
def test_home_dir_falls_back_to_convention(tmp_path) -> None:
|
||||
context = make_context(tmp_path, with_src=False)
|
||||
assert context.home_dir("alice") == "home/alice"
|
||||
|
||||
|
||||
def test_home_dir_uses_passwd_entry_when_available(tmp_path) -> None:
|
||||
dst = tmp_path / "dst"
|
||||
dst.mkdir()
|
||||
src = tmp_path / "src"
|
||||
src.mkdir()
|
||||
(src / "etc").mkdir()
|
||||
(src / "etc/passwd").write_text(
|
||||
"alice:x:1000:1000:Alice:/srv/alice:/bin/bash\n", encoding="utf-8"
|
||||
)
|
||||
context = Context(config=CONFIG, dst=dst, src=src)
|
||||
assert context.home_dir("alice") == "srv/alice"
|
||||
|
||||
@@ -55,6 +55,18 @@ def test_apply_writes_goproxy(tmp_path) -> None:
|
||||
assert go_env == "GOPROXY=https://example.invalid/\nGOSUMDB=off\n"
|
||||
|
||||
|
||||
def test_apply_writes_go_env_for_each_configured_user(tmp_path) -> None:
|
||||
dst = tmp_path / "dst"
|
||||
context = Context(
|
||||
config=make_config(CONFIG_WITH_REPO), dst=dst, src=None, users=["alice"]
|
||||
)
|
||||
|
||||
GoModule().apply(context)
|
||||
|
||||
go_env = (dst / "home/alice/.config/go/env").read_text(encoding="utf-8")
|
||||
assert go_env == "GOPROXY=https://example.invalid/\nGOSUMDB=off\n"
|
||||
|
||||
|
||||
def test_apply_joins_multiple_repositories_with_comma(tmp_path) -> None:
|
||||
dst = tmp_path / "dst"
|
||||
context = Context(config=make_config(CONFIG_WITH_TWO_REPOS), dst=dst, src=None)
|
||||
|
||||
@@ -63,6 +63,48 @@ def test_apply_writes_nuget_config(tmp_path) -> None:
|
||||
)
|
||||
|
||||
|
||||
def test_apply_marks_non_https_source_as_insecure(tmp_path) -> None:
|
||||
data = {
|
||||
"uuid": "12345678-1234-5678-1234-567812345678",
|
||||
"organisation": "InterDiode",
|
||||
"configurations": {
|
||||
"nuget": {
|
||||
"repositories": [{"name": "demo", "url": "http://example.invalid/"}]
|
||||
}
|
||||
},
|
||||
}
|
||||
dst = tmp_path / "dst"
|
||||
context = Context(config=make_config(data), dst=dst, src=None)
|
||||
|
||||
NuGetModule().apply(context)
|
||||
|
||||
content = (dst / "root/.nuget/NuGet/NuGet.Config").read_text(encoding="utf-8")
|
||||
assert content == (
|
||||
'<?xml version="1.0" encoding="utf-8"?>\n'
|
||||
"<configuration>\n"
|
||||
" <packageSources>\n"
|
||||
" <clear />\n"
|
||||
' <add key="demo" value="http://example.invalid/" allowInsecureConnections="true" />\n'
|
||||
" </packageSources>\n"
|
||||
"</configuration>\n"
|
||||
)
|
||||
|
||||
|
||||
def test_apply_writes_config_for_each_configured_user(tmp_path) -> None:
|
||||
dst = tmp_path / "dst"
|
||||
context = Context(
|
||||
config=make_config(CONFIG_WITH_REPO), dst=dst, src=None, users=["alice", "root"]
|
||||
)
|
||||
|
||||
NuGetModule().apply(context)
|
||||
|
||||
root_content = (dst / "root/.nuget/NuGet/NuGet.Config").read_text(encoding="utf-8")
|
||||
alice_content = (dst / "home/alice/.nuget/NuGet/NuGet.Config").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
assert alice_content == root_content
|
||||
|
||||
|
||||
def test_apply_writes_multiple_sources_in_order(tmp_path) -> None:
|
||||
dst = tmp_path / "dst"
|
||||
context = Context(config=make_config(CONFIG_WITH_TWO_REPOS), dst=dst, src=None)
|
||||
|
||||
+66
-2
@@ -80,7 +80,8 @@ def test_apply_writes_global_composer_config(tmp_path) -> None:
|
||||
"name": "demo",
|
||||
"type": "composer",
|
||||
"url": "https://example.invalid/composer",
|
||||
}
|
||||
},
|
||||
{"packagist.org": False},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -93,7 +94,70 @@ def test_apply_writes_multiple_repositories_in_order(tmp_path) -> None:
|
||||
|
||||
config_path = dst / "root/.composer/config.json"
|
||||
content = json.loads(config_path.read_text(encoding="utf-8"))
|
||||
assert [repo["name"] for repo in content["repositories"]] == ["demo", "other"]
|
||||
names = [repo["name"] for repo in content["repositories"] if "name" in repo]
|
||||
assert names == ["demo", "other"]
|
||||
assert {"packagist.org": False} in content["repositories"]
|
||||
|
||||
|
||||
def test_apply_disables_secure_http_and_packagist_when_repo_not_https(
|
||||
tmp_path,
|
||||
) -> None:
|
||||
data = {
|
||||
"uuid": "12345678-1234-5678-1234-567812345678",
|
||||
"organisation": "InterDiode",
|
||||
"configurations": {
|
||||
"php": {
|
||||
"repositories": [
|
||||
{"name": "demo", "url": "https://example.invalid/composer"},
|
||||
{"name": "insecure", "url": "http://other.invalid/composer"},
|
||||
]
|
||||
}
|
||||
},
|
||||
}
|
||||
dst = tmp_path / "dst"
|
||||
context = Context(config=make_config(data), dst=dst, src=None)
|
||||
|
||||
PhpModule().apply(context)
|
||||
|
||||
config_path = dst / "root/.composer/config.json"
|
||||
content = json.loads(config_path.read_text(encoding="utf-8"))
|
||||
assert content["config"] == {"secure-http": False}
|
||||
assert {"packagist.org": False} in content["repositories"]
|
||||
|
||||
|
||||
def test_apply_does_not_disable_secure_http_when_all_repos_https(tmp_path) -> None:
|
||||
dst = tmp_path / "dst"
|
||||
context = Context(config=make_config(CONFIG_WITH_REPO), dst=dst, src=None)
|
||||
|
||||
PhpModule().apply(context)
|
||||
|
||||
config_path = dst / "root/.composer/config.json"
|
||||
content = json.loads(config_path.read_text(encoding="utf-8"))
|
||||
assert "config" not in content
|
||||
|
||||
|
||||
def test_apply_always_disables_packagist(tmp_path) -> None:
|
||||
dst = tmp_path / "dst"
|
||||
context = Context(config=make_config(CONFIG_WITH_REPO), dst=dst, src=None)
|
||||
|
||||
PhpModule().apply(context)
|
||||
|
||||
config_path = dst / "root/.composer/config.json"
|
||||
content = json.loads(config_path.read_text(encoding="utf-8"))
|
||||
assert {"packagist.org": False} in content["repositories"]
|
||||
|
||||
|
||||
def test_apply_writes_composer_config_for_each_configured_user(tmp_path) -> None:
|
||||
dst = tmp_path / "dst"
|
||||
context = Context(
|
||||
config=make_config(CONFIG_WITH_REPO), dst=dst, src=None, users=["alice"]
|
||||
)
|
||||
|
||||
PhpModule().apply(context)
|
||||
|
||||
root_content = (dst / "root/.composer/config.json").read_bytes()
|
||||
alice_content = (dst / "home/alice/.composer/config.json").read_bytes()
|
||||
assert alice_content == root_content
|
||||
|
||||
|
||||
def test_apply_without_certificates_does_not_write_openssl_ini(tmp_path) -> None:
|
||||
|
||||
@@ -105,6 +105,22 @@ def test_apply_multiple_repositories(tmp_path) -> None:
|
||||
)
|
||||
|
||||
|
||||
def test_apply_writes_poetry_config_for_each_configured_user(tmp_path) -> None:
|
||||
config = Configuration.model_validate(ONE_REPO_CONFIG)
|
||||
dst = tmp_path / "dst"
|
||||
context = Context(config=config, dst=dst, src=None, users=["alice"])
|
||||
|
||||
PythonModule().apply(context)
|
||||
|
||||
root_content = (dst / "root/.config/pypoetry/config.toml").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
alice_content = (dst / "home/alice/.config/pypoetry/config.toml").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
assert alice_content == root_content
|
||||
|
||||
|
||||
def test_apply_writes_distutils_cfg_for_detected_python_prefixes(tmp_path) -> None:
|
||||
config = Configuration.model_validate(ONE_REPO_CONFIG)
|
||||
src = tmp_path / "src"
|
||||
|
||||
+104
-3
@@ -6,7 +6,15 @@ CONFIG_WITH_REPO = {
|
||||
"uuid": "12345678-1234-5678-1234-567812345678",
|
||||
"organisation": "InterDiode",
|
||||
"configurations": {
|
||||
"rust": {"repositories": [{"name": "demo", "url": "https://example.invalid/"}]}
|
||||
"rust": {
|
||||
"repositories": [
|
||||
{
|
||||
"name": "demo",
|
||||
"url": "https://example.invalid/",
|
||||
"is_sparse": True,
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -16,8 +24,16 @@ CONFIG_WITH_TWO_REPOS = {
|
||||
"configurations": {
|
||||
"rust": {
|
||||
"repositories": [
|
||||
{"name": "demo", "url": "https://example.invalid/"},
|
||||
{"name": "other", "url": "https://other.invalid/"},
|
||||
{
|
||||
"name": "demo",
|
||||
"url": "https://example.invalid/",
|
||||
"is_sparse": True,
|
||||
},
|
||||
{
|
||||
"name": "other",
|
||||
"url": "https://other.invalid/",
|
||||
"is_sparse": True,
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -61,6 +77,91 @@ def test_apply_writes_replacement_source(tmp_path) -> None:
|
||||
)
|
||||
|
||||
|
||||
def test_apply_sanitizes_repository_names_with_special_characters(tmp_path) -> None:
|
||||
data = {
|
||||
"uuid": "12345678-1234-5678-1234-567812345678",
|
||||
"organisation": "InterDiode",
|
||||
"configurations": {
|
||||
"rust": {
|
||||
"repositories": [
|
||||
{
|
||||
"name": "weird.demo]\nname",
|
||||
"url": "https://example.invalid/",
|
||||
"is_sparse": True,
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
}
|
||||
dst = tmp_path / "dst"
|
||||
context = Context(config=make_config(data), dst=dst, src=None)
|
||||
|
||||
RustModule().apply(context)
|
||||
|
||||
config_toml = (dst / ".cargo/config.toml").read_text(encoding="utf-8")
|
||||
assert config_toml == (
|
||||
"[source.crates-io]\n"
|
||||
'replace-with = "weird_demo__name"\n'
|
||||
"\n"
|
||||
"[source.weird_demo__name]\n"
|
||||
'registry = "sparse+https://example.invalid/"\n'
|
||||
)
|
||||
|
||||
|
||||
def test_apply_does_not_prefix_url_when_not_sparse(tmp_path) -> None:
|
||||
data = {
|
||||
"uuid": "12345678-1234-5678-1234-567812345678",
|
||||
"organisation": "InterDiode",
|
||||
"configurations": {
|
||||
"rust": {
|
||||
"repositories": [{"name": "demo", "url": "https://example.invalid/"}]
|
||||
}
|
||||
},
|
||||
}
|
||||
dst = tmp_path / "dst"
|
||||
context = Context(config=make_config(data), dst=dst, src=None)
|
||||
|
||||
RustModule().apply(context)
|
||||
|
||||
config_toml = (dst / ".cargo/config.toml").read_text(encoding="utf-8")
|
||||
assert config_toml == (
|
||||
"[source.crates-io]\n"
|
||||
'replace-with = "demo"\n'
|
||||
"\n"
|
||||
"[source.demo]\n"
|
||||
'registry = "https://example.invalid/"\n'
|
||||
)
|
||||
|
||||
|
||||
def test_apply_prefixes_url_with_sparse_when_configured(tmp_path) -> None:
|
||||
dst = tmp_path / "dst"
|
||||
context = Context(config=make_config(CONFIG_WITH_REPO), dst=dst, src=None)
|
||||
|
||||
RustModule().apply(context)
|
||||
|
||||
config_toml = (dst / ".cargo/config.toml").read_text(encoding="utf-8")
|
||||
assert config_toml == (
|
||||
"[source.crates-io]\n"
|
||||
'replace-with = "demo"\n'
|
||||
"\n"
|
||||
"[source.demo]\n"
|
||||
'registry = "sparse+https://example.invalid/"\n'
|
||||
)
|
||||
|
||||
|
||||
def test_apply_writes_cargo_config_for_each_configured_user(tmp_path) -> None:
|
||||
dst = tmp_path / "dst"
|
||||
context = Context(
|
||||
config=make_config(CONFIG_WITH_REPO), dst=dst, src=None, users=["alice"]
|
||||
)
|
||||
|
||||
RustModule().apply(context)
|
||||
|
||||
root_content = (dst / ".cargo/config.toml").read_text(encoding="utf-8")
|
||||
alice_content = (dst / "home/alice/.cargo/config.toml").read_text(encoding="utf-8")
|
||||
assert alice_content == root_content
|
||||
|
||||
|
||||
def test_apply_writes_additional_repositories_as_named_registries(tmp_path) -> None:
|
||||
dst = tmp_path / "dst"
|
||||
context = Context(config=make_config(CONFIG_WITH_TWO_REPOS), dst=dst, src=None)
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
from id_configurator.users import PasswdEntry, parse_login_defs_uid_min, parse_passwd
|
||||
|
||||
PASSWD_CONTENT = (
|
||||
"root:x:0:0:root:/root:/bin/bash\n"
|
||||
"alice:x:1000:1000:Alice:/home/alice:/bin/bash\n"
|
||||
"daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin\n"
|
||||
"sync:x:4:65534:sync:/bin:/bin/sync\n"
|
||||
"nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/false\n"
|
||||
"# a comment\n"
|
||||
"\n"
|
||||
)
|
||||
|
||||
|
||||
def test_parse_passwd_returns_one_entry_per_line() -> None:
|
||||
entries = parse_passwd(PASSWD_CONTENT)
|
||||
assert [entry.name for entry in entries] == [
|
||||
"root",
|
||||
"alice",
|
||||
"daemon",
|
||||
"sync",
|
||||
"nobody",
|
||||
]
|
||||
|
||||
|
||||
def test_parse_passwd_skips_comments_and_blank_lines() -> None:
|
||||
entries = parse_passwd(PASSWD_CONTENT)
|
||||
assert len(entries) == 5
|
||||
|
||||
|
||||
def test_parse_passwd_extracts_uid_home_and_shell() -> None:
|
||||
entries = {entry.name: entry for entry in parse_passwd(PASSWD_CONTENT)}
|
||||
assert entries["alice"].uid == 1000
|
||||
assert entries["alice"].home == "/home/alice"
|
||||
assert entries["alice"].shell == "/bin/bash"
|
||||
|
||||
|
||||
def test_parse_passwd_skips_malformed_lines() -> None:
|
||||
entries = parse_passwd(
|
||||
"tooshort:x:0\nnotanumber:x:abc:0:x:/home/x:/bin/bash\n"
|
||||
"root:x:0:0:root:/root:/bin/bash\n"
|
||||
)
|
||||
assert [entry.name for entry in entries] == ["root"]
|
||||
|
||||
|
||||
def test_has_login_shell_true_for_regular_shell() -> None:
|
||||
entry = PasswdEntry(name="alice", uid=1000, home="/home/alice", shell="/bin/bash") # noqa: B604
|
||||
assert entry.has_login_shell is True
|
||||
|
||||
|
||||
def test_has_login_shell_false_for_nologin_sync_or_false() -> None:
|
||||
nologin = PasswdEntry(
|
||||
name="daemon",
|
||||
uid=1,
|
||||
home="/usr/sbin",
|
||||
shell="/usr/sbin/nologin", # noqa: B604
|
||||
)
|
||||
sync = PasswdEntry(name="sync", uid=4, home="/bin", shell="/bin/sync") # noqa: B604
|
||||
false_shell = PasswdEntry(
|
||||
name="nobody",
|
||||
uid=65534,
|
||||
home="/nonexistent",
|
||||
shell="/usr/sbin/false", # noqa: B604
|
||||
)
|
||||
assert nologin.has_login_shell is False
|
||||
assert sync.has_login_shell is False
|
||||
assert false_shell.has_login_shell is False
|
||||
|
||||
|
||||
def test_is_regular_user_true_for_human_account() -> None:
|
||||
entry = PasswdEntry(name="alice", uid=1000, home="/home/alice", shell="/bin/bash") # noqa: B604
|
||||
assert entry.is_regular_user() is True
|
||||
|
||||
|
||||
def test_is_regular_user_true_for_root_despite_uid_zero() -> None:
|
||||
entry = PasswdEntry(name="root", uid=0, home="/root", shell="/bin/bash") # noqa: B604
|
||||
assert entry.is_regular_user() is True
|
||||
|
||||
|
||||
def test_is_regular_user_false_for_root_with_disabled_shell() -> None:
|
||||
entry = PasswdEntry(name="root", uid=0, home="/root", shell="/usr/sbin/nologin") # noqa: B604
|
||||
assert entry.is_regular_user() is False
|
||||
|
||||
|
||||
def test_is_regular_user_false_for_disabled_shell_despite_high_uid() -> None:
|
||||
entry = PasswdEntry(
|
||||
name="nobody",
|
||||
uid=65534,
|
||||
home="/nonexistent",
|
||||
shell="/usr/sbin/false", # noqa: B604
|
||||
)
|
||||
assert entry.is_regular_user() is False
|
||||
|
||||
|
||||
def test_is_regular_user_honors_custom_uid_min() -> None:
|
||||
entry = PasswdEntry(name="svc", uid=500, home="/home/svc", shell="/bin/bash") # noqa: B604
|
||||
assert entry.is_regular_user(uid_min=1000) is False
|
||||
assert entry.is_regular_user(uid_min=100) is True
|
||||
|
||||
|
||||
def test_parse_login_defs_uid_min_found() -> None:
|
||||
content = "# comment\nUID_MIN\t500\nGID_MIN\t500\n"
|
||||
assert parse_login_defs_uid_min(content) == 500
|
||||
|
||||
|
||||
def test_parse_login_defs_uid_min_missing() -> None:
|
||||
assert parse_login_defs_uid_min("GID_MIN\t500\n") is None
|
||||
|
||||
|
||||
def test_parse_login_defs_uid_min_malformed_value() -> None:
|
||||
assert parse_login_defs_uid_min("UID_MIN\tnot-a-number\n") is None
|
||||
+31
-10
@@ -313,10 +313,26 @@ def test_apply_matches_index_url_to_replace_with_unrelated_files_and_multiple_re
|
||||
assert (dst / "etc/yum.repos.d/second.repo").exists()
|
||||
|
||||
|
||||
def test_apply_sanitizes_repository_and_key_names_with_slash(
|
||||
tmp_path, monkeypatch
|
||||
) -> None:
|
||||
def test_apply_sanitizes_key_name_with_slash(tmp_path, monkeypatch) -> None:
|
||||
mock_run = _mock_gpg_success(monkeypatch)
|
||||
data = {
|
||||
"uuid": "12345678-1234-5678-1234-567812345678",
|
||||
"organisation": "InterDiode",
|
||||
"configurations": {
|
||||
"yum": {"keys": [{"name": "../../evil-key", "content": KEY_CONTENT}]}
|
||||
},
|
||||
}
|
||||
dst = tmp_path / "dst"
|
||||
context = Context(config=make_config(data), dst=dst, src=None)
|
||||
|
||||
YumModule().apply(context)
|
||||
|
||||
key_content = (dst / "etc/pki/rpm-gpg/RPM-GPG-KEY-.._.._evil-key").read_bytes()
|
||||
assert key_content == b"fake-rpm-gpg-key-bytes"
|
||||
mock_run.assert_called_once()
|
||||
|
||||
|
||||
def test_apply_sanitizes_repository_name_with_disallowed_characters(tmp_path) -> None:
|
||||
data = {
|
||||
"uuid": "12345678-1234-5678-1234-567812345678",
|
||||
"organisation": "InterDiode",
|
||||
@@ -324,11 +340,10 @@ def test_apply_sanitizes_repository_and_key_names_with_slash(
|
||||
"yum": {
|
||||
"repositories": [
|
||||
{
|
||||
"name": "../../evil-repo",
|
||||
"name": "weird/repo]here[bad",
|
||||
"index-url": "https://example.invalid/yum",
|
||||
}
|
||||
],
|
||||
"keys": [{"name": "../../evil-key", "content": KEY_CONTENT}],
|
||||
]
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -337,10 +352,16 @@ def test_apply_sanitizes_repository_and_key_names_with_slash(
|
||||
|
||||
YumModule().apply(context)
|
||||
|
||||
assert (dst / "etc/yum.repos.d/.._.._evil-repo.repo").exists()
|
||||
key_content = (dst / "etc/pki/rpm-gpg/RPM-GPG-KEY-.._.._evil-key").read_bytes()
|
||||
assert key_content == b"fake-rpm-gpg-key-bytes"
|
||||
mock_run.assert_called_once()
|
||||
repo_file = dst / "etc/yum.repos.d/weird_repo_here_bad.repo"
|
||||
assert repo_file.exists()
|
||||
repo_content = repo_file.read_text(encoding="utf-8")
|
||||
assert repo_content == (
|
||||
"[weird_repo_here_bad]\n"
|
||||
"name=weird_repo_here_bad\n"
|
||||
"baseurl=https://example.invalid/yum\n"
|
||||
"enabled=1\n"
|
||||
"gpgcheck=0\n"
|
||||
)
|
||||
|
||||
|
||||
def test_import_key_raises_when_gpg_binary_missing(tmp_path, monkeypatch) -> None:
|
||||
|
||||
Reference in New Issue
Block a user