mirror of
https://github.com/d9pouces/id_configurator.git
synced 2026-11-09 01:47:21 +01:00
6813c86fba
Signed-off-by: d9pouces <github@19pouces.net>
79 lines
2.6 KiB
Python
79 lines
2.6 KiB
Python
"""Parse `/etc/passwd` entries for the `--auto-users` CLI option.
|
|
|
|
A passwd(5) line is `name:passwd:uid:gid:gecos:home:shell`. `--auto-users`
|
|
expands to every entry that looks like a regular (non-system) account - see
|
|
`PasswdEntry.is_regular_user`.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
|
|
DEFAULT_UID_MIN = 1000
|
|
|
|
_DISABLED_SHELL_NAMES = {"nologin", "sync", "false"}
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class PasswdEntry:
|
|
name: str
|
|
uid: int
|
|
home: str
|
|
shell: str
|
|
|
|
@property
|
|
def has_login_shell(self) -> bool:
|
|
return self.shell.rsplit("/", 1)[-1] not in _DISABLED_SHELL_NAMES
|
|
|
|
def is_regular_user(self, uid_min: int = DEFAULT_UID_MIN) -> bool:
|
|
"""True unless `self` looks like a system account.
|
|
|
|
A login-disabling shell (`has_login_shell` False) only catches
|
|
service accounts that follow that convention; it misses others
|
|
(and could wrongly flag a merely locked human account). The
|
|
authoritative signal `useradd`/`adduser`/`systemd-sysusers` use
|
|
instead is the UID: system accounts get one below `uid_min` (the
|
|
`UID_MIN` from login.defs(5), 1000 by default). Both checks apply
|
|
together here - except for `root`, whose UID 0 is itself a system
|
|
convention rather than a sign it shouldn't be configured: it's
|
|
exempted from the UID check (though not from the shell one, so a
|
|
deliberately locked root account is still excluded).
|
|
"""
|
|
if not self.has_login_shell:
|
|
return False
|
|
return self.name == "root" or self.uid >= uid_min
|
|
|
|
|
|
def parse_passwd(content: str) -> list[PasswdEntry]:
|
|
entries = []
|
|
for line in content.splitlines():
|
|
if not line or line.startswith("#"):
|
|
continue
|
|
fields = line.split(":")
|
|
if len(fields) < 7:
|
|
continue
|
|
try:
|
|
uid = int(fields[2])
|
|
except ValueError:
|
|
continue
|
|
entries.append(
|
|
PasswdEntry(name=fields[0], uid=uid, home=fields[5], shell=fields[6])
|
|
)
|
|
return entries
|
|
|
|
|
|
def parse_login_defs_uid_min(content: str) -> int | None:
|
|
"""Extract `UID_MIN` from `/etc/login.defs` content (login.defs(5)), or
|
|
`None` if it isn't set there."""
|
|
for line in content.splitlines():
|
|
stripped = line.strip()
|
|
if not stripped or stripped.startswith("#"):
|
|
continue
|
|
parts = stripped.split()
|
|
if len(parts) >= 2 and parts[0] == "UID_MIN":
|
|
try:
|
|
return int(parts[1])
|
|
except ValueError:
|
|
return None
|
|
return None
|