diff --git a/.claude/skills/owncloud/SKILL.md b/.claude/skills/owncloud/SKILL.md index f0638ff2..b4af94be 100644 --- a/.claude/skills/owncloud/SKILL.md +++ b/.claude/skills/owncloud/SKILL.md @@ -68,8 +68,10 @@ py "$OWNCLOUD" config-set --value V [--type string|integer|double|boolean # files admin py "$OWNCLOUD" files-scan (--user jdoe | --all) [--path sub/dir] --confirm py "$OWNCLOUD" transfer-ownership [--path folder] --confirm -py "$OWNCLOUD" trashbin-cleanup (--user jdoe ... | --all-users) --confirm -py "$OWNCLOUD" versions-cleanup (--user jdoe ... | --all-users) --confirm +py "$OWNCLOUD" trashbin-cleanup --user jdoe --confirm +py "$OWNCLOUD" versions-cleanup --user jdoe --confirm +# fleet-wide purge needs the extra ack flag on top of --confirm: +py "$OWNCLOUD" versions-cleanup --all-users --force-all-users --confirm ``` ### Passwords + the credential rule @@ -90,6 +92,9 @@ line (suggested path `infrastructure/owncloud-users/.sops.yaml`). - `trashbin-cleanup` / `versions-cleanup` with **no** `--user` and **no** `--all-users` -> **hard-fail exit 2** (occ treats "no user" as *every* user; a fleet-wide purge must be made explicit with `--all-users`). +- `--all-users` on those two also **hard-fails exit 2** unless + `--force-all-users` is passed **in addition to** `--confirm` - a single stray + `--confirm` must never be enough to irreversibly purge every user's history. - invalid uid on `user-add` -> **exit 2**. ## Access channel diff --git a/.claude/skills/owncloud/scripts/dry_test.sh b/.claude/skills/owncloud/scripts/dry_test.sh index 5fbb545e..76466f04 100644 --- a/.claude/skills/owncloud/scripts/dry_test.sh +++ b/.claude/skills/owncloud/scripts/dry_test.sh @@ -50,6 +50,9 @@ chk 2 "user-modify no fields" -- py "$OC" user-modify zz.dry --confirm chk 2 "files-scan no target" -- py "$OC" files-scan --confirm chk 2 "trashbin-cleanup no target"-- py "$OC" trashbin-cleanup --confirm chk 2 "versions-cleanup no target"-- py "$OC" versions-cleanup --confirm +# --all-users must NOT execute on --confirm alone (needs --force-all-users) -> rc 2 +chk 2 "versions --all-users no force" -- py "$OC" versions-cleanup --all-users --confirm +chk 2 "trashbin --all-users no force" -- py "$OC" trashbin-cleanup --all-users --confirm echo "== read-only commands must succeed (rc 0) ==" chk 0 "status" -- py "$OC" status diff --git a/.claude/skills/owncloud/scripts/owncloud.py b/.claude/skills/owncloud/scripts/owncloud.py index 8bef46e1..5c0e3a23 100644 --- a/.claude/skills/owncloud/scripts/owncloud.py +++ b/.claude/skills/owncloud/scripts/owncloud.py @@ -490,6 +490,13 @@ def _cleanup(c: OwncloudClient, args, kind: str) -> int: "pass explicit --user, or --all-users to sweep everyone.", file=sys.stderr) return 2 + # A fleet-wide, irreversible purge must not be reachable by a single stray + # --confirm: require a dedicated acknowledgement flag on top of it. + if args.all_users and not args.force_all_users: + print(f"[ERROR] {kind}-cleanup --all-users irreversibly purges EVERY " + f"user's {kind} history. This needs --force-all-users IN ADDITION " + "to --confirm to proceed.", file=sys.stderr) + return 2 target = "ALL users" if args.all_users else ", ".join(users) if not _gated(f"permanently delete {kind} for {target}", args.confirm): return 3 @@ -570,8 +577,8 @@ def build_parser() -> argparse.ArgumentParser: # --- files admin --- fs = sub.add_parser("files-scan"); fs.add_argument("--user"); fs.add_argument("--all", action="store_true"); fs.add_argument("--path"); fs.add_argument("--confirm", action="store_true"); fs.add_argument("--json", action="store_true") to = sub.add_parser("transfer-ownership"); to.add_argument("source"); to.add_argument("dest"); to.add_argument("--path"); to.add_argument("--confirm", action="store_true"); to.add_argument("--json", action="store_true") - tc = sub.add_parser("trashbin-cleanup"); tc.add_argument("--user", action="append"); tc.add_argument("--all-users", action="store_true"); tc.add_argument("--confirm", action="store_true"); tc.add_argument("--json", action="store_true") - vc = sub.add_parser("versions-cleanup"); vc.add_argument("--user", action="append"); vc.add_argument("--all-users", action="store_true"); vc.add_argument("--confirm", action="store_true"); vc.add_argument("--json", action="store_true") + tc = sub.add_parser("trashbin-cleanup"); tc.add_argument("--user", action="append"); tc.add_argument("--all-users", action="store_true"); tc.add_argument("--force-all-users", action="store_true", help="required with --all-users + --confirm to purge everyone"); tc.add_argument("--confirm", action="store_true"); tc.add_argument("--json", action="store_true") + vc = sub.add_parser("versions-cleanup"); vc.add_argument("--user", action="append"); vc.add_argument("--all-users", action="store_true"); vc.add_argument("--force-all-users", action="store_true", help="required with --all-users + --confirm to purge everyone"); vc.add_argument("--confirm", action="store_true"); vc.add_argument("--json", action="store_true") return p