Layer gated admin writes onto the read-only GPS-audit skill, driven through occ over SSH (same paramiko/vault/filecache plumbing): - user lifecycle (add/delete/enable/disable/reset-password/modify), quota get/set/reset, groups (create/delete/members), read-only share listing (from oc_share; occ has no share create/list), apps, system config, maintenance mode, files scan, ownership transfer, trashbin/versions cleanup. - All writes gated behind --confirm (refuse -> rc 3); hard-fail guards (rc 2) for invalid uid, missing password, and all-users cleanup with no target. - --all-users trashbin/versions purge now ALSO requires --force-all-users in addition to --confirm, so a single stray --confirm cannot trigger an irreversible fleet-wide purge. Built against the verified live occ surface (occ list / occ help), not guessed flags. Fixes from code-review + security-review: Python-side share filtering (no MySQL SQL-injection via '' -doubling), occ reads no longer mask a failed read as healthy (maintenance/quota/config), guarded json.loads, clean --json output, and '--' end-of-options guards so dash-leading uids can't be parsed as occ flags. dry_test.sh added as a non-destructive regression harness (43 checks). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
77 lines
4.2 KiB
Bash
77 lines
4.2 KiB
Bash
#!/usr/bin/env bash
|
|
# Non-destructive verification for the owncloud skill.
|
|
# - every WRITE command is run WITHOUT --confirm -> must refuse (rc 3), no SSH write
|
|
# - hard-fail guards are run (validation precedes the gate, so no execution) -> rc 2
|
|
# - read-only commands must succeed (rc 0) and JSON reads must parse
|
|
# NOTHING here runs a state change on the server.
|
|
OC=".claude/skills/owncloud/scripts/owncloud.py"
|
|
pass=0; fail=0
|
|
chk() { # chk <expected_rc> <label> -- <cmd...>
|
|
local exp="$1" label="$2"; shift 3
|
|
"$@" >/dev/null 2>&1; local rc=$?
|
|
if [ "$rc" = "$exp" ]; then printf ' [PASS] %-40s (rc %s)\n' "$label" "$rc"; pass=$((pass+1))
|
|
else printf ' [FAIL] %-40s (got rc %s, want %s)\n' "$label" "$rc" "$exp"; fail=$((fail+1)); fi
|
|
}
|
|
jchk() { # jchk <label> -- <cmd...> (stdout must be valid JSON)
|
|
local label="$1"; shift 2
|
|
if "$@" 2>/dev/null | py -c "import sys,json; json.load(sys.stdin)" 2>/dev/null; then
|
|
printf ' [PASS] %-40s (valid JSON)\n' "$label"; pass=$((pass+1))
|
|
else printf ' [FAIL] %-40s (bad JSON)\n' "$label"; fail=$((fail+1)); fi
|
|
}
|
|
|
|
echo "== WRITE commands without --confirm must REFUSE (rc 3) =="
|
|
chk 3 "user-add" -- py "$OC" user-add zz.dry --gen-password
|
|
chk 3 "user-delete" -- py "$OC" user-delete zz.dry
|
|
chk 3 "user-enable" -- py "$OC" user-enable zz.dry
|
|
chk 3 "user-disable" -- py "$OC" user-disable zz.dry
|
|
chk 3 "user-reset-password" -- py "$OC" user-reset-password zz.dry --gen-password
|
|
chk 3 "user-modify" -- py "$OC" user-modify zz.dry --email a@b.com
|
|
chk 3 "quota-set" -- py "$OC" quota-set zz.dry "5 GB"
|
|
chk 3 "quota-reset" -- py "$OC" quota-reset zz.dry
|
|
chk 3 "group-add" -- py "$OC" group-add zz.dry
|
|
chk 3 "group-delete" -- py "$OC" group-delete zz.dry
|
|
chk 3 "group-add-member" -- py "$OC" group-add-member zz.dry -m sysadmin
|
|
chk 3 "group-remove-member" -- py "$OC" group-remove-member zz.dry -m sysadmin
|
|
chk 3 "maintenance on" -- py "$OC" maintenance on
|
|
chk 3 "maintenance off" -- py "$OC" maintenance off
|
|
chk 3 "app-enable" -- py "$OC" app-enable activity
|
|
chk 3 "app-disable" -- py "$OC" app-disable activity
|
|
chk 3 "config-set" -- py "$OC" config-set zz.key --value v
|
|
chk 3 "files-scan --user" -- py "$OC" files-scan --user zz.dry
|
|
chk 3 "files-scan --all" -- py "$OC" files-scan --all
|
|
chk 3 "transfer-ownership" -- py "$OC" transfer-ownership a b
|
|
chk 3 "trashbin-cleanup" -- py "$OC" trashbin-cleanup --user zz.dry
|
|
chk 3 "versions-cleanup" -- py "$OC" versions-cleanup --user zz.dry
|
|
|
|
echo "== hard-fail guards (validation precedes the gate; no execution) rc 2 =="
|
|
chk 2 "user-add bad uid" -- py "$OC" user-add "bad uid!" --gen-password --confirm
|
|
chk 2 "user-add no password" -- py "$OC" user-add validuid --confirm
|
|
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
|
|
chk 0 "groups" -- py "$OC" groups
|
|
chk 0 "group-members" -- py "$OC" group-members admin
|
|
chk 0 "user-groups" -- py "$OC" user-groups sysadmin
|
|
chk 0 "shares" -- py "$OC" shares
|
|
chk 0 "shares --user" -- py "$OC" shares --user sysadmin
|
|
chk 0 "apps" -- py "$OC" apps
|
|
chk 0 "maintenance status" -- py "$OC" maintenance status
|
|
chk 0 "config-get key" -- py "$OC" config-get maintenance
|
|
chk 0 "quota (read)" -- py "$OC" quota sysadmin
|
|
|
|
echo "== JSON read outputs must parse =="
|
|
jchk "groups --json" -- py "$OC" groups --json
|
|
jchk "apps --json" -- py "$OC" apps --json
|
|
jchk "shares --json" -- py "$OC" shares --json
|
|
|
|
echo
|
|
echo "==== $pass passed, $fail failed ===="
|
|
[ "$fail" = 0 ]
|