Files
sticknife-home/app/ipa.py
T
2026-08-15 11:15:31 +02:00

182 lines
5.9 KiB
Python

from __future__ import annotations
import os
import subprocess
import tempfile
from dataclasses import dataclass
from .config import Config
@dataclass
class IpaResult:
ok: bool
message: str
def kinit(config: Config) -> None:
if config.ipa_principal and config.ipa_keytab:
subprocess.run(
["kinit", "-kt", config.ipa_keytab, config.ipa_principal],
check=True,
text=True,
capture_output=True,
)
def create_user(config: Config, user: dict[str, str | None]) -> IpaResult:
username = user["username"] or ""
email = user["email"] or ""
full_name = user["full_name"] or username
parts = full_name.split(" ", 1)
first_name = parts[0]
last_name = parts[1] if len(parts) > 1 else username
if config.ipa_mode != "cli":
return IpaResult(True, f"dry-run: would create IPA user {username} <{email}>")
kinit(config)
add_cmd = [
"ipa",
"user-add",
username,
"--first",
first_name,
"--last",
last_name,
"--cn",
full_name,
"--email",
email,
"--shell",
config.ipa_default_shell,
"--homedir",
f"{config.ipa_home_root.rstrip('/')}/{username}",
"--random",
"--user-auth-type",
"password",
]
try:
subprocess.run(add_cmd, check=True, text=True, capture_output=True)
except subprocess.CalledProcessError as exc:
return IpaResult(False, exc.stderr.strip() or exc.stdout.strip() or str(exc))
return IpaResult(True, f"created IPA user {username}")
def add_default_group(config: Config, username: str) -> IpaResult:
if config.ipa_mode != "cli":
return IpaResult(True, f"dry-run: would add {username} to {config.default_ipa_group}")
try:
kinit(config)
subprocess.run(
["ipa", "group-add-member", config.default_ipa_group, "--users", username],
check=True,
text=True,
capture_output=True,
)
except subprocess.CalledProcessError as exc:
return IpaResult(False, exc.stderr.strip() or exc.stdout.strip() or str(exc))
return IpaResult(True, f"added {username} to {config.default_ipa_group}")
def set_user_enabled(config: Config, username: str, enabled: bool) -> IpaResult:
action = "enable" if enabled else "disable"
if config.ipa_mode != "cli":
return IpaResult(True, f"dry-run: would {action} IPA user {username}")
try:
kinit(config)
subprocess.run(
["ipa", f"user-{action}", username],
check=True,
text=True,
capture_output=True,
)
except subprocess.CalledProcessError as exc:
return IpaResult(False, exc.stderr.strip() or exc.stdout.strip() or str(exc))
return IpaResult(True, f"{action}d IPA user {username}")
def provision_user(config: Config, user: dict[str, str | None], *, active: bool = True) -> IpaResult:
username = user["username"] or ""
created = create_user(config, user)
if not created.ok:
return created
messages = [created.message]
if active:
grouped = add_default_group(config, username)
messages.append(grouped.message)
if not grouped.ok:
return IpaResult(False, "; ".join(messages))
else:
disabled = set_user_enabled(config, username, False)
messages.append(disabled.message)
if not disabled.ok:
return IpaResult(False, "; ".join(messages))
return IpaResult(True, "; ".join(messages))
def approve_user(config: Config, username: str) -> IpaResult:
enabled = set_user_enabled(config, username, True)
if not enabled.ok:
return enabled
grouped = add_default_group(config, username)
if not grouped.ok:
return IpaResult(False, f"{enabled.message}; {grouped.message}")
return IpaResult(True, f"{enabled.message}; {grouped.message}")
def set_password(config: Config, username: str, password: str) -> IpaResult:
if config.ipa_mode != "cli":
return IpaResult(True, f"dry-run: would set IPA password for {username}")
try:
kinit(config)
subprocess.run(
["ipa", "passwd", username],
input=f"{password}\n{password}\n",
check=True,
text=True,
capture_output=True,
)
except subprocess.CalledProcessError as exc:
return IpaResult(False, exc.stderr.strip() or exc.stdout.strip() or str(exc))
return IpaResult(True, f"set IPA password for {username}")
def email_matches(config: Config, username: str, email: str) -> bool:
try:
kinit(config)
result = subprocess.run(
["ipa", "user-show", username],
check=True,
text=True,
capture_output=True,
)
except subprocess.CalledProcessError:
return False
expected = f"email address: {email.lower()}"
return expected in result.stdout.lower()
def verify_password(config: Config, username: str, password: str) -> IpaResult:
realm = config.ipa_principal.rsplit("@", 1)[-1] if "@" in config.ipa_principal else "POTTERNET.LAN"
principal = f"{username}@{realm}"
with tempfile.NamedTemporaryFile(prefix="charon-krb5cc-") as cache:
env = os.environ.copy()
env["KRB5CCNAME"] = cache.name
try:
subprocess.run(
["kinit", principal],
input=f"{password}\n",
check=True,
text=True,
capture_output=True,
env=env,
)
except subprocess.CalledProcessError as exc:
return IpaResult(False, exc.stderr.strip() or exc.stdout.strip() or "old password did not authenticate")
finally:
subprocess.run(["kdestroy", "-c", cache.name], text=True, capture_output=True, env=env)
return IpaResult(True, f"verified password for {username}")