1#!/usr/bin/env python3
2import json
3import os
4from pathlib import Path
5import subprocess
6import sys
7import xml.etree.ElementTree as ET
8import zipfile
9
10
11data = json.load(sys.stdin)
12root = Path(data["hostRoot"]) / "config"
13uid = data["uid"]
14service = Path(data["hostRoot"]).name
15variable = json.loads(subprocess.check_output(
16 ["nomad", "var", "get", "-out=json", f"nomad/jobs/{service}/inputs/oidc"],
17 text=True, stderr=subprocess.DEVNULL,
18))["Items"]
19
20plugins = root / "plugins"
21plugins.mkdir(parents=True, exist_ok=True)
22os.chown(plugins, uid, uid)
23plugin = plugins / "SSO Authentication_4.0.0.4"
24files = {"meta.json", "Duende.IdentityModel.dll", "SSO-Auth.dll", "Duende.IdentityModel.OidcClient.dll"}
25if not all((plugin / name).is_file() for name in files):
26 with zipfile.ZipFile(Path(__file__).with_name("sso-authentication_4.0.0.4.zip")) as source:
27 if set(source.namelist()) != files:
28 raise ValueError("unexpected SSO plugin archive contents")
29 plugin.mkdir(exist_ok=True)
30 os.chown(plugin, uid, uid)
31 for name in files:
32 path = plugin / name
33 path.write_bytes(source.read(name))
34 os.chown(path, uid, uid)
35
36configs = plugins / "configurations"
37configs.mkdir(parents=True, exist_ok=True)
38os.chown(configs, uid, uid)
39path = configs / "SSO-Auth.xml"
40ET.register_namespace("xsi", "http://www.w3.org/2001/XMLSchema-instance")
41tree = ET.parse(path if path.exists() else Path(__file__).with_name("sso.xml"))
42items = tree.getroot().findall("./OidConfigs/item")
43matches = [item for item in items if item.findtext("./key/string") == "snow"]
44if len(matches) != 1:
45 raise ValueError("expected one Jellyfin SSO provider named snow")
46config = matches[0].find("./value/PluginConfiguration")
47for name, value in {
48 "OidEndpoint": variable["issuerUrl"],
49 "OidClientId": variable["clientId"],
50 "OidSecret": variable["clientSecret"],
51 "Enabled": "true",
52 "DefaultUsernameClaim": "preferred_username",
53}.items():
54 element = config.find(name)
55 if element is None:
56 element = ET.SubElement(config, name)
57 element.text = value
58content = ET.tostring(tree.getroot(), encoding="utf-8", xml_declaration=True)
59if not path.exists() or path.read_bytes() != content:
60 pending = path.with_suffix(".xml.pending")
61 pending.write_bytes(content)
62 os.chmod(pending, 0o600)
63 os.chown(pending, uid, uid)
64 os.replace(pending, path)
65
66branding = root / "config"
67branding.mkdir(exist_ok=True)
68os.chown(branding, uid, uid)
69target = branding / "branding.xml"
70if not target.exists():
71 target.touch()
72 os.chown(target, uid, uid)
73
74system = branding / "system.xml"
75if not system.exists() or not system.stat().st_size:
76 system.write_bytes(b"<ServerConfiguration><EnableMetrics>true</EnableMetrics></ServerConfiguration>")
77 os.chown(system, uid, uid)
78settings = ET.parse(system)
79metrics = settings.getroot().find("EnableMetrics")
80if metrics is None:
81 metrics = ET.SubElement(settings.getroot(), "EnableMetrics")
82if metrics.text != "true":
83 metrics.text = "true"
84 pending = system.with_suffix(".xml.pending")
85 settings.write(pending, encoding="utf-8", xml_declaration=True)
86 os.chown(pending, uid, uid)
87 os.replace(pending, system)