rebreak-monorepo/ops/mdm/profiles/generate-unsupervised-profile.py
chahinebrini b31066a04c feat(chat): native action sheet + Insta-style heart for DM messages
- ChatBubble: useActionSheet replaces custom Modal (native iOS popup, Android bottom sheet)
- DM mode (isDM prop): hides like-count, shows Insta-style heart badge under bubble when liked
- Group chat unchanged
- Cleanup: remove unused Modal/Platform imports, sheet styles, actionsOpen state
- deploy.sh: auto-detect ANDROID_HOME + auto-create local.properties for local Gradle
- NEXT_RELEASE.md: DM reactions release note
- Includes other staged work across binder-mac, marketing, ops/mdm, ios/
2026-05-30 09:14:32 +02:00

114 lines
4.1 KiB
Python

#!/usr/bin/env python3
"""
ReBreak Unsupervised Sideload Profile Generator
================================================
Generiert pro-User .mobileconfig mit frischen UUIDs + individuellem
RemovalPassword. Validiert das Resultat via `plutil -lint`.
KEIN Signing-Step (User-Entscheidung 2026-05-26: unsigned ausliefern).
iOS zeigt "Nicht überprüft" rot beim Install, akzeptabel für DiGA-Pilot.
Usage:
python3 generate-unsupervised-profile.py \\
--removal-password 482915 \\
--org "ReBreak GmbH" \\
--output /tmp/rebreak-schutz-user-abc123.mobileconfig
python3 generate-unsupervised-profile.py --batch users.csv --output-dir /tmp/profiles/
CSV-Format für --batch:
user_id,removal_password
abc123,482915
def456,719204
"""
import argparse
import csv
import plistlib
import subprocess
import sys
import uuid
from pathlib import Path
# Template-Quelldatei — wird gelesen, UUIDs + RemovalPassword werden ersetzt.
TEMPLATE_PATH = Path(__file__).parent / "rebreak-iphone-unsupervised-sideload.mobileconfig"
def generate_profile(removal_password: str, org_name: str = "ReBreak") -> bytes:
"""
Lädt das Template, generiert frische UUIDs für alle PayloadUUID-Felder,
ersetzt RemovalPassword + PayloadOrganization, returnt finale plist als bytes.
"""
with open(TEMPLATE_PATH, "rb") as f:
plist = plistlib.load(f)
# Top-Level
plist["PayloadUUID"] = str(uuid.uuid4()).upper()
plist["PayloadOrganization"] = org_name
plist["RemovalPassword"] = removal_password
# Sub-Payloads: jeder kriegt eine frische UUID
for sub in plist.get("PayloadContent", []):
sub["PayloadUUID"] = str(uuid.uuid4()).upper()
return plistlib.dumps(plist, fmt=plistlib.FMT_XML)
def validate_plist(profile_bytes: bytes, path: Path) -> None:
"""
Validiert via macOS plutil -lint. Schreibt temporär, lintet, löscht.
Wirft Exception bei Lint-Fehlern.
"""
path.write_bytes(profile_bytes)
result = subprocess.run(
["plutil", "-lint", str(path)],
capture_output=True,
text=True,
)
if result.returncode != 0:
raise RuntimeError(f"plutil -lint failed: {result.stdout} {result.stderr}")
def write_profile(removal_password: str, org_name: str, output_path: Path) -> None:
profile = generate_profile(removal_password=removal_password, org_name=org_name)
output_path.parent.mkdir(parents=True, exist_ok=True)
validate_plist(profile, output_path)
print(f"{output_path} (removal-PIN: {removal_password})")
def batch_from_csv(csv_path: Path, output_dir: Path, org_name: str) -> None:
with open(csv_path) as f:
reader = csv.DictReader(f)
for row in reader:
user_id = row["user_id"].strip()
removal_pin = row["removal_password"].strip()
output_path = output_dir / f"rebreak-schutz-{user_id}.mobileconfig"
write_profile(removal_pin, org_name, output_path)
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("--removal-password", help="6+ digit PIN für Profil-Removal (einzelner User)")
parser.add_argument("--org", default="ReBreak", help="PayloadOrganization-Name")
parser.add_argument("--output", help="Output-Pfad (einzelner User)")
parser.add_argument("--batch", help="CSV-Datei: user_id,removal_password")
parser.add_argument("--output-dir", help="Output-Verzeichnis für --batch")
args = parser.parse_args()
if args.batch:
if not args.output_dir:
parser.error("--batch braucht --output-dir")
batch_from_csv(Path(args.batch), Path(args.output_dir), args.org)
else:
if not (args.removal_password and args.output):
parser.error("--removal-password und --output sind required (oder --batch nutzen)")
if not args.removal_password.isdigit() or len(args.removal_password) < 6:
parser.error("--removal-password muss 6+ Ziffern haben")
write_profile(args.removal_password, args.org, Path(args.output))
return 0
if __name__ == "__main__":
sys.exit(main())